add HTTP status enums, rework ErrorReponse proxy to use them
authorJustin Wind <justin.wind+git@gmail.com>
Thu, 15 Feb 2024 19:04:50 +0000 (11:04 -0800)
committerJustin Wind <justin.wind+git@gmail.com>
Sat, 24 Feb 2024 23:12:54 +0000 (15:12 -0800)
CHANGELOG.md
eslint.config.js
lib/dingus.js
lib/enum.js
test/lib/enum.js

index b172d7c137db65eaa935c05d546c84e2ac71f3d6..2bfc96bce6298d9d20e6129fb373f9deae5d8937 100644 (file)
@@ -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
index 4f8156e3989653f8a373963061435a450db84a41..197cc3e3b041e3c8ed507416d4ba05c711cd22fb 100644 (file)
@@ -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",
+    },
   },
 ];
index 0a5f9d8d50badb5dc0ceb3b4a3c4d876a87cc58e..a5f77ab19fa02f1df8d8fec140ef5347c94eb5f2 100644 (file)
@@ -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
    */
index e24ed6f52a8b85347fbc2e55666fd9d2ae42a13e..8da3ad696ebc8fa0a8b0fa2c0be077caac88c28e 100644 (file)
@@ -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,
 };
index bdd353f358ee1a7b8e9a9c1823f5e0f645a45948..6247ee358fa990714bf83bf7409e0f782941c2dd 100644 (file)
@@ -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