change default of ignoreTrailingSlash to true
[squeep-api-dingus] / lib / dingus.js
index 4eded66a727e1d831e7249b462abb3d5da50fa17..0f2af748eca503201a9c2e08be97e1bae31462ba 100644 (file)
@@ -13,7 +13,7 @@ const querystring = require('querystring');
 const common = require('./common');
 const ContentNegotiation = require('./content-negotiation');
 const Enum = require('./enum');
-const { DingusError, ResponseError } = require('./errors');
+const { DingusError, ResponseError, RouterNoMethodError, RouterNoPathError } = require('./errors');
 const { extensionToMime } = require('./mime-helper');
 const Router = require('./router');
 const Template = require('./template');
@@ -22,12 +22,12 @@ const Template = require('./template');
 const _fileScope = common.fileScope(__filename);
 
 const defaultOptions = {
-  ignoreTrailingSlash: false,
+  ignoreTrailingSlash: true,
   proxyPrefix: '',
   strictAccept: true,
   selfBaseUrl: '',
   staticMetadata: true,
-  staticPath: undefined, // no reasonable default
+  staticPath: undefined, // No reasonable default
   trustProxy: true,
   querystring,
 };
@@ -36,7 +36,7 @@ class Dingus {
   /**
    * @param {Object} logger object which implements logging methods
    * @param {Object} options
-   * @param {Boolean} options.ignoreTrailingSlash 
+   * @param {Boolean} options.ignoreTrailingSlash requests for '/foo/' will match a '/foo' route
    * @param {string} options.proxyPrefix leading part of url path to strip
    * @param {Boolean} options.strictAccept whether to error on unsupported Accept type
    * @param {string} options.selfBaseUrl for constructing links
@@ -44,7 +44,7 @@ class Dingus {
    * @param {Boolean} options.trustProxy trust some header data to be provided by proxy
    * @param {Object} options.querystring alternate qs parser to use
    */
-  constructor(logger = common.nullLogger, options = {}) {
+  constructor(logger = console, options = {}) {
     common.setOptions(this, defaultOptions, options);
 
     this.router = new Router(options);
@@ -60,7 +60,6 @@ class Dingus {
     ];
 
     this.logger = logger;
-    common.ensureLoggerLevels(this.logger);
   }
 
 
@@ -139,9 +138,9 @@ class Dingus {
    */
   _getAddress(req) {
     // TODO: RFC7239 Forwarded support
-    const address = (this.trustProxy && req && req.getHeader(Enum.Header.XForwardedFor)) ||
-      (this.trustProxy && req && req.getHeader(Enum.Header.XRealIP)) ||
-      (req && req.connection && req.connection.remoteAddress) ||
+    const address = (this.trustProxy && req?.getHeader(Enum.Header.XForwardedFor)) ||
+      (this.trustProxy && req?.getHeader(Enum.Header.XRealIP)) ||
+      (req?.connection?.remoteAddress) ||
       '';
     return address.split(/\s*,\s*/u)[0];
   }
@@ -153,8 +152,8 @@ class Dingus {
    */
   _getProtocol(req) {
     // TODO: RFC7239 Forwarded support
-    const protocol = (this.trustProxy && req && req.getHeader(Enum.Header.XForwardedProto)) ||
-      ((req && req.connection && req.connection.encrypted) ? 'https' : 'http');
+    const protocol = (this.trustProxy && req?.getHeader(Enum.Header.XForwardedProto)) ||
+      ((req?.connection?.encrypted) ? 'https' : 'http');
     return protocol.split(/\s*,\s*/u)[0];
   }
 
@@ -269,18 +268,13 @@ class Dingus {
     try {
       ({ handler, handlerArgs } = this.router.lookup(req.method, pathPart, ctx));
     } catch (e) {
-      if (e instanceof DingusError) {
-        switch (e.message) {
-          case 'NoPath':
-            handler = this.handlerNotFound.bind(this);
-            break;
-          case 'NoMethod':
-            handler = this.handlerMethodNotAllowed.bind(this);
-            break;
-          default:
-            this.logger.error(_scope, 'unknown dingus error', { error: e });
-            handler = this.handlerInternalServerError.bind(this);
-        }
+      if (e instanceof RouterNoPathError) {
+        handler = this.handlerNotFound.bind(this);
+      } else if (e instanceof RouterNoMethodError) {
+        handler = this.handlerMethodNotAllowed.bind(this);
+      } else if (e instanceof DingusError) {
+        this.logger.error(_scope, 'unknown dingus error', { error: e });
+        handler = this.handlerInternalServerError.bind(this);
       } else if (e instanceof URIError) {
         handler = this.handlerBadRequest.bind(this);
       } else {
@@ -314,16 +308,11 @@ class Dingus {
    * Parse rawBody as contentType into ctx.parsedBody.
    * @param {string} contentType
    * @param {object} ctx
-   * @param {string|buffer}
+   * @param {string|buffer} rawBody
   */
   parseBody(contentType, ctx, rawBody) {
     const _scope = _fileScope('parseBody');
 
-    if (!rawBody) {
-      // 1.2.4 and earlier expected rawBody on context
-      rawBody = ctx.rawBody;
-    }
-
     switch (contentType) {
       case Enum.ContentType.ApplicationForm:
         ctx.parsedBody = this.querystring.parse(rawBody);
@@ -349,8 +338,9 @@ class Dingus {
    * Return all body data from a request.
    * @param {http.ClientRequest} req
    * @param {Number=} maximumBodySize
+   * @param {Boolean=} toString
    */
-  async bodyData(req, maximumBodySize) {
+  async bodyData(req, maximumBodySize, toString = true) {
     const _scope = _fileScope('bodyData');
     return new Promise((resolve, reject) => {
       const body = [];
@@ -363,7 +353,10 @@ class Dingus {
           reject(new ResponseError(Enum.ErrorResponse.RequestEntityTooLarge));
         }
       });
-      req.on('end', () => resolve(Buffer.concat(body).toString()));
+      req.on('end', () => {
+        const bodyBuffer = Buffer.concat(body);
+        resolve(toString ? bodyBuffer.toString() : bodyBuffer);
+      });
       req.on('error', (e) => {
         this.logger.error(_scope, 'failed', { error: e });
         reject(e);
@@ -535,7 +528,7 @@ class Dingus {
     // We will not deal with any subdirs, nor any dot-files.
     // (Note that we could not deal with subdirs even if we wanted, due to simple router matching scheme.)
     if (fileName.indexOf(path.sep) >= 0
-    ||  fileName.charAt(0) === '.') {
+    ||  fileName.startsWith('.')) {
       this.logger.debug(_scope, 'rejected filename', { fileName });
       return this.handlerNotFound(req, res, ctx);
     }
@@ -561,18 +554,20 @@ class Dingus {
         break;
       }
       const suffix = Enum.EncodingTypeSuffix[encoding];
-      if (suffix) {
-        const encodedFilePath = `${filePath}${suffix}`;
-        const [ encodedStat, encodedData ] = await this._readFileInfo(encodedFilePath);
-        if (encodedStat) {
-          ([ stat, data ] = [ encodedStat, encodedData ]);
-          ctx.selectedEncoding = encoding;
-          Dingus.addEncodingHeader(res, encoding);
-          res.setHeader(Enum.Header.Vary, Enum.Header.AcceptEncoding);
-          this.logger.debug(_scope, 'serving encoded version', { ctx, encodedFilePath });
-        }
-        break;
+      if (!suffix) {
+        this.logger.error(_scope, 'supported encoding missing mapped suffix', { ctx, encoding });
+        continue;
+      }
+      const encodedFilePath = `${filePath}${suffix}`;
+      const [ encodedStat, encodedData ] = await this._readFileInfo(encodedFilePath);
+      if (encodedStat) {
+        ([ stat, data ] = [ encodedStat, encodedData ]);
+        ctx.selectedEncoding = encoding;
+        Dingus.addEncodingHeader(res, encoding);
+        res.setHeader(Enum.Header.Vary, Enum.Header.AcceptEncoding);
+        this.logger.debug(_scope, 'serving encoded version', { ctx, encodedFilePath });
       }
+      break;
     }
 
     const lastModifiedDate = new Date(stat.mtimeMs);
@@ -646,7 +641,7 @@ class Dingus {
       res.setHeader(Enum.Header.ContentType, Enum.ContentType.TextPlain);
     }
 
-    if (err && err.statusCode) {
+    if (err?.statusCode) {
       res.statusCode = err.statusCode;
       body = this.renderError(res.getHeader(Enum.Header.ContentType), err);
       this.logger.debug(_scope, 'handler error', { err, req, res, ctx });