by default, handle HEAD requests automatically for routes with a GET handler
authorJustin Wind <justin.wind+git@gmail.com>
Sat, 26 Aug 2023 20:20:25 +0000 (13:20 -0700)
committerJustin Wind <justin.wind+git@gmail.com>
Sat, 26 Aug 2023 20:20:25 +0000 (13:20 -0700)
lib/dingus.js
test/lib/dingus.js

index 0f2af748eca503201a9c2e08be97e1bae31462ba..9bc428282b94c3f187563266377a370c65b38ccf 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, RouterNoMethodError, RouterNoPathError } = require('./errors');
+const { ResponseError, RouterNoPathError, RouterNoMethodError } = require('./errors');
 const { extensionToMime } = require('./mime-helper');
 const Router = require('./router');
 const Template = require('./template');
@@ -29,6 +29,8 @@ const defaultOptions = {
   staticMetadata: true,
   staticPath: undefined, // No reasonable default
   trustProxy: true,
+  intrinsicHeadMethod: true,
+  intrinsicHeadPersistBody: false,
   querystring,
 };
 
@@ -42,6 +44,8 @@ class Dingus {
    * @param {string} options.selfBaseUrl for constructing links
    * @param {Boolean} options.staticMetadata serve static headers with static files
    * @param {Boolean} options.trustProxy trust some header data to be provided by proxy
+   * @param {Boolean} options.intrinsicHeadMethod handle HEAD requests automatically if not specified as a route method
+   * @param {Boolean} options.intrinsicHeadPersistBody include un-sent body on ctx for automatic HEAD requests
    * @param {Object} options.querystring alternate qs parser to use
    */
   constructor(logger = console, options = {}) {
@@ -253,13 +257,14 @@ class Dingus {
 
 
   /**
-   * Dispatch the handler for a request
-   * @param {http.ClientRequest} req 
-   * @param {http.ServerResponse} res 
-   * @param {object} ctx 
+   * Resolve the handler to invoke for a request.
+   * @param {http.ClientRequest} req
+   * @param {http.ServerResponse} res
+   * @param {object} ctx
+   * @returns {object}
    */
-  async dispatch(req, res, ctx = {}) {
-    const _scope = _fileScope('dispatch');
+  _determineHandler(req, res, ctx) {
+    const _scope = _fileScope('_determineHandler');
 
     const { pathPart, queryParams } = this._splitUrl(req.url);
     ctx.queryParams = queryParams;
@@ -268,21 +273,59 @@ class Dingus {
     try {
       ({ handler, handlerArgs } = this.router.lookup(req.method, pathPart, ctx));
     } catch (e) {
-      if (e instanceof RouterNoPathError) {
+      if (e instanceof URIError) {
+        handler = this.handlerBadRequest.bind(this);
+      } else 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 });
+        if (this.intrinsicHeadMethod && req.method === 'HEAD') {
+          ({ handler, handlerArgs } = this._determineHeadHandler(req, res, ctx, pathPart));
+        } else {
+          handler = this.handlerMethodNotAllowed.bind(this);
+        }
+      } else {
+        this.logger.error(_scope, 'unexpected error', { error: e });
         handler = this.handlerInternalServerError.bind(this);
-      } else if (e instanceof URIError) {
-        handler = this.handlerBadRequest.bind(this);
+      }
+    }
+    return { handler, handlerArgs };
+  }
+
+
+  /**
+   * For intrinsic HEAD requests, resolve the handler to invoke.
+   * @param {http.ClientRequest} req
+   * @param {http.ServerResponse} res
+   * @param {object} ctx
+   * @param {string} pathPart
+   * @returns {object}
+   */
+  _determineHeadHandler(req, res, ctx, pathPart) {
+    const _scope = _fileScope('_determineHeadHandler');
+    let handler, handlerArgs = [];
+    try {
+      ({ handler, handlerArgs } = this.router.lookup('GET', pathPart, ctx));
+      Dingus.setHeadHandler(req, res, ctx, this.intrinsicHeadPersistBody);
+    } catch (e) {
+      if (e instanceof RouterNoMethodError) {
+        handler = this.handlerMethodNotAllowed.bind(this);
       } else {
-        this.logger.error(_scope, 'lookup failure', { error: e });
+        this.logger.error(_scope, 'unexpected error', { error: e });
         handler = this.handlerInternalServerError.bind(this);
       }
     }
+    return { handler, handlerArgs };
+  }
+
 
+  /**
+   * Dispatch the handler for a request
+   * @param {http.ClientRequest} req
+   * @param {http.ServerResponse} res
+   * @param {object} ctx
+   */
+  async dispatch(req, res, ctx = {}) {
+    const { handler, handlerArgs } = this._determineHandler(req, res, ctx);
     try {
       await this.preHandler(req, res, ctx);
       return await handler(req, res, ctx, ...handlerArgs);
index f374ba7d75b042f9bbc99a786c5f4b80016c934b..9d471229d3a26ebde87ebfae9d53efbde46c9aaa 100644 (file)
@@ -7,7 +7,7 @@ const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-requi
 const fs = require('fs');
 
 const Dingus = require('../../lib/dingus');
-const { DingusError } = require('../../lib/errors');
+const { DingusError, RouterNoMethodError } = require('../../lib/errors');
 const Enum = require('../../lib/enum');
 
 const noExpectedException = 'did not get expected exception';
@@ -355,6 +355,7 @@ describe('Dingus', function () {
       sinon.spy(dingus, 'handlerNotFound');
       sinon.spy(dingus, 'handlerBadRequest');
       sinon.spy(dingus, 'handlerInternalServerError');
+      sinon.spy(Dingus, 'setHeadHandler');
       stubHandler = sinon.stub();
     });
     afterEach(function () {
@@ -460,6 +461,40 @@ describe('Dingus', function () {
       assert.strictEqual(stubHandler.args[0][3], 'foo');
       assert.strictEqual(stubHandler.args[0][4], 'bar');
     });
+    describe('intrinsic HEAD handling', function () {
+      it('covers no intrinsic HEAD handling', async function () {
+        dingus.intrinsicHeadMethod = false;
+        dingus.on('GET', '/', stubHandler);
+        req.method = 'HEAD';
+        await dingus.dispatch(req, res, ctx);
+        assert(!stubHandler.called);
+        assert(dingus.handlerMethodNotAllowed.called);
+      });
+      it('calls HEAD setup and GET handler', async function () {
+        dingus.on('GET', '/', stubHandler);
+        req.method = 'HEAD';
+        await dingus.dispatch(req, res, ctx);
+        assert(Dingus.setHeadHandler.called);
+        assert(stubHandler.called);
+      });
+      it('covers no GET handler', async function () {
+        dingus.on('POST', '/', stubHandler);
+        req.method = 'HEAD';
+        await dingus.dispatch(req, res, ctx);
+        assert(!stubHandler.called);
+        assert(dingus.handlerMethodNotAllowed.called);
+      });
+      it('covers unexpected router error', async function () {
+        sinon.stub(dingus.router, 'lookup')
+          .onFirstCall().throws(new RouterNoMethodError())
+          .onSecondCall().throws(new DingusError())
+        ;
+        dingus.on('GET', '/', stubHandler);
+        req.method = 'HEAD';
+        await dingus.dispatch(req, res, ctx);
+        assert(dingus.handlerInternalServerError.called);
+      });
+    });
   }); // dispatch
 
   describe('parseBody', function () {