# Static Files & Multipart

Two related jobs live on this page: **sending** files down to clients (a static
asset directory, a one-off download) and **receiving** files from clients
(`multipart/form-data` uploads). All of the APIs live in the unstable HTTP
modules under `effect/unstable/http`.
**Platform services:** Everything here that touches the filesystem requires `FileSystem` and `Path`
  from core `effect`, provided by your platform layer (`@effect/platform-node`,
  Bun, etc.). See [/platform/file-system/](https://effect.plants.sh/platform/file-system/) and
  [/platform/path/](https://effect.plants.sh/platform/path/).

## Serving a static directory

`HttpStaticServer.layer` mounts a directory onto your `HttpRouter` so every
`GET` under a URL prefix is served from disk. This is the common case.

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

// Your normal API routes
const ApiLayer = HttpRouter.add("GET", "/health", HttpServerResponse.text("ok"))

// Serve ./public under /static (GET /static/app.css -> ./public/app.css)
const StaticLayer = HttpStaticServer.layer({
  root: "./public",
  prefix: "/static",
  cacheControl: "public, max-age=3600"
})

const AppLayer = Layer.mergeAll(ApiLayer, StaticLayer)
```

`HttpStaticServer.layer` adds a `GET` route at `prefix + "/*"` (or `"/*"` at the
root when no prefix is given). It only handles `GET` — wire up other methods
yourself. The handler resolves the request path **below `root`**: the path is
URL-decoded and normalized, and requests that escape `root` via `..`, null
bytes, or invalid encoding fail as route-not-found.

### Options

All `layer` / `make` options:

```ts
HttpStaticServer.layer({
  root: "./public",              // directory to serve (resolved to an absolute path)
  prefix: "/static",             // URL prefix (layer only); routes mount at prefix + "/*"
  index: "index.html",           // file served for directory requests (default "index.html")
  spa: true,                     // SPA fallback: serve `index` for extensionless HTML GETs
  cacheControl: "max-age=3600",  // value for the Cache-Control header (optional)
  mimeTypes: { glb: "model/gltf-binary" } // extra/override extension -> Content-Type entries
})
```

- **`index`** — the file served when the resolved path is a directory. Defaults
  to `"index.html"`; pass `index: undefined` to disable directory serving.
- **`spa`** — when `true`, a request for a path with no extension whose `Accept`
  header includes `text/html` and that does not match a file falls back to the
  index file. This is the standard single-page-app routing pattern.
- **`mimeTypes`** — merged over a built-in table (html, css, js, json, png, svg,
  woff2, mp4, wasm, ...). The extension key has no leading dot. Unknown
  extensions become `application/octet-stream`.

### Built-in HTTP semantics

The static handler does more than `read file -> respond`. For every served file
it sets `Content-Type` (from the extension), `Accept-Ranges: bytes`, and
`Cache-Control` if configured, and it honors:

- **Conditional requests** — `If-None-Match` (ETag) and `If-Modified-Since`
  return `304 Not Modified` with the validators echoed back.
- **Range requests** — a `Range: bytes=...` header produces a `206 Partial
  Content` response with the right `Content-Range`, or `416` when unsatisfiable.

These rely on file metadata supplied by the platform (`HttpPlatform`), which the
platform server layer provides automatically.

<Aside type="caution" title="Don't serve secrets">
  Dotfiles and anything else reachable under `root` (including symlinks and
  generated files) is served. Keep secrets outside the served tree.
</Aside>

### `HttpStaticServer.make` — the app value

When you need the handler as a value (to compose it manually, run conditionally,
or use outside a router), use `make`. It returns an `Effect` that yields an
`HttpApp` (an `Effect` that reads the `HttpServerRequest` and produces a
response).

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

const program = Effect.gen(function* () {
  const staticApp = yield* HttpStaticServer.make({ root: "./public", spa: true })

  // `staticApp` requires an HttpServerRequest in context; run it inside a handler
  // or provide a request to invoke it directly.
  return staticApp
})

// `make` requires FileSystem | Path | HttpPlatform; it fails with PlatformError
// while resolving the root, and the returned app fails with HttpServerError.
```

`layer` is just `make` plus a `router.add("GET", "/*", ...)` with errors mapped
to responses, so prefer `layer` unless you need the raw value.

## One-off file responses

To send a single file (a download, a generated report) without a static
directory, build the response directly.

### `HttpServerResponse.file`

Streams a filesystem path as the response body. Requires `HttpPlatform`, fails
with `PlatformError`, and supports `status`, `headers`, and byte-range options.

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

// Stream ./reports/q1.pdf as the response body
const download = HttpServerResponse.file("./reports/q1.pdf", {
  headers: { "Content-Disposition": 'attachment; filename="q1.pdf"' }
})
// => Effect<HttpServerResponse, PlatformError, HttpPlatform>

// Serve only bytes 0..1023 (e.g. a manual range implementation)
const head = HttpServerResponse.file("./video.mp4", { offset: 0, bytesToRead: 1024 })
```

### `HttpServerResponse.fileWeb`

Same idea for a Web `File`-like value (anything with `size` / `lastModified` /
`stream()`), e.g. a `File` you received elsewhere. Requires `HttpPlatform` and
cannot fail.

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

declare const upload: File // a Web File

const respond = HttpServerResponse.fileWeb(upload)
// => Effect<HttpServerResponse, never, HttpPlatform>
```

### `HttpBody.file` / `HttpBody.fileFromInfo`

If you only need the streaming **body** of a file (to compose it yourself),
`HttpBody.file` produces an `HttpBody.Stream` and stats the file to set the
content length. `fileFromInfo` skips the extra `stat` when you already hold the
`FileSystem.File.Info`.

```ts
import { Effect } from "effect"
import { HttpBody } from "effect/unstable/http"

const program = Effect.gen(function* () {
  const body = yield* HttpBody.file("./logo.png", { contentType: "image/png" })
  // => HttpBody.Stream  (requires FileSystem; fails with PlatformError)
  return body
})
```

For most responses prefer `HttpServerResponse.file` above, which sets the body
and requires `HttpPlatform` directly.

## Etag

`Etag` formats HTTP entity-tag values and provides a `Generator` service that
derives tags from file metadata. The static server already does conditional
handling for you; reach for this module when you implement caching by hand.

### `Etag.Etag`, `Etag.Weak`, `Etag.Strong`

The model is a tagged union. A **strong** tag asserts byte-for-byte identity; a
**weak** tag is only a cache-revalidation hint. `value` is the raw tag without
quotes or the `W/` prefix.

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

const strong: Etag.Strong = { _tag: "Strong", value: "33a64df5" }
const weak: Etag.Weak = { _tag: "Weak", value: "33a64df5" }
```

### `Etag.toString`

Formats an `Etag` as an HTTP header value, adding quotes and the `W/` prefix.

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

Etag.toString({ _tag: "Strong", value: "abc" }) // => "\"abc\""
Etag.toString({ _tag: "Weak", value: "abc" })   // => "W/\"abc\""
```

### `Etag.Generator`

A `Context.Service` with `fromFileInfo` (derives a tag from `FileSystem.File.Info`)
and `fromFileWeb` (from a Web `File`-like value). Both return `Effect<Etag>`.

```ts
import { Effect } from "effect"
import { Etag } from "effect/unstable/http"
import { FileSystem } from "effect"

const tagFor = Effect.gen(function* () {
  const gen = yield* Etag.Generator
  const fs = yield* FileSystem.FileSystem
  const info = yield* fs.stat("./logo.png")
  const etag = yield* gen.fromFileInfo(info)
  return Etag.toString(etag) // => "\"<size>-<mtime>\""  (hex size + hex mtime)
})
```

### `Etag.layer` / `Etag.layerWeak`

Provide the `Generator`. `layer` marks metadata-derived tags as **strong**;
`layerWeak` marks them **weak**. Use `layerWeak` when size + mtime are good cache
validators but not a byte-for-byte guarantee.

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

const StrongEtags = Etag.layer     // Layer<Etag.Generator> — strong tags
const WeakEtags = Etag.layerWeak   // Layer<Etag.Generator> — weak tags
```
**Note:** This module only *formats* and *generates* ETags. It does not parse incoming
  `If-None-Match` headers or implement conditional logic — `HttpStaticServer`
  does that internally.

## Receiving uploads: Multipart

`multipart/form-data` is how browsers submit forms with file inputs. The
`Multipart` module turns the incoming byte stream into typed `Part` values: text
inputs become `Field` values; file inputs become streamed `File` values that you
either read once or persist to scoped temp files.

### The common case: `schemaBodyMultipart`

The high-level path. `HttpServerRequest.schemaBodyMultipart` persists every part
to a scoped temporary directory, then decodes the resulting record with a
`Schema`. Combine `Schema.String` fields with `Multipart.SingleFileSchema` /
`Multipart.FilesSchema` to validate the whole form at once.

```ts
import { Effect, Schema } from "effect"
import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
import { Multipart } from "effect/unstable/http"

// Expect: a text field `title` and one uploaded file under `avatar`.
const UploadForm = Schema.Struct({
  title: Schema.String,
  avatar: Multipart.SingleFileSchema // decodes a 1-element file array to one PersistedFile
})

const handler = Effect.gen(function* () {
  const form = yield* HttpServerRequest.schemaBodyMultipart(UploadForm)
  // form.title  => string
  // form.avatar => Multipart.PersistedFile { key, name, contentType, path }
  return HttpServerResponse.text(`saved ${form.avatar.name} to ${form.avatar.path}`)
})

// schemaBodyMultipart requires HttpServerRequest | Scope | FileSystem | Path
// and fails with MultipartError | SchemaError.
const UploadRoute = HttpRouter.add("POST", "/upload", handler)
```
**Persisted paths are scoped:** The temp files written by persistence live only for the lifetime of the
  request `Scope`. Copy or move what you want to keep before the handler
  finishes. Client-provided file names are metadata — never treat `name` as a
  filesystem path.

### Streaming parts: `request.multipartStream`

For large uploads you may not want to buffer to disk. `multipartStream` emits
`Part` values as the parser reaches them, so you can pipe each `File.content`
straight to its destination.

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

const stream = Effect.gen(function* () {
  const request = yield* HttpServerRequest.HttpServerRequest
  yield* Stream.runForEach(request.multipartStream, (part) => {
    if (Multipart.isField(part)) {
      return Effect.log(`field ${part.key} = ${part.value}`)
    }
    // part is a File: stream its bytes somewhere (each File.content is one-shot)
    return Effect.log(`file ${part.name} (${part.contentType})`)
  })
  return HttpServerResponse.empty()
})
```

---

## Multipart reference

### `Multipart.Part`

The union `Field | File` emitted by the parser. Every part shares a `_tag` and
the multipart `TypeId` brand.

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

declare const part: Multipart.Part
part._tag // => "Field" | "File"
```

### `Multipart.Field`

A text part with a decoded string `value`, plus `key` (field name) and
`contentType`.

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

declare const field: Multipart.Field
field.key         // => "title"
field.value       // => "Hello"
field.contentType // => "text/plain"
```

### `Multipart.File`

A streamed upload. `content` is a one-shot `Stream<Uint8Array, MultipartError>`;
`contentEffect` collects the whole file into memory (use only for small files).
`name` is the client filename, `key` is the form field name.

```ts
import { Effect } from "effect"
import { Multipart } from "effect/unstable/http"

declare const file: Multipart.File
const bytes = file.contentEffect // => Effect<Uint8Array, MultipartError>  (buffers the file)
// file.content => Stream<Uint8Array, MultipartError>  (stream it instead)
```

### `Multipart.PersistedFile`

A `File` that has been written to disk by persistence. `path` points at the
file while the request scope is open.

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

declare const file: Multipart.PersistedFile
file.path        // => "/tmp/.../avatar.png"
file.name        // => "avatar.png"
file.contentType // => "image/png"
```

### `Multipart.Persisted`

The record produced by persistence: field names map to a string, an array of
strings (repeated fields), or an array of `PersistedFile`.

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

declare const data: Multipart.Persisted
// data["title"]  => string | string[]
// data["avatar"] => PersistedFile[]
```

### Guards: `isPart` / `isField` / `isFile` / `isPersistedFile`

Narrow `unknown` (or a `Part`) to the concrete variant.

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

declare const u: unknown
Multipart.isPart(u)          // => boolean (is it any multipart part?)
Multipart.isField(u)         // => boolean (is it a text Field?)
Multipart.isFile(u)          // => boolean (is it a streamed File?)
Multipart.isPersistedFile(u) // => boolean (is it a PersistedFile?)
```

### `Multipart.PersistedFileSchema`

A `Schema.declare` for a single `PersistedFile`. Encodes to/decodes from
`{ key, name, contentType, path }`. Use it to embed a file in a larger struct.

```ts
import { Schema } from "effect"
import { Multipart } from "effect/unstable/http"

const Form = Schema.Struct({
  note: Schema.String,
  attachment: Multipart.PersistedFileSchema // one PersistedFile
})
```

### `Multipart.FilesSchema`

`Schema.Array(PersistedFileSchema)` — decodes a field that may carry many files
(e.g. `<input type="file" multiple>`).

```ts
import { Schema } from "effect"
import { Multipart } from "effect/unstable/http"

const Gallery = Schema.Struct({
  images: Multipart.FilesSchema // PersistedFile[]
})
```

### `Multipart.SingleFileSchema`

Requires exactly one file and decodes to a single `PersistedFile` (it checks the
array length is 1, then unwraps it).

```ts
import { Schema } from "effect"
import { Multipart } from "effect/unstable/http"

const Form = Schema.Struct({
  avatar: Multipart.SingleFileSchema // exactly one PersistedFile, unwrapped
})
```

### `Multipart.schemaPersisted`

Builds a decoder `(input, options?) => Effect<A, SchemaError, RD>` for a
`Persisted`-shaped schema. This is the engine behind
`HttpServerRequest.schemaBodyMultipart`; use it directly when you already hold a
`Persisted` value.

```ts
import { Schema } from "effect"
import { Multipart } from "effect/unstable/http"

const decode = Multipart.schemaPersisted(
  Schema.Struct({ name: Schema.String, file: Multipart.SingleFileSchema })
)

declare const data: Multipart.Persisted
const result = decode(data) // => Effect<{ name: string; file: PersistedFile }, SchemaError>
```

### `Multipart.schemaJson`

Decodes a single field whose value is a JSON string. Useful when a form ships
structured data alongside files. Curried by field name or called with
`(persisted, field)`.

```ts
import { Schema } from "effect"
import { Multipart } from "effect/unstable/http"

const Meta = Schema.Struct({ tags: Schema.Array(Schema.String) })
const parseMeta = Multipart.schemaJson(Meta)("meta")

declare const data: Multipart.Persisted
const meta = parseMeta(data) // => Effect<{ tags: string[] }, SchemaError>  (parses data["meta"] as JSON)
```

### `Multipart.toPersisted`

Drains a stream of `Part` values into a `Persisted` record, writing each file to
a scoped temp directory. This is what `request.multipart` calls. Pass a custom
`writeFile` to control where files land.

```ts
import { Effect } from "effect"
import { HttpServerRequest } from "effect/unstable/http"
import { Multipart } from "effect/unstable/http"

const persist = Effect.gen(function* () {
  const request = yield* HttpServerRequest.HttpServerRequest
  const persisted = yield* Multipart.toPersisted(request.multipartStream)
  // => Persisted   (requires FileSystem | Path | Scope, fails with MultipartError)
  return persisted
})
```

### `Multipart.makeChannel`

The low-level parser: a `Channel` that consumes batches of `Uint8Array` request
chunks and emits batches of parsed `Part` values. `makeChannel` and
`multipartStream` are what you build everything else on; reach for it only for
custom transports.

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

declare const headers: Record<string, string>
const channel = Multipart.makeChannel(headers)
// => Channel<NonEmptyReadonlyArray<Part>, MultipartError, void, NonEmptyReadonlyArray<Uint8Array>, ...>
```

### `Multipart.collectUint8Array`

Runs a channel of byte chunks and folds it into one `Uint8Array`. Materializes
the full content in memory — it backs `File.contentEffect`.

```ts
import { Effect } from "effect"
import { Multipart } from "effect/unstable/http"

declare const file: Multipart.File
const all = file.contentEffect // built on collectUint8Array
// => Effect<Uint8Array, MultipartError>
```

### `Multipart.makeConfig`

Reads the parser limits out of the current fiber context (the `Max*` references
below plus `FieldMimeTypes`) and produces the low-level config from request
headers. Mostly internal; useful if you wire up `makeChannel` yourself.

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

declare const headers: Record<string, string>
const config = Multipart.makeConfig(headers) // => Effect<MP.BaseConfig>
```

## Multipart limits

Parser limits are read from `Context.Reference`s, so you tune them by providing a
context layer. The defaults: `MaxFieldSize` 10 MiB, `MaxParts`/`MaxFileSize`
unlimited, and `FieldMimeTypes` treats `application/json` parts as fields. When a
limit is exceeded the parser fails with the matching `MultipartError` reason.

### `Multipart.limitsServices`

Builds a `Context` containing limit overrides; provide it to a route or app.
This is the ergonomic way to set limits.

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

const limits = Multipart.limitsServices({
  maxParts: 20,
  maxFileSize: "5 MB",   // FileSystem.SizeInput
  maxFieldSize: "1 MB",
  maxTotalSize: "25 MB",
  fieldMimeTypes: ["application/json", "text/plain"]
})

const LimitsLayer = Layer.succeedContext(limits)
// merge LimitsLayer into your HttpRouter app to apply these limits
```

### `Multipart.MaxFieldSize`

`Context.Reference<FileSystem.SizeInput>` for the largest text field value.
Default 10 MiB. Exceeding it fails with reason `"FieldTooLarge"`.

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

Multipart.MaxFieldSize // Context.Reference, default 10 MiB
```

### `Multipart.MaxFileSize`

`Context.Reference<FileSystem.SizeInput | undefined>` for the largest single
file. Default `undefined` (unlimited). Exceeding it fails with `"FileTooLarge"`.

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

Multipart.MaxFileSize // Context.Reference, default undefined
```

### `Multipart.MaxParts`

`Context.Reference<number | undefined>` for the maximum number of parts. Default
`undefined` (unlimited). Exceeding it fails with `"TooManyParts"`.

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

Multipart.MaxParts // Context.Reference, default undefined
```

### `Multipart.FieldMimeTypes`

`Context.Reference<ReadonlyArray<string>>` — MIME-type fragments whose parts are
parsed as text fields rather than files. Default `["application/json"]`.

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

Multipart.FieldMimeTypes // Context.Reference, default ["application/json"]
```
**Note:** The total body size limit (`maxTotalSize`) is shared with the incoming-message
  body limit; `limitsServices` sets it for you. There is no `Multipart`-specific
  reference for it.

## Multipart errors

### `Multipart.MultipartError`

A `Data.TaggedError("MultipartError")` carrying a `reason`. Catch it with
`Effect.catchTag("MultipartError", ...)`; its `message` is the reason tag.

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

declare const UploadForm: any

const handler = HttpServerRequest.schemaBodyMultipart(UploadForm).pipe(
  Effect.catchTag("MultipartError", (error) =>
    HttpServerResponse.text(`upload failed: ${error.reason._tag}`, { status: 413 })
  )
)
```

### `Multipart.MultipartErrorReason`

A `Data.Error` whose `_tag` is one of `"FileTooLarge"`, `"FieldTooLarge"`,
`"BodyTooLarge"`, `"TooManyParts"`, `"InternalError"`, or `"Parse"`. Inspect it
to map limit failures to specific status codes.

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

declare const error: Multipart.MultipartError
switch (error.reason._tag) {
  case "FileTooLarge":   /* 413 */ break
  case "TooManyParts":   /* 413 */ break
  case "Parse":          /* 400 */ break
  // "FieldTooLarge" | "BodyTooLarge" | "InternalError"
}
```

## See also

- [/platform/file-system/](https://effect.plants.sh/platform/file-system/) — the `FileSystem` service used to stat/stream files.
- [/platform/path/](https://effect.plants.sh/platform/path/) — the `Path` service used to resolve paths safely.