| 1 | /** |
| 2 | * Fetch-based implementation of the Effect HTTP client service. |
| 3 | * |
| 4 | * This module provides an `HttpClient` layer that executes requests through a |
| 5 | * Web Fetch API implementation. It is the transport to use in browsers, edge |
| 6 | * runtimes, and Node.js environments where `globalThis.fetch` is available, or |
| 7 | * anywhere a compatible fetch function can be supplied. |
| 8 | * |
| 9 | * @stability unstable |
| 10 | * @since 4.0.0 |
| 11 | */ |
| 12 | import * as Context from "../Context.ts" |
| 13 | import * as Effect from "../Effect.ts" |
| 14 | import type * as Layer from "../Layer.ts" |
| 15 | import * as Stream from "../Stream.ts" |
| 16 | import * as Headers from "./Headers.ts" |
| 17 | import * as HttpClient from "./HttpClient.ts" |
| 18 | import * as HttpClientError from "./HttpClientError.ts" |
| 19 | import * as HttpClientResponse from "./HttpClientResponse.ts" |
| 20 | |
| 21 | /** |
| 22 | * Context reference for the `fetch` implementation used by the fetch-based HTTP client. |
| 23 | * |
| 24 | * **Details** |
| 25 | * |
| 26 | * Defaults to `globalThis.fetch`. |
| 27 | * |
| 28 | * @stability unstable |
| 29 | * @category services |
| 30 | * @since 4.0.0 |
| 31 | */ |
| 32 | export const Fetch = Context.Reference<typeof globalThis.fetch>("effect/http/FetchHttpClient/Fetch", { |
| 33 | defaultValue: () => globalThis.fetch |
| 34 | }) |
| 35 | |
| 36 | /** |
| 37 | * Service that contains default fetch options for the fetch-based HTTP client. |
| 38 | * |
| 39 | * **When to use** |
| 40 | * |
| 41 | * Use to provide default credentials, cache, redirect, integrity, or other |
| 42 | * fetch options for outgoing HTTP requests. |
| 43 | * |
| 44 | * **Details** |
| 45 | * |
| 46 | * Request-specific method, headers, body, and abort signal are supplied by the client when a request is executed. |
| 47 | * |
| 48 | * @stability unstable |
| 49 | * @category services |
| 50 | * @since 4.0.0 |
| 51 | */ |
| 52 | export class RequestInit extends Context.Service<RequestInit, globalThis.RequestInit>()( |
| 53 | "effect/http/FetchHttpClient/RequestInit" |
| 54 | ) {} |
| 55 | |
| 56 | const fetch: HttpClient.HttpClient = HttpClient.make((request, url, signal, fiber) => { |
| 57 | const fetch = fiber.getRef(Fetch) |
| 58 | const options: globalThis.RequestInit = Context.getOrUndefined(fiber.context, RequestInit) ?? {} |
| 59 | let headers = options.headers |
| 60 | ? Headers.merge(Headers.fromInput(options.headers as Headers.Input), request.headers) |
| 61 | : request.headers |
| 62 | if (headers["content-length"]) { |
| 63 | headers = Headers.remove(headers, "content-length") |
| 64 | } |
| 65 | const send = (body: BodyInit | undefined) => |
| 66 | Effect.map( |
| 67 | Effect.tryPromise({ |
| 68 | try: () => |
| 69 | fetch(url, { |
| 70 | ...options, |
| 71 | method: request.method, |
| 72 | headers, |
| 73 | body, |
| 74 | duplex: typeof ReadableStream !== "undefined" && body instanceof ReadableStream ? "half" : undefined, |
| 75 | signal |
| 76 | } as any), |
| 77 | catch: (cause) => |
| 78 | new HttpClientError.HttpClientError({ |
| 79 | reason: new HttpClientError.TransportError({ |
| 80 | request, |
| 81 | cause |
| 82 | }) |
| 83 | }) |
| 84 | }), |
| 85 | (response) => HttpClientResponse.fromWeb(request, response) |
| 86 | ) |
| 87 | switch (request.body._tag) { |
| 88 | case "Raw": |
| 89 | case "Uint8Array": |
| 90 | return send(request.body.body as any) |
| 91 | case "FormData": |
| 92 | return send(request.body.formData) |
| 93 | case "Stream": |
| 94 | return Effect.flatMap(Stream.toReadableStreamEffect(request.body.stream), send) |
| 95 | } |
| 96 | return send(undefined) |
| 97 | }) |
| 98 | |
| 99 | /** |
| 100 | * Layer that provides an `HttpClient` implementation backed by the configured |
| 101 | * `Fetch` function. |
| 102 | * |
| 103 | * **When to use** |
| 104 | * |
| 105 | * Use when an Effect program should execute `HttpClient` requests through the |
| 106 | * platform `fetch` implementation, especially in browser, edge, or Node.js |
| 107 | * runtimes with `globalThis.fetch`. |
| 108 | * |
| 109 | * **Details** |
| 110 | * |
| 111 | * The layer uses the current `Fetch` reference and optional `RequestInit` |
| 112 | * service for each request. Request-specific method, headers, body, and abort |
| 113 | * signal are supplied by the client and override matching `RequestInit` fields. |
| 114 | * |
| 115 | * **Gotchas** |
| 116 | * |
| 117 | * Fetch behavior comes from the runtime's implementation, so CORS, cookies, |
| 118 | * redirects, abort handling, and streaming support can vary by platform. Stream |
| 119 | * request bodies are sent as Web streams with `duplex: "half"`, and any |
| 120 | * `content-length` header is removed before calling `fetch`. |
| 121 | * |
| 122 | * @see {@link Fetch} for supplying the fetch implementation used by this layer |
| 123 | * @see {@link RequestInit} for default `RequestInit` options applied before request-specific fields |
| 124 | * |
| 125 | * @stability unstable |
| 126 | * @category layers |
| 127 | * @since 4.0.0 |
| 128 | */ |
| 129 | export const layer: Layer.Layer<HttpClient.HttpClient> = HttpClient.layerMergedContext(Effect.succeed(fetch)) |