Proxy
H3 proxy utilities.
#fetchWithEvent(event, url, init?)
Make a fetch request carrying the event's context.
Behavior depends on the target:
An internal url (starting with /) is dispatched via event.app.fetch() (sub-request) and never leaves the process. It inherits the incoming request's filtered headers (via getProxyRequestHeaders) and runtime metadata (ip, waitUntil, ...).
An external url is sent with native fetch(url, init) unchanged — the event's headers and context are not inherited (forwarding cookies or authorization to arbitrary hosts would be unsafe). A streamed init.body is given duplex: "half" when unset, which Node's fetch requires.
Security: Never pass unsanitized user input as the url. Callers are responsible for validating and restricting the URL.
#getProxyRequestHeaders(event)
Get the request headers object without headers known to cause issues when proxying.
#proxy(event, target, opts)
Make a proxy request to a target URL and send the response back to the client.
If the target starts with /, the request is dispatched internally via event.app.fetch() (sub-request) and never leaves the process. This bypasses any external security layer (reverse proxy auth, IP allowlisting, mTLS).
Upstream 3xx responses are passed through to the client by default rather than followed. Set fetchOptions: { redirect: "follow" } to follow them instead — but following a redirect with a streamed request body can fail, since the body cannot be replayed once consumed. (Internal sub-requests via event.app.fetch() never follow redirects.)
Limitations (inherited from fetch): upstream response bodies are always decompressed (compression is not preserved end-to-end), the host header is rewritten to the target (preserving it via forwardHeaders: ["host"] works on Node.js but may be ignored on other runtimes), and unix sockets, TLS options, or connection agents require a runtime-specific escape hatch (e.g. undici's dispatcher in fetchOptions on Node.js). On browser and service-worker runtimes, redirect: "manual" produces an unrelayable opaque-redirect for external targets (a 502 is returned) — set fetchOptions: { redirect: "follow" } there.
Security: Never pass unsanitized user input as the target. Callers are responsible for validating and restricting the target URL (e.g. allowlisting hosts, blocking internal paths, enforcing protocol).
Credential forwarding: proxy does not forward the incoming request's headers automatically — only headers the caller explicitly passes via opts.headers (or fetchOptions.headers) are sent, verbatim. Do not pass the client's Cookie or Authorization headers through to an upstream you do not fully trust. Note that opts.filterHeaders has no effect here — it is only applied by proxyRequest (which does forward the incoming headers and offers filterHeaders: ["cookie", "authorization"] as the mitigation).
#proxyRequest(event, target, opts)
Proxy the incoming request to a target URL.
If the target starts with /, the request is handled internally by the app router via event.app.fetch() instead of making an external HTTP request.
The request body is streamed to the target without buffering. Per the Fetch standard, a request body can only be consumed once, so reading it beforehand (e.g. via readBody(), readFormData(), or body-reading middleware) locks the stream and proxying fails. If you need to inspect the body and still proxy it, read from a clone and leave the original event untouched.
Upstream 3xx responses are passed through to the client by default rather than followed. Set fetchOptions: { redirect: "follow" } to follow them instead — but following a redirect with a streamed request body can fail, since the body cannot be replayed once consumed.
Security: Never pass unsanitized user input as the target. Callers are responsible for validating and restricting the target URL (e.g. allowlisting hosts, blocking internal paths, enforcing protocol). Consider using bodyLimit() middleware to prevent large request bodies from consuming excessive resources when proxying untrusted input.
Credential forwarding: the incoming request's Cookie and Authorization headers are forwarded to the target verbatim. This is the correct behavior for a same-trust reverse proxy, but leaks the client's credentials to any upstream you do not fully trust. When proxying to a not-fully-trusted upstream, strip them with filterHeaders: ["cookie", "authorization"]. (This differs from fetchWithEvent, which never forwards the event's headers to an external URL.)
Example:
app.all("/proxy", async (event) => {
const body = await event.req.clone().json(); // read from the clone
// ...inspect body...
return proxyRequest(event, "/target"); // original stream still intact
});