Skip to content

Plugin passes the raw Node request to Next, bypassing Harper's mount-relative path rewriting #61

Description

@kriszyp

Summary

The plugin hands Next.js the raw Node request instead of the Harper Request, so it never sees the mount-relative path Harper computed. An app mounted at a urlPath therefore gives Next the un-stripped URL, which Next cannot route.

What happens

Harper's router already does the right thing. server/middlewareChain.ts builds a separate middleware chain per urlPath/host, and when a mounted route matches it wraps the request with stripPrefix(request, route.urlPath) — a Proxy that rewrites pathname and url (keeping the original available as originalPathname) — then dispatches only that route's chain.

The plugin then reaches past that Proxy to the object underneath:

// src/plugin.ts
requestHandler(request._nodeRequest, request._nodeResponse, urlParse(request._nodeRequest.url, true))

_nodeRequest is the underlying Node IncomingMessage. The Proxy rewrote request.url, not request._nodeRequest.url. So for an app mounted at /foo, a request to /foo/file.html reaches Next as /foo/file.html when Harper had already resolved it to /file.html.

Suggested fix

Request.withNodeAdapter() exists for exactly this. It proxies _nodeRequest but overrides method, url and headers with the current Request's values — its docstring notes these "may have been modified by middleware" — and gives back a ServerResponse that captures status/headers/body:

return request.withNodeAdapter((req, res) => requestHandler(req, res, urlParse(req.url, true)));

Two things to carry across from the adapter's contract:

  • It resolves to { status, headers, body } where body is a PassThrough. The docstring flags that an error listener must be attached before the body is consumed, or a connection reset after headers are sent throws an uncaught exception.
  • The upgrade handler (/_next/webpack-hmr) uses _nodeRequest too. That is the upgrade path rather than the HTTP path, so it needs its own assessment rather than the same change applied blindly.

Not sufficient on its own

Fixing the inbound path makes Next route correctly under a mount, but Next also generates URLs — page links and especially /_next/* asset paths. Those would still be emitted at the root and 404 outside the mount. Emitting prefixed URLs requires basePath, which Next bakes in at build time.

So the complete story for serving a Next app under a urlPath is:

  1. this fix, so inbound requests are mount-relative; and
  2. the app built with basePath matching its mount.

Worth deciding whether the plugin should require/inject basePath when a urlPath is configured, or fail loudly when they disagree — silently serving an app whose assets all 404 is the worst of the three.

Why it matters now

This is the blocker for hosting multiple Next apps on one Harper instance via urlPath mounting, which is the "simpler apps" tier of the application-isolation work (HarperFast/harper#642, and the thread-isolation design that came out of it). Today a single Next app at the root works; two apps under different urlPaths do not.

🤖 Filed by Claude on behalf of Kris

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Fields

    Priority

    P2

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions