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