Skip to content

Support custom or additional headers on error response (for CORS etc) #312

Description

@jetibest

Context
When you have a resource that can be fetched from another domain you must set Access-Control-* headers on the response. Normally send() passes through the original response object and it's all good. But when the resource cannot be found, say it's a 404 error, then all headers are cleared in the error handler, and only some basic headers are set.

Problem
This means that the javascript fetch() call from another domain will return a CORS error, and will never find a 404 status code.

Workaround
I wrote some workaround to fix this issue in my webapp, I was surprised to find there was no event or option to allow passing through certain headers or updating headers before the error response is sent.

For reference, this is how I fixed it, not sure if there are any other headers that may be worth passing through, or if it's only the CORS and any custom headers (like X-Powered-By or whatever).

import statuses from 'statuses';
import escapeHtml from 'escape-html';

const response_stream = send(...);

const createHtmlDocument = function(title, body)
{
    return '<!DOCTYPE html>\n' +
        '<html lang="en">\n' +
        '<head>\n' +
        '<meta charset="utf-8">\n' +
        '<title>' + title + '</title>\n' +
        '</head>\n' +
        '<body>\n' +
        '<pre>' + body + '</pre>\n' +
        '</body>\n' +
        '</html>\n';
};

response_stream.on('error', (err) =>
{
    const msg = statuses.message[err.status] || String(err.status);
    const doc = createHtmlDocument('Error', escapeHtml(msg));
    const res = response_stream.res;

    // set status code
    res.statusCode = err.status;

    // clear all headers unless it's preserved (for CORS purposes etc)
    const headers = res.getHeaders();
    for(const k in headers)
    {
        const name = k.toLowerCase();
        const value = headers[k];

        if(name === 'vary') continue;
        if(name.startsWith('x-')) continue;
        if(name.startsWith('access-control-')) continue;

        res.removeHeader(k);
    }

    // add error headers
    if(err && err.headers)
    {
        for(const k in err.headers)
        {
            res.setHeader(k, err.headers[k]);
        }
    }

    // set error headers
    res.setHeader('Content-Type', 'text/html; charset=UTF-8');
    res.setHeader('Content-Length', Buffer.byteLength(doc));
    res.setHeader('Content-Security-Policy', "default-src 'none'");
    res.setHeader('X-Content-Type-Options', 'nosniff');
    res.end(doc);
});

response_stream.pipe(response);

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions