4 * A very minimal API server framework.
5 * Just a self-contained router and some request glue.
9 * @typedef {import('node:http')} http
13 const { promises: fsPromises
} = require('node:fs');
14 const path
= require('node:path');
15 const querystring
= require('node:querystring');
16 const common
= require('./common');
17 const ContentNegotiation
= require('./content-negotiation');
18 const Enum
= require('./enum');
19 const { ResponseError
, RouterNoPathError
, RouterNoMethodError
} = require('./errors');
20 const { extensionToMime
} = require('./mime-helper');
21 const Router
= require('./router');
22 const Template
= require('./template');
25 const { fileScope
} = require('@squeep/log-helper');
26 const _fileScope
= fileScope(__filename
);
28 const defaultOptions
= {
29 ignoreTrailingSlash: true,
34 staticPath: undefined, // No reasonable default
36 intrinsicHeadMethod: true,
37 intrinsicHeadPersistBody: false,
41 const cookieSplitRE
= /\s*;\s*/;
42 const headerParseRE
= /^(?<name
>[^:]+): +(?<value
>.*)$/;
43 const lineBreakRE
= /\r\n|\n|\r/;
44 const proxySplitRE
= /\s*,\s*/u;
48 * @param {object} logger console-like object which implements logging methods
49 * @param {object} options options
50 * @param {boolean} options.ignoreTrailingSlash requests for '/foo/' will match a '/foo' route
51 * @param {string} options.proxyPrefix leading part of url path to strip
52 * @param {boolean} options.strictAccept whether to error on unsupported Accept type
53 * @param {string} options.selfBaseUrl for constructing links
54 * @param {boolean} options.staticMetadata serve static headers with static files
55 * @param {boolean} options.trustProxy trust some header data to be provided by proxy
56 * @param {boolean} options.intrinsicHeadMethod handle HEAD requests automatically if not specified as a route method
57 * @param {boolean} options.intrinsicHeadPersistBody include un-sent body on ctx for automatic HEAD requests
58 * @param {object} options.querystring alternate qs parser to use
60 constructor(logger
= console
, options
= {}) {
61 common
.setOptions(this, defaultOptions
, options
);
63 this.router
= new Router(options
);
65 if (!this.proxyPrefix
) {
66 this._stripPrefix
= (p
) => p
;
69 this.responseTypes
= [
70 Enum
.ContentType
.TextHTML
,
71 Enum
.ContentType
.TextPlain
,
72 Enum
.ContentType
.ApplicationJson
,
80 * Resolve relative and empty paths in url
81 * @param {string} p path
82 * @returns {string} normalized path
85 const pathNorm
= path
.normalize(p
); // This isn't perfectly correct, but it's easy...
86 return this._stripPrefix(pathNorm
);
91 * Remove a leading portion of url path
92 * N.B. This method gets obliterated if there is no prefix defined at construction
93 * @param {string} p path
94 * @returns {string} de-prefixed path
97 if (p
.startsWith(this.proxyPrefix
)) {
98 return p
.slice(this.proxyPrefix
.length
);
105 * Returns the path part, and querystring object, from a request url.
106 * @param {string} url url
107 * @returns {object} path string and query params
110 const [ p
, qs
] = common
.splitFirst(url
, '?');
112 pathPart: this._normalizePath(p
),
113 queryParams: this.querystring
.parse(qs
),
119 * Insert a new path handler
120 * @param {string|string[]} methods methods to match for this path, '*' allowed
121 * @param {string} urlPath request path to match
122 * @param {Function} handler handler to invoke on match
123 * @param {any[]} handlerArgs additional arguments for handler
125 on(methods
, urlPath
, handler
, ...handlerArgs
) {
126 this.router
.on(methods
, urlPath
, handler
, handlerArgs
);
131 * Common header tagging for all requests.
132 * Add our own identifier, and persist any external transit identifiers.
133 * Sets requestId on ctx to a new uuid.
134 * If X-Request-Id or X-Correlation-Id exist on incoming headers, sets them
135 * on outgoing headers and sets on ctx.
136 * @param {http.ClientRequest} req request
137 * @param {http.ServerResponse} res response
138 * @param {object} ctx context
139 * @returns {string} id added request
141 static tagContext(req
, res
, ctx
) {
142 const requestId
= common
.requestId();
143 ctx
.requestId
= requestId
;
144 res
.setHeader(Enum
.Header
.RequestId
, requestId
);
145 [Enum
.Header
.XRequestId
, Enum
.Header
.XCorrelationId
].forEach((h
) => {
146 const v
= req
.getHeader(h
);
148 ctx
[h
.replace(/-/g
, '')] = v
;
158 * @param {http.ClientRequest} req request
159 * @returns {string} first forwarded header ip
162 // TODO: RFC7239 Forwarded support
163 const address
= (this.trustProxy
&& req
?.getHeader(Enum
.Header
.XForwardedFor
)) ||
164 (this.trustProxy
&& req
?.getHeader(Enum
.Header
.XRealIP
)) ||
165 (req
?.connection
?.remoteAddress
) ||
167 return address
.split(proxySplitRE
)[0];
173 * @param {http.ClientRequest} req request
174 * @returns {string} first forwarded header protocol
177 // TODO: RFC7239 Forwarded support
178 const protocol
= (this.trustProxy
&& req
?.getHeader(Enum
.Header
.XForwardedProto
)) ||
179 ((req
?.connection
?.encrypted
) ? 'https' : 'http');
180 return protocol
.split(proxySplitRE
)[0];
185 * Sets ctx.clientAddress and ctx.clientProtocol.
186 * @param {http.ClientRequest} req request
187 * @param {http.ServerResponse} res response
188 * @param {object} ctx context
190 clientAddressContext(req
, res
, ctx
) {
191 ctx
.clientAddress
= this._getAddress(req
);
192 ctx
.clientProtocol
= this._getProtocol(req
);
197 * Sets ctx.cookie from Cookie header.
198 * @param {http.ClientRequest} req request
199 * @param {http.ServerResponse} res response
200 * @param {object} ctx context
202 static ingestCookie(req
, res
, ctx
) {
204 req
.getHeader(Enum
.Header
.Cookie
)?.split(cookieSplitRE
).forEach((cookie
) => {
205 const [ name
, value
] = common
.splitFirst(cookie
, '=', null).map((x
) => {
207 return decodeURIComponent(x
.trim());
208 } catch (e
) { // eslint-disable-line no-unused-vars
212 if (name
&& !(name
in ctx
.cookie
)) {
213 const isQuoted
= value
?.startsWith('"') && value
.endsWith('"');
214 ctx
.cookie
[name
] = isQuoted
? value
.slice(1, -1) : value
; // eslint-disable-line security/detect-object-injection
221 * Called before every request handler.
222 * Sets tracking identifiers and client information on ctx.
223 * @param {http.ClientRequest} req request
224 * @param {http.ServerResponse} res response
225 * @param {object} ctx context
227 async
preHandler(req
, res
, ctx
) {
228 this.constructor.tagContext(req
, res
, ctx
);
229 this.clientAddressContext(req
, res
, ctx
);
230 this.constructor.ingestCookie(req
, res
, ctx
);
235 * Helper for collecting chunks as array of buffers.
236 * @param {Buffer[]} chunks array to collect chunks in
237 * @param {string|Buffer} chunk chunk to collect
238 * @param {string} encoding chunk encoding
240 static pushBufChunk(chunks
, chunk
, encoding
= 'utf8') {
242 if (typeof chunk
=== 'string') {
243 chunk
= Buffer
.from(chunk
, encoding
);
251 * Sets ctx.responseBody and calls handler upon res.end().
252 * @param {http.ClientRequest} req request
253 * @param {http.ServerResponse} res response
254 * @param {object} ctx context
255 * @param {*} handler fn(req, res, ctx)
257 static setEndBodyHandler(req
, res
, ctx
, handler
) {
258 const origWrite
= res
.write
.bind(res
);
259 const origEnd
= res
.end
.bind(res
);
261 res
.write = function (chunk
, encoding
, ...rest
) {
262 Dingus
.pushBufChunk(chunks
, chunk
, encoding
);
263 return origWrite(chunk
, encoding
, ...rest
);
265 res
.end = function (data
, encoding
, ...rest
) {
266 Dingus
.pushBufChunk(chunks
, data
, encoding
);
267 ctx
.responseBody
= Buffer
.concat(chunks
);
268 handler(req
, res
, ctx
);
269 return origEnd(data
, encoding
, ...rest
);
275 * Intercept writes for head requests, do not send to client,
276 * but send length, and make body available in context.
277 * N.B. If persisted, ctx.responseBody will be a raw buffer, be aware when logging.
278 * @param {http.ClientRequest} req request
279 * @param {http.ServerResponse} res response
280 * @param {object} ctx context
281 * @param {boolean} persistResponseBody store body buffer on context
283 static setHeadHandler(req
, res
, ctx
, persistResponseBody
= false) {
284 if (req
.method
=== 'HEAD') {
285 const origEnd
= res
.end
.bind(res
);
287 res
.write = function (chunk
, encoding
) {
288 Dingus
.pushBufChunk(chunks
, chunk
, encoding
);
289 // No call to original res.write.
291 res
.end = function (data
, encoding
, ...rest
) {
292 Dingus
.pushBufChunk(chunks
, data
, encoding
);
293 const responseBody
= Buffer
.concat(chunks
);
294 res
.setHeader(Enum
.Header
.ContentLength
, Buffer
.byteLength(responseBody
));
295 if (persistResponseBody
) {
296 ctx
.responseBody
= responseBody
;
298 return origEnd(undefined, encoding
, ...rest
);
305 * Resolve the handler to invoke for a request.
306 * @param {http.ClientRequest} req request
307 * @param {http.ServerResponse} res response
308 * @param {object} ctx context
309 * @returns {object} handler and args
311 _determineHandler(req
, res
, ctx
) {
312 const _scope
= _fileScope('_determineHandler');
314 const { pathPart
, queryParams
} = this._splitUrl(req
.url
);
315 ctx
.queryParams
= queryParams
;
317 let handler
, handlerArgs
= [];
319 ({ handler
, handlerArgs
} = this.router
.lookup(req
.method
, pathPart
, ctx
));
321 if (e
instanceof URIError
) {
322 handler
= this.handlerBadRequest
.bind(this);
323 } else if (e
instanceof RouterNoPathError
) {
324 handler
= this.handlerNotFound
.bind(this);
325 } else if (e
instanceof RouterNoMethodError
) {
326 if (this.intrinsicHeadMethod
&& req
.method
=== 'HEAD') {
327 ({ handler
, handlerArgs
} = this._determineHeadHandler(req
, res
, ctx
, pathPart
));
329 handler
= this.handlerMethodNotAllowed
.bind(this);
332 this.logger
.error(_scope
, 'unexpected error', { error: e
});
333 handler
= this.handlerInternalServerError
.bind(this);
336 return { handler
, handlerArgs
};
341 * For intrinsic HEAD requests, resolve the handler to invoke.
342 * @param {http.ClientRequest} req request
343 * @param {http.ServerResponse} res response
344 * @param {object} ctx context
345 * @param {string} pathPart router path
346 * @returns {object} hander and handler args
348 _determineHeadHandler(req
, res
, ctx
, pathPart
) {
349 const _scope
= _fileScope('_determineHeadHandler');
350 let handler
, handlerArgs
= [];
352 ({ handler
, handlerArgs
} = this.router
.lookup('GET', pathPart
, ctx
));
353 Dingus
.setHeadHandler(req
, res
, ctx
, this.intrinsicHeadPersistBody
);
355 if (e
instanceof RouterNoMethodError
) {
356 handler
= this.handlerMethodNotAllowed
.bind(this);
358 this.logger
.error(_scope
, 'unexpected error', { error: e
});
359 handler
= this.handlerInternalServerError
.bind(this);
362 return { handler
, handlerArgs
};
367 * Dispatch the handler for a request
368 * @param {http.ClientRequest} req request
369 * @param {http.ServerResponse} res response
370 * @param {object} ctx context
371 * @returns {Promise<void>} handler promise
373 async
dispatch(req
, res
, ctx
= {}) {
374 const { handler
, handlerArgs
} = this._determineHandler(req
, res
, ctx
);
376 await
this.preHandler(req
, res
, ctx
);
377 return await
handler(req
, res
, ctx
, ...handlerArgs
);
380 this.sendErrorResponse(e
, req
, res
, ctx
);
386 * Return normalized type, without any parameters.
387 * @param {http.ClientRequest} req request
388 * @returns {string} bare content type
390 static getRequestContentType(req
) {
391 const contentType
= req
.getHeader(Enum
.Header
.ContentType
);
392 return (contentType
|| '').split(';')[0].trim().toLowerCase();
397 * Parse rawBody as contentType into ctx.parsedBody.
398 * @param {string} contentType type
399 * @param {object} ctx context
400 * @param {string|Buffer} rawBody body data
402 parseBody(contentType
, ctx
, rawBody
) {
403 const _scope
= _fileScope('parseBody');
405 switch (contentType
) {
406 case Enum
.ContentType
.ApplicationForm:
407 ctx
.parsedBody
= this.querystring
.parse(rawBody
);
410 case Enum
.ContentType
.ApplicationJson:
412 ctx
.parsedBody
= JSON
.parse(rawBody
);
414 this.logger
.debug(_scope
, 'JSON parse failed', { requestId: ctx
.requestId
, error: e
});
415 throw new ResponseError(Enum
.ErrorResponse
.BadRequest
, e
.message
);
420 this.logger
.debug(_scope
, 'unhandled content-type', { requestId: ctx
.requestId
, contentType
});
421 throw new ResponseError(Enum
.ErrorResponse
.UnsupportedMediaType
);
427 * Return all body data from a request.
428 * @param {http.ClientRequest} req request
429 * @param {number=} maximumBodySize bytes
430 * @param {boolean=} toString whether to convert body data buffer to string
431 * @returns {Promise<Buffer|string>} body data
433 async
bodyData(req
, maximumBodySize
, toString
= true) {
434 const _scope
= _fileScope('bodyData');
435 return new Promise((resolve
, reject
) => {
438 req
.on('data', (chunk
) => {
440 length
+= Buffer
.byteLength(chunk
);
441 if (maximumBodySize
&& length
> maximumBodySize
) {
442 this.logger
.debug(_scope
, 'body data exceeded limit', { length
, maximumBodySize
});
443 reject(new ResponseError(Enum
.ErrorResponse
.RequestEntityTooLarge
));
446 req
.on('end', () => {
447 const bodyBuffer
= Buffer
.concat(body
);
448 resolve(toString
? bodyBuffer
.toString() : bodyBuffer
);
450 req
.on('error', (e
) => {
451 this.logger
.error(_scope
, 'failed', { error: e
});
459 * Read and parse request body data.
460 * Sets ctx.parsedBody, and optionally ctx.rawBody.
461 * @param {http.ClientRequest} req request
462 * @param {http.ServerResponse} res response
463 * @param {object} ctx context
464 * @param {object} options options
465 * @param {boolean} options.parseEmptyBody attempt to parse an empty body, results may vary by encoding
466 * @param {boolean} options.persistRawBody store raw body buffer on context
467 * @param {number=} options.maximumBodySize bytes
469 async
ingestBody(req
, res
, ctx
, { parseEmptyBody
= true, persistRawBody
= false, maximumBodySize
} = {}) {
470 const rawBody
= await
this.bodyData(req
, maximumBodySize
);
471 if (persistRawBody
) {
472 ctx
.rawBody
= rawBody
;
474 if (rawBody
|| parseEmptyBody
) {
475 const contentType
= Dingus
.getRequestContentType(req
);
476 this.parseBody(contentType
, ctx
, rawBody
);
482 * Return the best matching response type.
483 * @param {string[]} responseTypes available content types
484 * @param {http.ClientRequest} req request
485 * @returns {string} preferred content type
487 static getResponseContentType(responseTypes
, req
) {
488 const acceptHeader
= req
.getHeader(Enum
.Header
.Accept
);
489 return ContentNegotiation
.accept(responseTypes
, acceptHeader
);
494 * Returns a list of the most-preferred content encodings for the response.
495 * @param {string[]} responseEncodings available encodings
496 * @param {http.ClientRequest} req request
497 * @returns {string} preferred encoding
499 static getResponseEncoding(responseEncodings
, req
) {
500 const acceptEncodingHeader
= req
.getHeader(Enum
.Header
.AcceptEncoding
);
501 return ContentNegotiation
.preferred(responseEncodings
, acceptEncodingHeader
);
506 * Set the best content type for the response.
507 * Sets ctx.responseType, and Content-Type header.
508 * @param {string[]} responseTypes default first
509 * @param {http.ClientRequest} req request
510 * @param {http.ServerResponse} res response
511 * @param {object} ctx context
513 setResponseType(responseTypes
, req
, res
, ctx
) {
514 const _scope
= _fileScope('setResponseType');
515 ctx
.responseType
= Dingus
.getResponseContentType(responseTypes
, req
);
516 if (!ctx
.responseType
) {
517 if (this.strictAccept
) {
518 this.logger
.debug(_scope
, 'unhandled strict accept', { requestId: req
.requestId
});
519 throw new ResponseError(Enum
.ErrorResponse
.NotAcceptable
);
521 ctx
.responseType
= responseTypes
[0];
524 res
.setHeader(Enum
.Header
.ContentType
, ctx
.responseType
);
529 * Inserts an encoding into Content-Encoding header.
530 * @param {http.ServerResponse} res response
531 * @param {string} encoding encoding
533 static addEncodingHeader(res
, encoding
) {
534 const existingEncodings
= res
.getHeader(Enum
.Header
.ContentEncoding
);
535 if (existingEncodings
) {
536 encoding
= `${encoding}, ${existingEncodings}`;
538 res
.setHeader(Enum
.Header
.ContentEncoding
, encoding
);
543 * Attempt to fetch both data and metadata for a file.
544 * @param {string} filePath path to file
545 * @returns {Promise<[object=, object=]>} array with file contents and stats
547 async
_readFileInfo(filePath
) {
548 const _scope
= _fileScope('_readFileInfo');
551 // eslint-disable-next-line security/detect-non-literal-fs-filename
552 const stat
= fsPromises
.stat(filePath
);
553 // eslint-disable-next-line security/detect-non-literal-fs-filename
554 const data
= fsPromises
.readFile(filePath
);
555 result
= await Promise
.all([stat
, data
]);
557 if (['ENOENT', 'EACCES', 'EISDIR', 'ENAMETOOLONG', 'EINVAL'].includes(e
.code
)) {
560 this.logger
.error(_scope
, 'fs error', { error: e
, filePath
});
568 * Potentially add additional headers from static file meta-file.
569 * @param {http.ServerResponse} res response
570 * @param {string} directory path to file
571 * @param {string} fileName - already normalized and filtered
572 * @returns {boolean} if meta headers were added
574 async
_serveFileMetaHeaders(res
, directory
, fileName
) {
575 const _scope
= _fileScope('_serveFileMetaHeaders');
578 const metaPrefix
= '.';
579 const metaSuffix
= '.meta';
580 const metaFileName
= `${metaPrefix}${fileName}${metaSuffix}`;
581 const metaFilePath
= path
.join(directory
, metaFileName
);
583 const [stat
, data
] = await
this._readFileInfo(metaFilePath
);
588 const lines
= data
.toString().split(lineBreakRE
);
589 common
.unfoldHeaderLines(lines
);
591 lines
.forEach((line
) => {
593 const result
= headerParseRE
.exec(line
);
594 const { groups: header
} = result
;
595 res
.setHeader(header
.name
, header
.value
);
604 * Serve a file from a directory, with rudimentary cache awareness.
605 * This will also serve pre-encoded variations if available and requested.
606 * @param {http.ClientRequest} req request
607 * @param {http.ServerResponse} res response
608 * @param {object} ctx context
609 * @param {string} directory path to file
610 * @param {string} fileName file name
611 * @returns {Promise<void>=} serves file or renders status code
613 async
serveFile(req
, res
, ctx
, directory
, fileName
) {
614 const _scope
= _fileScope('serveFile');
615 this.logger
.debug(_scope
, 'called', { req
, ctx
});
617 // Require a directory field.
619 this.logger
.debug(_scope
, 'rejected unset directory', { fileName
});
620 return this.handlerNotFound(req
, res
, ctx
);
623 // Normalize the supplied path, as encoded path-navigation may have been (maliciously) present.
624 fileName
= path
.normalize(fileName
);
626 // We will not deal with any subdirs, nor any dot-files.
627 // (Note that we could not deal with subdirs even if we wanted, due to simple router matching scheme.)
628 if (fileName
.indexOf(path
.sep
) >= 0
629 || fileName
.startsWith('.')) {
630 this.logger
.debug(_scope
, 'rejected filename', { fileName
});
631 return this.handlerNotFound(req
, res
, ctx
);
634 const filePath
= path
.join(directory
, fileName
);
636 // File must exist, before any alternate static encodings will be considered.
637 let [stat
, data
] = await
this._readFileInfo(filePath
);
639 return this.handlerNotFound(req
, res
, ctx
);
642 // If encodings were requested, check for static versions to serve.
643 // Update stat and data if matching version is found.
644 ctx
.availableEncodings
= Dingus
.getResponseEncoding(Object
.values(Enum
.EncodingType
), req
);
645 if (ctx
.availableEncodings
.length
=== 0) {
646 // Identity encoding was specifically denied, and nothing else available.
647 this.logger
.debug(_scope
, 'no suitable encodings', { ctx
});
648 return this.handlerMethodNotAllowed(req
, res
, ctx
);
650 for (const encoding
of ctx
.availableEncodings
) {
651 if (encoding
=== Enum
.EncodingType
.Identity
) {
654 const suffix
= Enum
.EncodingTypeSuffix
[encoding
]; // eslint-disable-line security/detect-object-injection
656 this.logger
.error(_scope
, 'supported encoding missing mapped suffix', { ctx
, encoding
});
659 const encodedFilePath
= `${filePath}${suffix}`;
660 const [ encodedStat
, encodedData
] = await
this._readFileInfo(encodedFilePath
);
662 ([ stat
, data
] = [ encodedStat
, encodedData
]);
663 ctx
.selectedEncoding
= encoding
;
664 Dingus
.addEncodingHeader(res
, encoding
);
665 res
.setHeader(Enum
.Header
.Vary
, Enum
.Header
.AcceptEncoding
);
666 this.logger
.debug(_scope
, 'serving encoded version', { ctx
, encodedFilePath
});
671 const lastModifiedDate
= new Date(stat
.mtimeMs
);
672 res
.setHeader(Enum
.Header
.LastModified
, lastModifiedDate
.toGMTString());
674 const eTag
= common
.generateETag(filePath
, stat
, data
);
675 res
.setHeader(Enum
.Header
.ETag
, eTag
);
677 if (common
.isClientCached(req
, stat
.mtimeMs
, eTag
)) {
678 this.logger
.debug(_scope
, 'client cached file', { filePath
});
679 res
.statusCode
= 304; // Not Modified
684 // Set the type based on extension of un-encoded filename.
685 const ext
= path
.extname(filePath
).slice(1); // Drop the dot
686 const contentType
= extensionToMime(ext
);
687 res
.setHeader(Enum
.Header
.ContentType
, contentType
);
689 // We presume static files are relatively cacheable.
690 res
.setHeader(Enum
.Header
.CacheControl
, 'public');
692 if (this.staticMetadata
) {
693 ctx
.metaHeaders
= await
this._serveFileMetaHeaders(res
, directory
, fileName
);
696 this.logger
.debug(_scope
, 'serving file', { filePath
, contentType
});
702 * Return a content-type appropriate rendering of an errorResponse object.
703 * @param {string} contentType content-type of response
704 * @param {object} err either an Error object, or an error response
705 * @param {number} err.statusCode htttp status code
706 * @param {string} err.errorMessage http status message
707 * @param {string|string[]} err.details additional details to include
708 * @returns {string} rendered err
710 // eslint-disable-next-line class-methods-use-this
711 renderError(contentType
, err
) {
712 switch (contentType
) {
713 case Enum
.ContentType
.ApplicationJson:
714 return JSON
.stringify(err
);
716 case Enum
.ContentType
.TextHTML:
717 return Template
.errorHTML(err
);
719 case Enum
.ContentType
.TextPlain:
721 return [err
.errorMessage
, err
.details
].join('\r\n');
727 * Send an error response. Terminal.
728 * Logs any non-error-response errors as such.
729 * @param {object} err either an Error object, or an error response
730 * @param {http.ClientRequest} req request
731 * @param {http.ServerResponse} res response
732 * @param {object} ctx context
734 sendErrorResponse(err
, req
, res
, ctx
) {
735 const _scope
= _fileScope('sendErrorResponse');
738 // Default to a content type if one is not yet present
739 if (!res
.hasHeader(Enum
.Header
.ContentType
)) {
740 res
.setHeader(Enum
.Header
.ContentType
, Enum
.ContentType
.TextPlain
);
743 if (err
?.statusCode
) {
744 res
.statusCode
= err
.statusCode
;
745 body
= this.renderError(res
.getHeader(Enum
.Header
.ContentType
), err
);
746 this.logger
.debug(_scope
, 'handler error', { err
, req
, res
, ctx
});
748 res
.statusCode
= 500;
749 body
= this.renderError(res
.getHeader(Enum
.Header
.ContentType
), Enum
.ErrorResponse
.InternalServerError
);
750 this.logger
.error(_scope
, 'handler exception', { err
, req
, res
, ctx
});
757 * @param {http.ClientRequest} req request
758 * @param {http.ServerResponse} res response
759 * @param {object} ctx context
760 * @param {string} file - override ctx.params.file
762 async
handlerGetStaticFile(req
, res
, ctx
, file
) {
763 Dingus
.setHeadHandler(req
, res
, ctx
);
765 // Set a default response type to handle any errors; will be re-set to serve actual static content type.
766 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
768 await
this.serveFile(req
, res
, ctx
, this.staticPath
, file
|| ctx
.params
.file
);
773 * @param {http.ClientRequest} req request
774 * @param {http.ServerResponse} res response
775 * @param {object} ctx context
776 * @param {string} newPath url to redirect to
777 * @param {number=} statusCode status code to use for redirect, default 307
779 async
handlerRedirect(req
, res
, ctx
, newPath
, statusCode
= 307) {
780 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
781 res
.setHeader(Enum
.Header
.Location
, newPath
);
782 res
.statusCode
= statusCode
;
788 * @param {http.ClientRequest} req request
789 * @param {http.ServerResponse} res response
790 * @param {object} ctx context
792 async
handlerMethodNotAllowed(req
, res
, ctx
) {
793 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
794 throw new ResponseError(Enum
.ErrorResponse
.MethodNotAllowed
);
799 * @param {http.ClientRequest} req request
800 * @param {http.ServerResponse} res response
801 * @param {object} ctx context
803 async
handlerNotFound(req
, res
, ctx
) {
804 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
805 throw new ResponseError(Enum
.ErrorResponse
.NotFound
);
810 * @param {http.ClientRequest} req request
811 * @param {http.ServerResponse} res response
812 * @param {object} ctx context
814 async
handlerBadRequest(req
, res
, ctx
) {
815 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
816 throw new ResponseError(Enum
.ErrorResponse
.BadRequest
);
821 * @param {http.ClientRequest} req request
822 * @param {http.ServerResponse} res response
823 * @param {object} ctx context
825 async
handlerInternalServerError(req
, res
, ctx
) {
826 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
827 throw new ResponseError(Enum
.ErrorResponse
.InternalServerError
);
832 module
.exports
= Dingus
;