From 13945cdabe3e3ebcdb262df13857cb3a74d71a57 Mon Sep 17 00:00:00 2001 From: Justin Wind Date: Thu, 15 Feb 2024 11:04:50 -0800 Subject: [PATCH] add HTTP status enums, rework ErrorReponse proxy to use them --- CHANGELOG.md | 3 + eslint.config.js | 5 +- lib/dingus.js | 10 ++- lib/enum.js | 222 ++++++++++++++++++++++++++++++++++++----------- test/lib/enum.js | 31 +++++-- 5 files changed, 210 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b172d7c..2bfc96b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ Releases and notable changes to this project are documented here. ## [Unreleased] +- added HTTP status and message enums +- updated devDependencies + ## [v2.0.1] - 2023-11-14 - updated depdendencies diff --git a/eslint.config.js b/eslint.config.js index 4f8156e..197cc3e 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,7 +1,7 @@ 'use strict'; const globals = require('globals'); const js = require('@eslint/js'); -const node = require('eslint-plugin-node'); +const node = require('eslint-plugin-n'); const security = require('eslint-plugin-security'); const sonarjs = require('eslint-plugin-sonarjs'); @@ -104,5 +104,8 @@ module.exports = [ ...globals.mocha, }, }, + rules: { + "n/no-unpublished-require": "off", + }, }, ]; diff --git a/lib/dingus.js b/lib/dingus.js index 0a5f9d8..a5f77ab 100644 --- a/lib/dingus.js +++ b/lib/dingus.js @@ -118,6 +118,9 @@ class Dingus { /** * Common header tagging for all requests. * Add our own identifier, and persist any external transit identifiers. + * Sets requestId on ctx to a new uuid. + * If X-Request-Id or X-Correlation-Id exist on incoming headers, sets them + * on outgoing headers and sets on ctx. * @param {http.ClientRequest} req * @param {http.ServerResponse} res * @param {object} ctx @@ -164,7 +167,7 @@ class Dingus { /** - * + * Sets ctx.clientAddress and ctx.clientProtocol. * @param {http.ClientRequest} req * @param {http.ServerResponse} res * @param {object} ctx @@ -177,6 +180,7 @@ class Dingus { /** * Called before every request handler. + * Sets tracking identifiers and client information on ctx. * @param {http.ClientRequest} req * @param {http.ServerResponse} res * @param {object} ctx @@ -411,6 +415,7 @@ class Dingus { /** * Read and parse request body data. + * Sets ctx.parsedBody, and optionally ctx.rawBody. * @param {http.ClientRequest} req * @param {http.ServerResponse} res * @param {object} ctx @@ -454,6 +459,7 @@ class Dingus { /** * Set the best content type for the response. + * Sets ctx.responseType, and Content-Type header. * @param {string[]} responseTypes default first * @param {http.ClientRequest} req * @param {http.ServerResponse} res @@ -475,7 +481,7 @@ class Dingus { /** - * Inserts an encoding + * Inserts an encoding into Content-Encoding header. * @param {http.ServerResponse} res * @param {string} encoding */ diff --git a/lib/enum.js b/lib/enum.js index e24ed6f..8da3ad6 100644 --- a/lib/enum.js +++ b/lib/enum.js @@ -1,6 +1,5 @@ 'use strict'; - const ContentType = { TextHTML: 'text/html', TextPlain: 'text/plain', @@ -23,59 +22,176 @@ const EncodingTypeSuffix = { [EncodingType.XGzip]: '.gz', }; -const ErrorResponse = { - BadRequest: { - statusCode: 400, - errorMessage: 'Bad Request', - }, - Unauthorized: { - statusCode: 401, - errorMessage: 'Unauthorized', - }, - Forbidden: { - statusCode: 403, - errorMessage: 'Forbidden', - }, - ReallyForbidden: { - statusCode: 403, - errorMessage: 'F̦̩̫̼͔̫͓̃ͤ̈̆̀͑o̖̟͙̫̯̗̳̽ͦ̆́ͨr̩͉̰̗͉b̬̂͘į̟̬̓d͂͗҉̟͈̜͙ͅd͎̜̺̝͇͑̒̋̾ë̴̳̺͓̦̘́ͮ̈́ǹ͈̦̫̙', - }, - NotFound: { - statusCode: 404, - errorMessage: 'Not Found', - }, - MethodNotAllowed: { - statusCode: 405, - errorMessage: 'Method Not Allowed', - }, - NotAcceptable: { - statusCode: 406, - errorMessage: 'Not Acceptable', - }, - Gone: { - statusCode: 410, - errorMessage: 'Gone', - }, - RequestEntityTooLarge: { - statusCode: 413, - errorMessage: 'Request Entity Too Large', - }, - UnsupportedMediaType: { - statusCode: 415, - errorMessage: 'Unsupported Media Type', - }, - InternalServerError: { - statusCode: 500, - errorMessage: 'Internal Server Error', - }, +const HTTPStatusCode = { + + Continue: 100, + SwitchingProtocols: 101, + Processing: 102, + EarlyHints: 103, + + OK: 200, + Created: 201, + Accepted: 202, + NonAuthoritativeInformation: 203, + NoContent: 204, + ResetContent: 205, + PartialContent: 206, + MultiStatus: 207, + AlreadyReported: 208, + IMUsed: 226, + + MultipleChoices: 300, + MovedPermanently: 301, + Found: 302, + SeeOther: 303, + NotModified: 304, + UseProxy: 305, // Deprecated, do not use + SwitchProxy: 306, // No longer used + TemporaryRedirect: 307, + PermanentRedirect: 308, + + BadRequest: 400, + Unauthorized: 401, + PaymentRequired: 402, + Forbidden: 403, + NotFound: 404, + MethodNotAllowed: 405, + NotAcceptable: 406, + ProxyAuthenticationRequired: 407, + RequestTimeout: 408, + Conflict: 409, + Gone: 410, + LengthRequired: 411, + PreconditionFailed: 412, + ContentTooLarge: 413, + RequestEntityTooLarge: 413, // Alternate name + URITooLong: 414, + UnsupportedMediaType: 415, + RangeNotSatisfiable: 416, + ExpectationFailed: 417, + ImATeapot: 418, + MisdirectedRequest: 421, + UnprocessableContent: 422, + Locked: 423, + FailedDependency: 424, + TooEarly: 425, + UpgradeRequired: 426, + PreconditionRequired: 428, + TooManyRequests: 429, + RequestHeaderFieldsTooLarge: 431, + UnavailableForLegalReasons: 451, + + InternalServerError: 500, + NotImplemented: 501, + BadGateway: 502, + ServiceUnavailable: 503, + GatewayTimeout: 504, + HTTPVersionNotSupported: 505, + VariantAlsoNegotiates: 506, + InsufficientStorage: 507, + LoopDetected: 508, + NotExtended: 510, + NetworkAuthenticationRequired: 511, + +}; + +const HTTPStatusMessage = { + + 100: 'Continue', + 101: 'Switching Protocols', + 102: 'Processing', + 103: 'Early Hints', + + 200: 'OK', + 201: 'Created', + 202: 'Accepted', + 203: 'Non-Authoritative Information', + 204: 'No Content', + 205: 'Reset Content', + 206: 'Partial Content', + 207: 'Multi-Status', + 208: 'Already Reported', + 226: 'IM USed', + + 300: 'Multiple Choices', + 301: 'Moved Permanently', + 302: 'Found', + 303: 'See Other', + 304: 'Not Modified', + 305: 'Use Proxy', + 306: 'Switch Proxy', + 307: 'Temporary Redirect', + 308: 'Permanent Redirect', + + 400: 'Bad Request', + 401: 'Unauthorized', + 402: 'Payment Required', + 403: 'Forbidden', + '403.zalgo': 'F̦̩̫̼͔̫͓̃ͤ̈̆̀͑o̖̟͙̫̯̗̳̽ͦ̆́ͨr̩͉̰̗͉b̬̂͘į̟̬̓d͂͗҉̟͈̜͙ͅd͎̜̺̝͇͑̒̋̾ë̴̳̺͓̦̘́ͮ̈́ǹ͈̦̫̙', + 404: 'Not Found', + 405: 'Method Not Allowed', + 406: 'Not Acceptable', + 407: 'Proxy Authentication Required', + 408: 'Request Timeout', + 409: 'Conflict', + 410: 'Gone', + 411: 'Length Required', + 412: 'Precondition Failed', + 413: 'Content Too Large', + 414: 'URI Too Long', + 415: 'Unsupported Media Type', + 416: 'Range Not Satisfiable', + 417: 'Expectation Failed', + 418: 'I\'m A Teapot', + 421: 'Misdirected Request', + 422: 'Unprocessable Content', + 423: 'Locked', + 424: 'Failed Dependency', + 425: 'Too Early', + 426: 'Upgrade Required', + 428: 'Precondition Required', + 429: 'Too Many Requests', + 431: 'Request Header Fields Too Large', + 451: 'Unavailable For Legal Reasons', + + 500: 'Internal Server Error', + 501: 'Not Implemented', + 502: 'Bad Gateway', + 503: 'Service Unavailable', + 504: 'Gateway Timeout', + 505: 'HTTP Version Not Supported', + 506: 'Variant Also Negotiates', + 507: 'Insufficient Storage', + 508: 'Loop Detected', + 510: 'Not Extended', + 511: 'Network Authentication Required', + }; -const ErrorResponseDefaultProxy = new Proxy(ErrorResponse, { +function errorResponseGenerator () { + return Object.fromEntries( + Object.entries(HTTPStatusCode).map(([name, statusCode]) => { + const errorMessage = HTTPStatusMessage[statusCode]; // eslint-disable-line security/detect-object-injection + // istanbul ignore next + if (!errorMessage) { + throw new Error(`missing HTTPStatusMessage for ${statusCode} from HTTPStatusCode ${name}`); + } + return [name, { statusCode, errorMessage }]; + })); +} + +const ErrorResponse = errorResponseGenerator(); + +const ErrorResponseProxy = new Proxy(ErrorResponse, { get: (target, property) => { - // eslint-disable-next-line security/detect-object-injection - return (property in target) ? target[property] : { - errorMessage: `undefined error response '${property}'`, - }; + if (property in target) { + return target[property]; // eslint-disable-line security/detect-object-injection + } else { + const statusCode = HTTPStatusCode.InternalServerError; + const errorMessage = HTTPStatusMessage[statusCode]; // eslint-disable-line security/detect-object-injection + const details = [`undefined error response '${property}'`]; + return { statusCode, errorMessage, details }; + } }, }); @@ -104,6 +220,8 @@ module.exports = { ContentType, EncodingType, EncodingTypeSuffix, - ErrorResponse: ErrorResponseDefaultProxy, + ErrorResponse: ErrorResponseProxy, Header, + HTTPStatusCode, + HTTPStatusMessage, }; diff --git a/test/lib/enum.js b/test/lib/enum.js index bdd353f..6247ee3 100644 --- a/test/lib/enum.js +++ b/test/lib/enum.js @@ -4,22 +4,41 @@ const assert = require('assert'); const Enum = require('../../lib/enum'); +const { mergeEnum } = require('../../lib/common'); - -describe('enum', function () { +describe('Enum', function () { describe('ErrorResponse', function () { it('covers broken', function () { const result = Enum.ErrorResponse.notPresent; const expected = { - errorMessage: 'undefined error response \'notPresent\'', + statusCode: 500, + errorMessage: 'Internal Server Error', + details: ['undefined error response \'notPresent\''], }; assert.deepStrictEqual(result, expected); }); it('covers success', function () { - const { statusCode } = Enum.ErrorResponse.ReallyForbidden; - const expected = 403; - assert.deepStrictEqual(statusCode, expected); + const result = Enum.ErrorResponse.Forbidden; + const expected = { + statusCode: 403, + errorMessage: 'Forbidden', + }; + assert.deepStrictEqual(result, expected); + }); + it('can be merged', function () { + const NewResponse = { statusCode: 444, errorMessage: 'stuff' }; + const localErr = mergeEnum(Enum.ErrorResponse, { NewResponse }); + const newResult = localErr.NewResponse; + assert.deepStrictEqual(newResult, NewResponse); + const oldResult = localErr.NotFound; + assert.deepStrictEqual(oldResult, { statusCode: 404, errorMessage: 'Not Found' }); + const missingResult = localErr.Wrong; + assert.deepStrictEqual(missingResult, { + statusCode: 500, + errorMessage: 'Internal Server Error', + details: ['undefined error response \'Wrong\''], + }); }); }); // ErrorResponse -- 2.45.2