Making Requests
There are two ways to send a request. For the common case, the HttpClient
service has method accessors — get, post, put, patch, del, head,
options — that take a URL and an options object. For anything richer, you
build a request value with HttpClientRequest and hand it to client.execute.
Both return an Effect that yields a HttpClientResponse.
import { Effect, Schema } from "effect"import { HttpClient, HttpClientRequest } from "effect/unstable/http"
class CreatedTodo extends Schema.Class<CreatedTodo>("CreatedTodo")({ id: Schema.Number, title: Schema.String, completed: Schema.Boolean}) {}
const program = Effect.gen(function*() { const client = yield* HttpClient.HttpClient
// 1. The quick path: a GET with query parameters. const listResponse = yield* client.get("https://api.example.com/todos", { urlParams: { completed: "false", limit: "20" } })
// 2. The builder path: construct a request value, then execute it. // Useful when you want to compose several modifications. const created = yield* HttpClientRequest.post("https://api.example.com/todos").pipe( HttpClientRequest.acceptJson, HttpClientRequest.setHeader("x-request-source", "docs-example"), // bodyJsonUnsafe serializes a value to a JSON body. It is "unsafe" only in // that it assumes the value is JSON-encodable; it does not throw at runtime // for plain data. HttpClientRequest.bodyJsonUnsafe({ title: "Write docs", completed: false }), client.execute )
return { listResponse, created }})A HttpClientRequest is an immutable value: its method, URL, query
parameters (urlParams), hash, headers, and body are stored as separate
structured fields, and every combinator returns a new request. The base URL,
query string, and hash stay separate until the request is converted to a real
URL or Web Request, so you can update each part independently.
Method accessors vs. constructors
Section titled “Method accessors vs. constructors”There are two entry points that produce the same shapes.
The HttpClient accessors (client.get, client.post, …) build the
request and execute it in one step — they return an Effect<HttpClientResponse>.
There are seven: get, post, put, patch, del (for DELETE), head,
and options.
import { Effect } from "effect"import { HttpClient } from "effect/unstable/http"
const run = Effect.gen(function*() { const client = yield* HttpClient.HttpClient const res = yield* client.del("https://api.example.com/todos/42") return res})The HttpClientRequest constructors build a request value you can pass
around, transform, and later execute with client.execute. Each takes a
string | URL plus an optional Options.NoUrl object.
get / post / put / patch / delete / head / options / trace
Section titled “get / post / put / patch / delete / head / options / trace”One constructor per method. HttpClientRequest.delete is the DELETE
constructor; there is also a TRACE constructor.
import { HttpClientRequest } from "effect/unstable/http"
HttpClientRequest.get("https://api.example.com/todos")HttpClientRequest.post("https://api.example.com/todos")HttpClientRequest.put("https://api.example.com/todos/1")HttpClientRequest.patch("https://api.example.com/todos/1")HttpClientRequest.delete("https://api.example.com/todos/1")HttpClientRequest.head("https://api.example.com/todos/1")HttpClientRequest.options("https://api.example.com/todos")HttpClientRequest.trace("https://api.example.com/todos")// => each yields an immutable HttpClientRequest with the given methodThe starting point for all constructors: a GET request with no URL, query
parameters, hash, headers, or body.
import { HttpClientRequest } from "effect/unstable/http"
HttpClientRequest.empty.method// => "GET"HttpClientRequest.empty.url// => ""Creates a constructor for an arbitrary method, then call it with a URL. This is
what get, post, etc. are built from.
import { HttpClientRequest } from "effect/unstable/http"
const report = HttpClientRequest.make("REPORT" as any)const req = report("https://api.example.com/calendar")// => HttpClientRequest with method "REPORT"makeWith
Section titled “makeWith”The lowest-level constructor: build a request from fully normalized components
(method, url, UrlParams, hash Option, Headers, HttpBody). Rarely needed
directly — prefer the method constructors plus combinators.
import { Option } from "effect"import { HttpClientRequest, HttpBody, Headers, UrlParams } from "effect/unstable/http"
const req = HttpClientRequest.makeWith( "GET", "https://api.example.com", UrlParams.empty, Option.none(), Headers.empty, HttpBody.empty)// => HttpClientRequestisHttpClientRequest
Section titled “isHttpClientRequest”Type guard for request values.
import { HttpClientRequest } from "effect/unstable/http"
HttpClientRequest.isHttpClientRequest(HttpClientRequest.get("/x"))// => trueHttpClientRequest.isHttpClientRequest({})// => falseWeb Request conversion: fromWeb / toWebResult / toWeb
Section titled “Web Request conversion: fromWeb / toWebResult / toWeb”Bridge between HttpClientRequest and the platform Request. fromWeb reads a
Web Request into a request value; toWebResult returns a Result (failing
with UrlParamsError when the URL is invalid); toWeb is the effectful variant.
import { Effect } from "effect"import { HttpClientRequest } from "effect/unstable/http"
// Web Request -> HttpClientRequestconst fromBrowser = HttpClientRequest.fromWeb( new Request("https://api.example.com/todos", { method: "POST" }))// => HttpClientRequest with method "POST"
// HttpClientRequest -> Result<Request, UrlParamsError>const result = HttpClientRequest.toWebResult(fromBrowser)// => Result.Success(Request) | Result.Failure(UrlParamsError)
// HttpClientRequest -> Effect<Request, UrlParamsError>const effectful = HttpClientRequest.toWeb(fromBrowser, { signal: AbortSignal.timeout(5000) })// => Effect<Request, UrlParamsError>void effectfulvoid resultvoid EffectThe options object
Section titled “The options object”The method accessors and the HttpClientRequest.* constructors accept the same
HttpClientRequest.Options (minus method/url, which are already implied —
that subset is Options.NoUrl). Every field:
import { HttpClientRequest } from "effect/unstable/http"
const request = HttpClientRequest.get("https://api.example.com/search", { // Query string parameters. Accepts a record, an array of pairs, or UrlParams. urlParams: { q: "effect", page: "1" }, // Request headers. Accepts a record or a Headers value. headers: { authorization: "Bearer token" }, // URL fragment (without the leading "#"). hash: "results", // Set the Accept header to a specific media type. accept: "application/json", // Shortcut that sets Accept to application/json (overrides `accept` if both set). acceptJson: true, // An HttpBody value (see the body reference below). body: undefined})The full Options interface (the method and url fields are present when
constructing via make/modify, omitted in the NoUrl form used by the
constructors):
| Field | Type | Notes |
|---|---|---|
method | HttpMethod | The HTTP method. |
url | string | URL | A URL extracts its search params and hash into structured fields. |
urlParams | UrlParams.Input | Record, array of pairs, or UrlParams. |
hash | string | URL fragment without #. |
headers | Headers.Input | Record or Headers. |
accept | string | Sets the Accept header. |
acceptJson | boolean | Sets Accept: application/json. |
body | HttpBody.HttpBody | The request body. |
Building requests with combinators
Section titled “Building requests with combinators”Because a request is immutable, the combinators below each return a new request
and compose cleanly under .pipe. A request can be assembled independently of
any client and reused.
import { Effect } from "effect"import { HttpClient, HttpClientRequest } from "effect/unstable/http"
const search = HttpClientRequest.get("https://api.example.com/search").pipe( HttpClientRequest.bearerToken("secret-token"), HttpClientRequest.setUrlParams({ q: "effect", lang: "en" }), HttpClientRequest.acceptJson)
const run = Effect.gen(function*() { const client = yield* HttpClient.HttpClient return yield* client.execute(search)})modify
Section titled “modify”Applies an Options object to a request in one call — the same logic the
constructors use internally.
import { HttpClientRequest } from "effect/unstable/http"
HttpClientRequest.empty.pipe( HttpClientRequest.modify({ method: "POST", url: "https://api.example.com", acceptJson: true }))// => POST request with Accept: application/jsonsetMethod
Section titled “setMethod”Replaces the HTTP method.
import { HttpClientRequest } from "effect/unstable/http"
HttpClientRequest.get("https://api.example.com").pipe( HttpClientRequest.setMethod("POST")).method// => "POST"setUrl
Section titled “setUrl”Replaces the URL. When given a URL, its search parameters and fragment are
extracted into the request’s urlParams and hash fields; a string is kept
as-is.
import { HttpClientRequest } from "effect/unstable/http"
HttpClientRequest.empty.pipe( HttpClientRequest.setUrl(new URL("https://api.example.com/x?q=1#top")))// => url "https://api.example.com/x", urlParams { q: "1" }, hash Some("top")prependUrl / appendUrl
Section titled “prependUrl / appendUrl”Add a segment to the front or back of the URL, normalizing slashes (inserting or trimming exactly one).
import { HttpClientRequest } from "effect/unstable/http"
HttpClientRequest.get("/todos").pipe( HttpClientRequest.prependUrl("https://api.example.com")).url// => "https://api.example.com/todos"
HttpClientRequest.get("https://api.example.com/todos").pipe( HttpClientRequest.appendUrl("42")).url// => "https://api.example.com/todos/42"updateUrl
Section titled “updateUrl”Transforms the URL string with a function.
import { HttpClientRequest } from "effect/unstable/http"
HttpClientRequest.get("https://api.example.com/v1/todos").pipe( HttpClientRequest.updateUrl((u) => u.replace("/v1/", "/v2/"))).url// => "https://api.example.com/v2/todos"setHash / removeHash
Section titled “setHash / removeHash”Set or clear the URL fragment (stored without the leading #).
import { Option } from "effect"import { HttpClientRequest } from "effect/unstable/http"
const withHash = HttpClientRequest.get("https://api.example.com/docs").pipe( HttpClientRequest.setHash("section-2"))withHash.hash// => Option.some("section-2")
HttpClientRequest.removeHash(withHash).hash// => Option.none()void OptionsetHeader / setHeaders
Section titled “setHeader / setHeaders”Set one header, or merge a collection of headers (existing values with matching names are replaced).
import { HttpClientRequest } from "effect/unstable/http"
HttpClientRequest.get("https://api.example.com").pipe( HttpClientRequest.setHeader("x-trace-id", "abc123"), HttpClientRequest.setHeaders({ "x-region": "us-east", "x-tenant": "acme" }))// => request with all three headers setaccept / acceptJson
Section titled “accept / acceptJson”Set the Accept header to a media type, or directly to application/json.
import { HttpClientRequest } from "effect/unstable/http"
HttpClientRequest.get("https://api.example.com").pipe( HttpClientRequest.accept("text/csv"))// => Accept: text/csv
HttpClientRequest.get("https://api.example.com").pipe( HttpClientRequest.acceptJson)// => Accept: application/jsonsetUrlParam / setUrlParams / appendUrlParam / appendUrlParams
Section titled “setUrlParam / setUrlParams / appendUrlParam / appendUrlParams”set* replaces existing values for a key; append* keeps existing values so
repeated keys are preserved. The single-value variants take key, value; the
plural variants take a UrlParams.Input.
import { HttpClientRequest } from "effect/unstable/http"
HttpClientRequest.get("https://api.example.com/search").pipe( HttpClientRequest.setUrlParam("q", "effect"), HttpClientRequest.setUrlParams({ page: "1", limit: "20" }), HttpClientRequest.appendUrlParam("tag", "ts"), HttpClientRequest.appendUrlParams({ tag: "fp" }))// => query: q=effect&page=1&limit=20&tag=ts&tag=fpbearerToken / basicAuth
Section titled “bearerToken / basicAuth”Set the Authorization header. Both accept plain strings or Redacted values,
so secrets do not leak into logs.
import { Redacted } from "effect"import { HttpClientRequest } from "effect/unstable/http"
HttpClientRequest.get("https://api.example.com").pipe( HttpClientRequest.bearerToken(Redacted.make("secret-token")))// => Authorization: Bearer secret-token
HttpClientRequest.get("https://api.example.com").pipe( HttpClientRequest.basicAuth("user", Redacted.make("pass")))// => Authorization: Basic dXNlcjpwYXNzvoid RedactedRequest bodies
Section titled “Request bodies”Body combinators set the request body and, where the body carries it,
synchronize the Content-Type and Content-Length headers. Empty and
FormData bodies intentionally clear explicit content headers so the runtime
can supply multipart boundaries.
Most are pure functions returning a new request. The exceptions are
bodyJson, schemaBodyJson, and bodyFile, which return an Effect because
encoding (or reading a file) can fail.
setBody
Section titled “setBody”The primitive: attach a prebuilt HttpBody value. The other body combinators
are thin wrappers over this plus an HttpBody.* constructor.
import { HttpClientRequest, HttpBody } from "effect/unstable/http"
HttpClientRequest.post("https://api.example.com/upload").pipe( HttpClientRequest.setBody(HttpBody.text("hello")))// => Content-Type: text/plain, Content-Length: 5bodyText
Section titled “bodyText”A UTF-8 text body. Content type defaults to text/plain.
import { HttpClientRequest } from "effect/unstable/http"
HttpClientRequest.post("https://api.example.com/log").pipe( HttpClientRequest.bodyText("plain log line"))// => Content-Type: text/plainHttpClientRequest.post("https://api.example.com/doc").pipe( HttpClientRequest.bodyText("<x/>", "application/xml"))// => Content-Type: application/xmlbodyUint8Array
Section titled “bodyUint8Array”A raw byte body. Content type defaults to application/octet-stream.
import { HttpClientRequest } from "effect/unstable/http"
HttpClientRequest.post("https://api.example.com/blob").pipe( HttpClientRequest.bodyUint8Array(new Uint8Array([1, 2, 3]), "application/wasm"))// => Content-Type: application/wasm, Content-Length: 3bodyJsonUnsafe
Section titled “bodyJsonUnsafe”A JSON body via JSON.stringify. “Unsafe” because JSON.stringify can throw
(e.g. on a BigInt or a circular reference); fine for plain data you trust.
import { HttpClientRequest } from "effect/unstable/http"
HttpClientRequest.post("https://api.example.com/todos").pipe( HttpClientRequest.bodyJsonUnsafe({ title: "Ship it", completed: false }))// => Content-Type: application/jsonbodyJson
Section titled “bodyJson”The effectful JSON path: a JSON.stringify failure is captured as
HttpBodyError in the error channel instead of throwing.
import { Effect } from "effect"import { HttpClientRequest } from "effect/unstable/http"
const _ = Effect.gen(function*() { return yield* HttpClientRequest.post("https://api.example.com/todos").pipe( HttpClientRequest.bodyJson({ title: "Ship it" }) )})// => Effect<HttpClientRequest, HttpBodyError>schemaBodyJson
Section titled “schemaBodyJson”Encodes a value through a Schema (so the body matches a contract) and sets it
as a JSON body. Returns a function from value to Effect; schema and JSON
failures surface as HttpBodyError.
import { Effect, Schema } from "effect"import { HttpClientRequest } from "effect/unstable/http"
const NewTodo = Schema.Struct({ title: Schema.String, completed: Schema.Boolean})
// schemaBodyJson(schema) returns a function from value -> Effect of the request.const buildRequest = (todo: typeof NewTodo.Type) => HttpClientRequest.post("https://api.example.com/todos").pipe( HttpClientRequest.schemaBodyJson(NewTodo)(todo) )
const _ = Effect.gen(function*() { return yield* buildRequest({ title: "Ship it", completed: false })})// => Effect<HttpClientRequest, HttpBodyError>bodyUrlParams
Section titled “bodyUrlParams”An application/x-www-form-urlencoded body from URL-param input (record, pairs,
or UrlParams).
import { HttpClientRequest } from "effect/unstable/http"
HttpClientRequest.post("https://api.example.com/login").pipe( HttpClientRequest.bodyUrlParams({ username: "ada", password: "secret" }))// => Content-Type: application/x-www-form-urlencoded, body "username=ada&password=secret"bodyFormData
Section titled “bodyFormData”A multipart/form-data body from a Web FormData value. The content type and
length are left unset so the runtime can generate the multipart boundary.
import { HttpClientRequest } from "effect/unstable/http"
const fd = new FormData()fd.append("field", "value")
HttpClientRequest.post("https://api.example.com/upload").pipe( HttpClientRequest.bodyFormData(fd))// => multipart body (boundary added by the runtime)bodyFormDataRecord
Section titled “bodyFormDataRecord”Builds the FormData for you from a record. Array fields append each item under
the same key; primitives are stringified, File/Blob values are appended
directly, and null/undefined are skipped.
import { HttpClientRequest } from "effect/unstable/http"
HttpClientRequest.post("https://api.example.com/upload").pipe( HttpClientRequest.bodyFormDataRecord({ title: "Report", tags: ["q1", "draft"], note: null // skipped }))// => multipart body with title + two tags fieldsbodyStream
Section titled “bodyStream”A streaming byte body for large payloads you do not want to buffer. Optionally
provide contentType and contentLength; the type defaults to
application/octet-stream.
import { Stream } from "effect"import { HttpClientRequest } from "effect/unstable/http"
HttpClientRequest.post("https://api.example.com/upload").pipe( HttpClientRequest.bodyStream( Stream.make(new Uint8Array([1, 2]), new Uint8Array([3, 4])), { contentType: "application/octet-stream", contentLength: 4 } ))// => streamed request bodyvoid StreambodyFile
Section titled “bodyFile”Streams a file from disk. Returns an Effect requiring FileSystem; it stats
the file to set Content-Length and can fail with PlatformError.
import { Effect } from "effect"import { HttpClientRequest } from "effect/unstable/http"
const _ = Effect.gen(function*() { return yield* HttpClientRequest.post("https://api.example.com/upload").pipe( HttpClientRequest.bodyFile("/tmp/report.pdf", { contentType: "application/pdf" }) )})// => Effect<HttpClientRequest, PlatformError, FileSystem>toUrl / trace
Section titled “toUrl / trace”Assembles the request’s URL, query parameters, and hash into a single URL,
returning Option.none() if the result is not a valid URL.
import { HttpClientRequest } from "effect/unstable/http"
HttpClientRequest.get("https://api.example.com/search").pipe( HttpClientRequest.setUrlParam("q", "effect"), HttpClientRequest.setHash("top"), HttpClientRequest.toUrl)// => Option.some(URL "https://api.example.com/search?q=effect#top")The TRACE request constructor (listed above with the other method
constructors).
import { HttpClientRequest } from "effect/unstable/http"
HttpClientRequest.trace("https://api.example.com/debug").method// => "TRACE"HttpMethod reference
Section titled “HttpMethod reference”The HttpMethod module holds the supported method vocabulary used across the
HTTP client, server, and routing APIs. Methods are uppercase string literals:
"GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS" | "TRACE".
A readonly Set of every supported method literal — handy for membership tests
or iteration.
import { HttpMethod } from "effect/unstable/http"
HttpMethod.all.has("PATCH")// => trueallShort
Section titled “allShort”Tuples mapping each method to its short constructor name (note DELETE maps to
del).
import { HttpMethod } from "effect/unstable/http"
HttpMethod.allShort// => [["GET", "get"], ["POST", "post"], ["PUT", "put"], ["DELETE", "del"], ...]hasBody
Section titled “hasBody”Returns true when a method is treated as able to carry a body, narrowing to
HttpMethod.WithBody. The bodyless set is GET, HEAD, OPTIONS, TRACE;
DELETE is treated as able to carry a body.
import { HttpMethod } from "effect/unstable/http"
HttpMethod.hasBody("POST")// => trueHttpMethod.hasBody("GET")// => falseisHttpMethod
Section titled “isHttpMethod”Runtime refinement before accepting an unknown value as a method. Lowercase names are not valid.
import { HttpMethod } from "effect/unstable/http"
HttpMethod.isHttpMethod("GET")// => trueHttpMethod.isHttpMethod("get")// => falseWhat you get back
Section titled “What you get back”Executing a request yields a HttpClientResponse in the success channel and an
HttpClientError in the failure channel. By default a non-2xx status is not
an error — the response is returned as-is so you can inspect the status. To make
a bad status fail, use HttpClient.filterStatusOk (covered under
Resilience).
Decoding the body is covered next, in Handling responses.