# Proxy

Define `proxy.ts` at the project root to run logic before a request reaches any route in `app/`.
The proxy can inspect the request, then redirect, rewrite, reject, respond, or continue to the app
router:

```ts
import type { ProxyRoutes } from "@rebask/core/proxy"
import { rewrite } from "@rebask/core/proxy"

const routes: ProxyRoutes = {
  "/admin/*": (request) => {
    if (!request.headers.get("cookie")?.includes("session=")) {
      return Response.redirect(new URL("/login", request.url))
    }
  },
  "/old-blog/:slug": (_request, params) => {
    return rewrite(`/blog/${params.slug}`)
  },
  "/dashboard": (request) => {
    const headers = new Headers(request.headers)
    headers.set("x-user-role", "admin")
    return rewrite("/admin/dashboard", { headers })
  },
  "/api/health": () => Response.json({ ok: true }),
}

export default routes
```

## Proxy signature

Export a `ProxyRoutes` object as the default export from `proxy.ts`. Each key is a URL pattern and each value is a handler
that receives the standard Web [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request)
and the parameters extracted from the pattern:

```ts
import type { ProxyRoutes } from "@rebask/core/proxy"

const routes: ProxyRoutes = {
  "/api/:version/**:path": (request, params) => {
    const url = new URL(request.url)
    return Response.json({
      version: params.version,
      path: params.path,
      method: request.method,
    })
  },
}
```

Returning a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) ends proxy
processing. Use it for redirects, access errors, JSON responses, and other responses that should not
reach the application router. Return nothing to continue to the app router.

Proxy routes match every supported HTTP method. Use `request.method` to branch when needed.

## Route patterns

Proxy patterns are [rou3 route patterns](https://github.com/h3js/rou3#route-patterns). Every
pattern supported by rou3's compiled router is supported here:

| Pattern                     | Match                                            |
| --------------------------- | ------------------------------------------------ |
| `/about`                    | The exact `/about` path                          |
| `/blog/:slug`               | One segment, available as `params.slug`          |
| `/users/:id(\\d+)`          | One segment matching the regex                   |
| `/users/:id?`               | Zero or one segment                              |
| `/files/:path+`             | One or more segments, available as `params.path` |
| `/files/:path*`             | Zero or more segments                            |
| `/files/*`                  | The rest of the path, including `/`              |
| `/files/**`                 | Zero or more segments                            |
| `/files/**:path`            | One or more segments, available as `params.path` |
| `/blog/:id(\\d+){-:title}?` | Optional group                                   |

A trailing `*` can match zero segments. Use `:path+` or `**:path` when at least one segment is
required.

Proxy handlers receive rou3 params directly. Named captures use their name, unnamed captures use
`"0"`, `"1"`, …. Values are the percent-encoded strings rou3 returns; catch-all values are a single
string with `/` separators.

For example, `/api/users/123` matches `/api/**:path` with `params.path` set to `"users/123"`. Use
`decodeURIComponent(params.path)` when you need a decoded value. App routes decode params because
they follow the filesystem conventions.

## Static files

The proxy runs before files in `public/` are served, so a pattern that matches a static file answers
for it too. Return nothing to let the file through.

When using an adapter, a broad pattern like `/*` can also match build assets, so keep patterns
narrow or return early for paths you don't want to handle.

## Rewrites

Use `rewrite()` to send the request to another route without changing the browser URL:

```ts
import type { ProxyRoutes } from "@rebask/core/proxy"
import { rewrite } from "@rebask/core/proxy"

const routes: ProxyRoutes = {
  "/old-blog/:slug": (_request, params) => rewrite(`/blog/${params.slug}`),
}
```

The destination is matched by the application router using the original HTTP method and request
body. The browser continues to display the original URL.

### Forwarding headers

Pass `headers` to `rewrite()` when the destination needs modified request headers. The supplied
[Headers](https://developer.mozilla.org/en-US/docs/Web/API/Headers) object replaces the original request headers, so clone `request.headers` when existing
headers should be preserved:

```ts
import type { ProxyRoutes } from "@rebask/core/proxy"
import { rewrite } from "@rebask/core/proxy"

const routes: ProxyRoutes = {
  "/dashboard": (request) => {
    const headers = new Headers(request.headers)
    headers.set("x-user-role", "admin")
    headers.delete("x-legacy-role")
    return rewrite("/admin/dashboard", { headers })
  },
}
```

Rewrites preserve the original `Accept` header and Rebask's Flight headers. Proxy code cannot modify
these headers with `rewrite()`.

## External rewrites

Pass an absolute URL to `rewrite()` to proxy the request to another origin:

```ts
import type { ProxyRoutes } from "@rebask/core/proxy"
import { rewrite } from "@rebask/core/proxy"

const routes: ProxyRoutes = {
  "/api/upstream": (request) => {
    const url = new URL(request.url)
    return rewrite(`https://api.example.com/data${url.search}`)
  },
}
```

External rewrites proxy the original request method, body, and headers to the upstream URL, with
`Host` and `X-Forwarded-Host` set for the target and original host. Rebask does not follow redirects
returned by the upstream. If the upstream cannot be reached or does not respond before the timeout,
Rebask returns `502 Bad Gateway`.

The default timeout is 30 seconds. Configure it with
[`proxyTimeout`](../configuration.md#rebask-proxytimeout), or set it to `null` to disable it.

## Cookies in Proxy

Use `cookies(request)` to read request cookies and `cookies(response)` to set or delete cookies on
a response or rewrite:

```ts
import { cookies } from "@rebask/core/cookies"

const routes: ProxyRoutes = {
  "/login-required": (request) => {
    if (cookies(request).has("session")) return

    const response = Response.redirect(new URL("/login", request.url))
    cookies(response).set("login-attempt", "true", { httpOnly: true, path: "/" })
    return response
  },
}
```

See [`cookies`](../server-runtime-apis.md#cookies) in the Server Runtime APIs docs for the available options.

## Flight headers

Rebask hides its Flight protocol headers from the proxy because they control how a request is
handled, such as RSC navigations, actions, or resumes. They are restored after the proxy runs, so
those request types continue to work as expected. The proxy cannot read, remove, or replace them
with `rewrite()`.

These headers are framework state, not application input.
