1/**
2 * Provides the service used to run outgoing HTTP requests.
3 *
4 * `HttpClient` executes immutable `HttpClientRequest` values and returns
5 * `HttpClientResponse` values. Keeping HTTP behind this service lets programs,
6 * tests, and generated API clients use the same request model without depending
7 * on one concrete platform transport. This module includes request accessors,
8 * constructors and layers, request and response transformations, status
9 * filtering, retries, rate limiting, cookies, redirect handling, scoped request
10 * abortion, and tracing support.
11 *
12 * @stability unstable
13 * @since 4.0.0
14 */
15import type { NonEmptyReadonlyArray } from "../Array.ts"
16import * as Cause from "../Cause.ts"
17import { Clock } from "../Clock.ts"
18import * as Context from "../Context.ts"
19import * as Duration from "../Duration.ts"
20import * as Effect from "../Effect.ts"
21import * as Fiber from "../Fiber.ts"
22import { constant, constFalse, constTrue, dual, flow, identity } from "../Function.ts"
23import * as Inspectable from "../Inspectable.ts"
24import * as Layer from "../Layer.ts"
25import * as Option from "../Option.ts"
26import type * as RateLimiter from "../persistence/RateLimiter.ts"
27import { type Pipeable, pipeArguments } from "../Pipeable.ts"
28import * as Predicate from "../Predicate.ts"
29import * as Ref from "../Ref.ts"
30import * as Result from "../Result.ts"
31import * as Schedule from "../Schedule.ts"
32import type * as Scope from "../Scope.ts"
33import * as Stream from "../Stream.ts"
34import * as Tracer from "../Tracer.ts"
35import type { EqualsWith, ExcludeTag, ExtractTag, NoExcessProperties, NoInfer, Tags } from "../Types.ts"
36import * as Cookies from "./Cookies.ts"
37import * as Headers from "./Headers.ts"
38import * as HttpBody from "./HttpBody.ts"
39import * as Error from "./HttpClientError.ts"
40import * as HttpClientRequest from "./HttpClientRequest.ts"
41import * as HttpClientResponse from "./HttpClientResponse.ts"
42import * as HttpIncomingMessage from "./HttpIncomingMessage.ts"
43import * as HttpMethod from "./HttpMethod.ts"
44import * as TraceContext from "./HttpTraceContext.ts"
45import * as Url from "./Url.ts"
46
47const TypeId = "~effect/http/HttpClient"
48
49/**
50 * Returns `true` if the provided value is an `HttpClient`.
51 *
52 * @stability unstable
53 * @category guards
54 * @since 4.0.0
55 */
56export const isHttpClient = (u: unknown): u is HttpClient => Predicate.hasProperty(u, TypeId)
57
58/**
59 * HTTP client whose requests produce `HttpClientResponse` values and can fail with `HttpClientError`.
60 *
61 * @stability unstable
62 * @category models
63 * @since 4.0.0
64 */
65export interface HttpClient extends HttpClient.With<Error.HttpClientError> {}
66
67/**
68 * Namespace containing type-level members associated with `HttpClient`.
69 *
70 * @stability unstable
71 * @since 4.0.0
72 */
73export declare namespace HttpClient {
74 /**
75 * Parameterized HTTP client that may fail with `E` and require environment `R`.
76 *
77 * **Details**
78 *
79 * It exposes preprocessing, postprocessing, direct request execution, and method-specific helpers.
80 *
81 * @stability unstable
82 * @category models
83 * @since 4.0.0
84 */
85 export interface With<E, R = never> extends Pipeable, Inspectable.Inspectable {
86 readonly [TypeId]: typeof TypeId
87 readonly preprocess: Preprocess<E, R>
88 readonly postprocess: Postprocess<E, R>
89 readonly execute: (
90 request: HttpClientRequest.HttpClientRequest
91 ) => Effect.Effect<HttpClientResponse.HttpClientResponse, E, R>
92
93 readonly get: (
94 url: string | URL,
95 options?: HttpClientRequest.Options.NoUrl
96 ) => Effect.Effect<HttpClientResponse.HttpClientResponse, E, R>
97 readonly query: (
98 url: string | URL,
99 options?: HttpClientRequest.Options.NoUrl
100 ) => Effect.Effect<HttpClientResponse.HttpClientResponse, E, R>
101 readonly head: (
102 url: string | URL,
103 options?: HttpClientRequest.Options.NoUrl
104 ) => Effect.Effect<HttpClientResponse.HttpClientResponse, E, R>
105 readonly post: (
106 url: string | URL,
107 options?: HttpClientRequest.Options.NoUrl
108 ) => Effect.Effect<HttpClientResponse.HttpClientResponse, E, R>
109 readonly patch: (
110 url: string | URL,
111 options?: HttpClientRequest.Options.NoUrl
112 ) => Effect.Effect<HttpClientResponse.HttpClientResponse, E, R>
113 readonly put: (
114 url: string | URL,
115 options?: HttpClientRequest.Options.NoUrl
116 ) => Effect.Effect<HttpClientResponse.HttpClientResponse, E, R>
117 readonly del: (
118 url: string | URL,
119 options?: HttpClientRequest.Options.NoUrl
120 ) => Effect.Effect<HttpClientResponse.HttpClientResponse, E, R>
121 readonly options: (
122 url: string | URL,
123 options?: HttpClientRequest.Options.NoUrl
124 ) => Effect.Effect<HttpClientResponse.HttpClientResponse, E, R>
125 }
126
127 /**
128 * Effectful transformation applied to a request before the client executes it.
129 *
130 * @stability unstable
131 * @category models
132 * @since 4.0.0
133 */
134 export type Preprocess<E, R> = (
135 request: HttpClientRequest.HttpClientRequest
136 ) => Effect.Effect<HttpClientRequest.HttpClientRequest, E, R>
137
138 /**
139 * Function that turns a preprocessed request effect into the response effect executed by the client.
140 *
141 * @stability unstable
142 * @category models
143 * @since 4.0.0
144 */
145 export type Postprocess<E = never, R = never> = (
146 request: Effect.Effect<HttpClientRequest.HttpClientRequest, E, R>
147 ) => Effect.Effect<HttpClientResponse.HttpClientResponse, E, R>
148}
149
150/**
151 * Service tag for the default outgoing HTTP client service.
152 *
153 * **When to use**
154 *
155 * Use to provide the default outgoing HTTP client service used by request
156 * accessors such as `execute`, `get`, and `post`.
157 *
158 * @stability unstable
159 * @category services
160 * @since 4.0.0
161 */
162export const HttpClient: Context.Service<HttpClient, HttpClient> = Context.Service<HttpClient, HttpClient>(
163 "effect/HttpClient"
164)
165
166const accessor = (method: keyof HttpClient) => (...args: Array<any>): Effect.Effect<any, any, any> =>
167 Effect.flatMap(
168 HttpClient,
169 (client) => (client as any)[method](...args)
170 )
171
172/**
173 * Executes a prebuilt `HttpClientRequest` using the `HttpClient` service from the environment.
174 *
175 * @stability unstable
176 * @category accessors
177 * @since 4.0.0
178 */
179export const execute: (
180 request: HttpClientRequest.HttpClientRequest
181) => Effect.Effect<HttpClientResponse.HttpClientResponse, Error.HttpClientError, HttpClient> = accessor("execute")
182
183/**
184 * Executes a `GET` request using the `HttpClient` service from the environment.
185 *
186 * @stability unstable
187 * @category accessors
188 * @since 4.0.0
189 */
190export const get: (url: string | URL, options?: HttpClientRequest.Options.NoUrl | undefined) => Effect.Effect<
191 HttpClientResponse.HttpClientResponse,
192 Error.HttpClientError,
193 HttpClient
194> = accessor("get")
195
196/**
197 * Executes a `QUERY` request using the `HttpClient` service from the environment.
198 *
199 * @stability unstable
200 * @category accessors
201 * @since 4.0.0
202 */
203export const query: (url: string | URL, options?: HttpClientRequest.Options.NoUrl | undefined) => Effect.Effect<
204 HttpClientResponse.HttpClientResponse,
205 Error.HttpClientError,
206 HttpClient
207> = accessor("query")
208
209/**
210 * Executes a `HEAD` request using the `HttpClient` service from the environment.
211 *
212 * @stability unstable
213 * @category accessors
214 * @since 4.0.0
215 */
216export const head: (url: string | URL, options?: HttpClientRequest.Options.NoUrl | undefined) => Effect.Effect<
217 HttpClientResponse.HttpClientResponse,
218 Error.HttpClientError,
219 HttpClient
220> = accessor("head")
221
222/**
223 * Executes a `POST` request using the `HttpClient` service from the environment.
224 *
225 * @stability unstable
226 * @category accessors
227 * @since 4.0.0
228 */
229export const post: (url: string | URL, options?: HttpClientRequest.Options.NoUrl | undefined) => Effect.Effect<
230 HttpClientResponse.HttpClientResponse,
231 Error.HttpClientError,
232 HttpClient
233> = accessor("post")
234
235/**
236 * Executes a `PATCH` request using the `HttpClient` service from the environment.
237 *
238 * @stability unstable
239 * @category accessors
240 * @since 4.0.0
241 */
242export const patch: (url: string | URL, options?: HttpClientRequest.Options.NoUrl | undefined) => Effect.Effect<
243 HttpClientResponse.HttpClientResponse,
244 Error.HttpClientError,
245 HttpClient
246> = accessor("patch")
247
248/**
249 * Executes a `PUT` request using the `HttpClient` service from the environment.
250 *
251 * @stability unstable
252 * @category accessors
253 * @since 4.0.0
254 */
255export const put: (url: string | URL, options?: HttpClientRequest.Options.NoUrl | undefined) => Effect.Effect<
256 HttpClientResponse.HttpClientResponse,
257 Error.HttpClientError,
258 HttpClient
259> = accessor("put")
260
261/**
262 * Executes a `DELETE` request using the `HttpClient` service from the environment.
263 *
264 * @stability unstable
265 * @category accessors
266 * @since 4.0.0
267 */
268export const del: (url: string | URL, options?: HttpClientRequest.Options.NoUrl | undefined) => Effect.Effect<
269 HttpClientResponse.HttpClientResponse,
270 Error.HttpClientError,
271 HttpClient
272> = accessor("del")
273
274/**
275 * Executes an `OPTIONS` request using the `HttpClient` service from the environment.
276 *
277 * @stability unstable
278 * @category accessors
279 * @since 4.0.0
280 */
281export const options: (url: string | URL, options?: HttpClientRequest.Options.NoUrl | undefined) => Effect.Effect<
282 HttpClientResponse.HttpClientResponse,
283 Error.HttpClientError,
284 HttpClient
285> = accessor("options")
286
287/**
288 * Transforms a client by wrapping the response effect for each request.
289 *
290 * **Details**
291 *
292 * The transformation receives both the response effect and the original request, allowing it to change success, error, and environment behavior.
293 *
294 * @stability unstable
295 * @category mapping
296 * @since 4.0.0
297 */
298export const transform: {
299 <E, R, E1, R1>(
300 f: (
301 effect: Effect.Effect<HttpClientResponse.HttpClientResponse, E, R>,
302 request: HttpClientRequest.HttpClientRequest
303 ) => Effect.Effect<HttpClientResponse.HttpClientResponse, E1, R1>
304 ): (self: HttpClient.With<E, R>) => HttpClient.With<E | E1, R | R1>
305 <E, R, E1, R1>(
306 self: HttpClient.With<E, R>,
307 f: (
308 effect: Effect.Effect<HttpClientResponse.HttpClientResponse, E, R>,
309 request: HttpClientRequest.HttpClientRequest
310 ) => Effect.Effect<HttpClientResponse.HttpClientResponse, E1, R1>
311 ): HttpClient.With<E | E1, R | R1>
312} = dual(2, <E, R, E1, R1>(
313 self: HttpClient.With<E, R>,
314 f: (
315 effect: Effect.Effect<HttpClientResponse.HttpClientResponse, E, R>,
316 request: HttpClientRequest.HttpClientRequest
317 ) => Effect.Effect<HttpClientResponse.HttpClientResponse, E1, R1>
318): HttpClient.With<E | E1, R | R1> =>
319 makeWith(
320 Effect.flatMap((request) => f(self.postprocess(Effect.succeed(request)), request)),
321 self.preprocess
322 ))
323
324/**
325 * Transforms a client by applying an effectful transformation to each response effect.
326 *
327 * @stability unstable
328 * @category mapping
329 * @since 4.0.0
330 */
331export const transformResponse: {
332 <E, R, E1, R1>(
333 f: (
334 effect: Effect.Effect<HttpClientResponse.HttpClientResponse, E, R>
335 ) => Effect.Effect<HttpClientResponse.HttpClientResponse, E1, R1>
336 ): (self: HttpClient.With<E, R>) => HttpClient.With<E1, R1>
337 <E, R, E1, R1>(
338 self: HttpClient.With<E, R>,
339 f: (
340 effect: Effect.Effect<HttpClientResponse.HttpClientResponse, E, R>
341 ) => Effect.Effect<HttpClientResponse.HttpClientResponse, E1, R1>
342 ): HttpClient.With<E1, R1>
343} = dual(2, <E, R, E1, R1>(
344 self: HttpClient.With<E, R>,
345 f: (
346 effect: Effect.Effect<HttpClientResponse.HttpClientResponse, E, R>
347 ) => Effect.Effect<HttpClientResponse.HttpClientResponse, E1, R1>
348): HttpClient.With<E1, R1> => makeWith((request) => f(self.postprocess(request)), self.preprocess))
349
350const catch_: {
351 <E, E2, R2>(
352 f: (e: E) => Effect.Effect<HttpClientResponse.HttpClientResponse, E2, R2>
353 ): <R>(self: HttpClient.With<E, R>) => HttpClient.With<E2, R2 | R>
354 <E, R, E2, R2>(
355 self: HttpClient.With<E, R>,
356 f: (e: E) => Effect.Effect<HttpClientResponse.HttpClientResponse, E2, R2>
357 ): HttpClient.With<E2, R | R2>
358} = dual(
359 2,
360 <E, R, E2, R2>(
361 self: HttpClient.With<E, R>,
362 f: (e: E) => Effect.Effect<HttpClientResponse.HttpClientResponse, E2, R2>
363 ): HttpClient.With<E2, R | R2> => transformResponse(self, Effect.catch(f))
364)
365
366/**
367 * @stability unstable
368 */
369export {
370 /**
371 * Handles all client failures with an effectful recovery function and returns a transformed client.
372 *
373 * @stability unstable
374 * @category error handling
375 * @since 4.0.0
376 */
377 catch_ as catch
378}
379
380/**
381 * Handles client failures with one or more matching `_tag` values and returns a transformed client.
382 *
383 * @stability unstable
384 * @category error handling
385 * @since 4.0.0
386 */
387export const catchTag: {
388 <K extends Tags<E> | NonEmptyReadonlyArray<Tags<E>>, E, E1, R1>(
389 tag: K,
390 f: (
391 e: ExtractTag<NoInfer<E>, K extends NonEmptyReadonlyArray<string> ? K[number] : K>
392 ) => Effect.Effect<HttpClientResponse.HttpClientResponse, E1, R1>
393 ): <R>(
394 self: HttpClient.With<E, R>
395 ) => HttpClient.With<E1 | ExcludeTag<E, K extends NonEmptyReadonlyArray<string> ? K[number] : K>, R1 | R>
396 <R, E, K extends Tags<E> | NonEmptyReadonlyArray<Tags<E>>, R1, E1>(
397 self: HttpClient.With<E, R>,
398 tag: K,
399 f: (
400 e: ExtractTag<E, K extends NonEmptyReadonlyArray<string> ? K[number] : K>
401 ) => Effect.Effect<HttpClientResponse.HttpClientResponse, E1, R1>
402 ): HttpClient.With<E1 | ExcludeTag<E, K extends NonEmptyReadonlyArray<string> ? K[number] : K>, R1 | R>
403} = dual(
404 3,
405 <R, E, K extends Tags<E> | NonEmptyReadonlyArray<Tags<E>>, R1, E1>(
406 self: HttpClient.With<E, R>,
407 tag: K,
408 f: (
409 e: ExtractTag<E, K extends NonEmptyReadonlyArray<string> ? K[number] : K>
410 ) => Effect.Effect<HttpClientResponse.HttpClientResponse, E1, R1>
411 ): HttpClient.With<E1 | ExcludeTag<E, K extends NonEmptyReadonlyArray<string> ? K[number] : K>, R1 | R> =>
412 transformResponse(
413 self,
414 (effect) =>
415 Effect.catchTag<HttpClientResponse.HttpClientResponse, E, R, K, R1, E1, HttpClientResponse.HttpClientResponse>(
416 effect,
417 tag,
418 f
419 )
420 )
421)
422
423/**
424 * Handles client failures by matching their `_tag` values against a case map.
425 *
426 * @stability unstable
427 * @category error handling
428 * @since 4.0.0
429 */
430export const catchTags: {
431 <
432 E,
433 Cases extends
434 & {
435 [K in Extract<E, { _tag: string }>["_tag"]]+?: (
436 error: Extract<E, { _tag: K }>
437 ) => Effect.Effect<HttpClientResponse.HttpClientResponse, any, any>
438 }
439 & (unknown extends E ? {} : { [K in Exclude<keyof Cases, Extract<E, { _tag: string }>["_tag"]>]: never })
440 >(
441 cases: Cases
442 ): <R>(
443 self: HttpClient.With<E, R>
444 ) => HttpClient.With<
445 | Exclude<E, { _tag: keyof Cases }>
446 | {
447 [K in keyof Cases]: Cases[K] extends (...args: Array<any>) => Effect.Effect<any, infer E, any> ? E : never
448 }[keyof Cases],
449 | R
450 | {
451 [K in keyof Cases]: Cases[K] extends (...args: Array<any>) => Effect.Effect<any, any, infer R> ? R : never
452 }[keyof Cases]
453 >
454 <
455 E extends { _tag: string },
456 R,
457 Cases extends
458 & {
459 [K in Extract<E, { _tag: string }>["_tag"]]+?: (
460 error: Extract<E, { _tag: K }>
461 ) => Effect.Effect<HttpClientResponse.HttpClientResponse, any, any>
462 }
463 & (unknown extends E ? {} : { [K in Exclude<keyof Cases, Extract<E, { _tag: string }>["_tag"]>]: never })
464 >(
465 self: HttpClient.With<E, R>,
466 cases: Cases
467 ): HttpClient.With<
468 | Exclude<E, { _tag: keyof Cases }>
469 | {
470 [K in keyof Cases]: Cases[K] extends (...args: Array<any>) => Effect.Effect<any, infer E, any> ? E : never
471 }[keyof Cases],
472 | R
473 | {
474 [K in keyof Cases]: Cases[K] extends (...args: Array<any>) => Effect.Effect<any, any, infer R> ? R : never
475 }[keyof Cases]
476 >
477} = dual(
478 2,
479 <
480 E extends { _tag: string },
481 R,
482 Cases extends
483 & {
484 [K in Extract<E, { _tag: string }>["_tag"]]+?: (
485 error: Extract<E, { _tag: K }>
486 ) => Effect.Effect<HttpClientResponse.HttpClientResponse, any, any>
487 }
488 & (unknown extends E ? {}
489 : {
490 [
491 K in Exclude<
492 keyof Cases,
493 Extract<E, { _tag: string }>["_tag"]
494 >
495 ]: never
496 })
497 >(
498 self: HttpClient.With<E, R>,
499 cases: Cases
500 ): HttpClient.With<
501 | Exclude<E, { _tag: keyof Cases }>
502 | {
503 [K in keyof Cases]: Cases[K] extends (
504 ...args: Array<any>
505 ) => Effect.Effect<any, infer E, any> ? E
506 : never
507 }[keyof Cases],
508 | R
509 | {
510 [K in keyof Cases]: Cases[K] extends (
511 ...args: Array<any>
512 ) => Effect.Effect<any, any, infer R> ? R
513 : never
514 }[keyof Cases]
515 > => transformResponse(self, Effect.catchTags(cases) as any)
516)
517
518/**
519 * Filters the result of a response, or runs an alternative effect if the predicate fails.
520 *
521 * @stability unstable
522 * @category filtering
523 * @since 4.0.0
524 */
525export const filterOrElse: {
526 <B extends HttpClientResponse.HttpClientResponse, E2, R2>(
527 refinement: Predicate.Refinement<NoInfer<HttpClientResponse.HttpClientResponse>, B>,
528 orElse: (
529 response: EqualsWith<
530 HttpClientResponse.HttpClientResponse,
531 B,
532 NoInfer<HttpClientResponse.HttpClientResponse>,
533 Exclude<NoInfer<HttpClientResponse.HttpClientResponse>, B>
534 >
535 ) => Effect.Effect<HttpClientResponse.HttpClientResponse, E2, R2>
536 ): <E, R>(self: HttpClient.With<E, R>) => HttpClient.With<E2 | E, R2 | R>
537 <E2, R2>(
538 predicate: Predicate.Predicate<NoInfer<HttpClientResponse.HttpClientResponse>>,
539 orElse: (
540 response: NoInfer<HttpClientResponse.HttpClientResponse>
541 ) => Effect.Effect<HttpClientResponse.HttpClientResponse, E2, R2>
542 ): <E, R>(self: HttpClient.With<E, R>) => HttpClient.With<E2 | E, R2 | R>
543 <E, R, B extends HttpClientResponse.HttpClientResponse, E2, R2>(
544 self: HttpClient.With<E, R>,
545 refinement: Predicate.Refinement<HttpClientResponse.HttpClientResponse, B>,
546 orElse: (
547 response: EqualsWith<
548 HttpClientResponse.HttpClientResponse,
549 B,
550 HttpClientResponse.HttpClientResponse,
551 Exclude<HttpClientResponse.HttpClientResponse, B>
552 >
553 ) => Effect.Effect<HttpClientResponse.HttpClientResponse, E2, R2>
554 ): HttpClient.With<E2 | E, R2 | R>
555 <E, R, E2, R2>(
556 self: HttpClient.With<E, R>,
557 predicate: Predicate.Predicate<HttpClientResponse.HttpClientResponse>,
558 orElse: (
559 response: HttpClientResponse.HttpClientResponse
560 ) => Effect.Effect<HttpClientResponse.HttpClientResponse, E2, R2>
561 ): HttpClient.With<E2 | E, R2 | R>
562} = dual(3, (self, f, orElse) => transformResponse(self, Effect.filterOrElse(f, orElse)))
563
564/**
565 * Filters successful responses, or fails with the error produced by `orFailWith` when the predicate does not match.
566 *
567 * @stability unstable
568 * @category filtering
569 * @since 4.0.0
570 */
571export const filterOrFail: {
572 <B extends HttpClientResponse.HttpClientResponse, E2>(
573 refinement: Predicate.Refinement<NoInfer<HttpClientResponse.HttpClientResponse>, B>,
574 orFailWith: (response: NoInfer<HttpClientResponse.HttpClientResponse>) => E2
575 ): <E, R>(self: HttpClient.With<E, R>) => HttpClient.With<E2 | E, R>
576 <E2>(
577 predicate: Predicate.Predicate<NoInfer<HttpClientResponse.HttpClientResponse>>,
578 orFailWith: (response: NoInfer<HttpClientResponse.HttpClientResponse>) => E2
579 ): <E, R>(self: HttpClient.With<E, R>) => HttpClient.With<E2 | E, R>
580 <E, R, B extends HttpClientResponse.HttpClientResponse, E2>(
581 self: HttpClient.With<E, R>,
582 refinement: Predicate.Refinement<NoInfer<HttpClientResponse.HttpClientResponse>, B>,
583 orFailWith: (response: NoInfer<HttpClientResponse.HttpClientResponse>) => E2
584 ): HttpClient.With<E2 | E, R>
585 <E, R, E2>(
586 self: HttpClient.With<E, R>,
587 predicate: Predicate.Predicate<NoInfer<HttpClientResponse.HttpClientResponse>>,
588 orFailWith: (response: NoInfer<HttpClientResponse.HttpClientResponse>) => E2
589 ): HttpClient.With<E2 | E, R>
590} = dual(3, (self, f, orFailWith) => transformResponse(self, Effect.filterOrFail(f, orFailWith)))
591
592/**
593 * Filters responses by HTTP status code.
594 *
595 * @stability unstable
596 * @category filtering
597 * @since 4.0.0
598 */
599export const filterStatus: {
600 (f: (status: number) => boolean): <E, R>(self: HttpClient.With<E, R>) => HttpClient.With<E | Error.HttpClientError, R>
601 <E, R>(self: HttpClient.With<E, R>, f: (status: number) => boolean): HttpClient.With<E | Error.HttpClientError, R>
602} = dual(
603 2,
604 <E, R>(self: HttpClient.With<E, R>, f: (status: number) => boolean): HttpClient.With<E | Error.HttpClientError, R> =>
605 transformResponse(self, Effect.flatMap(HttpClientResponse.filterStatus(f)))
606)
607
608/**
609 * Filters responses that return a 2xx status code.
610 *
611 * @stability unstable
612 * @category filtering
613 * @since 4.0.0
614 */
615export const filterStatusOk: <E, R>(self: HttpClient.With<E, R>) => HttpClient.With<E | Error.HttpClientError, R> =
616 transformResponse(Effect.flatMap(HttpClientResponse.filterStatusOk))
617
618/**
619 * Constructs an `HttpClient.With` from a preprocessing function and a postprocessing function.
620 *
621 * **Details**
622 *
623 * `execute` applies preprocessing to the request and then passes the resulting request effect to postprocessing.
624 *
625 * @stability unstable
626 * @category constructors
627 * @since 4.0.0
628 */
629export const makeWith = <E2, R2, E, R>(
630 postprocess: (
631 request: Effect.Effect<HttpClientRequest.HttpClientRequest, E2, R2>
632 ) => Effect.Effect<HttpClientResponse.HttpClientResponse, E, R>,
633 preprocess: HttpClient.Preprocess<E2, R2>
634): HttpClient.With<E, R> => {
635 const self = Object.create(Proto)
636 self.preprocess = preprocess
637 self.postprocess = postprocess
638 self.execute = function(request: HttpClientRequest.HttpClientRequest) {
639 return postprocess(preprocess(request))
640 }
641 return self
642}
643
644const Proto = {
645 [TypeId]: TypeId,
646 pipe() {
647 return pipeArguments(this, arguments)
648 },
649 ...Inspectable.BaseProto,
650 toJSON() {
651 return {
652 _id: "effect/HttpClient"
653 }
654 },
655 ...Object.fromEntries(
656 HttpMethod.allShort.map((
657 [fullMethod, method]
658 ) => [method, function(this: HttpClient, url: string | URL, options?: HttpClientRequest.Options.NoUrl) {
659 return this.execute(HttpClientRequest.make(fullMethod)(url, options))
660 }])
661 )
662}
663
664/**
665 * Constructs an `HttpClient` from a low-level request runner.
666 *
667 * **Details**
668 *
669 * The runner receives the request, resolved URL, abort signal, and current fiber. The client wrapper handles URL construction failures, tracing and propagation, header redaction, and aborting non-scoped requests on interruption.
670 *
671 * @stability unstable
672 * @category constructors
673 * @since 4.0.0
674 */
675export const make = (
676 f: (
677 request: HttpClientRequest.HttpClientRequest,
678 url: URL,
679 signal: AbortSignal,
680 fiber: Fiber.Fiber<HttpClientResponse.HttpClientResponse, Error.HttpClientError>
681 ) => Effect.Effect<HttpClientResponse.HttpClientResponse, Error.HttpClientError>
682): HttpClient =>
683 makeWith((effect) =>
684 Effect.flatMap(effect, (request) =>
685 Effect.withFiber((fiber) => {
686 const scopedController = scopedRequests.get(request)
687 const controller = scopedController ?? new AbortController()
688 const urlResult = Url.make(request.url, request.urlParams, Option.getOrUndefined(request.hash))
689 if (Result.isFailure(urlResult)) {
690 return Effect.fail(
691 new Error.HttpClientError({
692 reason: new Error.InvalidUrlError({
693 request,
694 cause: urlResult.failure
695 })
696 })
697 )
698 }
699 const url = urlResult.success
700 const tracerDisabled = fiber.getRef(Tracer.DisablePropagation) ||
701 fiber.getRef(TracerDisabledWhen)(request)
702 if (tracerDisabled) {
703 const effect = f(request, url, controller.signal, fiber as any)
704 if (scopedController) return effect
705 return Effect.uninterruptibleMask((restore) =>
706 Effect.matchCauseEffect(restore(effect), {
707 onSuccess(response) {
708 responseRegistry.register(response, controller)
709 return Effect.succeed(new InterruptibleResponse(response, controller))
710 },
711 onFailure(cause) {
712 if (Cause.hasInterrupts(cause)) {
713 controller.abort()
714 }
715 return Effect.failCause(cause)
716 }
717 })
718 )
719 }
720 return Effect.useSpan(
721 fiber.getRef(SpanNameGenerator)(request),
722 { kind: "client" },
723 (span) => {
724 span.attribute("http.request.method", request.method)
725 span.attribute("server.address", url.origin)
726 if (url.port !== "") {
727 span.attribute("server.port", +url.port)
728 }
729 span.attribute("url.full", url.toString())
730 span.attribute("url.path", url.pathname)
731 span.attribute("url.scheme", url.protocol.slice(0, -1))
732 const query = url.search.slice(1)
733 if (query !== "") {
734 span.attribute("url.query", query)
735 }
736 const redactedHeaderNames = fiber.getRef(Headers.CurrentRedactedNames)
737 const headerFilter = fiber.getRef(TracerHeaderFilter)
738 for (const name in request.headers) {
739 if (!headerFilter(name, "request")) continue
740 span.attribute(
741 `http.request.header.${name}`,
742 Headers.isRedactedName(name, redactedHeaderNames) ? "<redacted>" : request.headers[name]
743 )
744 }
745 request = fiber.getRef(TracerPropagationEnabled)
746 ? HttpClientRequest.setHeaders(request, TraceContext.toHeaders(span))
747 : request
748 return Effect.uninterruptibleMask((restore) =>
749 restore(f(request, url, controller.signal, fiber as any)).pipe(
750 Effect.withParentSpan(span, { captureStackTrace: false }),
751 Effect.matchCauseEffect({
752 onSuccess: (response) => {
753 span.attribute("http.response.status_code", response.status)
754 for (const name in response.headers) {
755 if (!headerFilter(name, "response")) continue
756 span.attribute(
757 `http.response.header.${name}`,
758 Headers.isRedactedName(name, redactedHeaderNames) ? "<redacted>" : response.headers[name]
759 )
760 }
761
762 if (scopedController) return Effect.succeed(response)
763 responseRegistry.register(response, controller)
764 return Effect.succeed(new InterruptibleResponse(response, controller))
765 },
766 onFailure(cause) {
767 if (!scopedController && Cause.hasInterrupts(cause)) {
768 controller.abort()
769 }
770 return Effect.failCause(cause)
771 }
772 })
773 )
774 )
775 }
776 )
777 })), Effect.succeed as HttpClient.Preprocess<never, never>)
778
779/**
780 * Appends a transformation of the request object before sending it.
781 *
782 * @stability unstable
783 * @category mapping
784 * @since 4.0.0
785 */
786export const mapRequest: {
787 (
788 f: (a: HttpClientRequest.HttpClientRequest) => HttpClientRequest.HttpClientRequest
789 ): <E, R>(self: HttpClient.With<E, R>) => HttpClient.With<E, R>
790 <E, R>(
791 self: HttpClient.With<E, R>,
792 f: (a: HttpClientRequest.HttpClientRequest) => HttpClientRequest.HttpClientRequest
793 ): HttpClient.With<E, R>
794} = dual(
795 2,
796 <E, R>(
797 self: HttpClient.With<E, R>,
798 f: (a: HttpClientRequest.HttpClientRequest) => HttpClientRequest.HttpClientRequest
799 ): HttpClient.With<E, R> => makeWith(self.postprocess, (request) => Effect.map(self.preprocess(request), f))
800)
801
802/**
803 * Appends an effectful transformation of the request object before sending it.
804 *
805 * @stability unstable
806 * @category mapping
807 * @since 4.0.0
808 */
809export const mapRequestEffect: {
810 <E2, R2>(
811 f: (a: HttpClientRequest.HttpClientRequest) => Effect.Effect<HttpClientRequest.HttpClientRequest, E2, R2>
812 ): <E, R>(self: HttpClient.With<E, R>) => HttpClient.With<E | E2, R | R2>
813 <E, R, E2, R2>(
814 self: HttpClient.With<E, R>,
815 f: (a: HttpClientRequest.HttpClientRequest) => Effect.Effect<HttpClientRequest.HttpClientRequest, E2, R2>
816 ): HttpClient.With<E | E2, R | R2>
817} = dual(
818 2,
819 <E, R, E2, R2>(
820 self: HttpClient.With<E, R>,
821 f: (a: HttpClientRequest.HttpClientRequest) => Effect.Effect<HttpClientRequest.HttpClientRequest, E2, R2>
822 ): HttpClient.With<E | E2, R | R2> =>
823 makeWith(self.postprocess as any, (request) => Effect.flatMap(self.preprocess(request), f))
824)
825
826/**
827 * Prepends a transformation of the request object before sending it.
828 *
829 * @stability unstable
830 * @category mapping
831 * @since 4.0.0
832 */
833export const mapRequestInput: {
834 (
835 f: (a: HttpClientRequest.HttpClientRequest) => HttpClientRequest.HttpClientRequest
836 ): <E, R>(self: HttpClient.With<E, R>) => HttpClient.With<E, R>
837 <E, R>(
838 self: HttpClient.With<E, R>,
839 f: (a: HttpClientRequest.HttpClientRequest) => HttpClientRequest.HttpClientRequest
840 ): HttpClient.With<E, R>
841} = dual(
842 2,
843 <E, R>(
844 self: HttpClient.With<E, R>,
845 f: (a: HttpClientRequest.HttpClientRequest) => HttpClientRequest.HttpClientRequest
846 ): HttpClient.With<E, R> => makeWith(self.postprocess, (request) => self.preprocess(f(request)))
847)
848
849/**
850 * Prepends an effectful transformation of the request object before sending it.
851 *
852 * @stability unstable
853 * @category mapping
854 * @since 4.0.0
855 */
856export const mapRequestInputEffect: {
857 <E2, R2>(
858 f: (a: HttpClientRequest.HttpClientRequest) => Effect.Effect<HttpClientRequest.HttpClientRequest, E2, R2>
859 ): <E, R>(self: HttpClient.With<E, R>) => HttpClient.With<E | E2, R | R2>
860 <E, R, E2, R2>(
861 self: HttpClient.With<E, R>,
862 f: (a: HttpClientRequest.HttpClientRequest) => Effect.Effect<HttpClientRequest.HttpClientRequest, E2, R2>
863 ): HttpClient.With<E | E2, R | R2>
864} = dual(
865 2,
866 <E, R, E2, R2>(
867 self: HttpClient.With<E, R>,
868 f: (a: HttpClientRequest.HttpClientRequest) => Effect.Effect<HttpClientRequest.HttpClientRequest, E2, R2>
869 ): HttpClient.With<E | E2, R | R2> =>
870 makeWith(self.postprocess as any, (request) => Effect.flatMap(f(request), self.preprocess))
871)
872
873/**
874 * Namespace containing type-level helpers for retrying HTTP clients.
875 *
876 * @stability unstable
877 * @since 4.0.0
878 */
879export declare namespace Retry {
880 /**
881 * Computes the client type returned by `retry` for a given set of retry options.
882 *
883 * **Details**
884 *
885 * The result includes errors and requirements introduced by schedules and effectful retry predicates.
886 *
887 * @stability unstable
888 * @category error handling
889 * @since 4.0.0
890 */
891 export type Return<R, E, O extends NoExcessProperties<Effect.Retry.Options<E>, O>> = HttpClient.With<
892 | (O extends { schedule: Schedule.Schedule<infer _O, infer _I, infer _E, infer _R> } ? E | _E
893 : O extends { times: number } ? E
894 : O extends { until: Predicate.Refinement<E, infer E2> } ? E2
895 : E)
896 | (O extends { while: (...args: Array<any>) => Effect.Effect<infer _A, infer E, infer _R> } ? E : never)
897 | (O extends { until: (...args: Array<any>) => Effect.Effect<infer _A, infer E, infer _R> } ? E : never),
898 | R
899 | (O extends { schedule: Schedule.Schedule<infer _O, infer _I, infer _E, infer R> } ? R : never)
900 | (O extends { while: (...args: Array<any>) => Effect.Effect<infer _A, infer _E, infer R> } ? R : never)
901 | (O extends { until: (...args: Array<any>) => Effect.Effect<infer _A, infer _E, infer R> } ? R : never)
902 > extends infer Z ? Z : never
903}
904
905/**
906 * Retries the request based on a provided schedule or policy.
907 *
908 * @stability unstable
909 * @category error handling
910 * @since 4.0.0
911 */
912export const retry: {
913 <E, O extends NoExcessProperties<Effect.Retry.Options<E>, O>>(
914 options: O
915 ): <R>(self: HttpClient.With<E, R>) => Retry.Return<R, E, O>
916 <B, E, ES, R1>(
917 policy: Schedule.Schedule<B, NoInfer<E>, ES, R1>
918 ): <R>(self: HttpClient.With<E, R>) => HttpClient.With<E | ES, R1 | R>
919 <E, R, O extends NoExcessProperties<Effect.Retry.Options<E>, O>>(
920 self: HttpClient.With<E, R>,
921 options: O
922 ): Retry.Return<R, E, O>
923 <E, R, B, ES, R1>(
924 self: HttpClient.With<E, R>,
925 policy: Schedule.Schedule<B, E, ES, R1>
926 ): HttpClient.With<E | ES, R1 | R>
927} = dual(
928 2,
929 <E extends E0, E0, ES, R, R1, B>(
930 self: HttpClient.With<E, R>,
931 policy: Schedule.Schedule<B, E0, ES, R1>
932 ): HttpClient.With<E | ES, R | R1> => transformResponse(self, Effect.retry(policy))
933)
934
935/**
936 * Retries common transient errors, such as rate limiting, timeouts or network issues.
937 *
938 * **When to use**
939 *
940 * Use to focus on retrying errors, transient responses, or both.
941 *
942 * **Details**
943 *
944 * Specifying a `while` predicate allows you to consider other errors as
945 * transient, and is ignored in "response-only" mode.
946 *
947 * @stability unstable
948 * @category error handling
949 * @since 4.0.0
950 */
951export const retryTransient: {
952 <
953 E,
954 B = never,
955 ES = never,
956 R1 = never,
957 const RetryOn extends "errors-only" | "response-only" | "errors-and-responses" =
958 | "errors-only"
959 | "response-only"
960 | "errors-and-responses",
961 Input = RetryOn extends "errors-only" ? E
962 : RetryOn extends "response-only" ? HttpClientResponse.HttpClientResponse
963 : HttpClientResponse.HttpClientResponse | E
964 >(
965 options: {
966 readonly retryOn?: RetryOn | undefined
967 readonly while?: Predicate.Predicate<NoInfer<E | ES>>
968 readonly schedule?: Schedule.Schedule<B, NoInfer<Input>, ES, R1>
969 readonly times?: number
970 }
971 ): <R>(self: HttpClient.With<E, R>) => HttpClient.With<E | ES, R1 | R>
972 <
973 E,
974 R,
975 B = never,
976 ES = never,
977 R1 = never,
978 const RetryOn extends "errors-only" | "response-only" | "errors-and-responses" =
979 | "errors-only"
980 | "response-only"
981 | "errors-and-responses",
982 Input = RetryOn extends "errors-only" ? E
983 : RetryOn extends "response-only" ? HttpClientResponse.HttpClientResponse
984 : HttpClientResponse.HttpClientResponse | E
985 >(
986 self: HttpClient.With<E, R>,
987 options: {
988 readonly retryOn?: RetryOn | undefined
989 readonly while?: Predicate.Predicate<NoInfer<E | ES>>
990 readonly schedule?: Schedule.Schedule<B, NoInfer<Input>, ES, R1>
991 readonly times?: number
992 }
993 ): HttpClient.With<E | ES, R1 | R>
994 <B, E, ES = never, R1 = never>(
995 options: Schedule.Schedule<B, NoInfer<HttpClientResponse.HttpClientResponse | E>, ES, R1>
996 ): <R>(self: HttpClient.With<E, R>) => HttpClient.With<E | ES, R1 | R>
997 <E, R, B, ES = never, R1 = never>(
998 self: HttpClient.With<E, R>,
999 options: Schedule.Schedule<B, NoInfer<HttpClientResponse.HttpClientResponse | E>, ES, R1>
1000 ): HttpClient.With<E | ES, R1 | R>
1001} = dual(
1002 2,
1003 <
1004 E,
1005 R,
1006 B,
1007 ES = never,
1008 R1 = never
1009 >(
1010 self: HttpClient.With<E, R>,
1011 options: {
1012 readonly retryOn?: "errors-only" | "response-only" | "errors-and-responses" | undefined
1013 readonly while?: Predicate.Predicate<E | ES>
1014 readonly schedule?: Schedule.Schedule<B, any, ES, R1>
1015 readonly times?: number
1016 } | Schedule.Schedule<B, any, ES, R1>
1017 ): HttpClient.With<E | ES, R1 | R> => {
1018 const isOnlySchedule = Schedule.isSchedule(options)
1019 const retryOn = isOnlySchedule ? "errors-and-responses" : options.retryOn ?? "errors-and-responses"
1020 const schedule = isOnlySchedule ? options : options.schedule
1021 const passthroughSchedule = schedule && Schedule.passthrough(schedule)
1022 const times = isOnlySchedule ? undefined : options.times
1023 return transformResponse(
1024 self,
1025 flow(
1026 retryOn === "errors-only" ? identity : Effect.repeat({
1027 schedule: passthroughSchedule!,
1028 times,
1029 while: isTransientResponse
1030 }),
1031 retryOn === "response-only" ? identity : Effect.retry({
1032 while: isOnlySchedule || options.while === undefined
1033 ? isTransientError
1034 : Predicate.or(isTransientError, options.while),
1035 schedule,
1036 times
1037 })
1038 )
1039 )
1040 }
1041)
1042
1043/**
1044 * Namespace containing configuration types for `withRateLimiter`.
1045 *
1046 * @stability unstable
1047 * @since 4.0.0
1048 */
1049export declare namespace WithRateLimiter {
1050 /**
1051 * Options used to configure `withRateLimiter`.
1052 *
1053 * **Details**
1054 *
1055 * They define the backing limiter, initial limit window, keying strategy,
1056 * algorithm, token cost, retry count, and response header inspection.
1057 *
1058 * @stability unstable
1059 * @category rate limiting
1060 * @since 4.0.0
1061 */
1062 export interface Options {
1063 /**
1064 * The `RateLimiter` service to use for rate limiting.
1065 */
1066 readonly limiter: RateLimiter.RateLimiter
1067 /**
1068 * The initial rate limit window duration.
1069 */
1070 readonly window: Duration.Input
1071 /**
1072 * The initial maximum number of allowed requests in the window.
1073 */
1074 readonly limit: number
1075 /**
1076 * The key to identify the rate limit. Requests with the same key will share
1077 * the same rate limit. This can be used to implement per-user or
1078 * per-endpoint rate limits.
1079 */
1080 readonly key: string | ((request: HttpClientRequest.HttpClientRequest) => string)
1081 /**
1082 * Defaults to `"fixed-window"`.
1083 */
1084 readonly algorithm?: "fixed-window" | "token-bucket" | undefined
1085 /**
1086 * Defaults to `1`.
1087 */
1088 readonly tokens?: number | ((request: HttpClientRequest.HttpClientRequest) => number) | undefined
1089 /**
1090 * The maximum number of automatic retries after an HTTP `429` response.
1091 * Use a non-negative integer. Defaults to an unlimited number of retries.
1092 * Set to `0` to disable automatic retries.
1093 */
1094 readonly times?: number | undefined
1095 /**
1096 * Overrides the response header names used for rate limit inspection.
1097 * Header names are matched case-insensitively. An omitted field continues
1098 * using the built-in names, while a configured field replaces them for
1099 * that value. Header value formats are unchanged.
1100 */
1101 readonly responseHeaders?: {
1102 /**
1103 * Header containing the maximum number of requests in a window.
1104 */
1105 readonly limit?: string | undefined
1106 /**
1107 * Header containing the number of requests remaining in a window.
1108 */
1109 readonly remaining?: string | undefined
1110 /**
1111 * Header containing the rate limit reset time.
1112 */
1113 readonly reset?: string | undefined
1114 /**
1115 * Header containing the number of seconds until the rate limit resets.
1116 */
1117 readonly resetAfter?: string | undefined
1118 /**
1119 * Header containing the retry delay for an HTTP `429` response.
1120 */
1121 readonly retryAfter?: string | undefined
1122 } | undefined
1123 /**
1124 * Disables automatic limit updates, `Retry-After` delays, and adaptive
1125 * feedback from response headers. This does not disable automatic HTTP
1126 * `429` retries. Set `times` to `0` to disable them.
1127 */
1128 readonly disableResponseInspection?: boolean | undefined
1129 /**
1130 * Disables adaptive learning from `Retry-After` responses. Response
1131 * inspection and direct `Retry-After` delays remain enabled.
1132 */
1133 readonly disableAdaptiveLearning?: boolean | undefined
1134 }
1135}
1136
1137/**
1138 * Applies request rate limiting using the `RateLimiter` service.
1139 *
1140 * **Details**
1141 *
1142 * It can update limits by inspecting common rate limit response headers and
1143 * automatically retries HTTP `429` responses (or `HttpClientError` values
1144 * wrapping a `429` response) by forcing the retry back through the limiter.
1145 *
1146 * **Gotchas**
1147 *
1148 * Automatic HTTP `429` retries are unlimited unless `times` is specified.
1149 * Disabling response inspection does not disable retries; set `times` to `0`
1150 * to return or fail with the first `429`.
1151 *
1152 * @stability unstable
1153 * @category rate limiting
1154 * @since 4.0.0
1155 */
1156export const withRateLimiter: {
1157 (options: WithRateLimiter.Options): <E, R>(
1158 self: HttpClient.With<E, R>
1159 ) => HttpClient.With<E | RateLimiter.RateLimiterError, R>
1160 <E, R>(
1161 self: HttpClient.With<E, R>,
1162 options: WithRateLimiter.Options
1163 ): HttpClient.With<E | RateLimiter.RateLimiterError, R>
1164} = dual(2, <E, R>(
1165 self: HttpClient.With<E, R>,
1166 options: WithRateLimiter.Options
1167): HttpClient.With<E | RateLimiter.RateLimiterError, R> => {
1168 const initialState: RateLimiterState = {
1169 initial: true,
1170 limit: options.limit,
1171 window: Duration.max(Duration.fromInputUnsafe(options.window), Duration.millis(1))
1172 }
1173 const states = new Map<string, RateLimiterState>()
1174
1175 const keyOption = options.key
1176 const resolveKey: (request: HttpClientRequest.HttpClientRequest) => string = typeof keyOption === "function"
1177 ? keyOption
1178 : constant(keyOption)
1179 const tokensOption = options.tokens
1180 const resolveTokens: (request: HttpClientRequest.HttpClientRequest) => number = typeof tokensOption === "function"
1181 ? tokensOption
1182 : constant(tokensOption ?? 1)
1183 const adaptiveLearningEnabled = !options.disableAdaptiveLearning
1184 const headerNames = resolveRateLimiterHeaderNames(options.responseHeaders)
1185
1186 const getState = (key: string): RateLimiterState => {
1187 const current = states.get(key)
1188 if (current !== undefined) {
1189 return current
1190 }
1191 states.set(key, initialState)
1192 return initialState
1193 }
1194
1195 const onResponse = options.disableResponseInspection
1196 ? undefined
1197 : (clock: Clock, key: string, headers: Headers.Headers, tokens: number) => {
1198 const current = getState(key)
1199 const next = parseRateLimiterState(current, clock, headers, tokens, headerNames)
1200 if (next.limit !== current.limit || !Duration.equals(next.window, current.window)) {
1201 states.set(key, next)
1202 }
1203 }
1204
1205 return transform(self, function loop(effect, request, retries = 0): Effect.Effect<
1206 HttpClientResponse.HttpClientResponse,
1207 E | RateLimiter.RateLimiterError,
1208 R
1209 > {
1210 const fiber = Fiber.getCurrent()!
1211 const clock = fiber.getRef(Clock)
1212 const key = resolveKey(request)
1213 const tokens = Math.max(resolveTokens(request), 1)
1214 const current = getState(key)
1215 const canRetry = options.times === undefined || retries < options.times
1216 function retry(retryAfter: Duration.Duration | undefined) {
1217 return retryAfter
1218 ? Effect.flatMap(Effect.sleep(retryAfter), () => loop(effect, request, retries + 1))
1219 : loop(effect, request, retries + 1)
1220 }
1221 const inspectResponse = (
1222 response: HttpClientResponse.HttpClientResponse,
1223 adaptive: RateLimiter.AdaptiveConsumeResult | undefined
1224 ) => {
1225 onResponse?.(clock, key, response.headers, tokens)
1226 if (options.disableResponseInspection || response.status !== 429) {
1227 return Effect.succeed<Duration.Duration | undefined>(undefined)
1228 }
1229 const retryAfter = parseRetryAfter(clock, getHeader(response.headers, ...headerNames.retryAfter))
1230 if (retryAfter === undefined) {
1231 return Effect.succeed<Duration.Duration | undefined>(undefined)
1232 }
1233 const delay = parseRateLimitWindow(clock, response.headers, headerNames) ?? retryAfter
1234 if (adaptive === undefined) {
1235 return Effect.succeed<Duration.Duration | undefined>(delay)
1236 }
1237 return Effect.as(
1238 options.limiter.adaptiveFeedback({
1239 key,
1240 epoch: adaptive.epoch,
1241 tokens,
1242 status: response.status,
1243 retryAfter: delay
1244 }),
1245 delay
1246 )
1247 }
1248 return Effect.flatMap(
1249 options.limiter.consume({
1250 algorithm: options.algorithm,
1251 onExceeded: "delay",
1252 key,
1253 limit: current.limit,
1254 window: current.window,
1255 tokens
1256 }),
1257 ({ delay }) => {
1258 const runAdaptive = (): Effect.Effect<
1259 HttpClientResponse.HttpClientResponse,
1260 E | RateLimiter.RateLimiterError,
1261 R
1262 > => {
1263 const runRequest = (adaptive: RateLimiter.AdaptiveConsumeResult | undefined) => {
1264 const request = Effect.matchEffect(effect, {
1265 onSuccess(response) {
1266 return Effect.flatMap(inspectResponse(response, adaptive), (retryAfter) => {
1267 if (response.status !== 429 || !canRetry) return Effect.succeed(response)
1268 return retry(retryAfter)
1269 })
1270 },
1271 onFailure(error) {
1272 if (isTooManyRequestsHttpClientError(error)) {
1273 return Effect.flatMap(
1274 inspectResponse(error.reason.response, adaptive),
1275 (retryAfter) => canRetry ? retry(retryAfter) : Effect.fail(error)
1276 )
1277 }
1278 return Effect.fail(error)
1279 }
1280 })
1281 return adaptive === undefined || Duration.isZero(adaptive.delay)
1282 ? request
1283 : Effect.delay(request, adaptive.delay)
1284 }
1285 if (!adaptiveLearningEnabled) {
1286 return runRequest(undefined)
1287 }
1288 return Effect.flatMap(
1289 options.limiter.adaptiveConsume({
1290 key,
1291 tokens,
1292 fallbackLimit: current.limit,
1293 fallbackWindow: current.window
1294 }),
1295 (adaptive) => {
1296 if (!Duration.isZero(adaptive.delay) && adaptive.phase === "cooldown") {
1297 return Effect.flatMap(Effect.sleep(adaptive.delay), runAdaptive)
1298 }
1299 return runRequest(adaptive)
1300 }
1301 )
1302 }
1303 return Duration.isZero(delay) ? runAdaptive() : Effect.flatMap(Effect.sleep(delay), runAdaptive)
1304 }
1305 )
1306 })
1307})
1308
1309interface RateLimiterHeaderNames {
1310 readonly limit: ReadonlyArray<string>
1311 readonly remaining: ReadonlyArray<string>
1312 readonly reset: ReadonlyArray<string>
1313 readonly resetAfter: ReadonlyArray<string>
1314 readonly retryAfter: ReadonlyArray<string>
1315}
1316
1317const resolveRateLimiterHeaderNames = (
1318 responseHeaders: WithRateLimiter.Options["responseHeaders"]
1319): RateLimiterHeaderNames => ({
1320 limit: responseHeaders?.limit === undefined
1321 ? ["ratelimit-limit", "x-ratelimit-limit"]
1322 : [responseHeaders.limit.toLowerCase()],
1323 remaining: responseHeaders?.remaining === undefined
1324 ? ["ratelimit-remaining", "x-ratelimit-remaining"]
1325 : [responseHeaders.remaining.toLowerCase()],
1326 reset: responseHeaders?.reset === undefined
1327 ? ["ratelimit-reset", "x-ratelimit-reset"]
1328 : [responseHeaders.reset.toLowerCase()],
1329 resetAfter: responseHeaders?.resetAfter === undefined
1330 ? ["ratelimit-reset-after", "x-ratelimit-reset-after"]
1331 : [responseHeaders.resetAfter.toLowerCase()],
1332 retryAfter: responseHeaders?.retryAfter === undefined
1333 ? ["retry-after"]
1334 : [responseHeaders.retryAfter.toLowerCase()]
1335})
1336
1337interface RateLimiterState {
1338 readonly limit: number
1339 readonly window: Duration.Duration
1340 readonly initial: boolean
1341}
1342
1343const parseRateLimiterState = (
1344 state: RateLimiterState,
1345 clock: Clock,
1346 headers: Headers.Headers,
1347 tokens: number,
1348 headerNames: RateLimiterHeaderNames
1349): RateLimiterState => {
1350 const limit = parseRateLimitLimit(state, headers, tokens, headerNames) ?? state.limit
1351 const window = parseRateLimitWindow(clock, headers, headerNames) ?? state.window
1352 if (limit === state.limit && Duration.equals(window, state.window)) {
1353 return state
1354 }
1355 return { limit, window, initial: false }
1356}
1357
1358const parseRateLimitLimit = (
1359 state: RateLimiterState,
1360 headers: Headers.Headers,
1361 tokens: number,
1362 headerNames: RateLimiterHeaderNames
1363): number | undefined => {
1364 const raw = getHeader(headers, ...headerNames.limit)
1365 const value = parseNumberHeader(raw)
1366 if (value !== undefined && value > 0) {
1367 return value
1368 }
1369 const remaining = parseRateLimitRemaining(headers, headerNames)
1370 if (remaining === undefined) {
1371 return undefined
1372 }
1373 return state.initial ? remaining + tokens : Math.max(remaining + tokens, state.limit)
1374}
1375
1376const parseRateLimitRemaining = (
1377 headers: Headers.Headers,
1378 headerNames: RateLimiterHeaderNames
1379): number | undefined => {
1380 const raw = getHeader(headers, ...headerNames.remaining)
1381 const value = parseNumberHeader(raw)
1382 return value !== undefined && value >= 0 ? value : undefined
1383}
1384
1385const parseRateLimitWindow = (
1386 clock: Clock,
1387 headers: Headers.Headers,
1388 headerNames: RateLimiterHeaderNames
1389): Duration.Duration | undefined => {
1390 const resetAfter = parseResetAfter(getHeader(headers, ...headerNames.resetAfter))
1391 if (resetAfter !== undefined) {
1392 return resetAfter
1393 }
1394 return parseResetHeader(clock, getHeader(headers, ...headerNames.reset))
1395}
1396
1397const parseRetryAfter = (
1398 clock: Clock,
1399 value: string | undefined
1400): Duration.Duration | undefined => {
1401 if (value === undefined) {
1402 return undefined
1403 }
1404 const numeric = parseNumberHeader(value)
1405 if (numeric !== undefined) {
1406 return Duration.max(Duration.seconds(numeric), Duration.millis(1))
1407 }
1408 const parsedDate = Date.parse(value)
1409 if (Number.isNaN(parsedDate)) {
1410 return undefined
1411 }
1412 const millis = parsedDate - clock.currentTimeMillisUnsafe()
1413 if (millis <= 0) {
1414 return Duration.millis(1)
1415 }
1416 return Duration.millis(millis)
1417}
1418
1419const parseResetAfter = (value: string | undefined): Duration.Duration | undefined => {
1420 const numeric = parseNumberHeader(value)
1421 if (numeric === undefined || numeric <= 0) {
1422 return undefined
1423 }
1424 return Duration.max(Duration.seconds(numeric), Duration.millis(1))
1425}
1426
1427const parseResetHeader = (
1428 clock: Clock,
1429 value: string | undefined
1430): Duration.Duration | undefined => {
1431 const numeric = parseNumberHeader(value)
1432 if (numeric === undefined || numeric <= 0) {
1433 return undefined
1434 }
1435 const nowMillis = clock.currentTimeMillisUnsafe()
1436 if (numeric > 1_000_000_000_000) {
1437 return Duration.millis(Math.max(numeric - nowMillis, 1))
1438 }
1439 if (numeric > 1_000_000_000) {
1440 return Duration.millis(Math.max((numeric * 1_000) - nowMillis, 1))
1441 }
1442 return Duration.max(Duration.seconds(numeric), Duration.millis(1))
1443}
1444
1445const parseNumberHeader = (value: string | undefined): number | undefined => {
1446 if (value === undefined) {
1447 return undefined
1448 }
1449 const match = /-?\d+(?:\.\d+)?/.exec(value)
1450 if (match === null) {
1451 return undefined
1452 }
1453 const parsed = Number(match[0])
1454 return Number.isFinite(parsed) ? parsed : undefined
1455}
1456
1457const getHeader = (headers: Headers.Headers, ...keys: Array<string>): string | undefined => {
1458 for (let i = 0; i < keys.length; i++) {
1459 const value = headers[keys[i]]
1460 if (value !== undefined) {
1461 return value
1462 }
1463 }
1464 return undefined
1465}
1466
1467/**
1468 * Performs an additional effect after a successful request.
1469 *
1470 * @stability unstable
1471 * @category sequencing
1472 * @since 4.0.0
1473 */
1474export const tap: {
1475 <_, E2, R2>(
1476 f: (response: HttpClientResponse.HttpClientResponse) => Effect.Effect<_, E2, R2>
1477 ): <E, R>(self: HttpClient.With<E, R>) => HttpClient.With<E | E2, R | R2>
1478 <E, R, _, E2, R2>(
1479 self: HttpClient.With<E, R>,
1480 f: (response: HttpClientResponse.HttpClientResponse) => Effect.Effect<_, E2, R2>
1481 ): HttpClient.With<E | E2, R | R2>
1482} = dual(
1483 2,
1484 <E, R, _, E2, R2>(
1485 self: HttpClient.With<E, R>,
1486 f: (response: HttpClientResponse.HttpClientResponse) => Effect.Effect<_, E2, R2>
1487 ): HttpClient.With<E | E2, R | R2> => transformResponse(self, Effect.tap(f))
1488)
1489
1490/**
1491 * Performs an additional effect after an unsuccessful request.
1492 *
1493 * @stability unstable
1494 * @category sequencing
1495 * @since 4.0.0
1496 */
1497export const tapError: {
1498 <_, E, E2, R2>(
1499 f: (e: NoInfer<E>) => Effect.Effect<_, E2, R2>
1500 ): <R>(self: HttpClient.With<E, R>) => HttpClient.With<E | E2, R | R2>
1501 <E, R, _, E2, R2>(
1502 self: HttpClient.With<E, R>,
1503 f: (e: NoInfer<E>) => Effect.Effect<_, E2, R2>
1504 ): HttpClient.With<E | E2, R | R2>
1505} = dual(
1506 2,
1507 <E, R, _, E2, R2>(
1508 self: HttpClient.With<E, R>,
1509 f: (e: NoInfer<E>) => Effect.Effect<_, E2, R2>
1510 ): HttpClient.With<E | E2, R | R2> => transformResponse(self, Effect.tapError(f))
1511)
1512
1513/**
1514 * Performs an additional effect on the request before sending it.
1515 *
1516 * @stability unstable
1517 * @category sequencing
1518 * @since 4.0.0
1519 */
1520export const tapRequest: {
1521 <_, E2, R2>(
1522 f: (a: HttpClientRequest.HttpClientRequest) => Effect.Effect<_, E2, R2>
1523 ): <E, R>(self: HttpClient.With<E, R>) => HttpClient.With<E | E2, R | R2>
1524 <E, R, _, E2, R2>(
1525 self: HttpClient.With<E, R>,
1526 f: (a: HttpClientRequest.HttpClientRequest) => Effect.Effect<_, E2, R2>
1527 ): HttpClient.With<E | E2, R | R2>
1528} = dual(
1529 2,
1530 <E, R, _, E2, R2>(
1531 self: HttpClient.With<E, R>,
1532 f: (a: HttpClientRequest.HttpClientRequest) => Effect.Effect<_, E2, R2>
1533 ): HttpClient.With<E | E2, R | R2> =>
1534 makeWith(self.postprocess as any, (request) => Effect.tap(self.preprocess(request), f))
1535)
1536
1537/**
1538 * Adds a `Ref` of cookies to the client for handling cookies across requests.
1539 *
1540 * **When to use**
1541 *
1542 * Use to add shared cookie storage to a client so response cookies are retained
1543 * and sent by later requests.
1544 *
1545 * @stability unstable
1546 * @category cookies
1547 * @since 4.0.0
1548 */
1549export const withCookiesRef: {
1550 (ref: Ref.Ref<Cookies.Cookies>): <E, R>(self: HttpClient.With<E, R>) => HttpClient.With<E, R>
1551 <E, R>(self: HttpClient.With<E, R>, ref: Ref.Ref<Cookies.Cookies>): HttpClient.With<E, R>
1552} = dual(
1553 2,
1554 <E, R>(
1555 self: HttpClient.With<E, R>,
1556 ref: Ref.Ref<Cookies.Cookies>
1557 ): HttpClient.With<E, R> =>
1558 makeWith(
1559 (request: Effect.Effect<HttpClientRequest.HttpClientRequest, E, R>) =>
1560 Effect.tap(
1561 self.postprocess(request),
1562 (response) => Ref.update(ref, (cookies) => Cookies.merge(cookies, response.cookies))
1563 ),
1564 (request) =>
1565 Effect.flatMap(self.preprocess(request), (request) =>
1566 Effect.map(
1567 Ref.get(ref),
1568 (cookies) =>
1569 Cookies.isEmpty(cookies)
1570 ? request
1571 : HttpClientRequest.setHeader(request, "cookie", Cookies.toCookieHeader(cookies))
1572 ))
1573 )
1574)
1575
1576/**
1577 * Attaches the lifetime of the `HttpClientRequest` to a `Scope`.
1578 *
1579 * @stability unstable
1580 * @category resource management
1581 * @since 4.0.0
1582 */
1583export const withScope = <E, R>(
1584 self: HttpClient.With<E, R>
1585): HttpClient.With<E, R | Scope.Scope> =>
1586 transform(
1587 self,
1588 (effect, request) => {
1589 const controller = new AbortController()
1590 scopedRequests.set(request, controller)
1591 return Effect.andThen(
1592 Effect.addFinalizer(() => Effect.sync(() => controller.abort())),
1593 effect
1594 )
1595 }
1596 )
1597
1598/**
1599 * Enables following HTTP redirects up to a specified number of times.
1600 *
1601 * @stability unstable
1602 * @category redirects
1603 * @since 4.0.0
1604 */
1605export const followRedirects: {
1606 (maxRedirects?: number | undefined): <E, R>(self: HttpClient.With<E, R>) => HttpClient.With<E, R>
1607 <E, R>(self: HttpClient.With<E, R>, maxRedirects?: number | undefined): HttpClient.With<E, R>
1608} = dual((args) => isHttpClient(args[0]), <E, R>(
1609 self: HttpClient.With<E, R>,
1610 maxRedirects?: number | undefined
1611): HttpClient.With<E, R> =>
1612 makeWith(
1613 (request) => {
1614 const loop = (
1615 request: Effect.Effect<HttpClientRequest.HttpClientRequest, E, R>,
1616 redirects: number
1617 ): Effect.Effect<HttpClientResponse.HttpClientResponse, E, R> =>
1618 Effect.suspend(() => {
1619 let currentRequest: HttpClientRequest.HttpClientRequest | undefined
1620 return Effect.flatMap(
1621 self.postprocess(Effect.map(request, (request) => {
1622 currentRequest = request
1623 return request
1624 })),
1625 (response) => {
1626 if (
1627 response.status < 300 || response.status >= 400 || !response.headers.location ||
1628 redirects >= (maxRedirects ?? 10)
1629 ) {
1630 return Effect.succeed(response)
1631 }
1632 const url = new URL(response.headers.location, response.request.url)
1633 const request = currentRequest ?? response.request
1634 let nextRequest = request
1635 if (
1636 ((response.status === 301 || response.status === 302) && request.method === "POST") ||
1637 (response.status === 303 && request.method !== "GET" && request.method !== "HEAD")
1638 ) {
1639 nextRequest = HttpClientRequest.setMethod(nextRequest, "GET")
1640 nextRequest = HttpClientRequest.setBody(nextRequest, HttpBody.empty)
1641 }
1642 if (url.origin !== new URL(response.request.url).origin) {
1643 nextRequest = HttpClientRequest.removeHeader(nextRequest, "authorization")
1644 nextRequest = HttpClientRequest.removeHeader(nextRequest, "proxy-authorization")
1645 nextRequest = HttpClientRequest.removeHeader(nextRequest, "cookie")
1646 }
1647 return loop(Effect.succeed(HttpClientRequest.setUrl(nextRequest, url)), redirects + 1)
1648 }
1649 )
1650 })
1651 return loop(request, 0)
1652 },
1653 self.preprocess
1654 ))
1655
1656/**
1657 * Context reference for a predicate that disables client-side tracing for matching outgoing requests.
1658 *
1659 * @stability unstable
1660 * @category services
1661 * @since 4.0.0
1662 */
1663export const TracerDisabledWhen = Context.Reference<
1664 Predicate.Predicate<HttpClientRequest.HttpClientRequest>
1665>("effect/http/HttpClient/TracerDisabledWhen", {
1666 defaultValue: () => constFalse
1667})
1668
1669/**
1670 * Context reference for filtering request and response headers added to client spans.
1671 *
1672 * @stability unstable
1673 * @category services
1674 * @since 4.0.0
1675 */
1676export const TracerHeaderFilter = Context.Reference<
1677 (headerName: string, phase: "request" | "response") => boolean
1678>("effect/http/HttpClient/TracerHeaderFilter", {
1679 defaultValue: () => constTrue
1680})
1681
1682/**
1683 * Context reference that controls whether outgoing client spans are propagated to request headers.
1684 *
1685 * @stability unstable
1686 * @category services
1687 * @since 4.0.0
1688 */
1689export const TracerPropagationEnabled = Context.Reference<boolean>("effect/http/HttpClient/TracerPropagationEnabled", {
1690 defaultValue: constTrue
1691})
1692
1693/**
1694 * Context reference for generating the span name used for outgoing client request spans.
1695 *
1696 * @stability unstable
1697 * @category services
1698 * @since 4.0.0
1699 */
1700export const SpanNameGenerator = Context.Reference<
1701 (request: HttpClientRequest.HttpClientRequest) => string
1702>("effect/http/HttpClient/SpanNameGenerator", {
1703 defaultValue: () => (request) => `http.client ${request.method}`
1704})
1705
1706/**
1707 * Creates an `HttpClient` layer and merges the layer construction context into client response effects.
1708 *
1709 * @stability unstable
1710 * @category layers
1711 * @since 4.0.0
1712 */
1713export const layerMergedContext = <E, R>(
1714 effect: Effect.Effect<HttpClient, E, R>
1715): Layer.Layer<HttpClient, E, R> =>
1716 Layer.effect(HttpClient)(
1717 Effect.contextWith((context: Context.Context<never>) =>
1718 Effect.map(effect, (client) =>
1719 transformResponse(
1720 client,
1721 Effect.updateContext((input: Context.Context<never>) => Context.merge(context, input))
1722 ))
1723 )
1724 )
1725
1726// -----------------------------------------------------------------------------
1727// internal
1728// -----------------------------------------------------------------------------
1729
1730const responseRegistry = (() => {
1731 if ("FinalizationRegistry" in globalThis && globalThis.FinalizationRegistry) {
1732 const registry = new FinalizationRegistry((controller: AbortController) => {
1733 controller.abort()
1734 })
1735 return {
1736 register(response: HttpClientResponse.HttpClientResponse, controller: AbortController) {
1737 registry.register(response, controller, response)
1738 },
1739 unregister(response: HttpClientResponse.HttpClientResponse) {
1740 registry.unregister(response)
1741 }
1742 }
1743 }
1744
1745 const timers = new Map<HttpClientResponse.HttpClientResponse, any>()
1746 return {
1747 register(response: HttpClientResponse.HttpClientResponse, controller: AbortController) {
1748 timers.set(response, setTimeout(() => controller.abort(), 5000))
1749 },
1750 unregister(response: HttpClientResponse.HttpClientResponse) {
1751 const timer = timers.get(response)
1752 if (timer === undefined) return
1753 clearTimeout(timer)
1754 timers.delete(response)
1755 }
1756 }
1757})()
1758
1759const scopedRequests = new WeakMap<HttpClientRequest.HttpClientRequest, AbortController>()
1760
1761class InterruptibleResponse implements HttpClientResponse.HttpClientResponse, Pipeable {
1762 readonly original: HttpClientResponse.HttpClientResponse
1763 readonly controller: AbortController
1764
1765 constructor(
1766 original: HttpClientResponse.HttpClientResponse,
1767 controller: AbortController
1768 ) {
1769 this.original = original
1770 this.controller = controller
1771 }
1772
1773 readonly [HttpClientResponse.TypeId] = HttpClientResponse.TypeId
1774 readonly [HttpIncomingMessage.TypeId] = HttpIncomingMessage.TypeId
1775
1776 private applyInterrupt<A, E, R>(effect: Effect.Effect<A, E, R>) {
1777 return Effect.suspend(() => {
1778 responseRegistry.unregister(this.original)
1779 return Effect.onInterrupt(
1780 effect,
1781 () =>
1782 Effect.sync(() => {
1783 this.controller.abort()
1784 })
1785 )
1786 })
1787 }
1788
1789 get request() {
1790 return this.original.request
1791 }
1792
1793 get url() {
1794 return this.original.url
1795 }
1796
1797 get status() {
1798 return this.original.status
1799 }
1800
1801 get headers() {
1802 return this.original.headers
1803 }
1804
1805 get cookies() {
1806 return this.original.cookies
1807 }
1808
1809 get remoteAddress() {
1810 return this.original.remoteAddress
1811 }
1812
1813 get formData() {
1814 return this.applyInterrupt(this.original.formData)
1815 }
1816
1817 get text() {
1818 return this.applyInterrupt(this.original.text)
1819 }
1820
1821 get json() {
1822 return this.applyInterrupt(this.original.json)
1823 }
1824
1825 get urlParamsBody() {
1826 return this.applyInterrupt(this.original.urlParamsBody)
1827 }
1828
1829 get arrayBuffer() {
1830 return this.applyInterrupt(this.original.arrayBuffer)
1831 }
1832
1833 get stream() {
1834 return Stream.suspend(() => {
1835 responseRegistry.unregister(this.original)
1836 return Stream.ensuring(
1837 this.original.stream,
1838 Effect.sync(() => {
1839 this.controller.abort()
1840 })
1841 )
1842 })
1843 }
1844
1845 toJSON() {
1846 return this.original.toJSON()
1847 }
1848
1849 [Inspectable.NodeInspectSymbol]() {
1850 return this.original[Inspectable.NodeInspectSymbol]()
1851 }
1852
1853 pipe() {
1854 return pipeArguments(this, arguments)
1855 }
1856}
1857
1858const isTransientError = (error: unknown) => Cause.isTimeoutError(error) || isTransientHttpError(error)
1859
1860const isTransientHttpError = (error: unknown) =>
1861 Error.isHttpClientError(error) &&
1862 (error.reason._tag === "TransportError" ||
1863 (error.reason._tag === "StatusCodeError" && isTransientResponse(error.reason.response)))
1864
1865const isTooManyRequestsHttpClientError = (
1866 error: unknown
1867): error is Error.HttpClientError & { readonly reason: Error.StatusCodeError } =>
1868 Error.isHttpClientError(error) && error.reason._tag === "StatusCodeError" && error.reason.response.status === 429
1869
1870const isTransientResponse = (response: HttpClientResponse.HttpClientResponse) =>
1871 response.status === 408 ||
1872 response.status === 429 ||
1873 response.status === 500 ||
1874 response.status === 502 ||
1875 response.status === 503 ||
1876 response.status === 504