294 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
		
		
			
		
	
	
			294 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
|  | /*! | ||
|  |  * finalhandler | ||
|  |  * Copyright(c) 2014-2022 Douglas Christopher Wilson | ||
|  |  * MIT Licensed | ||
|  |  */ | ||
|  | 
 | ||
|  | 'use strict' | ||
|  | 
 | ||
|  | /** | ||
|  |  * Module dependencies. | ||
|  |  * @private | ||
|  |  */ | ||
|  | 
 | ||
|  | var debug = require('debug')('finalhandler') | ||
|  | var encodeUrl = require('encodeurl') | ||
|  | var escapeHtml = require('escape-html') | ||
|  | var onFinished = require('on-finished') | ||
|  | var parseUrl = require('parseurl') | ||
|  | var statuses = require('statuses') | ||
|  | 
 | ||
|  | /** | ||
|  |  * Module variables. | ||
|  |  * @private | ||
|  |  */ | ||
|  | 
 | ||
|  | var isFinished = onFinished.isFinished | ||
|  | 
 | ||
|  | /** | ||
|  |  * Create a minimal HTML document. | ||
|  |  * | ||
|  |  * @param {string} message | ||
|  |  * @private | ||
|  |  */ | ||
|  | 
 | ||
|  | function createHtmlDocument (message) { | ||
|  |   var body = escapeHtml(message) | ||
|  |     .replaceAll('\n', '<br>') | ||
|  |     .replaceAll('  ', '  ') | ||
|  | 
 | ||
|  |   return '<!DOCTYPE html>\n' + | ||
|  |     '<html lang="en">\n' + | ||
|  |     '<head>\n' + | ||
|  |     '<meta charset="utf-8">\n' + | ||
|  |     '<title>Error</title>\n' + | ||
|  |     '</head>\n' + | ||
|  |     '<body>\n' + | ||
|  |     '<pre>' + body + '</pre>\n' + | ||
|  |     '</body>\n' + | ||
|  |     '</html>\n' | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Module exports. | ||
|  |  * @public | ||
|  |  */ | ||
|  | 
 | ||
|  | module.exports = finalhandler | ||
|  | 
 | ||
|  | /** | ||
|  |  * Create a function to handle the final response. | ||
|  |  * | ||
|  |  * @param {Request} req | ||
|  |  * @param {Response} res | ||
|  |  * @param {Object} [options] | ||
|  |  * @return {Function} | ||
|  |  * @public | ||
|  |  */ | ||
|  | 
 | ||
|  | function finalhandler (req, res, options) { | ||
|  |   var opts = options || {} | ||
|  | 
 | ||
|  |   // get environment
 | ||
|  |   var env = opts.env || process.env.NODE_ENV || 'development' | ||
|  | 
 | ||
|  |   // get error callback
 | ||
|  |   var onerror = opts.onerror | ||
|  | 
 | ||
|  |   return function (err) { | ||
|  |     var headers | ||
|  |     var msg | ||
|  |     var status | ||
|  | 
 | ||
|  |     // ignore 404 on in-flight response
 | ||
|  |     if (!err && res.headersSent) { | ||
|  |       debug('cannot 404 after headers sent') | ||
|  |       return | ||
|  |     } | ||
|  | 
 | ||
|  |     // unhandled error
 | ||
|  |     if (err) { | ||
|  |       // respect status code from error
 | ||
|  |       status = getErrorStatusCode(err) | ||
|  | 
 | ||
|  |       if (status === undefined) { | ||
|  |         // fallback to status code on response
 | ||
|  |         status = getResponseStatusCode(res) | ||
|  |       } else { | ||
|  |         // respect headers from error
 | ||
|  |         headers = getErrorHeaders(err) | ||
|  |       } | ||
|  | 
 | ||
|  |       // get error message
 | ||
|  |       msg = getErrorMessage(err, status, env) | ||
|  |     } else { | ||
|  |       // not found
 | ||
|  |       status = 404 | ||
|  |       msg = 'Cannot ' + req.method + ' ' + encodeUrl(getResourceName(req)) | ||
|  |     } | ||
|  | 
 | ||
|  |     debug('default %s', status) | ||
|  | 
 | ||
|  |     // schedule onerror callback
 | ||
|  |     if (err && onerror) { | ||
|  |       setImmediate(onerror, err, req, res) | ||
|  |     } | ||
|  | 
 | ||
|  |     // cannot actually respond
 | ||
|  |     if (res.headersSent) { | ||
|  |       debug('cannot %d after headers sent', status) | ||
|  |       if (req.socket) { | ||
|  |         req.socket.destroy() | ||
|  |       } | ||
|  |       return | ||
|  |     } | ||
|  | 
 | ||
|  |     // send response
 | ||
|  |     send(req, res, status, headers, msg) | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Get headers from Error object. | ||
|  |  * | ||
|  |  * @param {Error} err | ||
|  |  * @return {object} | ||
|  |  * @private | ||
|  |  */ | ||
|  | 
 | ||
|  | function getErrorHeaders (err) { | ||
|  |   if (!err.headers || typeof err.headers !== 'object') { | ||
|  |     return undefined | ||
|  |   } | ||
|  | 
 | ||
|  |   return { ...err.headers } | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Get message from Error object, fallback to status message. | ||
|  |  * | ||
|  |  * @param {Error} err | ||
|  |  * @param {number} status | ||
|  |  * @param {string} env | ||
|  |  * @return {string} | ||
|  |  * @private | ||
|  |  */ | ||
|  | 
 | ||
|  | function getErrorMessage (err, status, env) { | ||
|  |   var msg | ||
|  | 
 | ||
|  |   if (env !== 'production') { | ||
|  |     // use err.stack, which typically includes err.message
 | ||
|  |     msg = err.stack | ||
|  | 
 | ||
|  |     // fallback to err.toString() when possible
 | ||
|  |     if (!msg && typeof err.toString === 'function') { | ||
|  |       msg = err.toString() | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   return msg || statuses.message[status] | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Get status code from Error object. | ||
|  |  * | ||
|  |  * @param {Error} err | ||
|  |  * @return {number} | ||
|  |  * @private | ||
|  |  */ | ||
|  | 
 | ||
|  | function getErrorStatusCode (err) { | ||
|  |   // check err.status
 | ||
|  |   if (typeof err.status === 'number' && err.status >= 400 && err.status < 600) { | ||
|  |     return err.status | ||
|  |   } | ||
|  | 
 | ||
|  |   // check err.statusCode
 | ||
|  |   if (typeof err.statusCode === 'number' && err.statusCode >= 400 && err.statusCode < 600) { | ||
|  |     return err.statusCode | ||
|  |   } | ||
|  | 
 | ||
|  |   return undefined | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Get resource name for the request. | ||
|  |  * | ||
|  |  * This is typically just the original pathname of the request | ||
|  |  * but will fallback to "resource" is that cannot be determined. | ||
|  |  * | ||
|  |  * @param {IncomingMessage} req | ||
|  |  * @return {string} | ||
|  |  * @private | ||
|  |  */ | ||
|  | 
 | ||
|  | function getResourceName (req) { | ||
|  |   try { | ||
|  |     return parseUrl.original(req).pathname | ||
|  |   } catch (e) { | ||
|  |     return 'resource' | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Get status code from response. | ||
|  |  * | ||
|  |  * @param {OutgoingMessage} res | ||
|  |  * @return {number} | ||
|  |  * @private | ||
|  |  */ | ||
|  | 
 | ||
|  | function getResponseStatusCode (res) { | ||
|  |   var status = res.statusCode | ||
|  | 
 | ||
|  |   // default status code to 500 if outside valid range
 | ||
|  |   if (typeof status !== 'number' || status < 400 || status > 599) { | ||
|  |     status = 500 | ||
|  |   } | ||
|  | 
 | ||
|  |   return status | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Send response. | ||
|  |  * | ||
|  |  * @param {IncomingMessage} req | ||
|  |  * @param {OutgoingMessage} res | ||
|  |  * @param {number} status | ||
|  |  * @param {object} headers | ||
|  |  * @param {string} message | ||
|  |  * @private | ||
|  |  */ | ||
|  | 
 | ||
|  | function send (req, res, status, headers, message) { | ||
|  |   function write () { | ||
|  |     // response body
 | ||
|  |     var body = createHtmlDocument(message) | ||
|  | 
 | ||
|  |     // response status
 | ||
|  |     res.statusCode = status | ||
|  | 
 | ||
|  |     if (req.httpVersionMajor < 2) { | ||
|  |       res.statusMessage = statuses.message[status] | ||
|  |     } | ||
|  | 
 | ||
|  |     // remove any content headers
 | ||
|  |     res.removeHeader('Content-Encoding') | ||
|  |     res.removeHeader('Content-Language') | ||
|  |     res.removeHeader('Content-Range') | ||
|  | 
 | ||
|  |     // response headers
 | ||
|  |     for (const [key, value] of Object.entries(headers ?? {})) { | ||
|  |       res.setHeader(key, value) | ||
|  |     } | ||
|  | 
 | ||
|  |     // security headers
 | ||
|  |     res.setHeader('Content-Security-Policy', "default-src 'none'") | ||
|  |     res.setHeader('X-Content-Type-Options', 'nosniff') | ||
|  | 
 | ||
|  |     // standard headers
 | ||
|  |     res.setHeader('Content-Type', 'text/html; charset=utf-8') | ||
|  |     res.setHeader('Content-Length', Buffer.byteLength(body, 'utf8')) | ||
|  | 
 | ||
|  |     if (req.method === 'HEAD') { | ||
|  |       res.end() | ||
|  |       return | ||
|  |     } | ||
|  | 
 | ||
|  |     res.end(body, 'utf8') | ||
|  |   } | ||
|  | 
 | ||
|  |   if (isFinished(req)) { | ||
|  |     write() | ||
|  |     return | ||
|  |   } | ||
|  | 
 | ||
|  |   // unpipe everything from the request
 | ||
|  |   req.unpipe() | ||
|  | 
 | ||
|  |   // flush the request
 | ||
|  |   onFinished(req, write) | ||
|  |   req.resume() | ||
|  | } |