Skip to content

Server fails to reuse Boom object #3378

Closed

Description

@wwalser

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

  1. sirgallifrey commented on Nov 13, 2016

    @sirgallifrey
    Contributor

    I've been able to reproduce it:

    "boom": "^4.2.0"
    "hapi": "^15.2.0"
    node v6.9.1

  2. AdriVanHoudt commented on Nov 13, 2016

    @AdriVanHoudt
    Contributor

    I can not reproduce this on

    boom: 4.20
    hapi: 15.2.0
    node: 4.6.1

  3. wwalser commented on Nov 13, 2016

    @wwalser
    ContributorAuthor

    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

  4. sirgallifrey commented on Nov 13, 2016

    @sirgallifrey
    Contributor

    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.

        handler: function (request, reply) {
            if (Object.keys(ERROR_TEST.output.headers).length > 0) {
                ERROR_TEST.output.headers = {};
            }
            reply(ERROR_TEST);
        }

    So I looked it up and changed this line https://github.com/hapijs/hapi/blob/master/lib/transmit.js#L117

    to:

    response.headers = Object.assign({}, error.headers);

    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

  5. AdriVanHoudt commented on Nov 13, 2016

    @AdriVanHoudt
    Contributor

    @sirgallifrey but why are the boom headers empty?

  6. sirgallifrey commented on Nov 13, 2016

    @sirgallifrey
    Contributor

    They start empty when I create the Boom like so

    console.log(Boom.forbidden('Error Test').output.headers); // {}

    The headers shouldn't be empty at this time?

    These headers get populated by transmit.marshal

  7. AdriVanHoudt commented on Nov 13, 2016

    @AdriVanHoudt
    Contributor
  8. sirgallifrey commented on Nov 13, 2016

    @sirgallifrey
    Contributor

    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)

  9. sirgallifrey commented on Nov 13, 2016

    @sirgallifrey
    Contributor

    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 === null this line wont be reached and the compressor will remain equal null.

    this is as far as I got by now, I will continue digging

  10. AdriVanHoudt commented on Nov 14, 2016

    @AdriVanHoudt
    Contributor

    @sirgallifrey Can you share a snippet that I can run to reproduce this? So I can help actually debugging :P

  11. sirgallifrey commented on Nov 14, 2016

    @sirgallifrey
    Contributor

    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

    '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}`);
    });
  12. wwalser commented on Nov 14, 2016

    @wwalser
    ContributorAuthor

    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 add Object.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:

    const reusableError = Boom.badRequest('foobar');
    server.route({
      method: 'GET',
      path: '/',
      handler: (request, reply) => {
        if (request.something) {
          reply(reusableError)
        }
        …
      }
    });

    It's not hard to change reusableError to:

    const reusableError = () => Boom.badRequest('foobar');

    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 reply aren't mutated, changing #L117 to response.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.

  13. AdriVanHoudt commented on Nov 15, 2016

    @AdriVanHoudt
    Contributor

    @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)

  14. AdriVanHoudt commented on Nov 15, 2016

    @AdriVanHoudt
    Contributor

    I'm gonna label this as a bug since this does used to work in node 4 and feels like it still should

  15. 4 remaining items

  16. wwalser commented on Nov 16, 2016

    @wwalser
    ContributorAuthor

    Test implemented:

    Test branch - Test commit - Travis build

    I've also implemented my proposed fix here and tested here.

  17. added a commit that references this issue on Nov 16, 2016
  18. sirgallifrey commented on Nov 16, 2016

    @sirgallifrey
    Contributor

    that's great @wwalser! I have just a small opinion on your fix, so I left a comment on your commit

  19. AdriVanHoudt commented on Nov 16, 2016

    @AdriVanHoudt
    Contributor

    @wwalser 👏 nice! Can you open a PR so the maintainers can look at it and merge the fix?

  20. AdriVanHoudt commented on Nov 16, 2016

    @AdriVanHoudt
    Contributor

    @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 error and data in your test to

                const error = new Error('test');
                const data = JSON.stringify({
                    statusCode: 500,
                    error: "Internal Server Error",
                    message: "An internal server error occurred"
                });

    the 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 .fail does not modify the original error object (being it boom or plain error)

  21. Marsup commented on Nov 16, 2016

    @Marsup
    Contributor

    Good luck cloning errors, they are full of internals, you'll probably mess up the stack or something.

  22. wwalser commented on Nov 16, 2016

    @wwalser
    ContributorAuthor

    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 calls transmit.prototype.fail. Then inside of fail, we clone boom.output.

    I'll add a test for reusing an error as well since this has come up twice in the discussion.

  23. added a commit that references this issue on Nov 29, 2016
  24. added this to the 16.0.0 milestone on Nov 29, 2016
  25. self-assigned this
    on Nov 29, 2016
  26. changed the title [-]Hapi Server becomes unresponsive after second call #140[/-] [+]Server fails to reuse Error object[/+] on Nov 29, 2016
  27. changed the title [-]Server fails to reuse Error object[/-] [+]Server fails to reuse Boom object[/+] on Nov 29, 2016
  28. locked as resolved and limited conversation to collaborators on Jan 10, 2020
  29. added a commit that references this issue on May 10, 2024
    09c2e33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugBug or defect

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions