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.
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
Section titled “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:
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.
Decoding helpers
Section titled “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:
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
Section titled “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:
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}`)) }) )})Streaming the body
Section titled “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:
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 for the full set of stream operators.
Reference
Section titled “Reference”The HttpClientResponse interface
Section titled “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 Effects (or a Stream) because reading can fail.
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:
Reads the body as a UTF-8 string. Cached, and populates arrayBuffer.
declare const res: import("effect/unstable/http/HttpClientResponse").HttpClientResponseres.text // => Effect<string, HttpClientError>Reads the body as text, then JSON.parses it; an empty body parses to null.
declare const res: import("effect/unstable/http/HttpClientResponse").HttpClientResponseres.json // => Effect<Schema.Json, HttpClientError> (e.g. { ok: true })urlParamsBody
Section titled “urlParamsBody”Parses a URL-encoded (application/x-www-form-urlencoded) body into UrlParams.
declare const res: import("effect/unstable/http/HttpClientResponse").HttpClientResponseres.urlParamsBody // => Effect<UrlParams, HttpClientError> e.g. [["a", "1"]]arrayBuffer
Section titled “arrayBuffer”Reads the raw body bytes. Cached, and populates text.
declare const res: import("effect/unstable/http/HttpClientResponse").HttpClientResponseres.arrayBuffer // => Effect<ArrayBuffer, HttpClientError>stream
Section titled “stream”The body as a byte stream. Fails with EmptyBodyError when there is no body.
declare const res: import("effect/unstable/http/HttpClientResponse").HttpClientResponseres.stream // => Stream<Uint8Array, HttpClientError>formData
Section titled “formData”Reads a multipart/form-data body into a Web FormData.
declare const res: import("effect/unstable/http/HttpClientResponse").HttpClientResponseres.formData // => Effect<FormData, HttpClientError>Schema decoders
Section titled “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
Section titled “schemaBodyJson”Reads the JSON body and decodes it with the schema. The most common decoder.
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
Section titled “schemaBodyUrlParams”Reads a URL-encoded body and decodes the parameters with the schema.
import { Schema } from "effect"import { HttpClientResponse } from "effect/unstable/http"
const Form = Schema.Struct({ token: Schema.String, ttl: Schema.NumberFromString })
declare const res: HttpClientResponse.HttpClientResponseHttpClientResponse.schemaBodyUrlParams(Form)(res)// body "token=abc&ttl=60" => { token: "abc", ttl: 60 }schemaJson
Section titled “schemaJson”Decodes the status, headers, and JSON body together as
{ status, headers, body }. Use when the contract spans more than the body.
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.HttpClientResponseHttpClientResponse.schemaJson(Envelope)(res)// => { status: 200, headers: { "x-request-id": "..." }, body: { ok: true } }schemaNoBody
Section titled “schemaNoBody”Decodes only { status, headers } without reading a body — useful for 204
responses or when only headers matter. Fails only with Schema.SchemaError.
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.HttpClientResponseHttpClientResponse.schemaNoBody(Meta)(res)// => { status: 201, headers: { location: "/things/9" } }schemaHeaders
Section titled “schemaHeaders”Validates and decodes just the response headers with the schema.
import { Schema } from "effect"import { HttpClientResponse } from "effect/unstable/http"
const RateLimit = Schema.Struct({ "x-ratelimit-remaining": Schema.NumberFromString})
declare const res: HttpClientResponse.HttpClientResponseHttpClientResponse.schemaHeaders(RateLimit)(res)// headers { "x-ratelimit-remaining": "42" } => { "x-ratelimit-remaining": 42 }Status helpers
Section titled “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
Section titled “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 above for a full example.
import { Effect } from "effect"import { HttpClientResponse } from "effect/unstable/http"
declare const res: HttpClientResponse.HttpClientResponseHttpClientResponse.matchStatus(res, { 204: () => Effect.succeed("no content"), "2xx": (ok) => ok.text, orElse: (other) => Effect.fail(new Error(`status ${other.status}`))})filterStatus
Section titled “filterStatus”Succeeds with the response when the status satisfies the predicate, otherwise
fails with an HttpClientError whose reason is a StatusCodeError.
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
Section titled “filterStatusOk”Shorthand for “only accept 2xx”. Fails with StatusCodeError for any other
status.
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
Section titled “Constructors and stream”fromWeb
Section titled “fromWeb”Wraps a platform Web Response and its originating HttpClientRequest into an
HttpClientResponse. Useful in adapters and tests.
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
Section titled “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 above.
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
declare const client: HttpClient.HttpClientHttpClientResponse.stream(client.get("https://api.example.com/big.bin"))// => Stream<Uint8Array, HttpClientError>Response error types
Section titled “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.
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
Section titled “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.
import { HttpClientError } from "effect/unstable/http"
declare const value: unknownHttpClientError.isHttpClientError(value) // => booleanTransportError (request)
Section titled “TransportError (request)”Transport-level failure while sending the request (DNS, connection reset, etc.). No response.
// error.reason._tag === "TransportError"// error.reason.request, error.reason.causeEncodeError (request)
Section titled “EncodeError (request)”Failure while encoding the request body before sending. No response.
// error.reason._tag === "EncodeError"InvalidUrlError (request)
Section titled “InvalidUrlError (request)”The request URL could not be constructed. No response.
// error.reason._tag === "InvalidUrlError"StatusCodeError (response)
Section titled “StatusCodeError (response)”A status filter (filterStatus / filterStatusOk) rejected the response’s
status. Carries the response.
// error.reason._tag === "StatusCodeError"// error.reason.response.status => 500DecodeError (response)
Section titled “DecodeError (response)”Reading or decoding the response body failed (bad JSON, stream read error, schema
mismatch upstream). Carries the response.
// error.reason._tag === "DecodeError"EmptyBodyError (response)
Section titled “EmptyBodyError (response)”An operation expected a body but the response had none (e.g. streaming an empty
body). Carries the response.
// 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.
Once you can read responses, the next step is making the client resilient — retries, timeouts, and rate limiting — covered in Resilience.