fetch-proxy
The fetch-proxy package creates a fetch-compatible function that forwards requests to another server. It handles URL rewriting, request forwarding, and automatic Set-Cookie domain/path rewriting so proxied cookies work correctly on the client.
Common use cases include proxying API requests to a backend service, forwarding to a development server, and building reverse proxy middleware.
Installation
# Via the meta-package
npm install remix
# Or individually
npm install @remix-run/fetch-proxyImports
import { createFetchProxy } from 'remix/fetch-proxy'
// Types
import type { FetchProxy, FetchProxyOptions } from 'remix/fetch-proxy'createFetchProxy(target, options?)
Creates a fetch function that forwards requests to the specified target server.
import { createFetchProxy } from 'remix/fetch-proxy'
let proxy = createFetchProxy('http://api.internal:8080')Parameters
| Parameter | Type | Description |
|---|---|---|
target | string | URL | The base URL of the server to forward requests to. |
options | FetchProxyOptions | Optional configuration for proxy behavior. |
Returns
A FetchProxy function with the same signature as the global fetch.
FetchProxy
The proxy function returned by createFetchProxy(). It has the same signature as the standard fetch function.
interface FetchProxy {
(input: URL | RequestInfo, init?: RequestInit): Promise<Response>
}FetchProxyOptions
| Option | Type | Default | Description |
|---|---|---|---|
fetch | typeof globalThis.fetch | globalThis.fetch | Custom fetch implementation to use for the actual request. |
rewriteCookieDomain | boolean | true | Rewrites the Domain attribute of Set-Cookie headers to match the incoming request's domain. |
rewriteCookiePath | boolean | true | Removes the proxy target's pathname prefix from the Path attribute of Set-Cookie headers. |
xForwardedHeaders | boolean | false | Adds X-Forwarded-Proto and X-Forwarded-Host headers to the proxied request. |
URL Rewriting
The proxy rewrites URLs by prepending the target's origin and pathname to the incoming request's pathname.
let proxy = createFetchProxy('http://api.internal:8080/v1')
// Request to /users becomes http://api.internal:8080/v1/users
let response = await proxy('http://localhost:3000/users')Query strings are preserved:
// Becomes http://api.internal:8080/v1/search?q=remix
await proxy('http://localhost:3000/search?q=remix')Cookie Rewriting
By default, Set-Cookie headers in the proxied response are rewritten so they work correctly on the client.
Domain Rewriting
When rewriteCookieDomain is true (the default), the Domain attribute of Set-Cookie headers is changed to match the incoming request's host:
# Original from target server:
Set-Cookie: session=abc; Domain=api.internal; Path=/
# Rewritten to:
Set-Cookie: session=abc; Domain=localhost:3000; Path=/Path Rewriting
When rewriteCookiePath is true (the default), the target's pathname prefix is stripped from the Path attribute:
# Target: http://api.internal:8080/v1
# Original from target:
Set-Cookie: token=xyz; Path=/v1/auth
# Rewritten to:
Set-Cookie: token=xyz; Path=/authIf the cookie path exactly matches the target pathname, it is rewritten to /.
Disabling Cookie Rewriting
let proxy = createFetchProxy('http://api.internal:8080', {
rewriteCookieDomain: false,
rewriteCookiePath: false,
})Forwarded Headers
Enable xForwardedHeaders to add standard proxy headers to the forwarded request:
let proxy = createFetchProxy('http://api.internal:8080', {
xForwardedHeaders: true,
})This adds:
X-Forwarded-Proto: The protocol of the original request (e.g.https).X-Forwarded-Host: The host of the original request (e.g.example.com).
These headers let the target server know the original client protocol and host, which is important for generating correct URLs in responses.
Request Forwarding
All properties of the original request are forwarded to the target:
- HTTP method
- Headers
- Body (streamed, not buffered)
- Cache, credentials, integrity, keepalive, mode, redirect, referrer, and referrerPolicy settings
- Abort signal
The request body is forwarded as a stream using the duplex: 'half' option, so large request bodies are not buffered in memory.
Usage with the Router
As a Route Handler
Proxy specific routes to a backend service:
import { createRouter } from 'remix/fetch-router'
import { createFetchProxy } from 'remix/fetch-proxy'
let apiProxy = createFetchProxy('http://api.internal:8080')
let router = createRouter()
router.get('/api/*path', ({ request }) => {
return apiProxy(request)
})As Development Proxy
Forward requests to a separate development server:
import { createFetchProxy } from 'remix/fetch-proxy'
let devProxy = createFetchProxy('http://localhost:5173', {
xForwardedHeaders: true,
})
let router = createRouter()
// Proxy static assets to Vite dev server
router.get('/assets/*path', ({ request }) => {
return devProxy(request)
})With Custom Fetch
Use a custom fetch implementation (e.g. for testing or to add default headers):
let proxy = createFetchProxy('http://api.internal:8080', {
fetch(input, init) {
let request = new Request(input, init)
request.headers.set('Authorization', `Bearer ${process.env.API_TOKEN}`)
return globalThis.fetch(request)
},
})Complete Example
import * as http from 'node:http'
import { createRouter } from 'remix/fetch-router'
import { route } from 'remix/fetch-router/routes'
import { createRequestListener } from 'remix/node-fetch-server'
import { createFetchProxy } from 'remix/fetch-proxy'
// Create proxies for backend services
let authProxy = createFetchProxy('http://auth-service:4000', {
xForwardedHeaders: true,
})
let apiProxy = createFetchProxy('http://api-service:5000/v2', {
xForwardedHeaders: true,
})
// Define routes
let routes = route({
home: '/',
auth: '/auth/*path',
api: '/api/*path',
})
// Create router
let router = createRouter()
router.get(routes.home, () => {
return new Response('<h1>Gateway</h1>', {
headers: { 'Content-Type': 'text/html' },
})
})
// Proxy auth routes
router.map(routes.auth, ({ request }) => {
return authProxy(request)
})
// Proxy API routes
router.map(routes.api, ({ request }) => {
return apiProxy(request)
})
// Start gateway server
let server = http.createServer(createRequestListener(router.fetch))
server.listen(3000, () => {
console.log('Gateway listening on http://localhost:3000')
})Related Packages
- fetch-router --- The router for defining routes that proxy to backend services.
- node-fetch-server --- Run the proxy gateway on Node.js.
- headers --- The
SetCookieclass used internally for cookie rewriting.