ctx.matchedPath now stores raw route rather than processed array
[squeep-api-dingus] / lib / dingus.js
1 /* eslint-disable security/detect-object-injection */
2 'use strict';
3
4 /**
5 * A very minimal API server framework.
6 * Just a self-contained router and some request glue.
7 */
8
9 require('./patches');
10 const { promises: fsPromises } = require('fs');
11 const path = require('path');
12 const querystring = require('querystring');
13 const common = require('./common');
14 const ContentNegotiation = require('./content-negotiation');
15 const Enum = require('./enum');
16 const { DingusError, ResponseError } = require('./errors');
17 const { extensionToMime } = require('./mime-helper');
18 const Router = require('./router');
19 const Template = require('./template');
20
21 // For logging.
22 const _fileScope = common.fileScope(__filename);
23
24 const defaultOptions = {
25 ignoreTrailingSlash: false,
26 proxyPrefix: '',
27 strictAccept: true,
28 selfBaseUrl: '',
29 staticMetadata: true,
30 staticPath: undefined, // No reasonable default
31 trustProxy: true,
32 querystring,
33 };
34
35 class Dingus {
36 /**
37 * @param {Object} logger object which implements logging methods
38 * @param {Object} options
39 * @param {Boolean} options.ignoreTrailingSlash
40 * @param {string} options.proxyPrefix leading part of url path to strip
41 * @param {Boolean} options.strictAccept whether to error on unsupported Accept type
42 * @param {string} options.selfBaseUrl for constructing links
43 * @param {Boolean} options.staticMetadata serve static headers with static files
44 * @param {Boolean} options.trustProxy trust some header data to be provided by proxy
45 * @param {Object} options.querystring alternate qs parser to use
46 */
47 constructor(logger = common.nullLogger, options = {}) {
48 common.setOptions(this, defaultOptions, options);
49
50 this.router = new Router(options);
51
52 if (!this.proxyPrefix) {
53 this._stripPrefix = (p) => p;
54 }
55
56 this.responseTypes = [
57 Enum.ContentType.TextHTML,
58 Enum.ContentType.TextPlain,
59 Enum.ContentType.ApplicationJson,
60 ];
61
62 this.logger = logger;
63 common.ensureLoggerLevels(this.logger);
64 }
65
66
67 /**
68 * Resolve relative and empty paths in url
69 * @param {string} p path
70 */
71 _normalizePath(p) {
72 const pathNorm = path.normalize(p); // This isn't perfectly correct, but it's easy...
73 return this._stripPrefix(pathNorm);
74 }
75
76
77 /**
78 * Remove a leading portion of url path
79 * N.B. This method gets obliterated if there is no prefix defined at construction
80 * @param {string} p path
81 */
82 _stripPrefix(p) {
83 if (p.startsWith(this.proxyPrefix)) {
84 return p.slice(this.proxyPrefix.length);
85 }
86 return p;
87 }
88
89
90 /**
91 * Returns the path part, and querystring object, from a request url.
92 * @param {string} url
93 */
94 _splitUrl(url) {
95 const [ p, qs ] = common.splitFirst(url, '?');
96 return {
97 pathPart: this._normalizePath(p),
98 queryParams: this.querystring.parse(qs),
99 };
100 }
101
102
103 /**
104 * Insert a new path handler
105 * @param {string} method
106 * @param {string} urlPath
107 * @param {fn} handler
108 */
109 on(method, urlPath, handler, ...handlerArgs) {
110 this.router.on(method, urlPath, handler, handlerArgs);
111 }
112
113
114 /**
115 * Common header tagging for all requests.
116 * Add our own identifier, and persist any external transit identifiers.
117 * @param {http.ClientRequest} req
118 * @param {http.ServerResponse} res
119 * @param {object} ctx
120 */
121 static tagContext(req, res, ctx) {
122 const requestId = common.requestId();
123 ctx.requestId = requestId;
124 res.setHeader(Enum.Header.RequestId, requestId);
125 [Enum.Header.XRequestId, Enum.Header.XCorrelationId].forEach((h) => {
126 const v = req.getHeader(h);
127 if (v) {
128 ctx[h.replace(/-/g, '')] = v;
129 res.setHeader(h, v);
130 }
131 });
132 return requestId;
133 }
134
135
136 /**
137 *
138 * @param {http.ClientRequest} req
139 */
140 _getAddress(req) {
141 // TODO: RFC7239 Forwarded support
142 const address = (this.trustProxy && req && req.getHeader(Enum.Header.XForwardedFor)) ||
143 (this.trustProxy && req && req.getHeader(Enum.Header.XRealIP)) ||
144 (req && req.connection && req.connection.remoteAddress) ||
145 '';
146 return address.split(/\s*,\s*/u)[0];
147 }
148
149
150 /**
151 *
152 * @param {http.ClientRequest} req
153 */
154 _getProtocol(req) {
155 // TODO: RFC7239 Forwarded support
156 const protocol = (this.trustProxy && req && req.getHeader(Enum.Header.XForwardedProto)) ||
157 ((req && req.connection && req.connection.encrypted) ? 'https' : 'http');
158 return protocol.split(/\s*,\s*/u)[0];
159 }
160
161
162 /**
163 *
164 * @param {http.ClientRequest} req
165 * @param {http.ServerResponse} res
166 * @param {object} ctx
167 */
168 clientAddressContext(req, res, ctx) {
169 ctx.clientAddress = this._getAddress(req);
170 ctx.clientProtocol = this._getProtocol(req);
171 }
172
173
174 /**
175 * Called before every request handler.
176 * @param {http.ClientRequest} req
177 * @param {http.ServerResponse} res
178 * @param {object} ctx
179 */
180 async preHandler(req, res, ctx) {
181 Dingus.tagContext(req, res, ctx);
182 this.clientAddressContext(req, res, ctx);
183 }
184
185
186 /**
187 * Helper for collecting chunks as array of buffers.
188 * @param {Buffer[]} chunks
189 * @param {string|Buffer} chunk
190 * @param {string} encoding
191 */
192 static pushBufChunk(chunks, chunk, encoding = 'utf8') {
193 if (chunk) {
194 if (typeof chunk === 'string') {
195 chunk = Buffer.from(chunk, encoding);
196 }
197 chunks.push(chunk);
198 }
199 }
200
201
202 /**
203 * Sets ctx.responseBody and calls handler upon res.end().
204 * @param {http.ClientRequest} req
205 * @param {http.ServerResponse} res
206 * @param {object} ctx
207 * @param {*} handler fn(req, res, ctx)
208 */
209 static setEndBodyHandler(req, res, ctx, handler) {
210 const origWrite = res.write.bind(res);
211 const origEnd = res.end.bind(res);
212 const chunks = [];
213 res.write = function (chunk, encoding, ...rest) {
214 Dingus.pushBufChunk(chunks, chunk, encoding);
215 return origWrite(chunk, encoding, ...rest);
216 };
217 res.end = function (data, encoding, ...rest) {
218 Dingus.pushBufChunk(chunks, data, encoding);
219 ctx.responseBody = Buffer.concat(chunks);
220 handler(req, res, ctx);
221 return origEnd(data, encoding, ...rest);
222 };
223 }
224
225
226 /**
227 * Intercept writes for head requests, do not send to client,
228 * but send length, and make body available in context.
229 * N.B. If persisted, ctx.responseBody will be a raw buffer, be aware when logging.
230 * @param {http.ClientRequest} req
231 * @param {http.ServerResponse} res
232 * @param {object} ctx
233 * @param {Boolean} persistResponseBody
234 */
235 static setHeadHandler(req, res, ctx, persistResponseBody = false) {
236 if (req.method === 'HEAD') {
237 const origEnd = res.end.bind(res);
238 const chunks = [];
239 res.write = function (chunk, encoding) {
240 Dingus.pushBufChunk(chunks, chunk, encoding);
241 // No call to original res.write.
242 };
243 res.end = function (data, encoding, ...rest) {
244 Dingus.pushBufChunk(chunks, data, encoding);
245 const responseBody = Buffer.concat(chunks);
246 res.setHeader(Enum.Header.ContentLength, Buffer.byteLength(responseBody));
247 if (persistResponseBody) {
248 ctx.responseBody = responseBody;
249 }
250 return origEnd(undefined, encoding, ...rest);
251 };
252 }
253 }
254
255
256 /**
257 * Dispatch the handler for a request
258 * @param {http.ClientRequest} req
259 * @param {http.ServerResponse} res
260 * @param {object} ctx
261 */
262 async dispatch(req, res, ctx = {}) {
263 const _scope = _fileScope('dispatch');
264
265 const { pathPart, queryParams } = this._splitUrl(req.url);
266 ctx.queryParams = queryParams;
267
268 let handler, handlerArgs = [];
269 try {
270 ({ handler, handlerArgs } = this.router.lookup(req.method, pathPart, ctx));
271 } catch (e) {
272 if (e instanceof DingusError) {
273 switch (e.message) {
274 case 'NoPath':
275 handler = this.handlerNotFound.bind(this);
276 break;
277 case 'NoMethod':
278 handler = this.handlerMethodNotAllowed.bind(this);
279 break;
280 default:
281 this.logger.error(_scope, 'unknown dingus error', { error: e });
282 handler = this.handlerInternalServerError.bind(this);
283 }
284 } else if (e instanceof URIError) {
285 handler = this.handlerBadRequest.bind(this);
286 } else {
287 this.logger.error(_scope, 'lookup failure', { error: e });
288 handler = this.handlerInternalServerError.bind(this);
289 }
290 }
291
292 try {
293 await this.preHandler(req, res, ctx);
294 return await handler(req, res, ctx, ...handlerArgs);
295 } catch (e) {
296 ctx.error = e;
297 this.sendErrorResponse(e, req, res, ctx);
298 }
299 }
300
301
302 /**
303 * Return normalized type, without any parameters.
304 * @param {http.ClientRequest} req
305 * @returns {string}
306 */
307 static getRequestContentType(req) {
308 const contentType = req.getHeader(Enum.Header.ContentType);
309 return (contentType || '').split(';')[0].trim().toLowerCase();
310 }
311
312
313 /**
314 * Parse rawBody as contentType into ctx.parsedBody.
315 * @param {string} contentType
316 * @param {object} ctx
317 * @param {string|buffer}
318 */
319 parseBody(contentType, ctx, rawBody) {
320 const _scope = _fileScope('parseBody');
321
322 if (!rawBody) {
323 // 1.2.4 and earlier expected rawBody on context
324 rawBody = ctx.rawBody;
325 }
326
327 switch (contentType) {
328 case Enum.ContentType.ApplicationForm:
329 ctx.parsedBody = this.querystring.parse(rawBody);
330 break;
331
332 case Enum.ContentType.ApplicationJson:
333 try {
334 ctx.parsedBody = JSON.parse(rawBody);
335 } catch (e) {
336 this.logger.debug(_scope, 'JSON parse failed', { requestId: ctx.requestId, error: e });
337 throw new ResponseError(Enum.ErrorResponse.BadRequest, e.message);
338 }
339 break;
340
341 default:
342 this.logger.debug(_scope, 'unhandled content-type', { requestId: ctx.requestId, contentType });
343 throw new ResponseError(Enum.ErrorResponse.UnsupportedMediaType);
344 }
345 }
346
347
348 /**
349 * Return all body data from a request.
350 * @param {http.ClientRequest} req
351 * @param {Number=} maximumBodySize
352 * @param {Boolean=} toString
353 */
354 async bodyData(req, maximumBodySize, toString = true) {
355 const _scope = _fileScope('bodyData');
356 return new Promise((resolve, reject) => {
357 const body = [];
358 let length = 0;
359 req.on('data', (chunk) => {
360 body.push(chunk);
361 length += Buffer.byteLength(chunk);
362 if (maximumBodySize && length > maximumBodySize) {
363 this.logger.debug(_scope, 'body data exceeded limit', { length, maximumBodySize });
364 reject(new ResponseError(Enum.ErrorResponse.RequestEntityTooLarge));
365 }
366 });
367 req.on('end', () => {
368 const bodyBuffer = Buffer.concat(body);
369 resolve(toString ? bodyBuffer.toString() : bodyBuffer);
370 });
371 req.on('error', (e) => {
372 this.logger.error(_scope, 'failed', { error: e });
373 reject(e);
374 });
375 });
376 }
377
378
379 /**
380 * Read and parse request body data.
381 * @param {http.ClientRequest} req
382 * @param {http.ServerResponse} res
383 * @param {object} ctx
384 * @param {object}
385 * @param {Boolean} .parseEmptyBody
386 * @param {Boolean} .persistRawBody
387 */
388 async ingestBody(req, res, ctx, { parseEmptyBody = true, persistRawBody = false, maximumBodySize } = {}) {
389 const rawBody = await this.bodyData(req, maximumBodySize);
390 if (persistRawBody) {
391 ctx.rawBody = rawBody;
392 }
393 if (rawBody || parseEmptyBody) {
394 const contentType = Dingus.getRequestContentType(req);
395 this.parseBody(contentType, ctx, rawBody);
396 }
397 }
398
399
400 /**
401 * Return the best matching response type.
402 * @param {string[]} responseTypes
403 * @param {http.ClientRequest} req
404 */
405 static getResponseContentType(responseTypes, req) {
406 const acceptHeader = req.getHeader(Enum.Header.Accept);
407 return ContentNegotiation.accept(responseTypes, acceptHeader);
408 }
409
410
411 /**
412 * Returns a list of the most-preferred content encodings for the response.
413 * @param {string[]} responseEncodings
414 * @param {http.ClientRequest} req
415 */
416 static getResponseEncoding(responseEncodings, req) {
417 const acceptEncodingHeader = req.getHeader(Enum.Header.AcceptEncoding);
418 return ContentNegotiation.preferred(responseEncodings, acceptEncodingHeader);
419 }
420
421
422 /**
423 * Set the best content type for the response.
424 * @param {string[]} responseTypes default first
425 * @param {http.ClientRequest} req
426 * @param {http.ServerResponse} res
427 * @param {object} ctx
428 */
429 setResponseType(responseTypes, req, res, ctx) {
430 const _scope = _fileScope('setResponseType');
431 ctx.responseType = Dingus.getResponseContentType(responseTypes, req);
432 if (!ctx.responseType) {
433 if (this.strictAccept) {
434 this.logger.debug(_scope, 'unhandled strict accept', { requestId: req.requestId });
435 throw new ResponseError(Enum.ErrorResponse.NotAcceptable);
436 } else {
437 ctx.responseType = responseTypes[0];
438 }
439 }
440 res.setHeader(Enum.Header.ContentType, ctx.responseType);
441 }
442
443
444 /**
445 * Inserts an encoding
446 * @param {http.ServerResponse} res
447 * @param {string} encoding
448 */
449 static addEncodingHeader(res, encoding) {
450 const existingEncodings = res.getHeader(Enum.Header.ContentEncoding);
451 if (existingEncodings) {
452 encoding = `${encoding}, ${existingEncodings}`;
453 }
454 res.setHeader(Enum.Header.ContentEncoding, encoding);
455 }
456
457
458 /**
459 * Attempt to fetch both data and metadata for a file.
460 * @param {string} filePath
461 */
462 async _readFileInfo(filePath) {
463 const _scope = _fileScope('_readFileInfo');
464 let result;
465 try {
466 // eslint-disable-next-line security/detect-non-literal-fs-filename
467 const stat = fsPromises.stat(filePath);
468 // eslint-disable-next-line security/detect-non-literal-fs-filename
469 const data = fsPromises.readFile(filePath);
470 result = await Promise.all([stat, data]);
471 } catch (e) {
472 if (['ENOENT', 'EACCES', 'EISDIR', 'ENAMETOOLONG', 'EINVAL'].includes(e.code)) {
473 return [null, null];
474 }
475 this.logger.error(_scope, 'fs error', { error: e, filePath });
476 throw e;
477 }
478 return result;
479 }
480
481
482 /**
483 * Potentially add additional headers from static file meta-file.
484 * @param {http.ServerResponse} res
485 * @param {string} directory
486 * @param {string} fileName - already normalized and filtered
487 */
488 async _serveFileMetaHeaders(res, directory, fileName) {
489 const _scope = _fileScope('_serveFileMetaHeaders');
490 this.logger.debug(_scope, 'called', { directory, fileName });
491
492 const metaPrefix = '.';
493 const metaSuffix = '.meta';
494 const metaFileName = `${metaPrefix}${fileName}${metaSuffix}`;
495 const metaFilePath = path.join(directory, metaFileName);
496
497 const [stat, data] = await this._readFileInfo(metaFilePath);
498 if (!stat) {
499 return;
500 }
501
502 const lineBreakRE = /\r\n|\n|\r/;
503 const lines = data.toString().split(lineBreakRE);
504 common.unfoldHeaderLines(lines);
505
506 const headerParseRE = /^(?<name>[^:]+): +(?<value>.*)$/;
507 lines.forEach((line) => {
508 if (line) {
509 const result = headerParseRE.exec(line);
510 const { groups: header } = result;
511 res.setHeader(header.name, header.value);
512 }
513 });
514 }
515
516
517 /**
518 * Serve a file from a directory, with rudimentary cache awareness.
519 * This will also serve pre-encoded variations if available and requested.
520 * @param {http.ClientRequest} req
521 * @param {http.ServerResponse} res
522 * @param {object} ctx
523 * @param {string} directory
524 * @param {string} fileName
525 */
526 async serveFile(req, res, ctx, directory, fileName) {
527 const _scope = _fileScope('serveFile');
528 this.logger.debug(_scope, 'called', { req, ctx });
529
530 // Require a directory field.
531 if (!directory) {
532 this.logger.debug(_scope, 'rejected unset directory', { fileName });
533 return this.handlerNotFound(req, res, ctx);
534 }
535
536 // Normalize the supplied path, as encoded path-navigation may have been (maliciously) present.
537 fileName = path.normalize(fileName);
538
539 // We will not deal with any subdirs, nor any dot-files.
540 // (Note that we could not deal with subdirs even if we wanted, due to simple router matching scheme.)
541 if (fileName.indexOf(path.sep) >= 0
542 || fileName.charAt(0) === '.') {
543 this.logger.debug(_scope, 'rejected filename', { fileName });
544 return this.handlerNotFound(req, res, ctx);
545 }
546
547 const filePath = path.join(directory, fileName);
548
549 // File must exist, before any alternate static encodings will be considered.
550 let [stat, data] = await this._readFileInfo(filePath);
551 if (!stat) {
552 return this.handlerNotFound(req, res, ctx);
553 }
554
555 // If encodings were requested, check for static versions to serve.
556 // Update stat and data if matching version is found.
557 ctx.availableEncodings = Dingus.getResponseEncoding(Object.values(Enum.EncodingType), req);
558 if (ctx.availableEncodings.length === 0) {
559 // Identity encoding was specifically denied, and nothing else available.
560 this.logger.debug(_scope, 'no suitable encodings', { ctx });
561 return this.handlerMethodNotAllowed(req, res, ctx);
562 }
563 for (const encoding of ctx.availableEncodings) {
564 if (encoding === Enum.EncodingType.Identity) {
565 break;
566 }
567 const suffix = Enum.EncodingTypeSuffix[encoding];
568 if (!suffix) {
569 this.logger.error(_scope, 'supported encoding missing mapped suffix', { ctx, encoding });
570 continue;
571 }
572 const encodedFilePath = `${filePath}${suffix}`;
573 const [ encodedStat, encodedData ] = await this._readFileInfo(encodedFilePath);
574 if (encodedStat) {
575 ([ stat, data ] = [ encodedStat, encodedData ]);
576 ctx.selectedEncoding = encoding;
577 Dingus.addEncodingHeader(res, encoding);
578 res.setHeader(Enum.Header.Vary, Enum.Header.AcceptEncoding);
579 this.logger.debug(_scope, 'serving encoded version', { ctx, encodedFilePath });
580 }
581 break;
582 }
583
584 const lastModifiedDate = new Date(stat.mtimeMs);
585 res.setHeader(Enum.Header.LastModified, lastModifiedDate.toGMTString());
586
587 const eTag = common.generateETag(filePath, stat, data);
588 res.setHeader(Enum.Header.ETag, eTag);
589
590 if (common.isClientCached(req, stat.mtimeMs, eTag)) {
591 this.logger.debug(_scope, 'client cached file', { filePath });
592 res.statusCode = 304; // Not Modified
593 res.end();
594 return;
595 }
596
597 // Set the type based on extension of un-encoded filename.
598 const ext = path.extname(filePath).slice(1); // Drop the dot
599 const contentType = extensionToMime(ext);
600 res.setHeader(Enum.Header.ContentType, contentType);
601
602 // We presume static files are relatively cacheable.
603 res.setHeader(Enum.Header.CacheControl, 'public');
604
605 if (this.staticMetadata) {
606 await this._serveFileMetaHeaders(res, directory, fileName);
607 }
608
609 this.logger.debug(_scope, 'serving file', { filePath, contentType });
610 res.end(data);
611 }
612
613
614 /**
615 * Return a content-type appropriate rendering of an errorResponse object.
616 * @param {string} type content-type of response
617 * @param {object} err either an Error object, or an error response
618 * @param {number} err.statusCode
619 * @param {string} err.errorMessage
620 * @param {string|string[]} err.details
621 */
622 // eslint-disable-next-line class-methods-use-this
623 renderError(contentType, err) {
624 switch (contentType) {
625 case Enum.ContentType.ApplicationJson:
626 return JSON.stringify(err);
627
628 case Enum.ContentType.TextHTML:
629 return Template.errorHTML(err);
630
631 case Enum.ContentType.TextPlain:
632 default:
633 return [err.errorMessage, err.details].join('\r\n');
634 }
635 }
636
637
638 /**
639 * Send an error response. Terminal.
640 * Logs any non-error-response errors as such.
641 * @param {object} err either an Error object, or an error response
642 * @param {http.ClientRequest} req
643 * @param {http.ServerResponse} res
644 * @param {object} ctx
645 */
646 sendErrorResponse(err, req, res, ctx) {
647 const _scope = _fileScope('sendErrorResponse');
648 let body;
649
650 // Default to a content type if one is not yet present
651 if (!res.hasHeader(Enum.Header.ContentType)) {
652 res.setHeader(Enum.Header.ContentType, Enum.ContentType.TextPlain);
653 }
654
655 if (err && err.statusCode) {
656 res.statusCode = err.statusCode;
657 body = this.renderError(res.getHeader(Enum.Header.ContentType), err);
658 this.logger.debug(_scope, 'handler error', { err, req, res, ctx });
659 } else {
660 res.statusCode = 500;
661 body = this.renderError(res.getHeader(Enum.Header.ContentType), Enum.ErrorResponse.InternalServerError);
662 this.logger.error(_scope, 'handler exception', { err, req, res, ctx });
663 }
664 res.end(body);
665 }
666
667
668 /**
669 * @param {http.ClientRequest} req
670 * @param {http.ServerResponse} res
671 * @param {object} ctx
672 * @param {String} file - override ctx.params.file
673 */
674 async handlerGetStaticFile(req, res, ctx, file) {
675 Dingus.setHeadHandler(req, res, ctx);
676
677 // Set a default response type to handle any errors; will be re-set to serve actual static content type.
678 this.setResponseType(this.responseTypes, req, res, ctx);
679
680 await this.serveFile(req, res, ctx, this.staticPath, file || ctx.params.file);
681 }
682
683
684 /**
685 * @param {http.ClientRequest} req
686 * @param {http.ServerResponse} res
687 * @param {Object} ctx
688 * @param {String} newPath
689 * @param {Number} statusCode
690 */
691 async handlerRedirect(req, res, ctx, newPath, statusCode = 307) {
692 this.setResponseType(this.responseTypes, req, res, ctx);
693 res.setHeader(Enum.Header.Location, newPath);
694 res.statusCode = statusCode;
695 res.end();
696 }
697
698
699 /**
700 * @param {http.ClientRequest} req
701 * @param {http.ServerResponse} res
702 * @param {object} ctx
703 */
704 async handlerMethodNotAllowed(req, res, ctx) {
705 this.setResponseType(this.responseTypes, req, res, ctx);
706 throw new ResponseError(Enum.ErrorResponse.MethodNotAllowed);
707 }
708
709
710 /**
711 * @param {http.ClientRequest} req
712 * @param {http.ServerResponse} res
713 * @param {object} ctx
714 */
715 async handlerNotFound(req, res, ctx) {
716 this.setResponseType(this.responseTypes, req, res, ctx);
717 throw new ResponseError(Enum.ErrorResponse.NotFound);
718 }
719
720
721 /**
722 * @param {http.ClientRequest} req
723 * @param {http.ServerResponse} res
724 * @param {object} ctx
725 */
726 async handlerBadRequest(req, res, ctx) {
727 this.setResponseType(this.responseTypes, req, res, ctx);
728 throw new ResponseError(Enum.ErrorResponse.BadRequest);
729 }
730
731
732 /**
733 * @param {http.ClientRequest} req
734 * @param {http.ServerResponse} res
735 * @param {object} ctx
736 */
737 async handlerInternalServerError(req, res, ctx) {
738 this.setResponseType(this.responseTypes, req, res, ctx);
739 throw new ResponseError(Enum.ErrorResponse.InternalServerError);
740 }
741
742 }
743
744 module.exports = Dingus;