-/* eslint-disable security/detect-object-injection */
'use strict';
/**
* Just a self-contained router and some request glue.
*/
+/**
+ * @typedef {import('node:http')} http
+ */
+
require('./patches');
-const { promises: fsPromises } = require('fs');
-const path = require('path');
-const querystring = require('querystring');
+const { promises: fsPromises } = require('node:fs');
+const path = require('node:path');
+const querystring = require('node:querystring');
const common = require('./common');
const ContentNegotiation = require('./content-negotiation');
const Enum = require('./enum');
-const { DingusError, ResponseError } = require('./errors');
+const { ResponseError, RouterNoPathError, RouterNoMethodError } = require('./errors');
const { extensionToMime } = require('./mime-helper');
const Router = require('./router');
const Template = require('./template');
// For logging.
-const _fileScope = common.fileScope(__filename);
+const { fileScope } = require('@squeep/log-helper');
+const _fileScope = fileScope(__filename);
const defaultOptions = {
- ignoreTrailingSlash: false,
+ ignoreTrailingSlash: true,
proxyPrefix: '',
strictAccept: true,
selfBaseUrl: '',
staticMetadata: true,
- staticPath: undefined, // no reasonable default
+ staticPath: undefined, // No reasonable default
trustProxy: true,
+ intrinsicHeadMethod: true,
+ intrinsicHeadPersistBody: false,
querystring,
};
+const cookieSplitRE = /\s*;\s*/;
+const headerParseRE = /^(?<name>[^:]+): +(?<value>.*)$/;
+const lineBreakRE = /\r\n|\n|\r/;
+const proxySplitRE = /\s*,\s*/u;
+
class Dingus {
/**
- * @param {Object} logger object which implements logging methods
- * @param {Object} options
- * @param {Boolean} options.ignoreTrailingSlash
+ * @param {object} logger console-like object which implements logging methods
+ * @param {object} options options
+ * @param {boolean} options.ignoreTrailingSlash requests for '/foo/' will match a '/foo' route
* @param {string} options.proxyPrefix leading part of url path to strip
- * @param {Boolean} options.strictAccept whether to error on unsupported Accept type
+ * @param {boolean} options.strictAccept whether to error on unsupported Accept type
* @param {string} options.selfBaseUrl for constructing links
- * @param {Boolean} options.staticMetadata serve static headers with static files
- * @param {Boolean} options.trustProxy trust some header data to be provided by proxy
- * @param {Object} options.querystring alternate qs parser to use
+ * @param {boolean} options.staticMetadata serve static headers with static files
+ * @param {boolean} options.trustProxy trust some header data to be provided by proxy
+ * @param {boolean} options.intrinsicHeadMethod handle HEAD requests automatically if not specified as a route method
+ * @param {boolean} options.intrinsicHeadPersistBody include un-sent body on ctx for automatic HEAD requests
+ * @param {object} options.querystring alternate qs parser to use
*/
- constructor(logger = common.nullLogger, options = {}) {
+ constructor(logger = console, options = {}) {
common.setOptions(this, defaultOptions, options);
this.router = new Router(options);
];
this.logger = logger;
- common.ensureLoggerLevels(this.logger);
}
/**
* Resolve relative and empty paths in url
* @param {string} p path
+ * @returns {string} normalized path
*/
_normalizePath(p) {
const pathNorm = path.normalize(p); // This isn't perfectly correct, but it's easy...
* Remove a leading portion of url path
* N.B. This method gets obliterated if there is no prefix defined at construction
* @param {string} p path
+ * @returns {string} de-prefixed path
*/
_stripPrefix(p) {
if (p.startsWith(this.proxyPrefix)) {
/**
* Returns the path part, and querystring object, from a request url.
- * @param {string} url
+ * @param {string} url url
+ * @returns {object} path string and query params
*/
_splitUrl(url) {
const [ p, qs ] = common.splitFirst(url, '?');
/**
* Insert a new path handler
- * @param {string} method
- * @param {string} urlPath
- * @param {fn} handler
+ * @param {string|string[]} methods methods to match for this path, '*' allowed
+ * @param {string} urlPath request path to match
+ * @param {Function} handler handler to invoke on match
+ * @param {any[]} handlerArgs additional arguments for handler
*/
- on(method, urlPath, handler, ...handlerArgs) {
- this.router.on(method, urlPath, handler, handlerArgs);
+ on(methods, urlPath, handler, ...handlerArgs) {
+ this.router.on(methods, urlPath, handler, handlerArgs);
}
/**
* Common header tagging for all requests.
* Add our own identifier, and persist any external transit identifiers.
- * @param {http.ClientRequest} req
- * @param {http.ServerResponse} res
- * @param {object} ctx
+ * 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 request
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
+ * @returns {string} id added request
*/
static tagContext(req, res, ctx) {
const requestId = common.requestId();
/**
*
- * @param {http.ClientRequest} req
+ * @param {http.ClientRequest} req request
+ * @returns {string} first forwarded header ip
*/
_getAddress(req) {
// TODO: RFC7239 Forwarded support
- const address = (this.trustProxy && req && req.getHeader(Enum.Header.XForwardedFor)) ||
- (this.trustProxy && req && req.getHeader(Enum.Header.XRealIP)) ||
- (req && req.connection && req.connection.remoteAddress) ||
+ const address = (this.trustProxy && req?.getHeader(Enum.Header.XForwardedFor)) ||
+ (this.trustProxy && req?.getHeader(Enum.Header.XRealIP)) ||
+ (req?.connection?.remoteAddress) ||
'';
- return address.split(/\s*,\s*/u)[0];
+ return address.split(proxySplitRE)[0];
}
/**
*
- * @param {http.ClientRequest} req
+ * @param {http.ClientRequest} req request
+ * @returns {string} first forwarded header protocol
*/
_getProtocol(req) {
// TODO: RFC7239 Forwarded support
- const protocol = (this.trustProxy && req && req.getHeader(Enum.Header.XForwardedProto)) ||
- ((req && req.connection && req.connection.encrypted) ? 'https' : 'http');
- return protocol.split(/\s*,\s*/u)[0];
+ const protocol = (this.trustProxy && req?.getHeader(Enum.Header.XForwardedProto)) ||
+ ((req?.connection?.encrypted) ? 'https' : 'http');
+ return protocol.split(proxySplitRE)[0];
}
/**
- *
- * @param {http.ClientRequest} req
- * @param {http.ServerResponse} res
- * @param {object} ctx
+ * Sets ctx.clientAddress and ctx.clientProtocol.
+ * @param {http.ClientRequest} req request
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
*/
clientAddressContext(req, res, ctx) {
ctx.clientAddress = this._getAddress(req);
}
+ /**
+ * Sets ctx.cookie from Cookie header.
+ * @param {http.ClientRequest} req request
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
+ */
+ static ingestCookie(req, res, ctx) {
+ ctx.cookie = {};
+ req.getHeader(Enum.Header.Cookie)?.split(cookieSplitRE).forEach((cookie) => {
+ const [ name, value ] = common.splitFirst(cookie, '=', null).map((x) => {
+ try {
+ return decodeURIComponent(x.trim());
+ } catch (e) { // eslint-disable-line no-unused-vars
+ return x;
+ }
+ });
+ if (name && !(name in ctx.cookie)) {
+ const isQuoted = value?.startsWith('"') && value.endsWith('"');
+ ctx.cookie[name] = isQuoted ? value.slice(1, -1) : value; // eslint-disable-line security/detect-object-injection
+ }
+ });
+ }
+
+
/**
* Called before every request handler.
- * @param {http.ClientRequest} req
- * @param {http.ServerResponse} res
- * @param {object} ctx
+ * Sets tracking identifiers and client information on ctx.
+ * @param {http.ClientRequest} req request
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
*/
async preHandler(req, res, ctx) {
- Dingus.tagContext(req, res, ctx);
+ this.constructor.tagContext(req, res, ctx);
this.clientAddressContext(req, res, ctx);
+ this.constructor.ingestCookie(req, res, ctx);
}
/**
* Helper for collecting chunks as array of buffers.
- * @param {Buffer[]} chunks
- * @param {string|Buffer} chunk
- * @param {string} encoding
+ * @param {Buffer[]} chunks array to collect chunks in
+ * @param {string|Buffer} chunk chunk to collect
+ * @param {string} encoding chunk encoding
*/
static pushBufChunk(chunks, chunk, encoding = 'utf8') {
if (chunk) {
/**
* Sets ctx.responseBody and calls handler upon res.end().
- * @param {http.ClientRequest} req
- * @param {http.ServerResponse} res
- * @param {object} ctx
+ * @param {http.ClientRequest} req request
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
* @param {*} handler fn(req, res, ctx)
*/
static setEndBodyHandler(req, res, ctx, handler) {
* Intercept writes for head requests, do not send to client,
* but send length, and make body available in context.
* N.B. If persisted, ctx.responseBody will be a raw buffer, be aware when logging.
- * @param {http.ClientRequest} req
- * @param {http.ServerResponse} res
- * @param {object} ctx
- * @param {Boolean} persistResponseBody
+ * @param {http.ClientRequest} req request
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
+ * @param {boolean} persistResponseBody store body buffer on context
*/
static setHeadHandler(req, res, ctx, persistResponseBody = false) {
if (req.method === 'HEAD') {
/**
- * Dispatch the handler for a request
- * @param {http.ClientRequest} req
- * @param {http.ServerResponse} res
- * @param {object} ctx
+ * Resolve the handler to invoke for a request.
+ * @param {http.ClientRequest} req request
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
+ * @returns {object} handler and args
*/
- async dispatch(req, res, ctx = {}) {
- const _scope = _fileScope('dispatch');
+ _determineHandler(req, res, ctx) {
+ const _scope = _fileScope('_determineHandler');
const { pathPart, queryParams } = this._splitUrl(req.url);
ctx.queryParams = queryParams;
try {
({ handler, handlerArgs } = this.router.lookup(req.method, pathPart, ctx));
} catch (e) {
- if (e instanceof DingusError) {
- switch (e.message) {
- case 'NoPath':
- handler = this.handlerNotFound.bind(this);
- break;
- case 'NoMethod':
- handler = this.handlerMethodNotAllowed.bind(this);
- break;
- default:
- this.logger.error(_scope, 'unknown dingus error', { error: e });
- handler = this.handlerInternalServerError.bind(this);
- }
- } else if (e instanceof URIError) {
+ if (e instanceof URIError) {
handler = this.handlerBadRequest.bind(this);
+ } else if (e instanceof RouterNoPathError) {
+ handler = this.handlerNotFound.bind(this);
+ } else if (e instanceof RouterNoMethodError) {
+ if (this.intrinsicHeadMethod && req.method === 'HEAD') {
+ ({ handler, handlerArgs } = this._determineHeadHandler(req, res, ctx, pathPart));
+ } else {
+ handler = this.handlerMethodNotAllowed.bind(this);
+ }
} else {
- this.logger.error(_scope, 'lookup failure', { error: e });
+ this.logger.error(_scope, 'unexpected error', { error: e });
handler = this.handlerInternalServerError.bind(this);
}
}
+ return { handler, handlerArgs };
+ }
+
+ /**
+ * For intrinsic HEAD requests, resolve the handler to invoke.
+ * @param {http.ClientRequest} req request
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
+ * @param {string} pathPart router path
+ * @returns {object} hander and handler args
+ */
+ _determineHeadHandler(req, res, ctx, pathPart) {
+ const _scope = _fileScope('_determineHeadHandler');
+ let handler, handlerArgs = [];
+ try {
+ ({ handler, handlerArgs } = this.router.lookup('GET', pathPart, ctx));
+ Dingus.setHeadHandler(req, res, ctx, this.intrinsicHeadPersistBody);
+ } catch (e) {
+ if (e instanceof RouterNoMethodError) {
+ handler = this.handlerMethodNotAllowed.bind(this);
+ } else {
+ this.logger.error(_scope, 'unexpected error', { error: e });
+ handler = this.handlerInternalServerError.bind(this);
+ }
+ }
+ return { handler, handlerArgs };
+ }
+
+
+ /**
+ * Dispatch the handler for a request
+ * @param {http.ClientRequest} req request
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
+ * @returns {Promise<void>} handler promise
+ */
+ async dispatch(req, res, ctx = {}) {
+ const { handler, handlerArgs } = this._determineHandler(req, res, ctx);
try {
await this.preHandler(req, res, ctx);
return await handler(req, res, ctx, ...handlerArgs);
/**
* Return normalized type, without any parameters.
- * @param {http.ClientRequest} req
- * @returns {string}
+ * @param {http.ClientRequest} req request
+ * @returns {string} bare content type
*/
static getRequestContentType(req) {
const contentType = req.getHeader(Enum.Header.ContentType);
/**
* Parse rawBody as contentType into ctx.parsedBody.
- * @param {string} contentType
- * @param {object} ctx
- * @param {string|buffer}
- */
+ * @param {string} contentType type
+ * @param {object} ctx context
+ * @param {string|Buffer} rawBody body data
+ */
parseBody(contentType, ctx, rawBody) {
const _scope = _fileScope('parseBody');
- if (!rawBody) {
- // 1.2.4 and earlier expected rawBody on context
- rawBody = ctx.rawBody;
- }
-
switch (contentType) {
case Enum.ContentType.ApplicationForm:
ctx.parsedBody = this.querystring.parse(rawBody);
/**
* Return all body data from a request.
- * @param {http.ClientRequest} req
- * @param {Number=} maximumBodySize
- * @param {Boolean=} toString
+ * @param {http.ClientRequest} req request
+ * @param {number=} maximumBodySize bytes
+ * @param {boolean=} toString whether to convert body data buffer to string
+ * @returns {Promise<Buffer|string>} body data
*/
async bodyData(req, maximumBodySize, toString = true) {
const _scope = _fileScope('bodyData');
/**
* Read and parse request body data.
- * @param {http.ClientRequest} req
- * @param {http.ServerResponse} res
- * @param {object} ctx
- * @param {object}
- * @param {Boolean} .parseEmptyBody
- * @param {Boolean} .persistRawBody
+ * Sets ctx.parsedBody, and optionally ctx.rawBody.
+ * @param {http.ClientRequest} req request
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
+ * @param {object} options options
+ * @param {boolean} options.parseEmptyBody attempt to parse an empty body, results may vary by encoding
+ * @param {boolean} options.persistRawBody store raw body buffer on context
+ * @param {number=} options.maximumBodySize bytes
*/
async ingestBody(req, res, ctx, { parseEmptyBody = true, persistRawBody = false, maximumBodySize } = {}) {
const rawBody = await this.bodyData(req, maximumBodySize);
/**
* Return the best matching response type.
- * @param {string[]} responseTypes
- * @param {http.ClientRequest} req
+ * @param {string[]} responseTypes available content types
+ * @param {http.ClientRequest} req request
+ * @returns {string} preferred content type
*/
static getResponseContentType(responseTypes, req) {
const acceptHeader = req.getHeader(Enum.Header.Accept);
/**
* Returns a list of the most-preferred content encodings for the response.
- * @param {string[]} responseEncodings
- * @param {http.ClientRequest} req
+ * @param {string[]} responseEncodings available encodings
+ * @param {http.ClientRequest} req request
+ * @returns {string} preferred encoding
*/
static getResponseEncoding(responseEncodings, req) {
const acceptEncodingHeader = req.getHeader(Enum.Header.AcceptEncoding);
/**
* 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
- * @param {object} ctx
+ * @param {http.ClientRequest} req request
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
*/
setResponseType(responseTypes, req, res, ctx) {
const _scope = _fileScope('setResponseType');
/**
- * Inserts an encoding
- * @param {http.ServerResponse} res
- * @param {string} encoding
+ * Inserts an encoding into Content-Encoding header.
+ * @param {http.ServerResponse} res response
+ * @param {string} encoding encoding
*/
static addEncodingHeader(res, encoding) {
const existingEncodings = res.getHeader(Enum.Header.ContentEncoding);
/**
* Attempt to fetch both data and metadata for a file.
- * @param {string} filePath
+ * @param {string} filePath path to file
+ * @returns {Promise<[object=, object=]>} array with file contents and stats
*/
async _readFileInfo(filePath) {
const _scope = _fileScope('_readFileInfo');
/**
* Potentially add additional headers from static file meta-file.
- * @param {http.ServerResponse} res
- * @param {string} directory
+ * @param {http.ServerResponse} res response
+ * @param {string} directory path to file
* @param {string} fileName - already normalized and filtered
+ * @returns {boolean} if meta headers were added
*/
async _serveFileMetaHeaders(res, directory, fileName) {
const _scope = _fileScope('_serveFileMetaHeaders');
- this.logger.debug(_scope, 'called', { directory, fileName });
+ let added = false;
const metaPrefix = '.';
const metaSuffix = '.meta';
const metaFileName = `${metaPrefix}${fileName}${metaSuffix}`;
const [stat, data] = await this._readFileInfo(metaFilePath);
if (!stat) {
- return;
+ return added;
}
- const lineBreakRE = /\r\n|\n|\r/;
const lines = data.toString().split(lineBreakRE);
common.unfoldHeaderLines(lines);
- const headerParseRE = /^(?<name>[^:]+): +(?<value>.*)$/;
lines.forEach((line) => {
if (line) {
const result = headerParseRE.exec(line);
const { groups: header } = result;
res.setHeader(header.name, header.value);
+ added = true;
}
});
+ return added;
}
/**
* Serve a file from a directory, with rudimentary cache awareness.
* This will also serve pre-encoded variations if available and requested.
- * @param {http.ClientRequest} req
- * @param {http.ServerResponse} res
- * @param {object} ctx
- * @param {string} directory
- * @param {string} fileName
+ * @param {http.ClientRequest} req request
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
+ * @param {string} directory path to file
+ * @param {string} fileName file name
+ * @returns {Promise<void>=} serves file or renders status code
*/
async serveFile(req, res, ctx, directory, fileName) {
const _scope = _fileScope('serveFile');
// We will not deal with any subdirs, nor any dot-files.
// (Note that we could not deal with subdirs even if we wanted, due to simple router matching scheme.)
if (fileName.indexOf(path.sep) >= 0
- || fileName.charAt(0) === '.') {
+ || fileName.startsWith('.')) {
this.logger.debug(_scope, 'rejected filename', { fileName });
return this.handlerNotFound(req, res, ctx);
}
if (encoding === Enum.EncodingType.Identity) {
break;
}
- const suffix = Enum.EncodingTypeSuffix[encoding];
- if (suffix) {
- const encodedFilePath = `${filePath}${suffix}`;
- const [ encodedStat, encodedData ] = await this._readFileInfo(encodedFilePath);
- if (encodedStat) {
- ([ stat, data ] = [ encodedStat, encodedData ]);
- ctx.selectedEncoding = encoding;
- Dingus.addEncodingHeader(res, encoding);
- res.setHeader(Enum.Header.Vary, Enum.Header.AcceptEncoding);
- this.logger.debug(_scope, 'serving encoded version', { ctx, encodedFilePath });
- }
- break;
+ const suffix = Enum.EncodingTypeSuffix[encoding]; // eslint-disable-line security/detect-object-injection
+ if (!suffix) {
+ this.logger.error(_scope, 'supported encoding missing mapped suffix', { ctx, encoding });
+ continue;
}
+ const encodedFilePath = `${filePath}${suffix}`;
+ const [ encodedStat, encodedData ] = await this._readFileInfo(encodedFilePath);
+ if (encodedStat) {
+ ([ stat, data ] = [ encodedStat, encodedData ]);
+ ctx.selectedEncoding = encoding;
+ Dingus.addEncodingHeader(res, encoding);
+ res.setHeader(Enum.Header.Vary, Enum.Header.AcceptEncoding);
+ this.logger.debug(_scope, 'serving encoded version', { ctx, encodedFilePath });
+ }
+ break;
}
const lastModifiedDate = new Date(stat.mtimeMs);
res.setHeader(Enum.Header.CacheControl, 'public');
if (this.staticMetadata) {
- await this._serveFileMetaHeaders(res, directory, fileName);
+ ctx.metaHeaders = await this._serveFileMetaHeaders(res, directory, fileName);
}
this.logger.debug(_scope, 'serving file', { filePath, contentType });
/**
* Return a content-type appropriate rendering of an errorResponse object.
- * @param {string} type content-type of response
+ * @param {string} contentType content-type of response
* @param {object} err either an Error object, or an error response
- * @param {number} err.statusCode
- * @param {string} err.errorMessage
- * @param {string|string[]} err.details
+ * @param {number} err.statusCode htttp status code
+ * @param {string} err.errorMessage http status message
+ * @param {string|string[]} err.details additional details to include
+ * @returns {string} rendered err
*/
// eslint-disable-next-line class-methods-use-this
renderError(contentType, err) {
* Send an error response. Terminal.
* Logs any non-error-response errors as such.
* @param {object} err either an Error object, or an error response
- * @param {http.ClientRequest} req
- * @param {http.ServerResponse} res
- * @param {object} ctx
+ * @param {http.ClientRequest} req request
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
*/
sendErrorResponse(err, req, res, ctx) {
const _scope = _fileScope('sendErrorResponse');
res.setHeader(Enum.Header.ContentType, Enum.ContentType.TextPlain);
}
- if (err && err.statusCode) {
+ if (err?.statusCode) {
res.statusCode = err.statusCode;
body = this.renderError(res.getHeader(Enum.Header.ContentType), err);
this.logger.debug(_scope, 'handler error', { err, req, res, ctx });
/**
- * @param {http.ClientRequest} req
- * @param {http.ServerResponse} res
- * @param {object} ctx
- * @param {String} file - override ctx.params.file
+ * @param {http.ClientRequest} req request
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
+ * @param {string} file - override ctx.params.file
*/
async handlerGetStaticFile(req, res, ctx, file) {
Dingus.setHeadHandler(req, res, ctx);
/**
- * @param {http.ClientRequest} req
- * @param {http.ServerResponse} res
- * @param {Object} ctx
- * @param {String} newPath
- * @param {Number} statusCode
- */
+ * @param {http.ClientRequest} req request
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
+ * @param {string} newPath url to redirect to
+ * @param {number=} statusCode status code to use for redirect, default 307
+ */
async handlerRedirect(req, res, ctx, newPath, statusCode = 307) {
this.setResponseType(this.responseTypes, req, res, ctx);
res.setHeader(Enum.Header.Location, newPath);
/**
- * @param {http.ClientRequest} req
- * @param {http.ServerResponse} res
- * @param {object} ctx
+ * @param {http.ClientRequest} req request
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
*/
async handlerMethodNotAllowed(req, res, ctx) {
this.setResponseType(this.responseTypes, req, res, ctx);
/**
- * @param {http.ClientRequest} req
- * @param {http.ServerResponse} res
- * @param {object} ctx
+ * @param {http.ClientRequest} req request
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
*/
async handlerNotFound(req, res, ctx) {
this.setResponseType(this.responseTypes, req, res, ctx);
/**
- * @param {http.ClientRequest} req
- * @param {http.ServerResponse} res
- * @param {object} ctx
+ * @param {http.ClientRequest} req request
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
*/
async handlerBadRequest(req, res, ctx) {
this.setResponseType(this.responseTypes, req, res, ctx);
/**
- * @param {http.ClientRequest} req
- * @param {http.ServerResponse} res
- * @param {object} ctx
+ * @param {http.ClientRequest} req request
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
*/
async handlerInternalServerError(req, res, ctx) {
this.setResponseType(this.responseTypes, req, res, ctx);