From dfb7c7f67215583c7af3a4f01b253add4229d15e Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Mon, 27 Jul 2026 11:42:34 -0600 Subject: [PATCH 1/4] docs: route applications by host/urlPath in the root config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to #595. That PR documented `host`/`urlPath` as a component-`config.yaml` setting, which described the mechanism but put the routing in the wrong place: where an application is served is a deployment concern, and a value checked into the application cannot be remapped per environment (the env-config overlay is root-config-only). Harper now treats the application's root-config entry as authoritative (HarperFast/harper PR pending), so lead with that placement: - `reference/http/overview.md` — mount an application from the root `harper-config.yaml`; a plugin's own `urlPath` positions it within the app and the mount is prefixed onto it, while a root-config `host` overrides one the app shipped. - `reference/components/plugin-api.md` — scope the plugin-level options to "within the application" and point at the root-config mount. - `reference/operations-api/operations.md` — document `host` on `deploy_component` and note that both it and `urlPath` are persisted to the root-config entry. - 5.2 release notes — lead with the application mount. --- reference/components/plugin-api.md | 4 +++- reference/http/overview.md | 29 ++++++++++++++++++++++---- reference/operations-api/operations.md | 3 ++- release-notes/v5-lincoln/5.2.md | 4 +++- 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/reference/components/plugin-api.md b/reference/components/plugin-api.md index ce801923..43211396 100644 --- a/reference/components/plugin-api.md +++ b/reference/components/plugin-api.md @@ -42,7 +42,9 @@ General plugin configuration options: - `host` — `string` _(optional)_ — Virtual hostname used to route the plugin's HTTP, WebSocket, and upgrade handlers - `timeout` — `number` _(optional)_ — Timeout in milliseconds for plugin operations. Takes precedence over the plugin's `defaultTimeout` and the system default (30 seconds) -`urlPath` and `host` are available in v5.2.0. Harper automatically passes them to handlers registered through the scoped `server` API. See [Middleware routing](../http/overview#middleware-routing) for an example and [`HttpOptions`](../http/api#httpoptions) for matching behavior. +`urlPath` and `host` are available in v5.2.0. Harper automatically passes them to handlers registered through the scoped `server` API. + +These position a plugin **within** its application. Where the application itself is served is set on the application's entry in the root `harper-config.yaml`; that mount is prefixed onto each plugin's `urlPath`, and a `host` there overrides one set here. See [Middleware routing](../http/overview#middleware-routing) for the full picture and [`HttpOptions`](../http/api#httpoptions) for matching behavior. ### File Entries diff --git a/reference/http/overview.md b/reference/http/overview.md index 3b60d086..dbf55e90 100644 --- a/reference/http/overview.md +++ b/reference/http/overview.md @@ -30,18 +30,39 @@ Request and response objects follow the [WHATWG Fetch API](https://developer.moz -Harper can route middleware by URL prefix, virtual hostname, or both. Set `urlPath` or `host` in a component's `config.yaml` to create a routed middleware chain without writing dispatch code: +Harper can route middleware by URL prefix, virtual hostname, or both, with no dispatch code in the application. Where an application is served is a deployment concern, so declare it on that application's entry in the root `harper-config.yaml`: ```yaml -rest: +# harper-config.yaml +my-app: host: api.example.com urlPath: /v1 +``` + +Every handler the application registers — HTTP, WebSocket, and upgrade — is then served under `api.example.com/v1`, and Harper removes `/v1` from the pathname before invoking the chain. Requests that match no routed chain use the default middleware chain. + +Because routing lives in the root config, the same application package can be mounted at a different hostname or path per environment without editing the application. The entry does not need a `package` — routing applies to any application in the components root, however it was deployed. + +You can also set it at deploy time: + +```bash +harper deploy project=my-app package=@my/app host=api.example.com urlPath=/v1 +``` + +#### Routing individual plugins + +A plugin's own `urlPath` sets where it sits **within** the application, and is configured in the application's `config.yaml`: + +```yaml +# my-app/config.yaml static: files: 'web/**' - host: www.example.com + urlPath: assets ``` -The `rest` handler receives requests under `api.example.com/v1`; Harper removes `/v1` from the pathname before invoking the chain. The `static` handler receives requests for `www.example.com`. Unmatched requests use the default middleware chain. +The application's mount composes with it rather than replacing it, so app-internal structure survives being relocated. With the root config above, the static files are served at `api.example.com/v1/assets/`. A plugin that configures no `urlPath` of its own is served at the mount itself (`api.example.com/v1`). + +An application can also set `host` per plugin, but a `host` on the root-config entry overrides it — the operator's choice of hostname wins over one the application shipped. Custom components can configure the same behavior programmatically with `server.http(listener, { host, urlPath })`. See [`HttpOptions`](./api#httpoptions) for matching priority and middleware ordering options. diff --git a/reference/operations-api/operations.md b/reference/operations-api/operations.md index 7a52b122..b914bdd1 100644 --- a/reference/operations-api/operations.md +++ b/reference/operations-api/operations.md @@ -604,7 +604,8 @@ Deploys a component. The `package` option accepts any valid NPM reference includ Additional parameters: -- `urlPath` — override the HTTP URL path the component is mounted at (e.g. `"/api/v2"`) +- `urlPath` — the HTTP URL path the component is mounted at (e.g. `"/api/v2"`). Persisted on the component's root-config entry; see [HTTP middleware routing](/reference/v5/http/overview#middleware-routing). +- `host` — the virtual hostname the component is served on (e.g. `"api.example.com"`). Must be a bare hostname — no scheme, port, or path. Persisted alongside `urlPath`. - `install_allow_scripts` — set to `true` to allow npm pre/post install scripts (disabled by default) - `credentials` — credentials for installing a component from a private npm registry or private git repository (see below) diff --git a/release-notes/v5-lincoln/5.2.md b/release-notes/v5-lincoln/5.2.md index 54bed7e3..f51339c8 100644 --- a/release-notes/v5-lincoln/5.2.md +++ b/release-notes/v5-lincoln/5.2.md @@ -30,7 +30,9 @@ The `set_configuration` operation now accepts `"replicated": true` to apply a co ### Middleware routing and ordering -Components can now declare `host` and `urlPath` in `config.yaml`, or pass them to `server.http()`, `server.ws()`, and `server.upgrade()`, to create middleware chains routed by virtual hostname, URL prefix, or both. The new `name`, `before`, and `after` options provide explicit middleware ordering. See [HTTP middleware routing](/reference/v5/http/overview#middleware-routing) and [`HttpOptions`](/reference/v5/http/api#httpoptions). +Applications can now be routed by virtual hostname, URL prefix, or both, with no dispatch code. Declare `host` and `urlPath` on the application's entry in the root `harper-config.yaml` — or pass them to `deploy_component` — and every handler the application registers is served under that hostname and path. Because the routing lives in the root config, the same application can be mounted differently per environment without editing it. A plugin's own `urlPath` still positions it within the application, and the application's mount is prefixed onto it. + +Components can also pass `host` and `urlPath` directly to `server.http()`, `server.ws()`, and `server.upgrade()`. The new `name`, `before`, and `after` options provide explicit middleware ordering. See [HTTP middleware routing](/reference/v5/http/overview#middleware-routing) and [`HttpOptions`](/reference/v5/http/api#httpoptions). ## Security From 046bb0cab4d328c2edf11a995e5270a3738c1aae Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Mon, 27 Jul 2026 12:10:53 -0600 Subject: [PATCH 2/4] docs: note what an application mount does not do MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follows the cross-model review of the core change: - A mount is a routing prefix, not an isolation boundary. Exported tables are instance-wide, so a table exported by one application is reachable through any mounted REST route. - Legacy `fastifyRoutes` register as a global fallback outside the routed chain, so a `host` mount cannot constrain them (a `urlPath` mount does apply). Harper warns in that case. - Host matching is case-insensitive, not case-sensitive as `HttpOptions` previously documented — hostnames are case-insensitive (RFC 4343) and clients send them lowercased, so the old behavior made a configured `API.example.com` unmatchable. - IPv6 hosts are configured as a bare literal, not bracketed; `urlPath` rejects `.` segments. --- reference/http/api.md | 2 +- reference/http/overview.md | 11 +++++++++++ reference/operations-api/operations.md | 4 ++-- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/reference/http/api.md b/reference/http/api.md index 3bc9d321..f21ae18f 100644 --- a/reference/http/api.md +++ b/reference/http/api.md @@ -48,7 +48,7 @@ To continue the middleware chain, call `next(request)`. To short-circuit, return | `before` | string | - | Run this entry before the named middleware entry | | `after` | string | - | Run this entry after the named middleware entry | | `urlPath` | string | - | Only handle requests whose pathname matches this prefix on a segment boundary (`/api` matches `/api` and `/api/x`, not `/apinews`). Harper removes the prefix before passing the request to the handler. | -| `host` | string | - | Only handle requests whose `Host` header matches this virtual hostname (case-sensitive, port ignored) | +| `host` | string | - | Only handle requests whose `Host` header matches this virtual hostname (case-insensitive, port ignored). IPv6 hosts are given as a bare literal (`::1`), not bracketed. | | `runFirst` | boolean | `false` | Deprecated. Insert this handler at the front of the chain. Use `before` or `after` for explicit ordering. | | `port` | number | `http.port` | Target the HTTP server on this port | | `securePort` | number | `http.securePort` | Target the HTTPS server on this port | diff --git a/reference/http/overview.md b/reference/http/overview.md index dbf55e90..c62112dd 100644 --- a/reference/http/overview.md +++ b/reference/http/overview.md @@ -64,6 +64,17 @@ The application's mount composes with it rather than replacing it, so app-intern An application can also set `host` per plugin, but a `host` on the root-config entry overrides it — the operator's choice of hostname wins over one the application shipped. +Handlers see the request with the mount already removed from the pathname, so application code addresses itself mount-relative and does not need to know where it is mounted. + +#### What a mount does not do + +A mount is a routing prefix, not an isolation boundary: + +- **It does not namespace resources.** Exported tables live in one instance-wide registry, so a table exported by one application is reachable through any mounted REST route, not only its own. +- **It does not constrain legacy Fastify routes by host.** `fastifyRoutes` registers as a global fallback outside the routed middleware chain, so those routes answer on every hostname. A `urlPath` mount does apply (it becomes the Fastify route prefix); a `host` mount does not, and Harper logs a warning when a host-mounted application declares `fastifyRoutes`. Port them to `server.http()` for host routing. + +Host matching ignores the port and is case-insensitive. IPv6 hosts are configured as a bare literal (`::1`), not bracketed. + Custom components can configure the same behavior programmatically with `server.http(listener, { host, urlPath })`. See [`HttpOptions`](./api#httpoptions) for matching priority and middleware ordering options. ## Protocols Served diff --git a/reference/operations-api/operations.md b/reference/operations-api/operations.md index b914bdd1..44afd8f9 100644 --- a/reference/operations-api/operations.md +++ b/reference/operations-api/operations.md @@ -604,8 +604,8 @@ Deploys a component. The `package` option accepts any valid NPM reference includ Additional parameters: -- `urlPath` — the HTTP URL path the component is mounted at (e.g. `"/api/v2"`). Persisted on the component's root-config entry; see [HTTP middleware routing](/reference/v5/http/overview#middleware-routing). -- `host` — the virtual hostname the component is served on (e.g. `"api.example.com"`). Must be a bare hostname — no scheme, port, or path. Persisted alongside `urlPath`. +- `urlPath` — the HTTP URL path the component is mounted at (e.g. `"/api/v2"`). Must not contain `..` or `.` path segments. Persisted on the component's root-config entry; see [HTTP middleware routing](/reference/v5/http/overview#middleware-routing). +- `host` — the virtual hostname the component is served on (e.g. `"api.example.com"`). Must be a bare hostname or IPv6 literal — no scheme, port, path, or brackets. Persisted alongside `urlPath`. - `install_allow_scripts` — set to `true` to allow npm pre/post install scripts (disabled by default) - `credentials` — credentials for installing a component from a private npm registry or private git repository (see below) From edd60458422a45fcd15f34cda953c07b03dde29b Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Mon, 27 Jul 2026 12:50:35 -0600 Subject: [PATCH 3/4] docs: note that deploy_component mount options require package Addresses PR review: - `deployComponentValidator` declares `.with('urlPath','package').with('host','package')`, so both are rejected on a payload-only deploy. The "Additional parameters" list implied otherwise; state the requirement and point payload deploys at the root-config entry. - Badge the new `host` parameter with v5.2.0 per the versioning rule. - Use a relative link for the middleware-routing reference, matching the rest of the file (21 relative links vs the one absolute one this PR introduced). --- reference/operations-api/operations.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/reference/operations-api/operations.md b/reference/operations-api/operations.md index 44afd8f9..1f653829 100644 --- a/reference/operations-api/operations.md +++ b/reference/operations-api/operations.md @@ -604,11 +604,13 @@ Deploys a component. The `package` option accepts any valid NPM reference includ Additional parameters: -- `urlPath` — the HTTP URL path the component is mounted at (e.g. `"/api/v2"`). Must not contain `..` or `.` path segments. Persisted on the component's root-config entry; see [HTTP middleware routing](/reference/v5/http/overview#middleware-routing). -- `host` — the virtual hostname the component is served on (e.g. `"api.example.com"`). Must be a bare hostname or IPv6 literal — no scheme, port, path, or brackets. Persisted alongside `urlPath`. +- `urlPath` — the HTTP URL path the component is mounted at (e.g. `"/api/v2"`). Must not contain `..` or `.` path segments. Persisted on the component's root-config entry; see [HTTP middleware routing](../http/overview.md#middleware-routing). +- `host` — the virtual hostname the component is served on (e.g. `"api.example.com"`). Must be a bare hostname or IPv6 literal — no scheme, port, path, or brackets. Persisted alongside `urlPath`. - `install_allow_scripts` — set to `true` to allow npm pre/post install scripts (disabled by default) - `credentials` — credentials for installing a component from a private npm registry or private git repository (see below) +`urlPath` and `host` both require `package` and are rejected on a payload-only deploy. To mount a payload-deployed component, add `host`/`urlPath` to its entry in the root `harper-config.yaml` instead. + #### Deploy credentials (`credentials`) When a component is installed from a private source, `credentials` supplies the authentication. It is an array of entries; each entry is one of two kinds, identified by its key: From 453f1d08db6a2d3a440d6e8dc7b6178c58b16912 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Tue, 28 Jul 2026 06:51:09 -0600 Subject: [PATCH 4/4] docs: fastifyRoutes host mount fails to load, not just a warning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit harper#1964's handleApplication throws when a host-mounted application declares fastifyRoutes, and componentLoader.ts catches it and marks the component failed — it doesn't continue running with a warning logged. Co-Authored-By: Claude Sonnet 5 --- reference/http/overview.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reference/http/overview.md b/reference/http/overview.md index c62112dd..7772b2a7 100644 --- a/reference/http/overview.md +++ b/reference/http/overview.md @@ -71,7 +71,7 @@ Handlers see the request with the mount already removed from the pathname, so ap A mount is a routing prefix, not an isolation boundary: - **It does not namespace resources.** Exported tables live in one instance-wide registry, so a table exported by one application is reachable through any mounted REST route, not only its own. -- **It does not constrain legacy Fastify routes by host.** `fastifyRoutes` registers as a global fallback outside the routed middleware chain, so those routes answer on every hostname. A `urlPath` mount does apply (it becomes the Fastify route prefix); a `host` mount does not, and Harper logs a warning when a host-mounted application declares `fastifyRoutes`. Port them to `server.http()` for host routing. +- **It does not constrain legacy Fastify routes by host.** `fastifyRoutes` registers as a global fallback outside the routed middleware chain, so those routes answer on every hostname. A `urlPath` mount does apply (it becomes the Fastify route prefix); a `host` mount does not, and Harper refuses to load a host-mounted application that declares `fastifyRoutes` rather than silently serving it unconstrained. Port them to `server.http()` for host routing. Host matching ignores the port and is case-insensitive. IPv6 hosts are configured as a bare literal (`::1`), not bracketed.