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
= /; */
;
45 * @param {object} logger console-like object which implements logging methods
46 * @param {object} options options
47 * @param {boolean} options.ignoreTrailingSlash requests for '/foo/' will match a '/foo' route
48 * @param {string} options.proxyPrefix leading part of url path to strip
49 * @param {boolean} options.strictAccept whether to error on unsupported Accept type
50 * @param {string} options.selfBaseUrl for constructing links
51 * @param {boolean} options.staticMetadata serve static headers with static files
52 * @param {boolean} options.trustProxy trust some header data to be provided by proxy
53 * @param {boolean} options.intrinsicHeadMethod handle HEAD requests automatically if not specified as a route method
54 * @param {boolean} options.intrinsicHeadPersistBody include un-sent body on ctx for automatic HEAD requests
55 * @param {object} options.querystring alternate qs parser to use
57 constructor(logger
= console
, options
= {}) {
58 common
.setOptions(this, defaultOptions
, options
);
60 this.router
= new Router(options
);
62 if (!this.proxyPrefix
) {
63 this._stripPrefix
= (p
) => p
;
66 this.responseTypes
= [
67 Enum
.ContentType
.TextHTML
,
68 Enum
.ContentType
.TextPlain
,
69 Enum
.ContentType
.ApplicationJson
,
77 * Resolve relative and empty paths in url
78 * @param {string} p path
79 * @returns {string} normalized path
82 const pathNorm
= path
.normalize(p
); // This isn't perfectly correct, but it's easy...
83 return this._stripPrefix(pathNorm
);
88 * Remove a leading portion of url path
89 * N.B. This method gets obliterated if there is no prefix defined at construction
90 * @param {string} p path
91 * @returns {string} de-prefixed path
94 if (p
.startsWith(this.proxyPrefix
)) {
95 return p
.slice(this.proxyPrefix
.length
);
102 * Returns the path part, and querystring object, from a request url.
103 * @param {string} url url
104 * @returns {object} path string and query params
107 const [ p
, qs
] = common
.splitFirst(url
, '?');
109 pathPart: this._normalizePath(p
),
110 queryParams: this.querystring
.parse(qs
),
116 * Insert a new path handler
117 * @param {string|string[]} methods methods to match for this path, '*' allowed
118 * @param {string} urlPath request path to match
119 * @param {Function} handler handler to invoke on match
120 * @param {any[]} handlerArgs additional arguments for handler
122 on(methods
, urlPath
, handler
, ...handlerArgs
) {
123 this.router
.on(methods
, urlPath
, handler
, handlerArgs
);
128 * Common header tagging for all requests.
129 * Add our own identifier, and persist any external transit identifiers.
130 * Sets requestId on ctx to a new uuid.
131 * If X-Request-Id or X-Correlation-Id exist on incoming headers, sets them
132 * on outgoing headers and sets on ctx.
133 * @param {http.ClientRequest} req request
134 * @param {http.ServerResponse} res response
135 * @param {object} ctx context
136 * @returns {string} id added request
138 static tagContext(req
, res
, ctx
) {
139 const requestId
= common
.requestId();
140 ctx
.requestId
= requestId
;
141 res
.setHeader(Enum
.Header
.RequestId
, requestId
);
142 [Enum
.Header
.XRequestId
, Enum
.Header
.XCorrelationId
].forEach((h
) => {
143 const v
= req
.getHeader(h
);
145 ctx
[h
.replace(/-/g
, '')] = v
;
155 * @param {http.ClientRequest} req request
156 * @returns {string} first forwarded header ip
159 // TODO: RFC7239 Forwarded support
160 const address
= (this.trustProxy
&& req
?.getHeader(Enum
.Header
.XForwardedFor
)) ||
161 (this.trustProxy
&& req
?.getHeader(Enum
.Header
.XRealIP
)) ||
162 (req
?.connection
?.remoteAddress
) ||
164 return address
.split(/\s*,\s*/u)[0];
170 * @param {http.ClientRequest} req request
171 * @returns {string} first forwarded header protocol
174 // TODO: RFC7239 Forwarded support
175 const protocol
= (this.trustProxy
&& req
?.getHeader(Enum
.Header
.XForwardedProto
)) ||
176 ((req
?.connection
?.encrypted
) ? 'https' : 'http');
177 return protocol
.split(/\s*,\s*/u)[0];
182 * Sets ctx.clientAddress and ctx.clientProtocol.
183 * @param {http.ClientRequest} req request
184 * @param {http.ServerResponse} res response
185 * @param {object} ctx context
187 clientAddressContext(req
, res
, ctx
) {
188 ctx
.clientAddress
= this._getAddress(req
);
189 ctx
.clientProtocol
= this._getProtocol(req
);
194 * Sets ctx.cookie from Cookie header.
195 * @param {http.ClientRequest} req request
196 * @param {http.ServerResponse} res response
197 * @param {object} ctx context
199 static ingestCookie(req
, res
, ctx
) {
201 req
.getHeader(Enum
.Header
.Cookie
)?.split(cookieSplitRE
).forEach((cookie
) => {
202 const [ name
, value
] = common
.splitFirst(cookie
, '=', null).map((x
) => {
204 return decodeURIComponent(x
.trim());
205 } catch (e
) { // eslint-disable-line no-unused-vars
209 if (name
&& !(name
in ctx
.cookie
)) {
210 const isQuoted
= value
?.startsWith('"') && value
.endsWith('"');
211 ctx
.cookie
[name
] = isQuoted
? value
.slice(1, -1) : value
; // eslint-disable-line security/detect-object-injection
218 * Called before every request handler.
219 * Sets tracking identifiers and client information on ctx.
220 * @param {http.ClientRequest} req request
221 * @param {http.ServerResponse} res response
222 * @param {object} ctx context
224 async
preHandler(req
, res
, ctx
) {
225 this.constructor.tagContext(req
, res
, ctx
);
226 this.clientAddressContext(req
, res
, ctx
);
227 this.constructor.ingestCookie(req
, res
, ctx
);
232 * Helper for collecting chunks as array of buffers.
233 * @param {Buffer[]} chunks array to collect chunks in
234 * @param {string|Buffer} chunk chunk to collect
235 * @param {string} encoding chunk encoding
237 static pushBufChunk(chunks
, chunk
, encoding
= 'utf8') {
239 if (typeof chunk
=== 'string') {
240 chunk
= Buffer
.from(chunk
, encoding
);
248 * Sets ctx.responseBody and calls handler upon res.end().
249 * @param {http.ClientRequest} req request
250 * @param {http.ServerResponse} res response
251 * @param {object} ctx context
252 * @param {*} handler fn(req, res, ctx)
254 static setEndBodyHandler(req
, res
, ctx
, handler
) {
255 const origWrite
= res
.write
.bind(res
);
256 const origEnd
= res
.end
.bind(res
);
258 res
.write = function (chunk
, encoding
, ...rest
) {
259 Dingus
.pushBufChunk(chunks
, chunk
, encoding
);
260 return origWrite(chunk
, encoding
, ...rest
);
262 res
.end = function (data
, encoding
, ...rest
) {
263 Dingus
.pushBufChunk(chunks
, data
, encoding
);
264 ctx
.responseBody
= Buffer
.concat(chunks
);
265 handler(req
, res
, ctx
);
266 return origEnd(data
, encoding
, ...rest
);
272 * Intercept writes for head requests, do not send to client,
273 * but send length, and make body available in context.
274 * N.B. If persisted, ctx.responseBody will be a raw buffer, be aware when logging.
275 * @param {http.ClientRequest} req request
276 * @param {http.ServerResponse} res response
277 * @param {object} ctx context
278 * @param {boolean} persistResponseBody store body buffer on context
280 static setHeadHandler(req
, res
, ctx
, persistResponseBody
= false) {
281 if (req
.method
=== 'HEAD') {
282 const origEnd
= res
.end
.bind(res
);
284 res
.write = function (chunk
, encoding
) {
285 Dingus
.pushBufChunk(chunks
, chunk
, encoding
);
286 // No call to original res.write.
288 res
.end = function (data
, encoding
, ...rest
) {
289 Dingus
.pushBufChunk(chunks
, data
, encoding
);
290 const responseBody
= Buffer
.concat(chunks
);
291 res
.setHeader(Enum
.Header
.ContentLength
, Buffer
.byteLength(responseBody
));
292 if (persistResponseBody
) {
293 ctx
.responseBody
= responseBody
;
295 return origEnd(undefined, encoding
, ...rest
);
302 * Resolve the handler to invoke for a request.
303 * @param {http.ClientRequest} req request
304 * @param {http.ServerResponse} res response
305 * @param {object} ctx context
306 * @returns {object} handler and args
308 _determineHandler(req
, res
, ctx
) {
309 const _scope
= _fileScope('_determineHandler');
311 const { pathPart
, queryParams
} = this._splitUrl(req
.url
);
312 ctx
.queryParams
= queryParams
;
314 let handler
, handlerArgs
= [];
316 ({ handler
, handlerArgs
} = this.router
.lookup(req
.method
, pathPart
, ctx
));
318 if (e
instanceof URIError
) {
319 handler
= this.handlerBadRequest
.bind(this);
320 } else if (e
instanceof RouterNoPathError
) {
321 handler
= this.handlerNotFound
.bind(this);
322 } else if (e
instanceof RouterNoMethodError
) {
323 if (this.intrinsicHeadMethod
&& req
.method
=== 'HEAD') {
324 ({ handler
, handlerArgs
} = this._determineHeadHandler(req
, res
, ctx
, pathPart
));
326 handler
= this.handlerMethodNotAllowed
.bind(this);
329 this.logger
.error(_scope
, 'unexpected error', { error: e
});
330 handler
= this.handlerInternalServerError
.bind(this);
333 return { handler
, handlerArgs
};
338 * For intrinsic HEAD requests, resolve the handler to invoke.
339 * @param {http.ClientRequest} req request
340 * @param {http.ServerResponse} res response
341 * @param {object} ctx context
342 * @param {string} pathPart router path
343 * @returns {object} hander and handler args
345 _determineHeadHandler(req
, res
, ctx
, pathPart
) {
346 const _scope
= _fileScope('_determineHeadHandler');
347 let handler
, handlerArgs
= [];
349 ({ handler
, handlerArgs
} = this.router
.lookup('GET', pathPart
, ctx
));
350 Dingus
.setHeadHandler(req
, res
, ctx
, this.intrinsicHeadPersistBody
);
352 if (e
instanceof RouterNoMethodError
) {
353 handler
= this.handlerMethodNotAllowed
.bind(this);
355 this.logger
.error(_scope
, 'unexpected error', { error: e
});
356 handler
= this.handlerInternalServerError
.bind(this);
359 return { handler
, handlerArgs
};
364 * Dispatch the handler for a request
365 * @param {http.ClientRequest} req request
366 * @param {http.ServerResponse} res response
367 * @param {object} ctx context
368 * @returns {Promise<void>} handler promise
370 async
dispatch(req
, res
, ctx
= {}) {
371 const { handler
, handlerArgs
} = this._determineHandler(req
, res
, ctx
);
373 await
this.preHandler(req
, res
, ctx
);
374 return await
handler(req
, res
, ctx
, ...handlerArgs
);
377 this.sendErrorResponse(e
, req
, res
, ctx
);
383 * Return normalized type, without any parameters.
384 * @param {http.ClientRequest} req request
385 * @returns {string} bare content type
387 static getRequestContentType(req
) {
388 const contentType
= req
.getHeader(Enum
.Header
.ContentType
);
389 return (contentType
|| '').split(';')[0].trim().toLowerCase();
394 * Parse rawBody as contentType into ctx.parsedBody.
395 * @param {string} contentType type
396 * @param {object} ctx context
397 * @param {string|Buffer} rawBody body data
399 parseBody(contentType
, ctx
, rawBody
) {
400 const _scope
= _fileScope('parseBody');
402 switch (contentType
) {
403 case Enum
.ContentType
.ApplicationForm:
404 ctx
.parsedBody
= this.querystring
.parse(rawBody
);
407 case Enum
.ContentType
.ApplicationJson:
409 ctx
.parsedBody
= JSON
.parse(rawBody
);
411 this.logger
.debug(_scope
, 'JSON parse failed', { requestId: ctx
.requestId
, error: e
});
412 throw new ResponseError(Enum
.ErrorResponse
.BadRequest
, e
.message
);
417 this.logger
.debug(_scope
, 'unhandled content-type', { requestId: ctx
.requestId
, contentType
});
418 throw new ResponseError(Enum
.ErrorResponse
.UnsupportedMediaType
);
424 * Return all body data from a request.
425 * @param {http.ClientRequest} req request
426 * @param {number=} maximumBodySize bytes
427 * @param {boolean=} toString whether to convert body data buffer to string
428 * @returns {Promise<Buffer|string>} body data
430 async
bodyData(req
, maximumBodySize
, toString
= true) {
431 const _scope
= _fileScope('bodyData');
432 return new Promise((resolve
, reject
) => {
435 req
.on('data', (chunk
) => {
437 length
+= Buffer
.byteLength(chunk
);
438 if (maximumBodySize
&& length
> maximumBodySize
) {
439 this.logger
.debug(_scope
, 'body data exceeded limit', { length
, maximumBodySize
});
440 reject(new ResponseError(Enum
.ErrorResponse
.RequestEntityTooLarge
));
443 req
.on('end', () => {
444 const bodyBuffer
= Buffer
.concat(body
);
445 resolve(toString
? bodyBuffer
.toString() : bodyBuffer
);
447 req
.on('error', (e
) => {
448 this.logger
.error(_scope
, 'failed', { error: e
});
456 * Read and parse request body data.
457 * Sets ctx.parsedBody, and optionally ctx.rawBody.
458 * @param {http.ClientRequest} req request
459 * @param {http.ServerResponse} res response
460 * @param {object} ctx context
461 * @param {object} options options
462 * @param {boolean} options.parseEmptyBody attempt to parse an empty body, results may vary by encoding
463 * @param {boolean} options.persistRawBody store raw body buffer on context
464 * @param {number=} options.maximumBodySize bytes
466 async
ingestBody(req
, res
, ctx
, { parseEmptyBody
= true, persistRawBody
= false, maximumBodySize
} = {}) {
467 const rawBody
= await
this.bodyData(req
, maximumBodySize
);
468 if (persistRawBody
) {
469 ctx
.rawBody
= rawBody
;
471 if (rawBody
|| parseEmptyBody
) {
472 const contentType
= Dingus
.getRequestContentType(req
);
473 this.parseBody(contentType
, ctx
, rawBody
);
479 * Return the best matching response type.
480 * @param {string[]} responseTypes available content types
481 * @param {http.ClientRequest} req request
482 * @returns {string} preferred content type
484 static getResponseContentType(responseTypes
, req
) {
485 const acceptHeader
= req
.getHeader(Enum
.Header
.Accept
);
486 return ContentNegotiation
.accept(responseTypes
, acceptHeader
);
491 * Returns a list of the most-preferred content encodings for the response.
492 * @param {string[]} responseEncodings available encodings
493 * @param {http.ClientRequest} req request
494 * @returns {string} preferred encoding
496 static getResponseEncoding(responseEncodings
, req
) {
497 const acceptEncodingHeader
= req
.getHeader(Enum
.Header
.AcceptEncoding
);
498 return ContentNegotiation
.preferred(responseEncodings
, acceptEncodingHeader
);
503 * Set the best content type for the response.
504 * Sets ctx.responseType, and Content-Type header.
505 * @param {string[]} responseTypes default first
506 * @param {http.ClientRequest} req request
507 * @param {http.ServerResponse} res response
508 * @param {object} ctx context
510 setResponseType(responseTypes
, req
, res
, ctx
) {
511 const _scope
= _fileScope('setResponseType');
512 ctx
.responseType
= Dingus
.getResponseContentType(responseTypes
, req
);
513 if (!ctx
.responseType
) {
514 if (this.strictAccept
) {
515 this.logger
.debug(_scope
, 'unhandled strict accept', { requestId: req
.requestId
});
516 throw new ResponseError(Enum
.ErrorResponse
.NotAcceptable
);
518 ctx
.responseType
= responseTypes
[0];
521 res
.setHeader(Enum
.Header
.ContentType
, ctx
.responseType
);
526 * Inserts an encoding into Content-Encoding header.
527 * @param {http.ServerResponse} res response
528 * @param {string} encoding encoding
530 static addEncodingHeader(res
, encoding
) {
531 const existingEncodings
= res
.getHeader(Enum
.Header
.ContentEncoding
);
532 if (existingEncodings
) {
533 encoding
= `${encoding}, ${existingEncodings}`;
535 res
.setHeader(Enum
.Header
.ContentEncoding
, encoding
);
540 * Attempt to fetch both data and metadata for a file.
541 * @param {string} filePath path to file
542 * @returns {Promise<[object=, object=]>} array with file contents and stats
544 async
_readFileInfo(filePath
) {
545 const _scope
= _fileScope('_readFileInfo');
548 // eslint-disable-next-line security/detect-non-literal-fs-filename
549 const stat
= fsPromises
.stat(filePath
);
550 // eslint-disable-next-line security/detect-non-literal-fs-filename
551 const data
= fsPromises
.readFile(filePath
);
552 result
= await Promise
.all([stat
, data
]);
554 if (['ENOENT', 'EACCES', 'EISDIR', 'ENAMETOOLONG', 'EINVAL'].includes(e
.code
)) {
557 this.logger
.error(_scope
, 'fs error', { error: e
, filePath
});
565 * Potentially add additional headers from static file meta-file.
566 * @param {http.ServerResponse} res response
567 * @param {string} directory path to file
568 * @param {string} fileName - already normalized and filtered
569 * @returns {boolean} if meta headers were added
571 async
_serveFileMetaHeaders(res
, directory
, fileName
) {
572 const _scope
= _fileScope('_serveFileMetaHeaders');
575 const metaPrefix
= '.';
576 const metaSuffix
= '.meta';
577 const metaFileName
= `${metaPrefix}${fileName}${metaSuffix}`;
578 const metaFilePath
= path
.join(directory
, metaFileName
);
580 const [stat
, data
] = await
this._readFileInfo(metaFilePath
);
585 const lineBreakRE
= /\r\n|\n|\r/;
586 const lines
= data
.toString().split(lineBreakRE
);
587 common
.unfoldHeaderLines(lines
);
589 const headerParseRE
= /^(?<name
>[^:]+): +(?<value
>.*)$/;
590 lines
.forEach((line
) => {
592 const result
= headerParseRE
.exec(line
);
593 const { groups: header
} = result
;
594 res
.setHeader(header
.name
, header
.value
);
603 * Serve a file from a directory, with rudimentary cache awareness.
604 * This will also serve pre-encoded variations if available and requested.
605 * @param {http.ClientRequest} req request
606 * @param {http.ServerResponse} res response
607 * @param {object} ctx context
608 * @param {string} directory path to file
609 * @param {string} fileName file name
610 * @returns {Promise<void>=} serves file or renders status code
612 async
serveFile(req
, res
, ctx
, directory
, fileName
) {
613 const _scope
= _fileScope('serveFile');
614 this.logger
.debug(_scope
, 'called', { req
, ctx
});
616 // Require a directory field.
618 this.logger
.debug(_scope
, 'rejected unset directory', { fileName
});
619 return this.handlerNotFound(req
, res
, ctx
);
622 // Normalize the supplied path, as encoded path-navigation may have been (maliciously) present.
623 fileName
= path
.normalize(fileName
);
625 // We will not deal with any subdirs, nor any dot-files.
626 // (Note that we could not deal with subdirs even if we wanted, due to simple router matching scheme.)
627 if (fileName
.indexOf(path
.sep
) >= 0
628 || fileName
.startsWith('.')) {
629 this.logger
.debug(_scope
, 'rejected filename', { fileName
});
630 return this.handlerNotFound(req
, res
, ctx
);
633 const filePath
= path
.join(directory
, fileName
);
635 // File must exist, before any alternate static encodings will be considered.
636 let [stat
, data
] = await
this._readFileInfo(filePath
);
638 return this.handlerNotFound(req
, res
, ctx
);
641 // If encodings were requested, check for static versions to serve.
642 // Update stat and data if matching version is found.
643 ctx
.availableEncodings
= Dingus
.getResponseEncoding(Object
.values(Enum
.EncodingType
), req
);
644 if (ctx
.availableEncodings
.length
=== 0) {
645 // Identity encoding was specifically denied, and nothing else available.
646 this.logger
.debug(_scope
, 'no suitable encodings', { ctx
});
647 return this.handlerMethodNotAllowed(req
, res
, ctx
);
649 for (const encoding
of ctx
.availableEncodings
) {
650 if (encoding
=== Enum
.EncodingType
.Identity
) {
653 const suffix
= Enum
.EncodingTypeSuffix
[encoding
]; // eslint-disable-line security/detect-object-injection
655 this.logger
.error(_scope
, 'supported encoding missing mapped suffix', { ctx
, encoding
});
658 const encodedFilePath
= `${filePath}${suffix}`;
659 const [ encodedStat
, encodedData
] = await
this._readFileInfo(encodedFilePath
);
661 ([ stat
, data
] = [ encodedStat
, encodedData
]);
662 ctx
.selectedEncoding
= encoding
;
663 Dingus
.addEncodingHeader(res
, encoding
);
664 res
.setHeader(Enum
.Header
.Vary
, Enum
.Header
.AcceptEncoding
);
665 this.logger
.debug(_scope
, 'serving encoded version', { ctx
, encodedFilePath
});
670 const lastModifiedDate
= new Date(stat
.mtimeMs
);
671 res
.setHeader(Enum
.Header
.LastModified
, lastModifiedDate
.toGMTString());
673 const eTag
= common
.generateETag(filePath
, stat
, data
);
674 res
.setHeader(Enum
.Header
.ETag
, eTag
);
676 if (common
.isClientCached(req
, stat
.mtimeMs
, eTag
)) {
677 this.logger
.debug(_scope
, 'client cached file', { filePath
});
678 res
.statusCode
= 304; // Not Modified
683 // Set the type based on extension of un-encoded filename.
684 const ext
= path
.extname(filePath
).slice(1); // Drop the dot
685 const contentType
= extensionToMime(ext
);
686 res
.setHeader(Enum
.Header
.ContentType
, contentType
);
688 // We presume static files are relatively cacheable.
689 res
.setHeader(Enum
.Header
.CacheControl
, 'public');
691 if (this.staticMetadata
) {
692 ctx
.metaHeaders
= await
this._serveFileMetaHeaders(res
, directory
, fileName
);
695 this.logger
.debug(_scope
, 'serving file', { filePath
, contentType
});
701 * Return a content-type appropriate rendering of an errorResponse object.
702 * @param {string} contentType content-type of response
703 * @param {object} err either an Error object, or an error response
704 * @param {number} err.statusCode htttp status code
705 * @param {string} err.errorMessage http status message
706 * @param {string|string[]} err.details additional details to include
707 * @returns {string} rendered err
709 // eslint-disable-next-line class-methods-use-this
710 renderError(contentType
, err
) {
711 switch (contentType
) {
712 case Enum
.ContentType
.ApplicationJson:
713 return JSON
.stringify(err
);
715 case Enum
.ContentType
.TextHTML:
716 return Template
.errorHTML(err
);
718 case Enum
.ContentType
.TextPlain:
720 return [err
.errorMessage
, err
.details
].join('\r\n');
726 * Send an error response. Terminal.
727 * Logs any non-error-response errors as such.
728 * @param {object} err either an Error object, or an error response
729 * @param {http.ClientRequest} req request
730 * @param {http.ServerResponse} res response
731 * @param {object} ctx context
733 sendErrorResponse(err
, req
, res
, ctx
) {
734 const _scope
= _fileScope('sendErrorResponse');
737 // Default to a content type if one is not yet present
738 if (!res
.hasHeader(Enum
.Header
.ContentType
)) {
739 res
.setHeader(Enum
.Header
.ContentType
, Enum
.ContentType
.TextPlain
);
742 if (err
?.statusCode
) {
743 res
.statusCode
= err
.statusCode
;
744 body
= this.renderError(res
.getHeader(Enum
.Header
.ContentType
), err
);
745 this.logger
.debug(_scope
, 'handler error', { err
, req
, res
, ctx
});
747 res
.statusCode
= 500;
748 body
= this.renderError(res
.getHeader(Enum
.Header
.ContentType
), Enum
.ErrorResponse
.InternalServerError
);
749 this.logger
.error(_scope
, 'handler exception', { err
, req
, res
, ctx
});
756 * @param {http.ClientRequest} req request
757 * @param {http.ServerResponse} res response
758 * @param {object} ctx context
759 * @param {string} file - override ctx.params.file
761 async
handlerGetStaticFile(req
, res
, ctx
, file
) {
762 Dingus
.setHeadHandler(req
, res
, ctx
);
764 // Set a default response type to handle any errors; will be re-set to serve actual static content type.
765 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
767 await
this.serveFile(req
, res
, ctx
, this.staticPath
, file
|| ctx
.params
.file
);
772 * @param {http.ClientRequest} req request
773 * @param {http.ServerResponse} res response
774 * @param {object} ctx context
775 * @param {string} newPath url to redirect to
776 * @param {number=} statusCode status code to use for redirect, default 307
778 async
handlerRedirect(req
, res
, ctx
, newPath
, statusCode
= 307) {
779 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
780 res
.setHeader(Enum
.Header
.Location
, newPath
);
781 res
.statusCode
= statusCode
;
787 * @param {http.ClientRequest} req request
788 * @param {http.ServerResponse} res response
789 * @param {object} ctx context
791 async
handlerMethodNotAllowed(req
, res
, ctx
) {
792 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
793 throw new ResponseError(Enum
.ErrorResponse
.MethodNotAllowed
);
798 * @param {http.ClientRequest} req request
799 * @param {http.ServerResponse} res response
800 * @param {object} ctx context
802 async
handlerNotFound(req
, res
, ctx
) {
803 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
804 throw new ResponseError(Enum
.ErrorResponse
.NotFound
);
809 * @param {http.ClientRequest} req request
810 * @param {http.ServerResponse} res response
811 * @param {object} ctx context
813 async
handlerBadRequest(req
, res
, ctx
) {
814 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
815 throw new ResponseError(Enum
.ErrorResponse
.BadRequest
);
820 * @param {http.ClientRequest} req request
821 * @param {http.ServerResponse} res response
822 * @param {object} ctx context
824 async
handlerInternalServerError(req
, res
, ctx
) {
825 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
826 throw new ResponseError(Enum
.ErrorResponse
.InternalServerError
);
831 module
.exports
= Dingus
;