#935·evershop

[BUG] HEAD method not working

Author: networkinssCreated May 3, 2026Updated May 3, 2026

Describe the bug Evershop's request router matches HTTP methods literally against the methods array in each route.json. Because route definitions only declare GET (or POST etc.), a HEAD request to a URL that exists for GET falls through to the catch-all 404 handler instead of being treated as a body-less GET. This violates RFC 9110 §9.3.2 ("a server SHOULD send the same header fields in response to a HEAD request as it would have sent if the request had been a GET") and breaks any tooling that probes resources via HEAD — CDN/cache revalidation, uptime monitors, link checkers, browser prefetch.

To Reproduce Steps to reproduce the behavior:

  1. Run any Evershop instance with at least one product image uploaded:

    bash
    curl -F "[email protected]" http://localhost:3001/api/images/catalog/product
    # → /assets/catalog/product/some.png is now served
  2. Issue a GET to confirm the resource exists:

    bash
    curl -i http://localhost:3001/assets/catalog/product/some.png
    # HTTP/1.1 200 OK
    # Content-Type: image/png
  3. Issue a HEAD to the same URL:

    bash
    curl -I http://localhost:3001/assets/catalog/product/some.png
    # HTTP/1.1 404 Not Found
    # Content-Type: text/html; charset=utf-8

The same pattern reproduces against any storefront page (HEAD / → 404), any extension-defined GET API route, and any built-in catalog GET.

Expected behavior Either of:

  • Preferred: HEAD is treated as GET when no explicit HEAD route exists. Response is 200 OK with the same headers GET would produce, and an empty body. This matches RFC 9110 and Express's default behavior for app.get(path, handler).
  • Acceptable fallback: if the project deliberately wants method-explicit routing, HEAD to a GET-only resource should return 405 Method Not Allowed (with an Allow: GET header), not 404 Not Found. 404 lies about resource existence and breaks If-None-Match / If-Modified-Since revalidation.

Screenshots If applicable, add screenshots to help explain your problem.

Not applicable

Additional context

Root cause: In bin/lib/addDefaultMiddlewareFuncs.js, the route matcher does:

javascript
const matchedRoutes = routes.filter((r) => {
  const regexp = pathToRegexp(r.path, []);
  const match = regexp.exec(requestPath);
  if (match && r.method.includes(method)) {
    return true;
  }
  return false;
});

r.method is the literal array from route.json (e.g. ["GET"]). When the incoming method is "HEAD", includes("HEAD") returns false, no route matches, request.currentRoute stays unset, and the request flows through to the 404 handler in the same file (around line 214).

This contrasts with Express's stock behavior: app.get(path, handler) registers the handler for both GET and HEAD automatically, and Express strips the body before sending the HEAD response.

Why it matters in practice:

  • Cache revalidation. We hit this while debugging cache behavior on Cache-Control: max-age=31536000, immutable for product images: tools that try a HEAD first to check Last-Modified/ETag get a 404, masking the real state.
  • Uptime monitors and link checkers (most default to HEAD because it's cheap).
  • Browser prefetch / <link rel="preload"> validation.
  • Generic curl-based debugging — a developer running curl -sI <url> to confirm a route exists gets a misleading 404, even though the resource is reachable. This wasted several hours of debugging on our side.

Suggested fix: one-line change to the matcher to fall back to GET for HEAD:

javascript
const matchedRoutes = routes.filter((r) => {
  const regexp = pathToRegexp(r.path, []);
  if (!regexp.exec(requestPath)) return false;
  if (r.method.includes(method)) return true;
  if (method === 'HEAD' && r.method.includes('GET')) return true;
  return false;
});

Source: evershopcommerce/evershop