1 /* eslint-disable security/detect-object-injection */
5 * A very minimal API server framework.
6 * Just a self-contained router and some request glue.
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 { ResponseError
, RouterNoPathError
, RouterNoMethodError
} = require('./errors');
17 const { extensionToMime
} = require('./mime-helper');
18 const Router
= require('./router');
19 const Template
= require('./template');
22 const { fileScope
} = require('@squeep/log-helper');
23 const _fileScope
= fileScope(__filename
);
25 const defaultOptions
= {
26 ignoreTrailingSlash: true,
31 staticPath: undefined, // No reasonable default
33 intrinsicHeadMethod: true,
34 intrinsicHeadPersistBody: false,
40 * @param {Object} logger object which implements logging methods
41 * @param {Object} options
42 * @param {Boolean} options.ignoreTrailingSlash requests for '/foo/' will match a '/foo' route
43 * @param {string} options.proxyPrefix leading part of url path to strip
44 * @param {Boolean} options.strictAccept whether to error on unsupported Accept type
45 * @param {string} options.selfBaseUrl for constructing links
46 * @param {Boolean} options.staticMetadata serve static headers with static files
47 * @param {Boolean} options.trustProxy trust some header data to be provided by proxy
48 * @param {Boolean} options.intrinsicHeadMethod handle HEAD requests automatically if not specified as a route method
49 * @param {Boolean} options.intrinsicHeadPersistBody include un-sent body on ctx for automatic HEAD requests
50 * @param {Object} options.querystring alternate qs parser to use
52 constructor(logger
= console
, options
= {}) {
53 common
.setOptions(this, defaultOptions
, options
);
55 this.router
= new Router(options
);
57 if (!this.proxyPrefix
) {
58 this._stripPrefix
= (p
) => p
;
61 this.responseTypes
= [
62 Enum
.ContentType
.TextHTML
,
63 Enum
.ContentType
.TextPlain
,
64 Enum
.ContentType
.ApplicationJson
,
72 * Resolve relative and empty paths in url
73 * @param {string} p path
76 const pathNorm
= path
.normalize(p
); // This isn't perfectly correct, but it's easy...
77 return this._stripPrefix(pathNorm
);
82 * Remove a leading portion of url path
83 * N.B. This method gets obliterated if there is no prefix defined at construction
84 * @param {string} p path
87 if (p
.startsWith(this.proxyPrefix
)) {
88 return p
.slice(this.proxyPrefix
.length
);
95 * Returns the path part, and querystring object, from a request url.
99 const [ p
, qs
] = common
.splitFirst(url
, '?');
101 pathPart: this._normalizePath(p
),
102 queryParams: this.querystring
.parse(qs
),
108 * Insert a new path handler
109 * @param {string} method
110 * @param {string} urlPath
111 * @param {fn} handler
113 on(method
, urlPath
, handler
, ...handlerArgs
) {
114 this.router
.on(method
, urlPath
, handler
, handlerArgs
);
119 * Common header tagging for all requests.
120 * Add our own identifier, and persist any external transit identifiers.
121 * @param {http.ClientRequest} req
122 * @param {http.ServerResponse} res
123 * @param {object} ctx
125 static tagContext(req
, res
, ctx
) {
126 const requestId
= common
.requestId();
127 ctx
.requestId
= requestId
;
128 res
.setHeader(Enum
.Header
.RequestId
, requestId
);
129 [Enum
.Header
.XRequestId
, Enum
.Header
.XCorrelationId
].forEach((h
) => {
130 const v
= req
.getHeader(h
);
132 ctx
[h
.replace(/-/g
, '')] = v
;
142 * @param {http.ClientRequest} req
145 // TODO: RFC7239 Forwarded support
146 const address
= (this.trustProxy
&& req
?.getHeader(Enum
.Header
.XForwardedFor
)) ||
147 (this.trustProxy
&& req
?.getHeader(Enum
.Header
.XRealIP
)) ||
148 (req
?.connection
?.remoteAddress
) ||
150 return address
.split(/\s*,\s*/u)[0];
156 * @param {http.ClientRequest} req
159 // TODO: RFC7239 Forwarded support
160 const protocol
= (this.trustProxy
&& req
?.getHeader(Enum
.Header
.XForwardedProto
)) ||
161 ((req
?.connection
?.encrypted
) ? 'https' : 'http');
162 return protocol
.split(/\s*,\s*/u)[0];
168 * @param {http.ClientRequest} req
169 * @param {http.ServerResponse} res
170 * @param {object} ctx
172 clientAddressContext(req
, res
, ctx
) {
173 ctx
.clientAddress
= this._getAddress(req
);
174 ctx
.clientProtocol
= this._getProtocol(req
);
179 * Called before every request handler.
180 * @param {http.ClientRequest} req
181 * @param {http.ServerResponse} res
182 * @param {object} ctx
184 async
preHandler(req
, res
, ctx
) {
185 Dingus
.tagContext(req
, res
, ctx
);
186 this.clientAddressContext(req
, res
, ctx
);
191 * Helper for collecting chunks as array of buffers.
192 * @param {Buffer[]} chunks
193 * @param {string|Buffer} chunk
194 * @param {string} encoding
196 static pushBufChunk(chunks
, chunk
, encoding
= 'utf8') {
198 if (typeof chunk
=== 'string') {
199 chunk
= Buffer
.from(chunk
, encoding
);
207 * Sets ctx.responseBody and calls handler upon res.end().
208 * @param {http.ClientRequest} req
209 * @param {http.ServerResponse} res
210 * @param {object} ctx
211 * @param {*} handler fn(req, res, ctx)
213 static setEndBodyHandler(req
, res
, ctx
, handler
) {
214 const origWrite
= res
.write
.bind(res
);
215 const origEnd
= res
.end
.bind(res
);
217 res
.write = function (chunk
, encoding
, ...rest
) {
218 Dingus
.pushBufChunk(chunks
, chunk
, encoding
);
219 return origWrite(chunk
, encoding
, ...rest
);
221 res
.end = function (data
, encoding
, ...rest
) {
222 Dingus
.pushBufChunk(chunks
, data
, encoding
);
223 ctx
.responseBody
= Buffer
.concat(chunks
);
224 handler(req
, res
, ctx
);
225 return origEnd(data
, encoding
, ...rest
);
231 * Intercept writes for head requests, do not send to client,
232 * but send length, and make body available in context.
233 * N.B. If persisted, ctx.responseBody will be a raw buffer, be aware when logging.
234 * @param {http.ClientRequest} req
235 * @param {http.ServerResponse} res
236 * @param {object} ctx
237 * @param {Boolean} persistResponseBody
239 static setHeadHandler(req
, res
, ctx
, persistResponseBody
= false) {
240 if (req
.method
=== 'HEAD') {
241 const origEnd
= res
.end
.bind(res
);
243 res
.write = function (chunk
, encoding
) {
244 Dingus
.pushBufChunk(chunks
, chunk
, encoding
);
245 // No call to original res.write.
247 res
.end = function (data
, encoding
, ...rest
) {
248 Dingus
.pushBufChunk(chunks
, data
, encoding
);
249 const responseBody
= Buffer
.concat(chunks
);
250 res
.setHeader(Enum
.Header
.ContentLength
, Buffer
.byteLength(responseBody
));
251 if (persistResponseBody
) {
252 ctx
.responseBody
= responseBody
;
254 return origEnd(undefined, encoding
, ...rest
);
261 * Resolve the handler to invoke for a request.
262 * @param {http.ClientRequest} req
263 * @param {http.ServerResponse} res
264 * @param {object} ctx
267 _determineHandler(req
, res
, ctx
) {
268 const _scope
= _fileScope('_determineHandler');
270 const { pathPart
, queryParams
} = this._splitUrl(req
.url
);
271 ctx
.queryParams
= queryParams
;
273 let handler
, handlerArgs
= [];
275 ({ handler
, handlerArgs
} = this.router
.lookup(req
.method
, pathPart
, ctx
));
277 if (e
instanceof URIError
) {
278 handler
= this.handlerBadRequest
.bind(this);
279 } else if (e
instanceof RouterNoPathError
) {
280 handler
= this.handlerNotFound
.bind(this);
281 } else if (e
instanceof RouterNoMethodError
) {
282 if (this.intrinsicHeadMethod
&& req
.method
=== 'HEAD') {
283 ({ handler
, handlerArgs
} = this._determineHeadHandler(req
, res
, ctx
, pathPart
));
285 handler
= this.handlerMethodNotAllowed
.bind(this);
288 this.logger
.error(_scope
, 'unexpected error', { error: e
});
289 handler
= this.handlerInternalServerError
.bind(this);
292 return { handler
, handlerArgs
};
297 * For intrinsic HEAD requests, resolve the handler to invoke.
298 * @param {http.ClientRequest} req
299 * @param {http.ServerResponse} res
300 * @param {object} ctx
301 * @param {string} pathPart
304 _determineHeadHandler(req
, res
, ctx
, pathPart
) {
305 const _scope
= _fileScope('_determineHeadHandler');
306 let handler
, handlerArgs
= [];
308 ({ handler
, handlerArgs
} = this.router
.lookup('GET', pathPart
, ctx
));
309 Dingus
.setHeadHandler(req
, res
, ctx
, this.intrinsicHeadPersistBody
);
311 if (e
instanceof RouterNoMethodError
) {
312 handler
= this.handlerMethodNotAllowed
.bind(this);
314 this.logger
.error(_scope
, 'unexpected error', { error: e
});
315 handler
= this.handlerInternalServerError
.bind(this);
318 return { handler
, handlerArgs
};
323 * Dispatch the handler for a request
324 * @param {http.ClientRequest} req
325 * @param {http.ServerResponse} res
326 * @param {object} ctx
328 async
dispatch(req
, res
, ctx
= {}) {
329 const { handler
, handlerArgs
} = this._determineHandler(req
, res
, ctx
);
331 await
this.preHandler(req
, res
, ctx
);
332 return await
handler(req
, res
, ctx
, ...handlerArgs
);
335 this.sendErrorResponse(e
, req
, res
, ctx
);
341 * Return normalized type, without any parameters.
342 * @param {http.ClientRequest} req
345 static getRequestContentType(req
) {
346 const contentType
= req
.getHeader(Enum
.Header
.ContentType
);
347 return (contentType
|| '').split(';')[0].trim().toLowerCase();
352 * Parse rawBody as contentType into ctx.parsedBody.
353 * @param {string} contentType
354 * @param {object} ctx
355 * @param {string|buffer} rawBody
357 parseBody(contentType
, ctx
, rawBody
) {
358 const _scope
= _fileScope('parseBody');
360 switch (contentType
) {
361 case Enum
.ContentType
.ApplicationForm:
362 ctx
.parsedBody
= this.querystring
.parse(rawBody
);
365 case Enum
.ContentType
.ApplicationJson:
367 ctx
.parsedBody
= JSON
.parse(rawBody
);
369 this.logger
.debug(_scope
, 'JSON parse failed', { requestId: ctx
.requestId
, error: e
});
370 throw new ResponseError(Enum
.ErrorResponse
.BadRequest
, e
.message
);
375 this.logger
.debug(_scope
, 'unhandled content-type', { requestId: ctx
.requestId
, contentType
});
376 throw new ResponseError(Enum
.ErrorResponse
.UnsupportedMediaType
);
382 * Return all body data from a request.
383 * @param {http.ClientRequest} req
384 * @param {Number=} maximumBodySize
385 * @param {Boolean=} toString
387 async
bodyData(req
, maximumBodySize
, toString
= true) {
388 const _scope
= _fileScope('bodyData');
389 return new Promise((resolve
, reject
) => {
392 req
.on('data', (chunk
) => {
394 length
+= Buffer
.byteLength(chunk
);
395 if (maximumBodySize
&& length
> maximumBodySize
) {
396 this.logger
.debug(_scope
, 'body data exceeded limit', { length
, maximumBodySize
});
397 reject(new ResponseError(Enum
.ErrorResponse
.RequestEntityTooLarge
));
400 req
.on('end', () => {
401 const bodyBuffer
= Buffer
.concat(body
);
402 resolve(toString
? bodyBuffer
.toString() : bodyBuffer
);
404 req
.on('error', (e
) => {
405 this.logger
.error(_scope
, 'failed', { error: e
});
413 * Read and parse request body data.
414 * @param {http.ClientRequest} req
415 * @param {http.ServerResponse} res
416 * @param {object} ctx
418 * @param {Boolean} .parseEmptyBody
419 * @param {Boolean} .persistRawBody
421 async
ingestBody(req
, res
, ctx
, { parseEmptyBody
= true, persistRawBody
= false, maximumBodySize
} = {}) {
422 const rawBody
= await
this.bodyData(req
, maximumBodySize
);
423 if (persistRawBody
) {
424 ctx
.rawBody
= rawBody
;
426 if (rawBody
|| parseEmptyBody
) {
427 const contentType
= Dingus
.getRequestContentType(req
);
428 this.parseBody(contentType
, ctx
, rawBody
);
434 * Return the best matching response type.
435 * @param {string[]} responseTypes
436 * @param {http.ClientRequest} req
438 static getResponseContentType(responseTypes
, req
) {
439 const acceptHeader
= req
.getHeader(Enum
.Header
.Accept
);
440 return ContentNegotiation
.accept(responseTypes
, acceptHeader
);
445 * Returns a list of the most-preferred content encodings for the response.
446 * @param {string[]} responseEncodings
447 * @param {http.ClientRequest} req
449 static getResponseEncoding(responseEncodings
, req
) {
450 const acceptEncodingHeader
= req
.getHeader(Enum
.Header
.AcceptEncoding
);
451 return ContentNegotiation
.preferred(responseEncodings
, acceptEncodingHeader
);
456 * Set the best content type for the response.
457 * @param {string[]} responseTypes default first
458 * @param {http.ClientRequest} req
459 * @param {http.ServerResponse} res
460 * @param {object} ctx
462 setResponseType(responseTypes
, req
, res
, ctx
) {
463 const _scope
= _fileScope('setResponseType');
464 ctx
.responseType
= Dingus
.getResponseContentType(responseTypes
, req
);
465 if (!ctx
.responseType
) {
466 if (this.strictAccept
) {
467 this.logger
.debug(_scope
, 'unhandled strict accept', { requestId: req
.requestId
});
468 throw new ResponseError(Enum
.ErrorResponse
.NotAcceptable
);
470 ctx
.responseType
= responseTypes
[0];
473 res
.setHeader(Enum
.Header
.ContentType
, ctx
.responseType
);
478 * Inserts an encoding
479 * @param {http.ServerResponse} res
480 * @param {string} encoding
482 static addEncodingHeader(res
, encoding
) {
483 const existingEncodings
= res
.getHeader(Enum
.Header
.ContentEncoding
);
484 if (existingEncodings
) {
485 encoding
= `${encoding}, ${existingEncodings}`;
487 res
.setHeader(Enum
.Header
.ContentEncoding
, encoding
);
492 * Attempt to fetch both data and metadata for a file.
493 * @param {string} filePath
495 async
_readFileInfo(filePath
) {
496 const _scope
= _fileScope('_readFileInfo');
499 // eslint-disable-next-line security/detect-non-literal-fs-filename
500 const stat
= fsPromises
.stat(filePath
);
501 // eslint-disable-next-line security/detect-non-literal-fs-filename
502 const data
= fsPromises
.readFile(filePath
);
503 result
= await Promise
.all([stat
, data
]);
505 if (['ENOENT', 'EACCES', 'EISDIR', 'ENAMETOOLONG', 'EINVAL'].includes(e
.code
)) {
508 this.logger
.error(_scope
, 'fs error', { error: e
, filePath
});
516 * Potentially add additional headers from static file meta-file.
517 * @param {http.ServerResponse} res
518 * @param {string} directory
519 * @param {string} fileName - already normalized and filtered
521 async
_serveFileMetaHeaders(res
, directory
, fileName
) {
522 const _scope
= _fileScope('_serveFileMetaHeaders');
525 const metaPrefix
= '.';
526 const metaSuffix
= '.meta';
527 const metaFileName
= `${metaPrefix}${fileName}${metaSuffix}`;
528 const metaFilePath
= path
.join(directory
, metaFileName
);
530 const [stat
, data
] = await
this._readFileInfo(metaFilePath
);
535 const lineBreakRE
= /\r\n|\n|\r/;
536 const lines
= data
.toString().split(lineBreakRE
);
537 common
.unfoldHeaderLines(lines
);
539 const headerParseRE
= /^(?<name
>[^:]+): +(?<value
>.*)$/;
540 lines
.forEach((line
) => {
542 const result
= headerParseRE
.exec(line
);
543 const { groups: header
} = result
;
544 res
.setHeader(header
.name
, header
.value
);
553 * Serve a file from a directory, with rudimentary cache awareness.
554 * This will also serve pre-encoded variations if available and requested.
555 * @param {http.ClientRequest} req
556 * @param {http.ServerResponse} res
557 * @param {object} ctx
558 * @param {string} directory
559 * @param {string} fileName
561 async
serveFile(req
, res
, ctx
, directory
, fileName
) {
562 const _scope
= _fileScope('serveFile');
563 this.logger
.debug(_scope
, 'called', { req
, ctx
});
565 // Require a directory field.
567 this.logger
.debug(_scope
, 'rejected unset directory', { fileName
});
568 return this.handlerNotFound(req
, res
, ctx
);
571 // Normalize the supplied path, as encoded path-navigation may have been (maliciously) present.
572 fileName
= path
.normalize(fileName
);
574 // We will not deal with any subdirs, nor any dot-files.
575 // (Note that we could not deal with subdirs even if we wanted, due to simple router matching scheme.)
576 if (fileName
.indexOf(path
.sep
) >= 0
577 || fileName
.startsWith('.')) {
578 this.logger
.debug(_scope
, 'rejected filename', { fileName
});
579 return this.handlerNotFound(req
, res
, ctx
);
582 const filePath
= path
.join(directory
, fileName
);
584 // File must exist, before any alternate static encodings will be considered.
585 let [stat
, data
] = await
this._readFileInfo(filePath
);
587 return this.handlerNotFound(req
, res
, ctx
);
590 // If encodings were requested, check for static versions to serve.
591 // Update stat and data if matching version is found.
592 ctx
.availableEncodings
= Dingus
.getResponseEncoding(Object
.values(Enum
.EncodingType
), req
);
593 if (ctx
.availableEncodings
.length
=== 0) {
594 // Identity encoding was specifically denied, and nothing else available.
595 this.logger
.debug(_scope
, 'no suitable encodings', { ctx
});
596 return this.handlerMethodNotAllowed(req
, res
, ctx
);
598 for (const encoding
of ctx
.availableEncodings
) {
599 if (encoding
=== Enum
.EncodingType
.Identity
) {
602 const suffix
= Enum
.EncodingTypeSuffix
[encoding
];
604 this.logger
.error(_scope
, 'supported encoding missing mapped suffix', { ctx
, encoding
});
607 const encodedFilePath
= `${filePath}${suffix}`;
608 const [ encodedStat
, encodedData
] = await
this._readFileInfo(encodedFilePath
);
610 ([ stat
, data
] = [ encodedStat
, encodedData
]);
611 ctx
.selectedEncoding
= encoding
;
612 Dingus
.addEncodingHeader(res
, encoding
);
613 res
.setHeader(Enum
.Header
.Vary
, Enum
.Header
.AcceptEncoding
);
614 this.logger
.debug(_scope
, 'serving encoded version', { ctx
, encodedFilePath
});
619 const lastModifiedDate
= new Date(stat
.mtimeMs
);
620 res
.setHeader(Enum
.Header
.LastModified
, lastModifiedDate
.toGMTString());
622 const eTag
= common
.generateETag(filePath
, stat
, data
);
623 res
.setHeader(Enum
.Header
.ETag
, eTag
);
625 if (common
.isClientCached(req
, stat
.mtimeMs
, eTag
)) {
626 this.logger
.debug(_scope
, 'client cached file', { filePath
});
627 res
.statusCode
= 304; // Not Modified
632 // Set the type based on extension of un-encoded filename.
633 const ext
= path
.extname(filePath
).slice(1); // Drop the dot
634 const contentType
= extensionToMime(ext
);
635 res
.setHeader(Enum
.Header
.ContentType
, contentType
);
637 // We presume static files are relatively cacheable.
638 res
.setHeader(Enum
.Header
.CacheControl
, 'public');
640 if (this.staticMetadata
) {
641 ctx
.metaHeaders
= await
this._serveFileMetaHeaders(res
, directory
, fileName
);
644 this.logger
.debug(_scope
, 'serving file', { filePath
, contentType
});
650 * Return a content-type appropriate rendering of an errorResponse object.
651 * @param {string} type content-type of response
652 * @param {object} err either an Error object, or an error response
653 * @param {number} err.statusCode
654 * @param {string} err.errorMessage
655 * @param {string|string[]} err.details
657 // eslint-disable-next-line class-methods-use-this
658 renderError(contentType
, err
) {
659 switch (contentType
) {
660 case Enum
.ContentType
.ApplicationJson:
661 return JSON
.stringify(err
);
663 case Enum
.ContentType
.TextHTML:
664 return Template
.errorHTML(err
);
666 case Enum
.ContentType
.TextPlain:
668 return [err
.errorMessage
, err
.details
].join('\r\n');
674 * Send an error response. Terminal.
675 * Logs any non-error-response errors as such.
676 * @param {object} err either an Error object, or an error response
677 * @param {http.ClientRequest} req
678 * @param {http.ServerResponse} res
679 * @param {object} ctx
681 sendErrorResponse(err
, req
, res
, ctx
) {
682 const _scope
= _fileScope('sendErrorResponse');
685 // Default to a content type if one is not yet present
686 if (!res
.hasHeader(Enum
.Header
.ContentType
)) {
687 res
.setHeader(Enum
.Header
.ContentType
, Enum
.ContentType
.TextPlain
);
690 if (err
?.statusCode
) {
691 res
.statusCode
= err
.statusCode
;
692 body
= this.renderError(res
.getHeader(Enum
.Header
.ContentType
), err
);
693 this.logger
.debug(_scope
, 'handler error', { err
, req
, res
, ctx
});
695 res
.statusCode
= 500;
696 body
= this.renderError(res
.getHeader(Enum
.Header
.ContentType
), Enum
.ErrorResponse
.InternalServerError
);
697 this.logger
.error(_scope
, 'handler exception', { err
, req
, res
, ctx
});
704 * @param {http.ClientRequest} req
705 * @param {http.ServerResponse} res
706 * @param {object} ctx
707 * @param {String} file - override ctx.params.file
709 async
handlerGetStaticFile(req
, res
, ctx
, file
) {
710 Dingus
.setHeadHandler(req
, res
, ctx
);
712 // Set a default response type to handle any errors; will be re-set to serve actual static content type.
713 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
715 await
this.serveFile(req
, res
, ctx
, this.staticPath
, file
|| ctx
.params
.file
);
720 * @param {http.ClientRequest} req
721 * @param {http.ServerResponse} res
722 * @param {Object} ctx
723 * @param {String} newPath
724 * @param {Number} statusCode
726 async
handlerRedirect(req
, res
, ctx
, newPath
, statusCode
= 307) {
727 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
728 res
.setHeader(Enum
.Header
.Location
, newPath
);
729 res
.statusCode
= statusCode
;
735 * @param {http.ClientRequest} req
736 * @param {http.ServerResponse} res
737 * @param {object} ctx
739 async
handlerMethodNotAllowed(req
, res
, ctx
) {
740 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
741 throw new ResponseError(Enum
.ErrorResponse
.MethodNotAllowed
);
746 * @param {http.ClientRequest} req
747 * @param {http.ServerResponse} res
748 * @param {object} ctx
750 async
handlerNotFound(req
, res
, ctx
) {
751 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
752 throw new ResponseError(Enum
.ErrorResponse
.NotFound
);
757 * @param {http.ClientRequest} req
758 * @param {http.ServerResponse} res
759 * @param {object} ctx
761 async
handlerBadRequest(req
, res
, ctx
) {
762 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
763 throw new ResponseError(Enum
.ErrorResponse
.BadRequest
);
768 * @param {http.ClientRequest} req
769 * @param {http.ServerResponse} res
770 * @param {object} ctx
772 async
handlerInternalServerError(req
, res
, ctx
) {
773 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
774 throw new ResponseError(Enum
.ErrorResponse
.InternalServerError
);
779 module
.exports
= Dingus
;