-
Notifications
You must be signed in to change notification settings - Fork 751
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Stream support for FileUpload #471
Conversation
Hi @jjsquillante, thanks for your kind words as well as the contribution! :) This looks good to me, but going to ask some colleagues with better JS skills than mine to take a look as well. |
lib/StripeMethod.js
Outdated
}) | ||
.on('end', function() { | ||
var dataBuffered = Object.assign({}, data); | ||
dataBuffered.file.data = Buffer.concat(bufferArray); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this is fine, but would you mind adding var Buffer = require('safe-buffer').Buffer;
at the top of the file? That way if we add more code that uses Buffer
in this file it will use the safe version.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ob-stripe thanks for the quick response and catching my oversight on safe-buffer
! I noticed safe-buffer
in the MultipartDataGenerator.js file, but totally forgot about requiring it here. I'll update the code asap and keep a look out for any other feedback from @jlomas-stripe @rattrayalex-stripe .
Hey hey. Super-cool. :) I think there should probably be a test in the describe('FileUpload', function() {
it('Allows you to upload a file as a stream', function() {
var testFilename = path.join(__dirname, 'resources/data/minimal.pdf');
var f = fs.createReadStream(testFilename);
return expect(
stripe.fileUploads.create({
purpose: 'dispute_evidence',
file: {
data: f,
name: 'minimal.pdf',
type: 'application/octet-stream',
},
}).then(null, function(error) {
return error;
})
).to.eventually.have.nested.property('size', 739);
});
}); More generally, we have a I feel like the right way to do this is to refactor the |
@jlomas-stripe thanks for the response! totally agree - my initial strategy was to modify |
You're welcome! :) cc @ob-stripe / @brandur-stripe / @rattrayalex-stripe on my above suggestion; does that make sense to y'all? What jjsquillante has done here definitely works just fine, but it feels a bit out of place in terms of where it lives, since it only affects one type of API request. |
Thanks for the effort here @jjsquillante!
Yeah, agreed that |
lib/utils.js
Outdated
*/ | ||
checkForStream: function (obj) { | ||
if (obj.file && obj.file.data) { | ||
return typeof obj.file.data.on === 'function' && typeof obj.file.data.emit === 'function'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not do an instanceof
check? This is probably fine too, just curious.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rattrayalex-stripe good question! An instanceof
check would have been more explicit, but I'm not aware of require('events').EventEmitter
available in the utils file. In retrospect, I was trying to implement the feature with just minimal additions to the existing code. When it comes to collaboration, though, it makes sense to be more explicit - I should have probably just required the events module! Thanks for the feedback. I plan to refactor the requestDataProcessor
function to async, and if I need this check, I'll implement via instanceof
. Let me know if you have any other feedback - thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool, thanks! That reasoning makes sense. A duck-type check as you have here may make more sense; let's see how it looks after the refactor. Cheers
Nice, thanks @jjsquillante ! Other than a +1 to @jlomas-stripe 's suggestions, this looks good to me. |
@ob-stripe @brandur-stripe @jlomas-stripe @rattrayalex-stripe thanks for all of the feedback. I appreciate you all being open to my contribution and I have enjoyed working within Stripe's library. Sounds like the best strategy is to close this PR and I'll resolve to refactor the |
@jjsquillante You're very welcome, and I'm glad you like the library! Since this PR represents the reason/inspiration for the refactor, and since the tests for streaming |
@jlomas-stripe ok - sounds good. So to confirm, I'll leave this PR open and will update once the refactor of |
Sure, or just make all the changes in here and re-title - whatever you prefer! |
Hey all - @jlomas-stripe @rattrayalex-stripe @ob-stripe |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool. :) There are a couple of changes in the tests that need made and a few other comments. Hopefully @rattrayalex-stripe or @ob-stripe can take a peek as well and provide their thoughts!
test/resources/FileUploads.spec.js
Outdated
var testFilename = path.join(__dirname, 'data/minimal.pdf'); | ||
var f = fs.createReadStream(testFilename); | ||
|
||
stripe.fileUploads.create({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You need to return
this so Mocha knows it's async otherwise it will always pass (because it won't wait for the result in the Promise.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah, my fault! nice catch @jlomas-stripe - I just ran it through the debugger with an intentional fail and noticed exactly what you were talking about. I'll update this asap.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No worries. This is a super-insidious mocha thing. Not including a done
callback and then doing something async used to catch me out unfortunately often in the good ol' days. 😂
test/resources/FileUploads.spec.js
Outdated
var testFilename = path.join(__dirname, 'data/minimal.pdf'); | ||
var f = fs.createReadStream(testFilename); | ||
|
||
stripe.fileUploads.create({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here: you need to return
this so Mocha knows it's async otherwise it will always pass (because it won't wait for the result in the Promise.
lib/StripeResource.js
Outdated
var requestData; | ||
|
||
if (self.requestDataProcessor) { | ||
requestData = self.requestDataProcessor(method, data, options.headers); | ||
requestProcessor = self.requestDataProcessor(method, data); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this now gets a function, rather than just returning the data, it might be clearer if it were called something like requestDataProcessorFn
or getRequestDataProcessorFn
I think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The variable also might be a bit clearer if it were named processRequestData
since that's what it's going to to.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks for the feedback. I'll update the variable names to be more clear.
lib/MultipartDataGenerator.js
Outdated
@@ -39,7 +39,10 @@ function multipartDataGenerator(method, data, headers) { | |||
} | |||
push('--' + segno + '--'); | |||
|
|||
return buffer; | |||
if (!callback) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function doesn't have any async pieces; does it really need the callback in here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you're correct - definitely does not need to have a callback in there. I'll update. thanks @jlomas-stripe
lib/StripeResource.js
Outdated
if (requestProcessor) { | ||
requestProcessor(method, data, options.headers, function(err, buffer) { | ||
if (err) { | ||
var handleError = self._errorHandler({}, callback); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this one is probably better handled with a different kind of error, rather than overloading the connection error, since this isn't a connection (or other Stripe) error; (i presume) it'll be something related to the Buffer itself, so we should probably expose it as such.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, cool - makes sense. Looking back now, it was kind of a 'cop out' to overload the connection error. I'll resolve to update and expose the error as such.
lib/resources/FileUploads.js
Outdated
} else { | ||
return utils.stringifyRequestData(data); | ||
} | ||
|
||
function determineSource(data) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the approach you've got here, this function does more than just determineSource
- it getProcessorForSourceType
- so it probably should be renamed so it's clearer as to what you're getting back.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sounds good, i'll rename and update!
lib/resources/FileUploads.js
Outdated
@@ -9,14 +10,45 @@ module.exports = StripeResource.extend({ | |||
|
|||
overrideHost: 'uploads.stripe.com', | |||
|
|||
requestDataProcessor: function(method, data, headers) { | |||
requestDataProcessor: function(method, data) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This got a bit more complicated than I expected - I imagined something very similar to what you'd done previously, except that the entire requestDataProcessor
function would just get an additional callback
parameter that was always used (either sync or async), and the main code flow would either hit some 'default' function with callback, or the one specified here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jlomas-stripe Definitely a bit more complicated but I believe I vetted the scenario you've mentioned and the nature of streams
posed a problem. If I'm understanding your comment (correct me if I'm wrong), the gist is: requestDataProcessor
would send the buffered multipart/form-data
back to _request
via the additional callback
- which ever way handled (ie. data processed via stream
or already buffered data). From there _request
can proceed as normal.
The trouble I encountered is that when the stream
is set by attaching an event listener .on('data')
, the code is handled via process.nextTick()
which runs the callback after the rest of the code and before the event loop. If I remember correctly, this solution didn't wait for data
to be passed back to the callback
, but instead proceed with data as undefined
and continue through _request
without the buffered data. The way I figured, returns a function in both scenarios so we can wait for the stream
to finish processing. This will guarantee the buffered data exists when the rest of _request
is processed by invoking the setHeadersAndMakeRequest
within the callback
from either scenario. My apologies if that was long-winded! 🙂
If that logic may be flawed or there's a better way to go about this - I'm fine with scrapping this solution for the sake of clarity and going back to the drawing board. Let me know!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is sort of what I was thinking from the _request
perspective:
_request: function (method, path, data, auth, options, callback) {
var requestData;
function makeRequestWithData(error, data) {
if (error) { /* something */ }
requestData = data;
/* some stuff */
this._stripe.getClientUserAgent(function(cua) {
/* some stuff */
makeRequest();
});
}
if (self.requestDataProcessor) {
self.requestDataProcessor(method, data, options.headers, makeRequestWithData);
} else {
makeRequestWithData(null, utils.stringifyRequestData(data || {}));
}
}
(Note, this is just what I was thinking; I haven't tested it so I may be out to lunch 😂)
I.e., just adding one more function call to 'asyncify' the flow from 'process the data' to 'make the request'. This doesn't even need to be done async (the else
is a synchronous function call here), but it at least allows for it.
I think adding that one function call, plus adding a callback argument to requestDataProcessor
, is enough to make this handle both cases.
And then rather than returning a function from requestDataProcessor
, you'd just hit the callback that was provided to it when it's done, in stream.on('end')
, as you're doing now.
If you were ending up with data
being undefined
, maybe the callback was firing early/more than once, or something was returning a value when you wanted it to be 'returned' via the callback instead...?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jlomas-stripe ohhh no I misunderstood! All good, yea that'll definitely work inside _request
- I have some free time later and can put it to action. thanks for further explaining!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jlomas-stripe just tested it and worked perfectly. I appreciate the help simplifying. I'll update the PR with the changes requested in the review.
Thanks – I'll defer to @ob-stripe on this one |
hey all - I've made the requested updates from the review. Let me know if there's any other changes required or something I may have forgot! Thanks |
@jlomas-stripe Mind doing another review? Cheers :) |
@ob-stripe / @jjsquillante Yup, for sure; sorry for the delay there. WIll peek it this morning. 👍 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks fantastic - thanks again for taking this on! A couple of comments inline, but this is nearly there. :)
lib/resources/FileUploads.js
Outdated
data = data || {}; | ||
|
||
if (method === 'POST') { | ||
return multipartDataGenerator(method, data, headers); | ||
return getProcessorForSourceType(data); | ||
} else { | ||
return utils.stringifyRequestData(data); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to make use of the callback flow here, so it's probably return callback(null, utils.stringifyRequestData(data));
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh wow, great catch - my fault - I totally missed that! Thanks
lib/StripeResource.js
Outdated
|
||
var apiVersion = this._stripe.getApiField('version'); | ||
if (error) { | ||
var handleError = self._errorHandlerStream(callback); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd rather see this FileUpload-specific error generation/handling pushed to FileUploads.js
, and have this just be more generic here so it can handle future cases, i.e. just:
if (error) {
return callback(error);
}
At this point in the code, we don't (or shouldn't) really care what kind of error this is, we should just expose it in the callback
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updated
lib/StripeResource.js
Outdated
@@ -212,6 +212,20 @@ StripeResource.prototype = { | |||
} | |||
}, | |||
|
|||
_errorHandlerStream: function(callback) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Per comment further down, I think the StreamProcessingError
should be created (and wrap the incoming error) in FileUploads.js
, since it's so specific to FileUploads.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Apparently I meant 'further up' 😂)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good point! - removed and updated StreamProcessingError
to FileUploads.
lib/resources/FileUploads.js
Outdated
var buffer = fn(method, bufferData, headers); | ||
callback(null, buffer); | ||
}).on('error', function(err) { | ||
callback(err, null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Per comments above, this is probably the right place to new up the StreamProcessorError
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updated - thanks!
test/flows.spec.js
Outdated
}) | ||
).to.eventually.have.nested.property('size', 739); | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't actually test that the error comes back as expected; this should do it:
it('Surfaces stream errors correctly', function(done) {
var mockedStream = new stream.Readable();
mockedStream._read = function() {};
var fakeError = new Error('I am a fake error');
process.nextTick(function() {
mockedStream.emit('error', fakeError);
});
stripe.fileUploads.create({
purpose: 'dispute_evidence',
file: {
data: mockedStream,
name: 'minimal.pdf',
type: 'application/octet-stream',
},
}).catch(function(error) {
expect(error.message).to.equal('An error occurred while attempting to process the file for upload.');
expect(error.detail).to.equal(fakeError);
done();
});
});
We should also test that you can still provide a (non-Stream) file, which is just the same as what you've already got except with fs.readFileSync()
instead of fs.createReadStream()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(I tried doing this in a promisey way but I don't trust promises in Mocha I couldn't get it to work. ¯\_(ツ)_/¯ :) )
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added both tests (for synchronous file uploads and testing for the stream error to return properly) - thanks!
@jlomas-stripe awesome, thanks for reviewing again! I'll look over the changes requested and revise based on your comments. |
Awesome - you're welcome! And I'll try to be more timely in my next review. :) |
@jlomas-stripe updated the code and revised based on your comments - let me know if there's anything that needs to be changed! thanks |
@jjsquillante Looks fantastic - thank you! :) @ob-stripe / @rattrayalex-stripe Can one of y'all please take a rip through this and make sure it all makes sense to you as well? Thanks! |
Hold on - I didn't actually approve this, just the changes - I'm not sure how that happened. Weird. I'd really like @ob-stripe or @rattrayalex-stripe to just have a quick peek first. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me. @rattrayalex-stripe, mind taking a final 👀?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM! Thanks @jjsquillante
@jlomas-stripe @ob-stripe @rattrayalex-stripe @brandur-stripe |
Unsubscribe.
On 15 Jul 2018 21:55, James Squillante <notifications@github.com> wrote:
@jjsquillante commented on this pull request.
________________________________
In lib/resources/FileUploads.js<#471 (comment)>:
data = data || {};
if (method === 'POST') {
- return multipartDataGenerator(method, data, headers);
+ return getProcessorForSourceType(data);
} else {
return utils.stringifyRequestData(data);
oh wow, great catch - my fault - I totally missed that! Thanks
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub<#471 (comment)>, or mute the thread<https://github.com/notifications/unsubscribe-auth/AHSs70LZo4XoY6ag7r3MTN0_InlNYI_lks5uG6w_gaJpZM4U1Rn->.
|
No problemo - thanks for taking this on! :) @ob-stripe can you merge && release? Or ... can ... I? 😂 |
I'll take :) |
Released as 6.3.0. |
Hey all at Stripe!
First, I want to say: from documentation to code, Stripe communicates its design of the API libraries extremely well - thank you very much!
Ok down to the PR, I was implementing Stripe for a recent project, and figured in order to truly understand the Stripe-node library, I might as well dive in to an issue or feature request to get my hands dirty. I noticed issue #401 and decided to give it a go.
This Pull Request allows a user to upload a file by passing a stream. Within the
StripeMethod
, we now check for a stream, convert it to a buffer, and begin the_request
process as normal.I've added tests to confirm that the new feature is working as designed and did not break any existing functionality.
I'm certainly open to any feedback, so if there's anything you'd like to see different or that can be improved, let me know!
Please reach out if there's anything that I can help further clarify. Thanks!