log a little less about static meta headers
[squeep-api-dingus] / lib / dingus.js
1 /* eslint-disable security/detect-object-injection */
2 'use strict';
3
4 /**
5 * A very minimal API server framework.
6 * Just a self-contained router and some request glue.
7 */
8
9 require('./patches');
10 const { promises: fsPromises } = require('fs');
11 const path = require('path');
12 const querystring = require('querystring');
13 const common = require('./common');
14 const ContentNegotiation = require('./content-negotiation');
15 const Enum = require('./enum');
16 const { ResponseError, RouterNoPathError, RouterNoMethodError } = require('./errors');
17 const { extensionToMime } = require('./mime-helper');
18 const Router = require('./router');
19 const Template = require('./template');
20
21 // For logging.
22 const { fileScope } = require('@squeep/log-helper');
23 const _fileScope = fileScope(__filename);
24
25 const defaultOptions = {
26 ignoreTrailingSlash: true,
27 proxyPrefix: '',
28 strictAccept: true,
29 selfBaseUrl: '',
30 staticMetadata: true,
31 staticPath: undefined, // No reasonable default
32 trustProxy: true,
33 intrinsicHeadMethod: true,
34 intrinsicHeadPersistBody: false,
35 querystring,
36 };
37
38 class Dingus {
39 /**
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
51 */
52 constructor(logger = console, options = {}) {
53 common.setOptions(this, defaultOptions, options);
54
55 this.router = new Router(options);
56
57 if (!this.proxyPrefix) {
58 this._stripPrefix = (p) => p;
59 }
60
61 this.responseTypes = [
62 Enum.ContentType.TextHTML,
63 Enum.ContentType.TextPlain,
64 Enum.ContentType.ApplicationJson,
65 ];
66
67 this.logger = logger;
68 }
69
70
71 /**
72 * Resolve relative and empty paths in url
73 * @param {string} p path
74 */
75 _normalizePath(p) {
76 const pathNorm = path.normalize(p); // This isn't perfectly correct, but it's easy...
77 return this._stripPrefix(pathNorm);
78 }
79
80
81 /**
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
85 */
86 _stripPrefix(p) {
87 if (p.startsWith(this.proxyPrefix)) {
88 return p.slice(this.proxyPrefix.length);
89 }
90 return p;
91 }
92
93
94 /**
95 * Returns the path part, and querystring object, from a request url.
96 * @param {string} url
97 */
98 _splitUrl(url) {
99 const [ p, qs ] = common.splitFirst(url, '?');
100 return {
101 pathPart: this._normalizePath(p),
102 queryParams: this.querystring.parse(qs),
103 };
104 }
105
106
107 /**
108 * Insert a new path handler
109 * @param {string} method
110 * @param {string} urlPath
111 * @param {fn} handler
112 */
113 on(method, urlPath, handler, ...handlerArgs) {
114 this.router.on(method, urlPath, handler, handlerArgs);
115 }
116
117
118 /**
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
124 */
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);
131 if (v) {
132 ctx[h.replace(/-/g, '')] = v;
133 res.setHeader(h, v);
134 }
135 });
136 return requestId;
137 }
138
139
140 /**
141 *
142 * @param {http.ClientRequest} req
143 */
144 _getAddress(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) ||
149 '';
150 return address.split(/\s*,\s*/u)[0];
151 }
152
153
154 /**
155 *
156 * @param {http.ClientRequest} req
157 */
158 _getProtocol(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];
163 }
164
165
166 /**
167 *
168 * @param {http.ClientRequest} req
169 * @param {http.ServerResponse} res
170 * @param {object} ctx
171 */
172 clientAddressContext(req, res, ctx) {
173 ctx.clientAddress = this._getAddress(req);
174 ctx.clientProtocol = this._getProtocol(req);
175 }
176
177
178 /**
179 * Called before every request handler.
180 * @param {http.ClientRequest} req
181 * @param {http.ServerResponse} res
182 * @param {object} ctx
183 */
184 async preHandler(req, res, ctx) {
185 Dingus.tagContext(req, res, ctx);
186 this.clientAddressContext(req, res, ctx);
187 }
188
189
190 /**
191 * Helper for collecting chunks as array of buffers.
192 * @param {Buffer[]} chunks
193 * @param {string|Buffer} chunk
194 * @param {string} encoding
195 */
196 static pushBufChunk(chunks, chunk, encoding = 'utf8') {
197 if (chunk) {
198 if (typeof chunk === 'string') {
199 chunk = Buffer.from(chunk, encoding);
200 }
201 chunks.push(chunk);
202 }
203 }
204
205
206 /**
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)
212 */
213 static setEndBodyHandler(req, res, ctx, handler) {
214 const origWrite = res.write.bind(res);
215 const origEnd = res.end.bind(res);
216 const chunks = [];
217 res.write = function (chunk, encoding, ...rest) {
218 Dingus.pushBufChunk(chunks, chunk, encoding);
219 return origWrite(chunk, encoding, ...rest);
220 };
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);
226 };
227 }
228
229
230 /**
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
238 */
239 static setHeadHandler(req, res, ctx, persistResponseBody = false) {
240 if (req.method === 'HEAD') {
241 const origEnd = res.end.bind(res);
242 const chunks = [];
243 res.write = function (chunk, encoding) {
244 Dingus.pushBufChunk(chunks, chunk, encoding);
245 // No call to original res.write.
246 };
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;
253 }
254 return origEnd(undefined, encoding, ...rest);
255 };
256 }
257 }
258
259
260 /**
261 * Resolve the handler to invoke for a request.
262 * @param {http.ClientRequest} req
263 * @param {http.ServerResponse} res
264 * @param {object} ctx
265 * @returns {object}
266 */
267 _determineHandler(req, res, ctx) {
268 const _scope = _fileScope('_determineHandler');
269
270 const { pathPart, queryParams } = this._splitUrl(req.url);
271 ctx.queryParams = queryParams;
272
273 let handler, handlerArgs = [];
274 try {
275 ({ handler, handlerArgs } = this.router.lookup(req.method, pathPart, ctx));
276 } catch (e) {
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));
284 } else {
285 handler = this.handlerMethodNotAllowed.bind(this);
286 }
287 } else {
288 this.logger.error(_scope, 'unexpected error', { error: e });
289 handler = this.handlerInternalServerError.bind(this);
290 }
291 }
292 return { handler, handlerArgs };
293 }
294
295
296 /**
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
302 * @returns {object}
303 */
304 _determineHeadHandler(req, res, ctx, pathPart) {
305 const _scope = _fileScope('_determineHeadHandler');
306 let handler, handlerArgs = [];
307 try {
308 ({ handler, handlerArgs } = this.router.lookup('GET', pathPart, ctx));
309 Dingus.setHeadHandler(req, res, ctx, this.intrinsicHeadPersistBody);
310 } catch (e) {
311 if (e instanceof RouterNoMethodError) {
312 handler = this.handlerMethodNotAllowed.bind(this);
313 } else {
314 this.logger.error(_scope, 'unexpected error', { error: e });
315 handler = this.handlerInternalServerError.bind(this);
316 }
317 }
318 return { handler, handlerArgs };
319 }
320
321
322 /**
323 * Dispatch the handler for a request
324 * @param {http.ClientRequest} req
325 * @param {http.ServerResponse} res
326 * @param {object} ctx
327 */
328 async dispatch(req, res, ctx = {}) {
329 const { handler, handlerArgs } = this._determineHandler(req, res, ctx);
330 try {
331 await this.preHandler(req, res, ctx);
332 return await handler(req, res, ctx, ...handlerArgs);
333 } catch (e) {
334 ctx.error = e;
335 this.sendErrorResponse(e, req, res, ctx);
336 }
337 }
338
339
340 /**
341 * Return normalized type, without any parameters.
342 * @param {http.ClientRequest} req
343 * @returns {string}
344 */
345 static getRequestContentType(req) {
346 const contentType = req.getHeader(Enum.Header.ContentType);
347 return (contentType || '').split(';')[0].trim().toLowerCase();
348 }
349
350
351 /**
352 * Parse rawBody as contentType into ctx.parsedBody.
353 * @param {string} contentType
354 * @param {object} ctx
355 * @param {string|buffer} rawBody
356 */
357 parseBody(contentType, ctx, rawBody) {
358 const _scope = _fileScope('parseBody');
359
360 switch (contentType) {
361 case Enum.ContentType.ApplicationForm:
362 ctx.parsedBody = this.querystring.parse(rawBody);
363 break;
364
365 case Enum.ContentType.ApplicationJson:
366 try {
367 ctx.parsedBody = JSON.parse(rawBody);
368 } catch (e) {
369 this.logger.debug(_scope, 'JSON parse failed', { requestId: ctx.requestId, error: e });
370 throw new ResponseError(Enum.ErrorResponse.BadRequest, e.message);
371 }
372 break;
373
374 default:
375 this.logger.debug(_scope, 'unhandled content-type', { requestId: ctx.requestId, contentType });
376 throw new ResponseError(Enum.ErrorResponse.UnsupportedMediaType);
377 }
378 }
379
380
381 /**
382 * Return all body data from a request.
383 * @param {http.ClientRequest} req
384 * @param {Number=} maximumBodySize
385 * @param {Boolean=} toString
386 */
387 async bodyData(req, maximumBodySize, toString = true) {
388 const _scope = _fileScope('bodyData');
389 return new Promise((resolve, reject) => {
390 const body = [];
391 let length = 0;
392 req.on('data', (chunk) => {
393 body.push(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));
398 }
399 });
400 req.on('end', () => {
401 const bodyBuffer = Buffer.concat(body);
402 resolve(toString ? bodyBuffer.toString() : bodyBuffer);
403 });
404 req.on('error', (e) => {
405 this.logger.error(_scope, 'failed', { error: e });
406 reject(e);
407 });
408 });
409 }
410
411
412 /**
413 * Read and parse request body data.
414 * @param {http.ClientRequest} req
415 * @param {http.ServerResponse} res
416 * @param {object} ctx
417 * @param {object}
418 * @param {Boolean} .parseEmptyBody
419 * @param {Boolean} .persistRawBody
420 */
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;
425 }
426 if (rawBody || parseEmptyBody) {
427 const contentType = Dingus.getRequestContentType(req);
428 this.parseBody(contentType, ctx, rawBody);
429 }
430 }
431
432
433 /**
434 * Return the best matching response type.
435 * @param {string[]} responseTypes
436 * @param {http.ClientRequest} req
437 */
438 static getResponseContentType(responseTypes, req) {
439 const acceptHeader = req.getHeader(Enum.Header.Accept);
440 return ContentNegotiation.accept(responseTypes, acceptHeader);
441 }
442
443
444 /**
445 * Returns a list of the most-preferred content encodings for the response.
446 * @param {string[]} responseEncodings
447 * @param {http.ClientRequest} req
448 */
449 static getResponseEncoding(responseEncodings, req) {
450 const acceptEncodingHeader = req.getHeader(Enum.Header.AcceptEncoding);
451 return ContentNegotiation.preferred(responseEncodings, acceptEncodingHeader);
452 }
453
454
455 /**
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
461 */
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);
469 } else {
470 ctx.responseType = responseTypes[0];
471 }
472 }
473 res.setHeader(Enum.Header.ContentType, ctx.responseType);
474 }
475
476
477 /**
478 * Inserts an encoding
479 * @param {http.ServerResponse} res
480 * @param {string} encoding
481 */
482 static addEncodingHeader(res, encoding) {
483 const existingEncodings = res.getHeader(Enum.Header.ContentEncoding);
484 if (existingEncodings) {
485 encoding = `${encoding}, ${existingEncodings}`;
486 }
487 res.setHeader(Enum.Header.ContentEncoding, encoding);
488 }
489
490
491 /**
492 * Attempt to fetch both data and metadata for a file.
493 * @param {string} filePath
494 */
495 async _readFileInfo(filePath) {
496 const _scope = _fileScope('_readFileInfo');
497 let result;
498 try {
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]);
504 } catch (e) {
505 if (['ENOENT', 'EACCES', 'EISDIR', 'ENAMETOOLONG', 'EINVAL'].includes(e.code)) {
506 return [null, null];
507 }
508 this.logger.error(_scope, 'fs error', { error: e, filePath });
509 throw e;
510 }
511 return result;
512 }
513
514
515 /**
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
520 */
521 async _serveFileMetaHeaders(res, directory, fileName) {
522 const _scope = _fileScope('_serveFileMetaHeaders');
523
524 let added = false;
525 const metaPrefix = '.';
526 const metaSuffix = '.meta';
527 const metaFileName = `${metaPrefix}${fileName}${metaSuffix}`;
528 const metaFilePath = path.join(directory, metaFileName);
529
530 const [stat, data] = await this._readFileInfo(metaFilePath);
531 if (!stat) {
532 return added;
533 }
534
535 const lineBreakRE = /\r\n|\n|\r/;
536 const lines = data.toString().split(lineBreakRE);
537 common.unfoldHeaderLines(lines);
538
539 const headerParseRE = /^(?<name>[^:]+): +(?<value>.*)$/;
540 lines.forEach((line) => {
541 if (line) {
542 const result = headerParseRE.exec(line);
543 const { groups: header } = result;
544 res.setHeader(header.name, header.value);
545 added = true;
546 }
547 });
548 return added;
549 }
550
551
552 /**
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
560 */
561 async serveFile(req, res, ctx, directory, fileName) {
562 const _scope = _fileScope('serveFile');
563 this.logger.debug(_scope, 'called', { req, ctx });
564
565 // Require a directory field.
566 if (!directory) {
567 this.logger.debug(_scope, 'rejected unset directory', { fileName });
568 return this.handlerNotFound(req, res, ctx);
569 }
570
571 // Normalize the supplied path, as encoded path-navigation may have been (maliciously) present.
572 fileName = path.normalize(fileName);
573
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);
580 }
581
582 const filePath = path.join(directory, fileName);
583
584 // File must exist, before any alternate static encodings will be considered.
585 let [stat, data] = await this._readFileInfo(filePath);
586 if (!stat) {
587 return this.handlerNotFound(req, res, ctx);
588 }
589
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);
597 }
598 for (const encoding of ctx.availableEncodings) {
599 if (encoding === Enum.EncodingType.Identity) {
600 break;
601 }
602 const suffix = Enum.EncodingTypeSuffix[encoding];
603 if (!suffix) {
604 this.logger.error(_scope, 'supported encoding missing mapped suffix', { ctx, encoding });
605 continue;
606 }
607 const encodedFilePath = `${filePath}${suffix}`;
608 const [ encodedStat, encodedData ] = await this._readFileInfo(encodedFilePath);
609 if (encodedStat) {
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 });
615 }
616 break;
617 }
618
619 const lastModifiedDate = new Date(stat.mtimeMs);
620 res.setHeader(Enum.Header.LastModified, lastModifiedDate.toGMTString());
621
622 const eTag = common.generateETag(filePath, stat, data);
623 res.setHeader(Enum.Header.ETag, eTag);
624
625 if (common.isClientCached(req, stat.mtimeMs, eTag)) {
626 this.logger.debug(_scope, 'client cached file', { filePath });
627 res.statusCode = 304; // Not Modified
628 res.end();
629 return;
630 }
631
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);
636
637 // We presume static files are relatively cacheable.
638 res.setHeader(Enum.Header.CacheControl, 'public');
639
640 if (this.staticMetadata) {
641 ctx.metaHeaders = await this._serveFileMetaHeaders(res, directory, fileName);
642 }
643
644 this.logger.debug(_scope, 'serving file', { filePath, contentType });
645 res.end(data);
646 }
647
648
649 /**
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
656 */
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);
662
663 case Enum.ContentType.TextHTML:
664 return Template.errorHTML(err);
665
666 case Enum.ContentType.TextPlain:
667 default:
668 return [err.errorMessage, err.details].join('\r\n');
669 }
670 }
671
672
673 /**
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
680 */
681 sendErrorResponse(err, req, res, ctx) {
682 const _scope = _fileScope('sendErrorResponse');
683 let body;
684
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);
688 }
689
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 });
694 } else {
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 });
698 }
699 res.end(body);
700 }
701
702
703 /**
704 * @param {http.ClientRequest} req
705 * @param {http.ServerResponse} res
706 * @param {object} ctx
707 * @param {String} file - override ctx.params.file
708 */
709 async handlerGetStaticFile(req, res, ctx, file) {
710 Dingus.setHeadHandler(req, res, ctx);
711
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);
714
715 await this.serveFile(req, res, ctx, this.staticPath, file || ctx.params.file);
716 }
717
718
719 /**
720 * @param {http.ClientRequest} req
721 * @param {http.ServerResponse} res
722 * @param {Object} ctx
723 * @param {String} newPath
724 * @param {Number} statusCode
725 */
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;
730 res.end();
731 }
732
733
734 /**
735 * @param {http.ClientRequest} req
736 * @param {http.ServerResponse} res
737 * @param {object} ctx
738 */
739 async handlerMethodNotAllowed(req, res, ctx) {
740 this.setResponseType(this.responseTypes, req, res, ctx);
741 throw new ResponseError(Enum.ErrorResponse.MethodNotAllowed);
742 }
743
744
745 /**
746 * @param {http.ClientRequest} req
747 * @param {http.ServerResponse} res
748 * @param {object} ctx
749 */
750 async handlerNotFound(req, res, ctx) {
751 this.setResponseType(this.responseTypes, req, res, ctx);
752 throw new ResponseError(Enum.ErrorResponse.NotFound);
753 }
754
755
756 /**
757 * @param {http.ClientRequest} req
758 * @param {http.ServerResponse} res
759 * @param {object} ctx
760 */
761 async handlerBadRequest(req, res, ctx) {
762 this.setResponseType(this.responseTypes, req, res, ctx);
763 throw new ResponseError(Enum.ErrorResponse.BadRequest);
764 }
765
766
767 /**
768 * @param {http.ClientRequest} req
769 * @param {http.ServerResponse} res
770 * @param {object} ctx
771 */
772 async handlerInternalServerError(req, res, ctx) {
773 this.setResponseType(this.responseTypes, req, res, ctx);
774 throw new ResponseError(Enum.ErrorResponse.InternalServerError);
775 }
776
777 }
778
779 module.exports = Dingus;