Skip to content

static.urlPath subpath mounts return 404 in 5.1.x #1730

Description

@hdbjeff

static.urlPath subpath mounts return 404 in 5.1.x

Summary

A minimal Harper app using the built-in static component returns 404 for static pages mounted under /app in 5.1.17. The same static configuration works in 5.0.32.

This appears to be a regression in the built-in static component when urlPath is configured. Root-mounted static serving still works in 5.1.17.

Versions Tested

  • Works: harper@5.0.32
  • Fails: harper@5.1.17

Minimal Config

static:
  files: 'site/**'
  urlPath: 'app'
  index: true
  extensions: ['html']

The same result occurs with:

urlPath: '/app'

Expected Behavior

Requests under /app should serve files from site/**, matching 5.0.32 behavior:

  • /app redirects to /app/
  • /app/ serves site/index.html
  • /app/index.html serves site/index.html
  • /app/search serves site/search.html when extensions: ['html'] is configured
  • /app/logo.png serves site/logo.png

Actual Behavior In 5.1.17

All /app static requests return 404.

Reproduction

Create a minimal app:

APP=$(mktemp -d)
mkdir -p "$APP/site"

cat > "$APP/package.json" <<'JSON'
{
  "name": "harper-static-urlpath-repro",
  "version": "1.0.0",
  "type": "module"
}
JSON

cat > "$APP/site/index.html" <<'HTML'
<!doctype html>
<title>static repro</title>
<h1>ok</h1>
HTML

cat > "$APP/site/search.html" <<'HTML'
<!doctype html>
<title>search</title>
<h1>search</h1>
HTML

printf 'logo\n' > "$APP/site/logo.png"

cat > "$APP/config.yaml" <<'YAML'
static:
  files: 'site/**'
  urlPath: 'app'
  index: true
  extensions: ['html']
YAML

Run the app with an isolated Harper root. Replace the package version to compare:

VERSION=5.1.17
ROOT=$(mktemp -d)
HOME_DIR=$(mktemp -d)
LOG=$(mktemp)

(
  cd "$APP" &&
  env \
    HOME="$HOME_DIR" \
    ROOTPATH="$ROOT" \
    HDB_ADMIN_USERNAME=admin \
    HDB_ADMIN_PASSWORD=password \
    NODE_HOSTNAME=localhost \
    DEFAULTS_MODE=dev \
    npm exec --yes --package "harper@$VERSION" -- harper dev . > "$LOG" 2>&1
) &
PID=$!

for i in $(seq 1 90); do
  node -e "fetch('http://127.0.0.1:9926/', { redirect: 'manual' }).then(() => process.exit(0)).catch(() => process.exit(1))" >/dev/null 2>&1 && break
  sleep 1
done

node <<'NODE'
const paths = [
  '/',
  '/index.html',
  '/search',
  '/search.html',
  '/logo.png',
  '/app',
  '/app/',
  '/app/index.html',
  '/app/search',
  '/app/logo.png',
];

for (const path of paths) {
  const response = await fetch(`http://127.0.0.1:9926${path}`, { redirect: 'manual' });
  console.log(
    path.padEnd(18),
    response.status,
    (response.headers.get('location') || '').padEnd(8),
    response.headers.get('content-type') || ''
  );
  await response.body?.cancel();
}
NODE

kill -INT "$PID"
wait "$PID"

Observed Results

With harper@5.0.32:

/                  404
/index.html        404
/search            404
/search.html       404
/logo.png          404
/app               301 /app/
/app/              200          text/html; charset=utf-8
/app/index.html    200          text/html; charset=utf-8
/app/search        200          text/html; charset=utf-8
/app/logo.png      200          image/png

With harper@5.1.17:

/                  404
/index.html        404
/search            404
/search.html       404
/logo.png          404
/app               404
/app/              404
/app/index.html    404
/app/search        404
/app/logo.png      404

Control case: removing urlPath in 5.1.17 makes root-mounted static serving work:

/                  200          text/html; charset=utf-8
/index.html        200          text/html; charset=utf-8
/search            200          text/html; charset=utf-8
/search.html       200          text/html; charset=utf-8
/logo.png          200          image/png
/app/              404
/app/index.html    404

Likely Cause

This appears related to the 5.1 routing/middleware changes.

In 5.0.32, the static component registers an HTTP handler with { runFirst: true } and looks up static files using the original request path.

In 5.1.17, component urlPath is injected into server.http routing options. That creates two failure modes:

  1. urlPath: app

    • Static file entries are indexed under /app/....
    • The route matcher appears to use the raw route urlPath string, so app does not match request path /app/....
    • Result: the static handler is not invoked for /app/....
  2. urlPath: /app

    • The route matches /app/....
    • The 5.1 router strips /app before invoking the static handler.
    • The static handler then looks up /index.html, but entries were indexed as /app/index.html.
    • Result: handler runs, but lookup misses and returns 404.

So the issue is not that static serving is generally broken in 5.1.17. It is specifically static.urlPath subpath mounting.

Suggested Fix Direction

The static component probably needs one of:

  • opt out of 5.1 prefix stripping,
  • index static entries relative to the stripped request path when mounted via urlPath,
  • or have the middleware route matcher and static entry path derivation agree on the same normalized path semantics.

If static.urlPath is no longer intended to mount static files under a URL prefix in 5.1, the migration path should be documented and the old config should fail loudly instead of serving all pages as 404.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions