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.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, and cookies 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 |
| schemaJson(schema) | JSON body | { status, headers, body } together |
| schemaNoBody(schema) | nothing | { status, headers } only |
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. Once you can read responses, the next step is making the client resilient — retries, timeouts, and rate limiting — covered in Resilience.