# Handling Responses

A `HttpClientResponse` carries the status, headers, and a lazily-read body. You
can read the body in several shapes — raw text, JSON, bytes, or a stream — but
the idiomatic path is to decode it through a `Schema`, so the value you work
with is fully typed and validated. Decoding helpers live on
`HttpClientResponse`, and they are designed to be used with `Effect.flatMap`
right after the request.

```ts
import { Effect, Schema } from "effect"
import { HttpClient, HttpClientResponse } from "effect/unstable/http"

class User extends Schema.Class<User>("User")({
  id: Schema.Number,
  name: Schema.String,
  email: Schema.String
}) {}

const getUser = Effect.fn("getUser")(function*(id: number) {
  const client = yield* HttpClient.HttpClient

  return yield* client.get(`https://api.example.com/users/${id}`).pipe(
    // schemaBodyJson reads the JSON body and decodes it with the schema.
    // It fails with HttpClientError (read/empty body) or Schema.SchemaError
    // (the body did not match the shape).
    Effect.flatMap(HttpClientResponse.schemaBodyJson(User))
  )
})
```

## Reading the body directly

When you do not need a schema, read the body through the accessors on the
response. Each one is an `Effect`, because reading can fail:

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

const raw = Effect.gen(function*() {
  const client = yield* HttpClient.HttpClient
  const response = yield* client.get("https://api.example.com/health")

  // response.text         : Effect<string, HttpClientError>
  // response.json         : Effect<Schema.Json, HttpClientError>  (parsed JSON)
  // response.urlParamsBody: Effect<UrlParams, HttpClientError>
  // response.arrayBuffer  : Effect<ArrayBuffer, HttpClientError>
  // response.stream       : Stream<Uint8Array, HttpClientError>
  const text = yield* response.text

  return { status: response.status, headers: response.headers, text }
})
```

The response also exposes `status`, `headers`, `cookies`, `remoteAddress`, and
the original `request` synchronously, plus a `formData` effect for
`multipart/form-data` responses.
**Note:** The body accessors are cached. Reading `text` or `arrayBuffer` populates the
other (`text` derives from the buffered bytes and vice versa), so repeated reads
do not re-consume the underlying one-shot Web stream. `json` parses an empty
text body as `null` rather than failing.

## Decoding helpers

| Helper | Reads | Decodes |
| --- | --- | --- |
| `schemaBodyJson(schema)` | JSON body | the body against `schema` |
| `schemaBodyUrlParams(schema)` | URL-encoded body | the params against `schema` |
| `schemaJson(schema)` | JSON body | `{ status, headers, body }` together |
| `schemaNoBody(schema)` | nothing | `{ status, headers }` only |
| `schemaHeaders(schema)` | headers | the headers against `schema` |

`schemaJson` is handy when the contract spans more than the body — for example
when a header or the status code is part of the decoded value:

```ts
import { Effect, Schema } from "effect"
import { HttpClient, HttpClientResponse } from "effect/unstable/http"

// Decode the status, a header, and the body in one pass.
const Paged = Schema.Struct({
  status: Schema.Number,
  headers: Schema.Struct({
    "x-total-count": Schema.String
  }),
  body: Schema.Array(Schema.String)
})

const listTags = Effect.gen(function*() {
  const client = yield* HttpClient.HttpClient
  return yield* client.get("https://api.example.com/tags").pipe(
    Effect.flatMap(HttpClientResponse.schemaJson(Paged))
  )
})
```

## Branching on status

Sometimes the response shape depends on the status code — a `404` means "absent"
rather than "error", or a `2xx` and a `4xx` decode to different schemas.
`HttpClientResponse.matchStatus` lets you handle exact codes and status classes
(`"2xx"`, `"3xx"`, `"4xx"`, `"5xx"`) with a required `orElse`:

```ts
import { Effect, Option, Schema } from "effect"
import { HttpClient, HttpClientResponse } from "effect/unstable/http"

class User extends Schema.Class<User>("User")({
  id: Schema.Number,
  name: Schema.String
}) {}

const findUser = Effect.fn("findUser")(function*(id: number) {
  const client = yield* HttpClient.HttpClient
  const response = yield* client.get(`https://api.example.com/users/${id}`)

  return yield* response.pipe(
    HttpClientResponse.matchStatus({
      // 404 is an expected outcome, not a failure.
      404: () => Effect.succeed(Option.none<User>()),
      // Any other 2xx: decode the body.
      "2xx": (ok) =>
        HttpClientResponse.schemaBodyJson(User)(ok).pipe(Effect.map(Option.some)),
      // Everything else is unexpected — surface the response.
      orElse: (other) =>
        Effect.fail(new Error(`Unexpected status ${other.status}`))
    })
  )
})
```
**Note:** `matchStatus` checks an exact status code first, then the matching class, then
falls back to `orElse`. Because `orElse` is required, every status is handled —
the result type is the union of all branch return types.

## Streaming the body

For large or open-ended responses, read the body as a `Stream` of bytes instead
of buffering it. `response.stream` is a `Stream<Uint8Array, HttpClientError>`,
and `HttpClientResponse.stream` lifts a response *effect* straight into a stream
so you can pipe a request directly into stream processing:

```ts
import { Effect, Stream } from "effect"
import { HttpClient, HttpClientResponse } from "effect/unstable/http"

const downloadLineCount = Effect.gen(function*() {
  const client = yield* HttpClient.HttpClient

  // HttpClientResponse.stream takes the request effect and yields the body as a
  // byte stream — nothing is buffered into memory all at once.
  const bytes = HttpClientResponse.stream(
    client.get("https://api.example.com/export.csv")
  )

  return yield* bytes.pipe(
    Stream.decodeText(),
    Stream.splitLines,
    Stream.runCount
  )
})
```

See [Streaming](https://effect.plants.sh/streaming/) for the full set of stream operators.

---

## Reference

### The `HttpClientResponse` interface

A response extends the shared `HttpIncomingMessage` body/header surface and adds
client-specific fields. The synchronous fields are plain properties; the body
accessors are `Effect`s (or a `Stream`) because reading can fail.

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

const inspect = Effect.gen(function*() {
  const client = yield* HttpClient.HttpClient
  const res = yield* client.get("https://api.example.com/health")

  res.status        // => 200            (number)
  res.headers       // => { "content-type": "application/json", ... }
  res.cookies       // => Cookies parsed from Set-Cookie headers
  res.remoteAddress // => Option<string> (none for the Web client)
  res.request       // => the originating HttpClientRequest
})
```

| Field | Type | Notes |
| --- | --- | --- |
| `status` | `number` | The HTTP status code. Data, not an error, until a filter is applied. |
| `headers` | `Headers` | Lowercase, single-value map from the `Headers` module. |
| `cookies` | `Cookies` | Parsed lazily from `Set-Cookie` headers and cached. |
| `remoteAddress` | `Option<string>` | Peer address when the platform exposes it; `Option.none()` for the Web client. |
| `request` | `HttpClientRequest` | The request that produced this response. |
| `formData` | `Effect<FormData, HttpClientError>` | Reads a `multipart/form-data` body. |

The body accessors inherited from `HttpIncomingMessage`:

#### `text`

Reads the body as a UTF-8 string. Cached, and populates `arrayBuffer`.

```ts
declare const res: import("effect/unstable/http/HttpClientResponse").HttpClientResponse
res.text // => Effect<string, HttpClientError>
```

#### `json`

Reads the body as text, then `JSON.parse`s it; an empty body parses to `null`.

```ts
declare const res: import("effect/unstable/http/HttpClientResponse").HttpClientResponse
res.json // => Effect<Schema.Json, HttpClientError>  (e.g. { ok: true })
```

#### `urlParamsBody`

Parses a URL-encoded (`application/x-www-form-urlencoded`) body into `UrlParams`.

```ts
declare const res: import("effect/unstable/http/HttpClientResponse").HttpClientResponse
res.urlParamsBody // => Effect<UrlParams, HttpClientError>  e.g. [["a", "1"]]
```

#### `arrayBuffer`

Reads the raw body bytes. Cached, and populates `text`.

```ts
declare const res: import("effect/unstable/http/HttpClientResponse").HttpClientResponse
res.arrayBuffer // => Effect<ArrayBuffer, HttpClientError>
```

#### `stream`

The body as a byte stream. Fails with `EmptyBodyError` when there is no body.

```ts
declare const res: import("effect/unstable/http/HttpClientResponse").HttpClientResponse
res.stream // => Stream<Uint8Array, HttpClientError>
```

#### `formData`

Reads a `multipart/form-data` body into a Web `FormData`.

```ts
declare const res: import("effect/unstable/http/HttpClientResponse").HttpClientResponse
res.formData // => Effect<FormData, HttpClientError>
```

### Schema decoders

Each decoder is a function `(schema) => (response) => Effect`. They read part of
the response and decode it, failing with `HttpClientError` (when the body cannot
be read) or `Schema.SchemaError` (when the value does not match the schema).

#### `schemaBodyJson`

Reads the JSON body and decodes it with the schema. The most common decoder.

```ts
import { Effect, Schema } from "effect"
import { HttpClient, HttpClientResponse } from "effect/unstable/http"

const Todo = Schema.Struct({ id: Schema.Number, title: Schema.String })

Effect.gen(function*() {
  const client = yield* HttpClient.HttpClient
  const todo = yield* client.get("https://api.example.com/todos/1").pipe(
    Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo))
  )
  // todo => { id: 1, title: "Wash the car" }
})
```

#### `schemaBodyUrlParams`

Reads a URL-encoded body and decodes the parameters with the schema.

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

const Form = Schema.Struct({ token: Schema.String, ttl: Schema.NumberFromString })

declare const res: HttpClientResponse.HttpClientResponse
HttpClientResponse.schemaBodyUrlParams(Form)(res)
// body "token=abc&ttl=60" => { token: "abc", ttl: 60 }
```

#### `schemaJson`

Decodes the status, headers, and JSON body together as
`{ status, headers, body }`. Use when the contract spans more than the body.

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

const Envelope = Schema.Struct({
  status: Schema.Number,
  headers: Schema.Struct({ "x-request-id": Schema.String }),
  body: Schema.Struct({ ok: Schema.Boolean })
})

declare const res: HttpClientResponse.HttpClientResponse
HttpClientResponse.schemaJson(Envelope)(res)
// => { status: 200, headers: { "x-request-id": "..." }, body: { ok: true } }
```

#### `schemaNoBody`

Decodes only `{ status, headers }` without reading a body — useful for `204`
responses or when only headers matter. Fails only with `Schema.SchemaError`.

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

const Meta = Schema.Struct({
  status: Schema.Number,
  headers: Schema.Struct({ location: Schema.String })
})

declare const res: HttpClientResponse.HttpClientResponse
HttpClientResponse.schemaNoBody(Meta)(res)
// => { status: 201, headers: { location: "/things/9" } }
```

#### `schemaHeaders`

Validates and decodes just the response headers with the schema.

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

const RateLimit = Schema.Struct({
  "x-ratelimit-remaining": Schema.NumberFromString
})

declare const res: HttpClientResponse.HttpClientResponse
HttpClientResponse.schemaHeaders(RateLimit)(res)
// headers { "x-ratelimit-remaining": "42" } => { "x-ratelimit-remaining": 42 }
```

### Status helpers

By default a non-2xx status is just data — the response succeeds. These helpers
turn the status into success/failure or branch on it.

#### `matchStatus`

Pattern matches on the status, checking an exact code first, then the status
class (`"2xx"`, `"3xx"`, `"4xx"`, `"5xx"`), then the required `orElse`. See
[Branching on status](#branching-on-status) above for a full example.

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

declare const res: HttpClientResponse.HttpClientResponse
HttpClientResponse.matchStatus(res, {
  204: () => Effect.succeed("no content"),
  "2xx": (ok) => ok.text,
  orElse: (other) => Effect.fail(new Error(`status ${other.status}`))
})
```

#### `filterStatus`

Succeeds with the response when the status satisfies the predicate, otherwise
fails with an `HttpClientError` whose reason is a `StatusCodeError`.

```ts
import { Effect } from "effect"
import { HttpClient, HttpClientResponse } from "effect/unstable/http"

Effect.gen(function*() {
  const client = yield* HttpClient.HttpClient
  yield* client.get("https://api.example.com/x").pipe(
    Effect.flatMap(HttpClientResponse.filterStatus((status) => status < 400))
    // status 500 => fails with HttpClientError(StatusCodeError)
  )
})
```

#### `filterStatusOk`

Shorthand for "only accept 2xx". Fails with `StatusCodeError` for any other
status.

```ts
import { Effect } from "effect"
import { HttpClient, HttpClientResponse } from "effect/unstable/http"

Effect.gen(function*() {
  const client = yield* HttpClient.HttpClient
  yield* client.get("https://api.example.com/x").pipe(
    Effect.flatMap(HttpClientResponse.filterStatusOk)
    // status 404 => fails with HttpClientError(StatusCodeError)
  )
})
```

### Constructors and stream

#### `fromWeb`

Wraps a platform Web `Response` and its originating `HttpClientRequest` into an
`HttpClientResponse`. Useful in adapters and tests.

```ts
import { HttpClientRequest, HttpClientResponse } from "effect/unstable/http"

const request = HttpClientRequest.get("https://api.example.com/ping")
const webResponse = new Response("pong", { status: 200 })

const res = HttpClientResponse.fromWeb(request, webResponse)
// res.status => 200 ; yield* res.text => "pong"
```

#### `stream`

Converts an `Effect<HttpClientResponse>` directly into a byte stream, failing the
stream if the request fails or the body is empty. See
[Streaming the body](#streaming-the-body) above.

```ts
import { HttpClient, HttpClientResponse } from "effect/unstable/http"

declare const client: HttpClient.HttpClient
HttpClientResponse.stream(client.get("https://api.example.com/big.bin"))
// => Stream<Uint8Array, HttpClientError>
```

## Response error types

All client failures are wrapped in a single `HttpClientError` from
`effect/unstable/http/HttpClientError`. The wrapper carries the originating
`request` and a `reason` describing exactly what went wrong; match on
`error.reason._tag` to recover differently per failure.

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

Effect.gen(function*() {
  const client = yield* HttpClient.HttpClient
  yield* client.get("https://api.example.com/x")
}).pipe(
  Effect.catchTag("HttpClientError", (error) => {
    error.request // => the failed HttpClientRequest
    error.response // => HttpClientResponse | undefined (only for response errors)
    switch (error.reason._tag) {
      case "TransportError":
        return Effect.logError("network failure")
      case "StatusCodeError":
        return Effect.logError(`bad status ${error.reason.response.status}`)
      default:
        return Effect.die(error)
    }
  })
)
```

The reasons split into two families. **Request errors** happen before a response
exists (`response` is `undefined`); **response errors** carry the response that
triggered them.

#### `HttpClientError`

The single tagged error (`_tag: "HttpClientError"`) wrapping every client
failure. Exposes `reason`, plus `request` and `response` getters that delegate to
the reason. Use `isHttpClientError` at untyped boundaries.

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

declare const value: unknown
HttpClientError.isHttpClientError(value) // => boolean
```

#### `TransportError` (request)

Transport-level failure while sending the request (DNS, connection reset, etc.).
No response.

```ts
// error.reason._tag === "TransportError"
// error.reason.request, error.reason.cause
```

#### `EncodeError` (request)

Failure while encoding the request body before sending. No response.

```ts
// error.reason._tag === "EncodeError"
```

#### `InvalidUrlError` (request)

The request URL could not be constructed. No response.

```ts
// error.reason._tag === "InvalidUrlError"
```

#### `StatusCodeError` (response)

A status filter (`filterStatus` / `filterStatusOk`) rejected the response's
status. Carries the `response`.

```ts
// error.reason._tag === "StatusCodeError"
// error.reason.response.status => 500
```

#### `DecodeError` (response)

Reading or decoding the response body failed (bad JSON, stream read error, schema
mismatch upstream). Carries the `response`.

```ts
// error.reason._tag === "DecodeError"
```

#### `EmptyBodyError` (response)

An operation expected a body but the response had none (e.g. streaming an empty
body). Carries the `response`.

```ts
// error.reason._tag === "EmptyBodyError"
```

The module also exports the reason unions `RequestError`
(`TransportError | EncodeError | InvalidUrlError`), `ResponseError`
(`StatusCodeError | DecodeError | EmptyBodyError`), and `HttpClientErrorReason`
(both combined), along with `HttpClientErrorSchema` for serializing a client
error across a boundary.
**Note:** Schema decoders (`schemaBodyJson` and friends) can fail with
`Schema.SchemaError` *in addition to* `HttpClientError` — the `HttpClientError`
covers reading the body, while `Schema.SchemaError` covers the value not matching
the schema. Handle both in the error channel.

Once you can read responses, the next step is making the client resilient —
retries, timeouts, and rate limiting — covered in
[Resilience](https://effect.plants.sh/http-client/resilience/).