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
= common
.fileScope(__filename
);
24 const defaultOptions
= {
25 ignoreTrailingSlash: true,
30 staticPath: undefined, // No reasonable default
32 intrinsicHeadMethod: true,
33 intrinsicHeadPersistBody: false,
39 * @param {Object} logger object which implements logging methods
40 * @param {Object} options
41 * @param {Boolean} options.ignoreTrailingSlash requests for '/foo/' will match a '/foo' route
42 * @param {string} options.proxyPrefix leading part of url path to strip
43 * @param {Boolean} options.strictAccept whether to error on unsupported Accept type
44 * @param {string} options.selfBaseUrl for constructing links
45 * @param {Boolean} options.staticMetadata serve static headers with static files
46 * @param {Boolean} options.trustProxy trust some header data to be provided by proxy
47 * @param {Boolean} options.intrinsicHeadMethod handle HEAD requests automatically if not specified as a route method
48 * @param {Boolean} options.intrinsicHeadPersistBody include un-sent body on ctx for automatic HEAD requests
49 * @param {Object} options.querystring alternate qs parser to use
51 constructor(logger
= console
, options
= {}) {
52 common
.setOptions(this, defaultOptions
, options
);
54 this.router
= new Router(options
);
56 if (!this.proxyPrefix
) {
57 this._stripPrefix
= (p
) => p
;
60 this.responseTypes
= [
61 Enum
.ContentType
.TextHTML
,
62 Enum
.ContentType
.TextPlain
,
63 Enum
.ContentType
.ApplicationJson
,
71 * Resolve relative and empty paths in url
72 * @param {string} p path
75 const pathNorm
= path
.normalize(p
); // This isn't perfectly correct, but it's easy...
76 return this._stripPrefix(pathNorm
);
81 * Remove a leading portion of url path
82 * N.B. This method gets obliterated if there is no prefix defined at construction
83 * @param {string} p path
86 if (p
.startsWith(this.proxyPrefix
)) {
87 return p
.slice(this.proxyPrefix
.length
);
94 * Returns the path part, and querystring object, from a request url.
98 const [ p
, qs
] = common
.splitFirst(url
, '?');
100 pathPart: this._normalizePath(p
),
101 queryParams: this.querystring
.parse(qs
),
107 * Insert a new path handler
108 * @param {string} method
109 * @param {string} urlPath
110 * @param {fn} handler
112 on(method
, urlPath
, handler
, ...handlerArgs
) {
113 this.router
.on(method
, urlPath
, handler
, handlerArgs
);
118 * Common header tagging for all requests.
119 * Add our own identifier, and persist any external transit identifiers.
120 * @param {http.ClientRequest} req
121 * @param {http.ServerResponse} res
122 * @param {object} ctx
124 static tagContext(req
, res
, ctx
) {
125 const requestId
= common
.requestId();
126 ctx
.requestId
= requestId
;
127 res
.setHeader(Enum
.Header
.RequestId
, requestId
);
128 [Enum
.Header
.XRequestId
, Enum
.Header
.XCorrelationId
].forEach((h
) => {
129 const v
= req
.getHeader(h
);
131 ctx
[h
.replace(/-/g
, '')] = v
;
141 * @param {http.ClientRequest} req
144 // TODO: RFC7239 Forwarded support
145 const address
= (this.trustProxy
&& req
?.getHeader(Enum
.Header
.XForwardedFor
)) ||
146 (this.trustProxy
&& req
?.getHeader(Enum
.Header
.XRealIP
)) ||
147 (req
?.connection
?.remoteAddress
) ||
149 return address
.split(/\s*,\s*/u)[0];
155 * @param {http.ClientRequest} req
158 // TODO: RFC7239 Forwarded support
159 const protocol
= (this.trustProxy
&& req
?.getHeader(Enum
.Header
.XForwardedProto
)) ||
160 ((req
?.connection
?.encrypted
) ? 'https' : 'http');
161 return protocol
.split(/\s*,\s*/u)[0];
167 * @param {http.ClientRequest} req
168 * @param {http.ServerResponse} res
169 * @param {object} ctx
171 clientAddressContext(req
, res
, ctx
) {
172 ctx
.clientAddress
= this._getAddress(req
);
173 ctx
.clientProtocol
= this._getProtocol(req
);
178 * Called before every request handler.
179 * @param {http.ClientRequest} req
180 * @param {http.ServerResponse} res
181 * @param {object} ctx
183 async
preHandler(req
, res
, ctx
) {
184 Dingus
.tagContext(req
, res
, ctx
);
185 this.clientAddressContext(req
, res
, ctx
);
190 * Helper for collecting chunks as array of buffers.
191 * @param {Buffer[]} chunks
192 * @param {string|Buffer} chunk
193 * @param {string} encoding
195 static pushBufChunk(chunks
, chunk
, encoding
= 'utf8') {
197 if (typeof chunk
=== 'string') {
198 chunk
= Buffer
.from(chunk
, encoding
);
206 * Sets ctx.responseBody and calls handler upon res.end().
207 * @param {http.ClientRequest} req
208 * @param {http.ServerResponse} res
209 * @param {object} ctx
210 * @param {*} handler fn(req, res, ctx)
212 static setEndBodyHandler(req
, res
, ctx
, handler
) {
213 const origWrite
= res
.write
.bind(res
);
214 const origEnd
= res
.end
.bind(res
);
216 res
.write = function (chunk
, encoding
, ...rest
) {
217 Dingus
.pushBufChunk(chunks
, chunk
, encoding
);
218 return origWrite(chunk
, encoding
, ...rest
);
220 res
.end = function (data
, encoding
, ...rest
) {
221 Dingus
.pushBufChunk(chunks
, data
, encoding
);
222 ctx
.responseBody
= Buffer
.concat(chunks
);
223 handler(req
, res
, ctx
);
224 return origEnd(data
, encoding
, ...rest
);
230 * Intercept writes for head requests, do not send to client,
231 * but send length, and make body available in context.
232 * N.B. If persisted, ctx.responseBody will be a raw buffer, be aware when logging.
233 * @param {http.ClientRequest} req
234 * @param {http.ServerResponse} res
235 * @param {object} ctx
236 * @param {Boolean} persistResponseBody
238 static setHeadHandler(req
, res
, ctx
, persistResponseBody
= false) {
239 if (req
.method
=== 'HEAD') {
240 const origEnd
= res
.end
.bind(res
);
242 res
.write = function (chunk
, encoding
) {
243 Dingus
.pushBufChunk(chunks
, chunk
, encoding
);
244 // No call to original res.write.
246 res
.end = function (data
, encoding
, ...rest
) {
247 Dingus
.pushBufChunk(chunks
, data
, encoding
);
248 const responseBody
= Buffer
.concat(chunks
);
249 res
.setHeader(Enum
.Header
.ContentLength
, Buffer
.byteLength(responseBody
));
250 if (persistResponseBody
) {
251 ctx
.responseBody
= responseBody
;
253 return origEnd(undefined, encoding
, ...rest
);
260 * Resolve the handler to invoke for a request.
261 * @param {http.ClientRequest} req
262 * @param {http.ServerResponse} res
263 * @param {object} ctx
266 _determineHandler(req
, res
, ctx
) {
267 const _scope
= _fileScope('_determineHandler');
269 const { pathPart
, queryParams
} = this._splitUrl(req
.url
);
270 ctx
.queryParams
= queryParams
;
272 let handler
, handlerArgs
= [];
274 ({ handler
, handlerArgs
} = this.router
.lookup(req
.method
, pathPart
, ctx
));
276 if (e
instanceof URIError
) {
277 handler
= this.handlerBadRequest
.bind(this);
278 } else if (e
instanceof RouterNoPathError
) {
279 handler
= this.handlerNotFound
.bind(this);
280 } else if (e
instanceof RouterNoMethodError
) {
281 if (this.intrinsicHeadMethod
&& req
.method
=== 'HEAD') {
282 ({ handler
, handlerArgs
} = this._determineHeadHandler(req
, res
, ctx
, pathPart
));
284 handler
= this.handlerMethodNotAllowed
.bind(this);
287 this.logger
.error(_scope
, 'unexpected error', { error: e
});
288 handler
= this.handlerInternalServerError
.bind(this);
291 return { handler
, handlerArgs
};
296 * For intrinsic HEAD requests, resolve the handler to invoke.
297 * @param {http.ClientRequest} req
298 * @param {http.ServerResponse} res
299 * @param {object} ctx
300 * @param {string} pathPart
303 _determineHeadHandler(req
, res
, ctx
, pathPart
) {
304 const _scope
= _fileScope('_determineHeadHandler');
305 let handler
, handlerArgs
= [];
307 ({ handler
, handlerArgs
} = this.router
.lookup('GET', pathPart
, ctx
));
308 Dingus
.setHeadHandler(req
, res
, ctx
, this.intrinsicHeadPersistBody
);
310 if (e
instanceof RouterNoMethodError
) {
311 handler
= this.handlerMethodNotAllowed
.bind(this);
313 this.logger
.error(_scope
, 'unexpected error', { error: e
});
314 handler
= this.handlerInternalServerError
.bind(this);
317 return { handler
, handlerArgs
};
322 * Dispatch the handler for a request
323 * @param {http.ClientRequest} req
324 * @param {http.ServerResponse} res
325 * @param {object} ctx
327 async
dispatch(req
, res
, ctx
= {}) {
328 const { handler
, handlerArgs
} = this._determineHandler(req
, res
, ctx
);
330 await
this.preHandler(req
, res
, ctx
);
331 return await
handler(req
, res
, ctx
, ...handlerArgs
);
334 this.sendErrorResponse(e
, req
, res
, ctx
);
340 * Return normalized type, without any parameters.
341 * @param {http.ClientRequest} req
344 static getRequestContentType(req
) {
345 const contentType
= req
.getHeader(Enum
.Header
.ContentType
);
346 return (contentType
|| '').split(';')[0].trim().toLowerCase();
351 * Parse rawBody as contentType into ctx.parsedBody.
352 * @param {string} contentType
353 * @param {object} ctx
354 * @param {string|buffer} rawBody
356 parseBody(contentType
, ctx
, rawBody
) {
357 const _scope
= _fileScope('parseBody');
359 switch (contentType
) {
360 case Enum
.ContentType
.ApplicationForm:
361 ctx
.parsedBody
= this.querystring
.parse(rawBody
);
364 case Enum
.ContentType
.ApplicationJson:
366 ctx
.parsedBody
= JSON
.parse(rawBody
);
368 this.logger
.debug(_scope
, 'JSON parse failed', { requestId: ctx
.requestId
, error: e
});
369 throw new ResponseError(Enum
.ErrorResponse
.BadRequest
, e
.message
);
374 this.logger
.debug(_scope
, 'unhandled content-type', { requestId: ctx
.requestId
, contentType
});
375 throw new ResponseError(Enum
.ErrorResponse
.UnsupportedMediaType
);
381 * Return all body data from a request.
382 * @param {http.ClientRequest} req
383 * @param {Number=} maximumBodySize
384 * @param {Boolean=} toString
386 async
bodyData(req
, maximumBodySize
, toString
= true) {
387 const _scope
= _fileScope('bodyData');
388 return new Promise((resolve
, reject
) => {
391 req
.on('data', (chunk
) => {
393 length
+= Buffer
.byteLength(chunk
);
394 if (maximumBodySize
&& length
> maximumBodySize
) {
395 this.logger
.debug(_scope
, 'body data exceeded limit', { length
, maximumBodySize
});
396 reject(new ResponseError(Enum
.ErrorResponse
.RequestEntityTooLarge
));
399 req
.on('end', () => {
400 const bodyBuffer
= Buffer
.concat(body
);
401 resolve(toString
? bodyBuffer
.toString() : bodyBuffer
);
403 req
.on('error', (e
) => {
404 this.logger
.error(_scope
, 'failed', { error: e
});
412 * Read and parse request body data.
413 * @param {http.ClientRequest} req
414 * @param {http.ServerResponse} res
415 * @param {object} ctx
417 * @param {Boolean} .parseEmptyBody
418 * @param {Boolean} .persistRawBody
420 async
ingestBody(req
, res
, ctx
, { parseEmptyBody
= true, persistRawBody
= false, maximumBodySize
} = {}) {
421 const rawBody
= await
this.bodyData(req
, maximumBodySize
);
422 if (persistRawBody
) {
423 ctx
.rawBody
= rawBody
;
425 if (rawBody
|| parseEmptyBody
) {
426 const contentType
= Dingus
.getRequestContentType(req
);
427 this.parseBody(contentType
, ctx
, rawBody
);
433 * Return the best matching response type.
434 * @param {string[]} responseTypes
435 * @param {http.ClientRequest} req
437 static getResponseContentType(responseTypes
, req
) {
438 const acceptHeader
= req
.getHeader(Enum
.Header
.Accept
);
439 return ContentNegotiation
.accept(responseTypes
, acceptHeader
);
444 * Returns a list of the most-preferred content encodings for the response.
445 * @param {string[]} responseEncodings
446 * @param {http.ClientRequest} req
448 static getResponseEncoding(responseEncodings
, req
) {
449 const acceptEncodingHeader
= req
.getHeader(Enum
.Header
.AcceptEncoding
);
450 return ContentNegotiation
.preferred(responseEncodings
, acceptEncodingHeader
);
455 * Set the best content type for the response.
456 * @param {string[]} responseTypes default first
457 * @param {http.ClientRequest} req
458 * @param {http.ServerResponse} res
459 * @param {object} ctx
461 setResponseType(responseTypes
, req
, res
, ctx
) {
462 const _scope
= _fileScope('setResponseType');
463 ctx
.responseType
= Dingus
.getResponseContentType(responseTypes
, req
);
464 if (!ctx
.responseType
) {
465 if (this.strictAccept
) {
466 this.logger
.debug(_scope
, 'unhandled strict accept', { requestId: req
.requestId
});
467 throw new ResponseError(Enum
.ErrorResponse
.NotAcceptable
);
469 ctx
.responseType
= responseTypes
[0];
472 res
.setHeader(Enum
.Header
.ContentType
, ctx
.responseType
);
477 * Inserts an encoding
478 * @param {http.ServerResponse} res
479 * @param {string} encoding
481 static addEncodingHeader(res
, encoding
) {
482 const existingEncodings
= res
.getHeader(Enum
.Header
.ContentEncoding
);
483 if (existingEncodings
) {
484 encoding
= `${encoding}, ${existingEncodings}`;
486 res
.setHeader(Enum
.Header
.ContentEncoding
, encoding
);
491 * Attempt to fetch both data and metadata for a file.
492 * @param {string} filePath
494 async
_readFileInfo(filePath
) {
495 const _scope
= _fileScope('_readFileInfo');
498 // eslint-disable-next-line security/detect-non-literal-fs-filename
499 const stat
= fsPromises
.stat(filePath
);
500 // eslint-disable-next-line security/detect-non-literal-fs-filename
501 const data
= fsPromises
.readFile(filePath
);
502 result
= await Promise
.all([stat
, data
]);
504 if (['ENOENT', 'EACCES', 'EISDIR', 'ENAMETOOLONG', 'EINVAL'].includes(e
.code
)) {
507 this.logger
.error(_scope
, 'fs error', { error: e
, filePath
});
515 * Potentially add additional headers from static file meta-file.
516 * @param {http.ServerResponse} res
517 * @param {string} directory
518 * @param {string} fileName - already normalized and filtered
520 async
_serveFileMetaHeaders(res
, directory
, fileName
) {
521 const _scope
= _fileScope('_serveFileMetaHeaders');
522 this.logger
.debug(_scope
, 'called', { directory
, fileName
});
524 const metaPrefix
= '.';
525 const metaSuffix
= '.meta';
526 const metaFileName
= `${metaPrefix}${fileName}${metaSuffix}`;
527 const metaFilePath
= path
.join(directory
, metaFileName
);
529 const [stat
, data
] = await
this._readFileInfo(metaFilePath
);
534 const lineBreakRE
= /\r\n|\n|\r/;
535 const lines
= data
.toString().split(lineBreakRE
);
536 common
.unfoldHeaderLines(lines
);
538 const headerParseRE
= /^(?<name
>[^:]+): +(?<value
>.*)$/;
539 lines
.forEach((line
) => {
541 const result
= headerParseRE
.exec(line
);
542 const { groups: header
} = result
;
543 res
.setHeader(header
.name
, header
.value
);
550 * Serve a file from a directory, with rudimentary cache awareness.
551 * This will also serve pre-encoded variations if available and requested.
552 * @param {http.ClientRequest} req
553 * @param {http.ServerResponse} res
554 * @param {object} ctx
555 * @param {string} directory
556 * @param {string} fileName
558 async
serveFile(req
, res
, ctx
, directory
, fileName
) {
559 const _scope
= _fileScope('serveFile');
560 this.logger
.debug(_scope
, 'called', { req
, ctx
});
562 // Require a directory field.
564 this.logger
.debug(_scope
, 'rejected unset directory', { fileName
});
565 return this.handlerNotFound(req
, res
, ctx
);
568 // Normalize the supplied path, as encoded path-navigation may have been (maliciously) present.
569 fileName
= path
.normalize(fileName
);
571 // We will not deal with any subdirs, nor any dot-files.
572 // (Note that we could not deal with subdirs even if we wanted, due to simple router matching scheme.)
573 if (fileName
.indexOf(path
.sep
) >= 0
574 || fileName
.startsWith('.')) {
575 this.logger
.debug(_scope
, 'rejected filename', { fileName
});
576 return this.handlerNotFound(req
, res
, ctx
);
579 const filePath
= path
.join(directory
, fileName
);
581 // File must exist, before any alternate static encodings will be considered.
582 let [stat
, data
] = await
this._readFileInfo(filePath
);
584 return this.handlerNotFound(req
, res
, ctx
);
587 // If encodings were requested, check for static versions to serve.
588 // Update stat and data if matching version is found.
589 ctx
.availableEncodings
= Dingus
.getResponseEncoding(Object
.values(Enum
.EncodingType
), req
);
590 if (ctx
.availableEncodings
.length
=== 0) {
591 // Identity encoding was specifically denied, and nothing else available.
592 this.logger
.debug(_scope
, 'no suitable encodings', { ctx
});
593 return this.handlerMethodNotAllowed(req
, res
, ctx
);
595 for (const encoding
of ctx
.availableEncodings
) {
596 if (encoding
=== Enum
.EncodingType
.Identity
) {
599 const suffix
= Enum
.EncodingTypeSuffix
[encoding
];
601 this.logger
.error(_scope
, 'supported encoding missing mapped suffix', { ctx
, encoding
});
604 const encodedFilePath
= `${filePath}${suffix}`;
605 const [ encodedStat
, encodedData
] = await
this._readFileInfo(encodedFilePath
);
607 ([ stat
, data
] = [ encodedStat
, encodedData
]);
608 ctx
.selectedEncoding
= encoding
;
609 Dingus
.addEncodingHeader(res
, encoding
);
610 res
.setHeader(Enum
.Header
.Vary
, Enum
.Header
.AcceptEncoding
);
611 this.logger
.debug(_scope
, 'serving encoded version', { ctx
, encodedFilePath
});
616 const lastModifiedDate
= new Date(stat
.mtimeMs
);
617 res
.setHeader(Enum
.Header
.LastModified
, lastModifiedDate
.toGMTString());
619 const eTag
= common
.generateETag(filePath
, stat
, data
);
620 res
.setHeader(Enum
.Header
.ETag
, eTag
);
622 if (common
.isClientCached(req
, stat
.mtimeMs
, eTag
)) {
623 this.logger
.debug(_scope
, 'client cached file', { filePath
});
624 res
.statusCode
= 304; // Not Modified
629 // Set the type based on extension of un-encoded filename.
630 const ext
= path
.extname(filePath
).slice(1); // Drop the dot
631 const contentType
= extensionToMime(ext
);
632 res
.setHeader(Enum
.Header
.ContentType
, contentType
);
634 // We presume static files are relatively cacheable.
635 res
.setHeader(Enum
.Header
.CacheControl
, 'public');
637 if (this.staticMetadata
) {
638 await
this._serveFileMetaHeaders(res
, directory
, fileName
);
641 this.logger
.debug(_scope
, 'serving file', { filePath
, contentType
});
647 * Return a content-type appropriate rendering of an errorResponse object.
648 * @param {string} type content-type of response
649 * @param {object} err either an Error object, or an error response
650 * @param {number} err.statusCode
651 * @param {string} err.errorMessage
652 * @param {string|string[]} err.details
654 // eslint-disable-next-line class-methods-use-this
655 renderError(contentType
, err
) {
656 switch (contentType
) {
657 case Enum
.ContentType
.ApplicationJson:
658 return JSON
.stringify(err
);
660 case Enum
.ContentType
.TextHTML:
661 return Template
.errorHTML(err
);
663 case Enum
.ContentType
.TextPlain:
665 return [err
.errorMessage
, err
.details
].join('\r\n');
671 * Send an error response. Terminal.
672 * Logs any non-error-response errors as such.
673 * @param {object} err either an Error object, or an error response
674 * @param {http.ClientRequest} req
675 * @param {http.ServerResponse} res
676 * @param {object} ctx
678 sendErrorResponse(err
, req
, res
, ctx
) {
679 const _scope
= _fileScope('sendErrorResponse');
682 // Default to a content type if one is not yet present
683 if (!res
.hasHeader(Enum
.Header
.ContentType
)) {
684 res
.setHeader(Enum
.Header
.ContentType
, Enum
.ContentType
.TextPlain
);
687 if (err
?.statusCode
) {
688 res
.statusCode
= err
.statusCode
;
689 body
= this.renderError(res
.getHeader(Enum
.Header
.ContentType
), err
);
690 this.logger
.debug(_scope
, 'handler error', { err
, req
, res
, ctx
});
692 res
.statusCode
= 500;
693 body
= this.renderError(res
.getHeader(Enum
.Header
.ContentType
), Enum
.ErrorResponse
.InternalServerError
);
694 this.logger
.error(_scope
, 'handler exception', { err
, req
, res
, ctx
});
701 * @param {http.ClientRequest} req
702 * @param {http.ServerResponse} res
703 * @param {object} ctx
704 * @param {String} file - override ctx.params.file
706 async
handlerGetStaticFile(req
, res
, ctx
, file
) {
707 Dingus
.setHeadHandler(req
, res
, ctx
);
709 // Set a default response type to handle any errors; will be re-set to serve actual static content type.
710 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
712 await
this.serveFile(req
, res
, ctx
, this.staticPath
, file
|| ctx
.params
.file
);
717 * @param {http.ClientRequest} req
718 * @param {http.ServerResponse} res
719 * @param {Object} ctx
720 * @param {String} newPath
721 * @param {Number} statusCode
723 async
handlerRedirect(req
, res
, ctx
, newPath
, statusCode
= 307) {
724 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
725 res
.setHeader(Enum
.Header
.Location
, newPath
);
726 res
.statusCode
= statusCode
;
732 * @param {http.ClientRequest} req
733 * @param {http.ServerResponse} res
734 * @param {object} ctx
736 async
handlerMethodNotAllowed(req
, res
, ctx
) {
737 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
738 throw new ResponseError(Enum
.ErrorResponse
.MethodNotAllowed
);
743 * @param {http.ClientRequest} req
744 * @param {http.ServerResponse} res
745 * @param {object} ctx
747 async
handlerNotFound(req
, res
, ctx
) {
748 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
749 throw new ResponseError(Enum
.ErrorResponse
.NotFound
);
754 * @param {http.ClientRequest} req
755 * @param {http.ServerResponse} res
756 * @param {object} ctx
758 async
handlerBadRequest(req
, res
, ctx
) {
759 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
760 throw new ResponseError(Enum
.ErrorResponse
.BadRequest
);
765 * @param {http.ClientRequest} req
766 * @param {http.ServerResponse} res
767 * @param {object} ctx
769 async
handlerInternalServerError(req
, res
, ctx
) {
770 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
771 throw new ResponseError(Enum
.ErrorResponse
.InternalServerError
);
776 module
.exports
= Dingus
;