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