Join GitHub today
GitHub is home to over 50 million developers working together to host and review code, manage projects, and build software together.Sign up
GitHub is where the world builds software
Millions of developers and companies build, ship, and maintain their software on GitHub — the largest and most advanced development platform in the world.
Unskippable 50ms delay when making a POST/PUT/DELETE request with form-urlencoded or json data #3205
If you make a request to Sails with any content-type that's not a multipart form upload, Sails's default body parser will sleep for 50 milliseconds before yielding control. You can verify this by checking out the latest upstream (2b908c9, at the time of writing) and running the following commands:
and then making a POST request to
You will observe the following in your Sails log output:
(I'm not sure where the
Anyway, here's the
And here's the
I'm sure there's a good reason for doing this, but it sure is unexpected, and I'd expect this behavior to be called out in large type in the README. I also expect that the vast majority of POST/PUT requests are using form-urlencoded or JSON content types instead of multipart file upload, which makes it a bit bewildering that these are handled by delaying.
You may be saying to yourself "50ms isn't actually that long." Without this in place, the average test time for a controller test drops from 70-100ms to 20-50ms, about a 100% speed improvement. Eyeballing our request logs, an average POST request takes anywhere from 80 to 150ms; it would be a pretty significant performance increase if this dropped to 30-100ms. The delay also means the web node is holding open the request/tying up memory for the duration of the 50ms sleep.
It looks like the offending code is here - the comment there about why the value is hard coded to 50ms seemed like it was committed when only half-complete. https://github.com/balderdashy/skipper/blob/master/lib/Parser/prototype.parseReq.js#L132. I'm also confused about why the behavior is to delay if the content-type is unknown, instead of just to hit the callback.
Anyway, I would appreciate advice on what I/we can do to mitigate this or work around it. Thanks!
This delay is covered in the FAQ for skipper. Its intent is to allow the body parser time to identity incoming file streams before passing control back to the controller. But, it should only need to do that for multipart requests; we should be able to avoid the delay for everything else. I'll look into it.
Ha! To be clear though, this delay isn't "fixing" anything. You can look at it instead as guaranteeing that any request containing a file will pass control to app code in 50ms, versus having to possibly wait multiple seconds (minutes? hours?) for the file to upload (in the default Express bodyparser case, this meant buffering to disk/memory) before being able to run your code. If your code uses text params to determine whether or not to abort the upload, it sure is nice to only have to wait 50ms to do so! Obviously this didn't make sense for non-multipart requests, hence this issue, which is now fixed.
@RWOverdijk I'm not sure I follow what you mean here, but I should reiterate that the 50ms timeout is not a hack-- it's intentional (more on that in a sec). Also, just to be clear, it is not a sleep because it does not block-- the Node process continues to handle requests.
@AeroNotix If performance patches for open-source modules aren't drama, I don't know what is. Maybe you could write a telenovela about this?
This is a closed issue that has been resolved with a fix published to NPM for over six months now, so I'm going to go ahead and wrap up:
You can choose to use any body parser you like in Sails-- like all of the other built-in middleware, it is completely configurable. As @sgress454 hinted at, our approach in the default body parser, Skipper, was a deliberate design decision I made in order to empower Sails apps to support terabyte file uploads instead of the original default behavior in Express and Sails, which left your app vulnerable to DoS attacks out of the box. The easy solution would have been to remove file upload support from core, which is what Express did (rightly IMO-- it's a different kind of framework). But I believed then (and still do now) that for Sails, that would be a cop-out.
What was not a deliberate decision was that, if an incoming request did not include a content-type header that explicitly declared itself a multipart upload, the underlying bodyparser (multiparty) would emit an
Re: error handling in general, rather than expecting you to bind an error listener on the multiparty form instance yourself in your app code and risk you forgetting to do so, crashing the Node process and thus your app, we handle it for you in Skipper core. The only problem is that multiparty does not emit a
As you've probably figured out, the real issue was we were using the approach that Express and Connect's body parser used at the time (and still use today), which was to attempt to parse with each type of sub-parser. This relies on content-type headers to determine the appropriate behavior and, if the headers recognized by a particular parser are not sent, then that parser is skipped.... err... except for multiparty, which emits an error on its form stream, which Skipper handles to prevent from crashing the process-- but then
When replacing the built-in body parser with multiparty, we didn't notice the decrease in performance because it isn't visible to the naked eye. But luckily, at this point (Sep 2015), Shyp had scaled to the point where they making significant investment in performance anyway, and Kevin had a sharp eye. When profiling their Sails app, he noticed this and provided us with the excellent report above. After verifying that there wouldn't be any side-effects to compatibility, @sgress454 quickly published a patch that gave every Sails app a nice performance boost.
This was fixed in Skipper v0.5.7, so if you haven't run
Thanks again for the help everyone.