Skip to content

Response validation error breaks reply lifecycle #1325

@marcobiraghi

Description

@marcobiraghi

🐛 Bug Report

If response validation fails, reply.send() will thrown an Error (as expected), but the error is not handled by Fastify and it breaks the response lifecycle.

This is the code of reply.send. I commented the 2 important lines.

Reply.prototype.send = function(payload) {
  if (this.sent) {
    this.res.log.warn({ err: new Error('Reply already sent') }, 'Reply already sent');
    return;
  }
  
  // ⚠️ If in this method is thrown an error, this flag remains true ⚠️
  this.sent = true;

  if (payload instanceof Error || this._isError === true) {
    handleError(this, payload, onSendHook);
    return;
  }

  if (payload === undefined) {
    onSendHook(this, payload);
    return;
  }

  var contentType = getHeader(this, 'content-type');
  var hasContentType = contentType !== undefined;

  if (payload !== null) {
    if (Buffer.isBuffer(payload) || typeof payload.pipe === 'function') {
      if (hasContentType === false) {
        this._headers['content-type'] = CONTENT_TYPE.OCTET;
      }
      onSendHook(this, payload);
      return;
    }

    if (hasContentType === false && typeof payload === 'string') {
      this._headers['content-type'] = CONTENT_TYPE.PLAIN;
      onSendHook(this, payload);
      return;
    }
  }

  if (this._serializer) {
    payload = this._serializer(payload);
  } else if (hasContentType === false || contentType.indexOf('application/json') > -1) {
    if (hasContentType === false || contentType.indexOf('charset') === -1) {
      this._headers['content-type'] = CONTENT_TYPE.JSON;
    }
    // ⚠️ Throw error on validation error ⚠️
    payload = serialize(this.context, payload, this.res.statusCode);
    flatstr(payload);
  }

  onSendHook(this, payload);
};

When serialize(this.context, payload, this.res.statusCode); throws an error, onSendHook is never called, but this.sent remains true. This is not really true, because to the client is not sent any response. In my route I can catch the error and would treat it as server error:

try {
  reply.send(invalidResponse)
} catch (error) {
  // We tried to return an invalid response. Handle it as a server error
  reply.code(500).send('Internal Server Error'); // throw reply already sent
}

But it throws reply already sent, due to the sent flag set to true. In this case, to the client isn't sent anything and the connection still opened until timeout.

To Reproduce

Steps to reproduce the behavior:

Just create a route and return an invalid response.

const options = {
  schema: {
    response: {
      200: {
        type: 'object',
        required: ['_id', 'value'],
        properties: {
          _id: {
            type: 'string',
          },
          value: {
            type: 'string',
          },
        },
      },
    },
  },
};

fastify.get('/', options, (request, reply) => {
  try {
    // Throw an error due to prop name typo
    reply.send({ _id: 'someid', valu: 'wrong_prop_name' });
  } catch (error) {
    // throw reply already sent
    reply.code(500).send('Internal Server Error');
  }
});

The app hangs up with "reply already sent" and nothing is sent to the client

Expected behavior

I expect that:

  • Fastify returns a server error automatically, on validation error. OR
  • Sent isn't set to true until the response I really sent

My Environment

  • node version: 10
  • fastify version: 1.13.1
  • os: Mac, Linux. Windows not tested

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugConfirmed bug

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions