# Middleware

HTTP middleware lets you wrap a request handler with cross-cutting behavior:
access logging, trace spans, CORS, proxy-header handling, and search-param
parsing. In Effect v4 server middleware lives in
`effect/unstable/http/HttpMiddleware`, and `HttpRouter` provides shortcuts for
the most common cases.

```ts
import { Effect, Layer } from "effect"
import {
  HttpMiddleware,
  HttpRouter,
  HttpServerResponse
} from "effect/unstable/http"

const HelloRoute = HttpRouter.add(
  "GET",
  "/hello",
  Effect.succeed(HttpServerResponse.text("Hello, World!"))
)

// Apply CORS to every route in the router
const CorsLayer = HttpRouter.cors()

const App = Layer.mergeAll(HelloRoute, CorsLayer)
```

## Mental model

An `HttpMiddleware` is just a function from one HTTP handler effect to another:

```ts
interface HttpMiddleware {
  <E, R>(
    self: Effect.Effect<HttpServerResponse, E, R | HttpServerRequest>
  ): Effect.Effect<HttpServerResponse, any, any>
}
```

Because the handler effect carries the per-request `HttpServerRequest` in its
context, middleware can inspect or rewrite the request, provide request-scoped
services, attach pre-response hooks, or observe the handler's exit, all while
preserving normal Effect error and interruption semantics.

There are two places middleware runs, and the distinction matters:

- **Router middleware** (`HttpRouter.middleware`, `HttpRouter.cors`,
  `HttpRouter.disableLogger`) wraps the route handlers. Use this when middleware
  needs to **provide a service**, **handle a route error**, or **change the
  response** before it is sent.
- **Server middleware** (the `middleware` option of `HttpRouter.serve` /
  `HttpRouter.toWebHandler`) wraps the entire server chain, including the act of
  sending the response. Changes it makes to the response are **not** reflected
  in what the client receives. This is where the access `logger` is installed by
  default.
**Server middleware cannot edit the response:** The `middleware` passed to `serve`/`toWebHandler` runs around response sending.
If you need to mutate the outgoing response (add headers, etc.), use
`HttpRouter.middleware` instead so the change happens before the response is
written.

## Applying middleware

### Built-in HttpMiddleware on the server chain

`HttpRouter.serve` installs `HttpMiddleware.logger` for you unless you pass
`disableLogger: true`. You can compose additional server-level middleware via
the `middleware` option:

```ts
import {
  HttpMiddleware,
  HttpRouter,
  HttpServer
} from "effect/unstable/http"

const ServerLayer = HttpRouter.serve(App, {
  // wrap the server chain with proxy-header trust + tracing
  middleware: (httpApp) =>
    httpApp.pipe(HttpMiddleware.xForwardedHeaders, HttpMiddleware.tracer)
})
```

### Router middleware via `HttpRouter.middleware`

`HttpRouter.middleware` turns an `HttpMiddleware` (or an effect that builds one)
into a `Middleware` whose `.layer` you provide to the routes it should wrap. By
default it affects only the routes it is provided to; pass `{ global: true }`
to apply it to every route.

```ts
import { Context, Effect, Layer } from "effect"
import { HttpRouter, HttpServerResponse } from "effect/unstable/http"

class CurrentSession extends Context.Service<CurrentSession, {
  readonly token: string
}>()("CurrentSession") {}

// Middleware that *provides* a service to the wrapped handlers.
// The `provides` config records what the middleware supplies.
const SessionMiddleware = HttpRouter.middleware<{
  provides: CurrentSession
}>()(
  Effect.gen(function*() {
    yield* Effect.log("SessionMiddleware initialized")
    return (httpEffect) =>
      Effect.provideService(httpEffect, CurrentSession, {
        token: "dummy-token"
      })
  })
).layer

const ProtectedRoute = HttpRouter.add(
  "GET",
  "/me",
  Effect.gen(function*() {
    const session = yield* CurrentSession // available thanks to the middleware
    return HttpServerResponse.text(`token: ${session.token}`)
  })
).pipe(Layer.provide(SessionMiddleware))
```

### Router shortcuts

- `HttpRouter.cors(options?)` — installs `HttpMiddleware.cors` globally for the
  router. Returns a `Layer<never, never, HttpRouter>`.
- `HttpRouter.disableLogger` — a ready-made layer that disables the access
  logger for the routes it is provided to.

```ts
import { Effect, Layer } from "effect"
import { HttpRouter, HttpServerResponse } from "effect/unstable/http"

const Healthcheck = HttpRouter.add(
  "GET",
  "/health",
  Effect.succeed(HttpServerResponse.text("ok"))
).pipe(
  // don't log the (noisy) healthcheck route
  Layer.provide(HttpRouter.disableLogger)
)
```

## HttpMiddleware reference

Every public export of `effect/unstable/http/HttpMiddleware`.

### `make`

Defines an `HttpMiddleware` while preserving its precise type. This is the entry
point for writing your own middleware: take the handler effect, transform it,
and return the new effect.

```ts
import { Effect } from "effect"
import {
  HttpMiddleware,
  HttpServerRequest,
  HttpServerResponse
} from "effect/unstable/http"

// A middleware that adds an `x-powered-by` header by rewriting the response.
const poweredBy = HttpMiddleware.make((httpApp) =>
  Effect.map(httpApp, (response) =>
    HttpServerResponse.setHeader(response, "x-powered-by", "Effect")
  )
)

// Use it like any other middleware:
const handler = poweredBy(
  Effect.succeed(HttpServerResponse.text("hi"))
) satisfies Effect.Effect<
  HttpServerResponse.HttpServerResponse,
  never,
  HttpServerRequest.HttpServerRequest
>
```

A middleware can also read the request via `Effect.withFiber` /
`HttpServerRequest`, provide request-scoped services, or branch on the exit.

### `logger`

Logs each sent HTTP response with `http.method`, `http.url`, and `http.status`
annotations, inside an `http.span` log span. On failure it logs the stripped
cause. `HttpRouter.serve` installs this for you by default.

```ts
import { HttpMiddleware, HttpServerResponse } from "effect/unstable/http"

const app = HttpMiddleware.logger(
  // => logs: "Sent HTTP response"  http.method=GET http.url=/ http.status=200
  HttpServerResponse.text("ok") as any
)
```

### `withLoggerDisabled`

Runs an effect with response logging disabled for the **current** server
request. Useful inside a specific handler when you don't want the default
`logger` to emit a line.

```ts
import { Effect } from "effect"
import { HttpMiddleware, HttpServerResponse } from "effect/unstable/http"

const quietHandler = HttpMiddleware.withLoggerDisabled(
  Effect.succeed(HttpServerResponse.text("no log line for this one"))
)
// => the `logger` middleware skips this request
```
**Note:** `HttpRouter.disableLogger` is the layer form of this, built from
`HttpRouter.middleware(HttpMiddleware.withLoggerDisabled)` — provide it to the
routes whose logging you want suppressed.

### `cors`

Handles CORS preflight (`OPTIONS`) requests and appends the configured CORS
headers to responses. Call it with no arguments for permissive defaults
(`access-control-allow-origin: *` and all common methods), or pass an options
object.

```ts
import { HttpMiddleware } from "effect/unstable/http"

const corsMiddleware = HttpMiddleware.cors({
  // string[] OR a Predicate<string>; [] means "*"
  allowedOrigins: ["https://app.example.com"],
  // default: ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"]
  allowedMethods: ["GET", "POST"],
  // [] reflects the request's Access-Control-Request-Headers
  allowedHeaders: ["Content-Type", "Authorization"],
  // headers the browser is allowed to read off the response
  exposedHeaders: ["X-Custom"],
  // sets access-control-allow-credentials: true
  credentials: true,
  // access-control-max-age (seconds) for preflight caching
  maxAge: 600
})
// OPTIONS requests => 204 with the computed preflight headers
// other requests   => original response + access-control-* headers appended
```

The options object (all fields optional):

| Field | Type | Default | Effect |
| --- | --- | --- | --- |
| `allowedOrigins` | `ReadonlyArray<string> \| Predicate<string>` | `[]` | `[]` emits `access-control-allow-origin: *`; one origin pins it (with `Vary: Origin`); many or a predicate reflects the matching `Origin`. |
| `allowedMethods` | `ReadonlyArray<string>` | `["GET","HEAD","PUT","PATCH","POST","DELETE"]` | `access-control-allow-methods` on preflight. |
| `allowedHeaders` | `ReadonlyArray<string>` | `[]` | `access-control-allow-headers`; empty reflects the request's `Access-Control-Request-Headers`. |
| `exposedHeaders` | `ReadonlyArray<string>` | `[]` | `access-control-expose-headers`. |
| `credentials` | `boolean` | `false` | when `true`, adds `access-control-allow-credentials: true`. |
| `maxAge` | `number` | `undefined` | `access-control-max-age` (seconds). |
**Note:** `HttpRouter.cors(options?)` is the global-router form of this middleware —
`HttpRouter.middleware(HttpMiddleware.cors(options), { global: true })`.

### `tracer`

Creates a server trace span (`kind: "server"`) for each request and records HTTP
request/response attributes (`http.request.method`, `url.full`, `url.path`,
`http.response.status_code`, redacted request/response headers, client address,
user agent). The parent span is extracted from the request's trace-context
headers when present.

```ts
import { HttpMiddleware, HttpRouter } from "effect/unstable/http"

const ServerLayer = HttpRouter.serve(App, {
  middleware: HttpMiddleware.tracer
})
// each request => a "http.server GET" span with url.*/http.* attributes
```

Tracing is skipped when the fiber's `TracerEnabled` ref is off, or when
`TracerDisabledWhen` matches the request.

### `SpanNameGenerator`

A `Context.Reference` holding the function that names server spans. Defaults to
``(request) => `http.server ${request.method}` ``. Override it via
`Layer.succeed`.

```ts
import { Layer } from "effect"
import { HttpMiddleware } from "effect/unstable/http"

const SpanNames = Layer.succeed(HttpMiddleware.SpanNameGenerator)(
  (request) => `${request.method} ${request.url}`
)
// spans now named e.g. "GET /users/42"
```

### `TracerDisabledWhen`

A `Context.Reference` holding a `Predicate<HttpServerRequest>`. When it returns
`true` for a request, `tracer` produces no span for that request. Defaults to
`constFalse` (never disabled).

```ts
import { Layer } from "effect"
import { HttpMiddleware } from "effect/unstable/http"

const NoTraceForMetrics = Layer.succeed(HttpMiddleware.TracerDisabledWhen)(
  (request) => request.url.startsWith("/metrics")
)
// => /metrics requests are not traced
```

### `layerTracerDisabledForUrls`

Convenience constructor that builds a `TracerDisabledWhen` layer disabling
tracing for requests whose URL **exactly** matches one of the supplied URLs.

```ts
import { HttpMiddleware } from "effect/unstable/http"

const NoTraceLayer = HttpMiddleware.layerTracerDisabledForUrls([
  "/healthz",
  "/metrics"
])
// => exact-match URLs above are excluded from tracing
```

### `searchParamsParser`

Parses the current request URL's search parameters and provides them as
`HttpServerRequest.ParsedSearchParams`, removing that requirement from the
wrapped handler. (Routes served through `HttpRouter` already receive
`ParsedSearchParams`; this middleware is for handlers built outside the router.)

```ts
import { Effect } from "effect"
import { HttpMiddleware, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"

const handler = HttpMiddleware.searchParamsParser(
  Effect.gen(function*() {
    const params = yield* HttpServerRequest.ParsedSearchParams
    // GET /search?q=effect&tag=a&tag=b
    // => params = { q: "effect", tag: ["a", "b"] }
    return HttpServerResponse.text(JSON.stringify(params))
  })
)
```

### `xForwardedHeaders`

Trusts reverse-proxy headers: when `X-Forwarded-Host` is present it rewrites the
request `host` header, and it sets the request's `remoteAddress` from the first
entry of `X-Forwarded-For`. Apply this at the edge of your server when running
behind a trusted proxy/load balancer.

```ts
import { HttpMiddleware, HttpRouter } from "effect/unstable/http"

const ServerLayer = HttpRouter.serve(App, {
  middleware: HttpMiddleware.xForwardedHeaders
})
// request headers: x-forwarded-host: api.example.com, x-forwarded-for: 203.0.113.5, 10.0.0.1
// => request.headers.host becomes "api.example.com"
// => request.remoteAddress becomes Option.some("203.0.113.5")
```
**Only trust forwarded headers behind a proxy:** `X-Forwarded-*` headers are client-controllable. Enable `xForwardedHeaders` only
when your server sits behind a proxy that strips and re-sets these headers.

### `HttpMiddleware` (type) and `HttpMiddleware.Applied`

`HttpMiddleware` is the function interface shown in [Mental model](#mental-model).
The `HttpMiddleware.Applied<A, E, R>` namespace type represents middleware
already specialized to a concrete transformed app type `A` — useful when typing
a middleware whose output effect type you want to pin down.

```ts
import type { HttpMiddleware } from "effect/unstable/http"

// A middleware whose result is exactly the app type A:
declare const mw: HttpMiddleware.HttpMiddleware
```

## Router middleware reference

These live in `effect/unstable/http/HttpRouter` and build on the middleware
above.

### `HttpRouter.middleware`

Creates route-scoped (or, with `{ global: true }`, global) router middleware.
Provide its `.layer` to the route layers it should wrap. The optional type
config records what the middleware `provides` and which errors it `handles`, so
the router can subtract those from each route's requirements at the type level.

```ts
import { Effect, Layer } from "effect"
import { HttpMiddleware, HttpRouter, HttpServerResponse } from "effect/unstable/http"

// Wrap a built-in HttpMiddleware as router middleware:
const CorsMiddleware = HttpRouter.middleware(HttpMiddleware.cors()).layer

const Route = HttpRouter.add(
  "GET",
  "/",
  Effect.succeed(HttpServerResponse.text("hi"))
).pipe(Layer.provide(CorsMiddleware))
```

To install for every route, pass `{ global: true }`, which returns a layer
directly instead of a `Middleware` value:

```ts
import { HttpMiddleware, HttpRouter } from "effect/unstable/http"

const GlobalCors = HttpRouter.middleware(HttpMiddleware.cors(), {
  global: true
})
```

### `HttpRouter.cors`

Shortcut for global CORS: `HttpRouter.middleware(HttpMiddleware.cors(options), { global: true })`.
Takes the same options as `HttpMiddleware.cors`, except `allowedOrigins` here is
restricted to `ReadonlyArray<string>` (no predicate form).

```ts
import { HttpRouter } from "effect/unstable/http"

const CorsLayer = HttpRouter.cors({ allowedOrigins: ["https://app.example.com"] })
// => Layer<never, never, HttpRouter> applied to all routes
```

### `HttpRouter.disableLogger`

A prebuilt layer (`HttpRouter.middleware(HttpMiddleware.withLoggerDisabled).layer`)
that suppresses the access logger for the routes it is provided to.

```ts
import { Effect, Layer } from "effect"
import { HttpRouter, HttpServerResponse } from "effect/unstable/http"

const Route = HttpRouter.add(
  "GET",
  "/ping",
  Effect.succeed(HttpServerResponse.text("pong"))
).pipe(Layer.provide(HttpRouter.disableLogger))
```

### `HttpRouter.provideRequest`

Provides request-level dependencies to some routes by building a `Layer` and
supplying its services to the wrapped handlers (a thin wrapper over
`HttpRouter.middleware<{ provides }>`). Use it when a middleware-provided service
comes from its own layer.

```ts
import { Context, Effect, Layer } from "effect"
import { HttpRouter, HttpServerResponse } from "effect/unstable/http"

class Db extends Context.Service<Db, { readonly url: string }>()("Db") {}
const DbLayer = Layer.succeed(Db)({ url: "postgres://..." })

const Route = HttpRouter.add(
  "GET",
  "/users",
  Effect.gen(function*() {
    const db = yield* Db // provided per-request
    return HttpServerResponse.text(db.url)
  })
).pipe(HttpRouter.provideRequest(DbLayer))
```

## See also

- [HTTP Server](https://effect.plants.sh/http-server/) for routing, request decoding, and serving.
- The [HTTP API](https://effect.plants.sh/http-api/) builder layers its own typed middleware
  (`HttpApiMiddleware`) on top of this module.