UNPKG

@hapi/hapi

Version:

HTTP Server framework

389 lines (270 loc) 9.57 kB
'use strict'; const Http = require('http'); const Ammo = require('@hapi/ammo'); const Boom = require('@hapi/boom'); const Bounce = require('@hapi/bounce'); const Hoek = require('@hapi/hoek'); const Teamwork = require('@hapi/teamwork'); const Config = require('./config'); const internals = {}; exports.send = async function (request) { const response = request.response; try { if (response.isBoom) { await internals.fail(request, response); return; } await internals.marshal(response); await internals.transmit(response); } catch (err) { Bounce.rethrow(err, 'system'); request._setResponse(err); return internals.fail(request, err); } }; internals.marshal = async function (response) { for (const func of response.request._route._marshalCycle) { await func(response); } }; internals.fail = async function (request, boom) { const response = internals.error(request, boom); request.response = response; // Not using request._setResponse() to avoid double log try { await internals.marshal(response); } catch (err) { Bounce.rethrow(err, 'system'); // Failed to marshal an error - replace with minimal representation of original error const minimal = { statusCode: response.statusCode, error: Http.STATUS_CODES[response.statusCode], message: boom.message }; response._payload = new request._core.Response.Payload(JSON.stringify(minimal), {}); } return internals.transmit(response); }; internals.error = function (request, boom) { const error = boom.output; const response = new request._core.Response(error.payload, request, { error: boom }); response.code(error.statusCode); response.headers = Hoek.clone(error.headers); // Prevent source from being modified return response; }; internals.transmit = function (response) { const request = response.request; const length = internals.length(response); // Pipes const encoding = request._core.compression.encoding(response, length); const ranger = encoding ? null : internals.range(response, length); const compressor = internals.encoding(response, encoding); // Connection: close const isInjection = request.isInjected; if (!(isInjection || request._core.started) || request._isPayloadPending && !request.raw.req._readableState.ended) { response._header('connection', 'close'); } // Write headers internals.writeHead(response); // Injection if (isInjection) { request.raw.res[Config.symbol] = { request }; if (response.variety === 'plain') { request.raw.res[Config.symbol].result = response._isPayloadSupported() ? response.source : null; } } // Finalize response stream const stream = internals.chain([response._payload, response._tap(), compressor, ranger]); return internals.pipe(request, stream); }; internals.length = function (response) { const request = response.request; const header = response.headers['content-length']; if (header === undefined) { return null; } let length = header; if (typeof length === 'string') { length = parseInt(header, 10); if (!isFinite(length)) { delete response.headers['content-length']; return null; } } // Empty response if (length === 0 && !response._statusCode && response.statusCode === 200 && request.route.settings.response.emptyStatusCode !== 200) { response.code(204); delete response.headers['content-length']; } return length; }; internals.range = function (response, length) { const request = response.request; if (!length || !request.route.settings.response.ranges || request.method !== 'get' || response.statusCode !== 200) { return null; } response._header('accept-ranges', 'bytes'); if (!request.headers.range) { return null; } // Check If-Range if (request.headers['if-range'] && request.headers['if-range'] !== response.headers.etag) { // Ignoring last-modified date (weak) return null; } // Parse header const ranges = Ammo.header(request.headers.range, length); if (!ranges) { const error = Boom.rangeNotSatisfiable(); error.output.headers['content-range'] = 'bytes */' + length; throw error; } // Prepare transform if (ranges.length !== 1) { // Ignore requests for multiple ranges return null; } const range = ranges[0]; response.code(206); response.bytes(range.to - range.from + 1); response._header('content-range', 'bytes ' + range.from + '-' + range.to + '/' + length); return new Ammo.Clip(range); }; internals.encoding = function (response, encoding) { const request = response.request; const header = response.headers['content-encoding'] || encoding; if (header && response.headers.etag && response.settings.varyEtag) { response.headers.etag = response.headers.etag.slice(0, -1) + '-' + header + '"'; } if (!encoding || response.statusCode === 206 || !response._isPayloadSupported()) { return null; } delete response.headers['content-length']; response._header('content-encoding', encoding); const compressor = request._core.compression.encoder(request, encoding); if (response.variety === 'stream' && typeof response._payload.setCompressor === 'function') { response._payload.setCompressor(compressor); } return compressor; }; internals.pipe = function (request, stream) { const team = new Teamwork.Team(); // Write payload const env = { stream, request, team }; if (request._closed) { // The request has already been aborted - no need to wait or attempt to write. internals.end(env, 'aborted'); return team.work; } const aborted = internals.end.bind(null, env, 'aborted'); const close = internals.end.bind(null, env, 'close'); const end = internals.end.bind(null, env, null); request.raw.req.on('aborted', aborted); request.raw.res.on('close', close); request.raw.res.on('error', end); request.raw.res.on('finish', end); if (stream.writeToStream) { stream.writeToStream(request.raw.res); } else { stream.on('error', end); stream.on('close', aborted); stream.pipe(request.raw.res); } return team.work; }; internals.end = function (env, event, err) { const { request, stream, team } = env; if (!team) { // Used instead of cleaning up emitter listeners return; } env.team = null; if (request.raw.res.writableEnded) { request.info.responded = Date.now(); team.attend(); return; } if (err) { request.raw.res.destroy(); request._core.Response.drain(stream); } // Update reported response to reflect the error condition const origResponse = request.response; const error = err ? Boom.boomify(err) : new Boom.Boom(`Request ${event}`, { statusCode: request.route.settings.response.disconnectStatusCode, data: origResponse }); request._setResponse(error); // Make inject throw a disconnect error if (request.raw.res[Config.symbol]) { request.raw.res[Config.symbol].error = event ? error : new Boom.Boom(`Response error`, { statusCode: request.route.settings.response.disconnectStatusCode, data: origResponse }); } if (event) { request._log(['response', 'error', event]); } else { request._log(['response', 'error'], err); } request.raw.res.end(); // Triggers injection promise resolve team.attend(); }; internals.writeHead = function (response) { const res = response.request.raw.res; const headers = Object.keys(response.headers); let i = 0; try { for (; i < headers.length; ++i) { const header = headers[i]; const value = response.headers[header]; if (value !== undefined) { res.setHeader(header, value); } } } catch (err) { for (--i; i >= 0; --i) { res.removeHeader(headers[i]); // Undo headers } throw Boom.boomify(err); } if (response.settings.message) { res.statusMessage = response.settings.message; } try { res.writeHead(response.statusCode); } catch (err) { throw Boom.boomify(err); } }; internals.chain = function (sources) { let from = sources[0]; for (let i = 1; i < sources.length; ++i) { const to = sources[i]; if (to) { from.on('close', internals.destroyPipe.bind(from, to)); from.on('error', internals.errorPipe.bind(from, to)); from = from.pipe(to); } } return from; }; internals.destroyPipe = function (to) { if (!this.readableEnded && !this.errored) { to.destroy(); } }; internals.errorPipe = function (to, err) { to.emit('error', err); };