Repository navigation
Server fails to reuse Boom object #3378
Description
I am re-creating this issue which was opened hapijs/boom#140 which is more likely an issue inside Hapi. I've seen the same behavior on an older vertion of Hapi and Boom (see original issue for my comment).
------- pasted issue -------
I recently started a new project and since I have used Hapi before and was happy with it, I used it again.
I have everything modularized, nice and pretty. The project includes a handlers folder with files that each contains handlers for the same resource. For example, in the same file, I would have a handler to get a resource by its ID and another one that would update a resource by its ID. Since I want handlers of the same resource to return the same errors (in the previous example, an example would be a Boom.notFound('Resource was not found');), I store them in a const and use the const where needed.
In my previous project, this gave me no issue, but it does now. The issue is that the server becomes unresponsive after the second call. Trying to find the source of the problem, I came up with the following code that reproduces the issue:
'use strict';
const Boom = require('boom');
const Hapi = require('hapi');
const ERROR_TEST = Boom.forbidden('Error Test');
const server = new Hapi.Server();
server.connection({ port: 3000 });
server.route({
method: 'GET',
path: '/',
handler: function (request, reply) {
reply(ERROR_TEST);
}
});
server.start((err) => {
if (err) {
throw err;
}
console.log(`Server running at: ${server.info.uri}`);
});With this code, I call the server and I get an error response, as expected. If I call it again, there's no response from the server. If I call it a third time, there's no response from the server. All subsequent calls return no response.
If I replace the const for its value, everything works, so I have been changing my code accordingly, but I know that in the previous project the server worked without the replacement. This indicates me that there was a bug introduced in later versions of either hapi or more likely, boom.
The problem is not solely with Boom.forbidden(). I have tried the same code with Boom.badRequest(), Boom.unauthorized(), Boom.paymentRequired(), and Boom.notFound(). I'm guessing that the other errors will behave similarly.
Old Project
node version: v6.0.0
hapi version: ^13.5.0
boom version: ^3.2.1
os: Debian Jessie
Inside a Docker Container
New Project
node version: v6.0.0
hapi version: ^15.0.3
boom version: ^4.0.0
os: Debian Jessie
Inside a Docker Container
Test Code in this issue
node version: v6.0.0
hapi version: ^15.2.0
boom version: ^4.2.0
os: OS X El Capitán
Outside a Docker Container
Activity
sirgallifrey commentedon Nov 13, 2016
I've been able to reproduce it:
"boom": "^4.2.0"
"hapi": "^15.2.0"
node v6.9.1
AdriVanHoudt commentedon Nov 13, 2016
I can not reproduce this on
boom: 4.20
hapi: 15.2.0
node: 4.6.1
wwalser commentedon Nov 13, 2016
My versions are quite old (should get around to bumping them), I do see the problem on:
Boom: 3.2.2
Hapi: 13.5.3
node: 6.2.0
sirgallifrey commentedon Nov 13, 2016
I noticed that the property output.headers gets populated after the first reply, an then the response is not send anymore. I made a little experiment where I set the headers back to an empty object and the response gets sent every time now.
So I looked it up and changed this line https://github.com/hapijs/hapi/blob/master/lib/transmit.js#L117
to:
So that the boom headers are copied to the response instead of using a direct reference and this worked as well. But I don't know if this is the right path to solving this problem
AdriVanHoudt commentedon Nov 13, 2016
@sirgallifrey but why are the boom headers empty?
sirgallifrey commentedon Nov 13, 2016
They start empty when I create the Boom like so
The headers shouldn't be empty at this time?
These headers get populated by transmit.marshal
AdriVanHoudt commentedon Nov 13, 2016
For forbidden yes https://github.com/hapijs/boom/blob/master/lib/index.js#L111, not for something like unauthorized https://github.com/hapijs/boom/blob/master/lib/index.js#L217
sirgallifrey commentedon Nov 13, 2016
When I create a unauthorized boom only with a message it wont get a header, when I set it with a scheme it get the 'WWW-Authenticate' header as documented. But with both booms I still got the same behavior described by @wwalser.
Maybe Booms are not made to be reused? Maybe the correct thing would be create a new one every time? (I'm just throwing ideas here)
sirgallifrey commentedon Nov 13, 2016
I got another clue, the only header that is messing things up is 'content-encoding' if I remove just him the response is sent every time. (I need to remove it every time before calling reply)
If 'content-encoding' is already set (see here) encoding will be set to null (here) unlike the successful response that got 'gzip' here, and because
encoding === nullthis line wont be reached and the compressor will remain equal null.this is as far as I got by now, I will continue digging
AdriVanHoudt commentedon Nov 14, 2016
@sirgallifrey Can you share a snippet that I can run to reproduce this? So I can help actually debugging :P
sirgallifrey commentedon Nov 14, 2016
I'm using the snippet @wwalser provided with:
boom: 4.2.0
hapi: 15.2.0
node: 6.9.1
if you want I can setup a docker env with node 6.9.1
wwalser commentedon Nov 14, 2016
I appreciate everyone who is pitching in to help find the problem. I've done a bit of investigation:
In that example code, If just after declaring
ERROR_TEST, you addObject.freeze(ERROR_TEST.output.headers);, then hit the route, you will very quickly spot the problem.I generally work under the assumption that an object that I have created and handed to Hapi won't be mutated but I can see why it's simpler to assume it does given JavaScript's leaning toward mutability. This thinking lead me to a few places in code where I have a pattern like:
It's not hard to change reusableError to:
It was just confusing when it initially happened and took an hour of debugging to discover that Boom was the thing causing the server to be completely silent.
The real problem was the lack of errors or warnings.
If you want to resolve this by ensuring that error objects passed into
replyaren't mutated, changing #L117 toresponse.headers = Hoek.clone(error.headers);will do the trick. If you want to resolve this by letting people know that re-using error objects is not supported, it's a little more tricky.AdriVanHoudt commentedon Nov 15, 2016
@sirgallifrey I realize I already had a snippet derp :D
I don't have node 6 atm so can't really test
@wwalser it might be handy to add a test that reproduces your error so someone can swoop in and easily fix it (or you if you find what is causing this in node 6 and not 4)
AdriVanHoudt commentedon Nov 15, 2016
I'm gonna label this as a bug since this does used to work in node 4 and feels like it still should
4 remaining items
wwalser commentedon Nov 16, 2016
Test implemented:
Test branch - Test commit - Travis build
I've also implemented my proposed fix here and tested here.
Proposed fix for issue hapijs#3378.
sirgallifrey commentedon Nov 16, 2016
that's great @wwalser! I have just a small opinion on your fix, so I left a comment on your commit
AdriVanHoudt commentedon Nov 16, 2016
@wwalser 👏 nice! Can you open a PR so the maintainers can look at it and merge the fix?
AdriVanHoudt commentedon Nov 16, 2016
@wwalser I did a quick test and I think this is also happening when replying with the same Error object twice. If you change the
erroranddatain your test tothe test also fails.
As for the proposed fix I think @sirgallifrey is right. I think cloning the full error is more future proof and makes more sense as in that
.faildoes not modify the original error object (being it boom or plain error)Marsup commentedon Nov 16, 2016
Good luck cloning errors, they are full of internals, you'll probably mess up the stack or something.
wwalser commentedon Nov 16, 2016
The proposed change fixes the problem with reusing errors and reusing booms. We don't clone the error, Hapi wraps errors using
Boom.wrap(which eventually gets to L#108) before it callstransmit.prototype.fail. Then inside offail, we cloneboom.output.I'll add a test for reusing an error as well since this has come up twice in the discussion.
Merge pull request #3380 from wwalser/reusable-booms
[-]Hapi Server becomes unresponsive after second call #140[/-][+]Server fails to reuse Error object[/+][-]Server fails to reuse Error object[/-][+]Server fails to reuse Boom object[/+]Prevent error headers from being modified. Closes hapijs#3378