1/**
2 * Describes immutable outgoing HTTP client requests.
3 *
4 * `HttpClientRequest` is the request model shared by Effect HTTP clients and
5 * platform adapters. A request stores its method, URL, query parameters, hash,
6 * headers, and body as structured data. This module includes constructors,
7 * helpers for updating requests, body encoders for common payloads, and
8 * conversions to and from Web `Request` values.
9 *
10 * @stability unstable
11 * @since 4.0.0
12 */
13import type * as ByteSize from "../ByteSize.ts"
14import * as Context from "../Context.ts"
15import * as Effect from "../Effect.ts"
16import type * as FileSystem from "../FileSystem.ts"
17import { dual } from "../Function.ts"
18import * as Inspectable from "../Inspectable.ts"
19import { stringOrRedacted } from "../internal/redacted.ts"
20import * as Option from "../Option.ts"
21import { type Pipeable, pipeArguments } from "../Pipeable.ts"
22import type * as PlatformError from "../PlatformError.ts"
23import { hasProperty } from "../Predicate.ts"
24import { redact } from "../Redactable.ts"
25import type * as Redacted from "../Redacted.ts"
26import * as Result from "../Result.ts"
27import type * as Schema from "../Schema.ts"
28import type { ParseOptions } from "../SchemaAST.ts"
29import * as Stream from "../Stream.ts"
30import * as Headers from "./Headers.ts"
31import * as HttpBody from "./HttpBody.ts"
32import { hasBody, type HttpMethod } from "./HttpMethod.ts"
33import * as bodyInternal from "./internal/httpBody.ts"
34import * as Url from "./Url.ts"
35import * as UrlParams from "./UrlParams.ts"
36
37const TypeId = "~effect/http/HttpClientRequest"
38
39/**
40 * Returns `true` when a value is an `HttpClientRequest`.
41 *
42 * @stability unstable
43 * @category guards
44 * @since 4.0.0
45 */
46export const isHttpClientRequest = (u: unknown): u is HttpClientRequest => hasProperty(u, TypeId)
47
48/**
49 * Immutable model of an outgoing HTTP client request, including its method, URL components, headers, and body.
50 *
51 * @stability unstable
52 * @category models
53 * @since 4.0.0
54 */
55export interface HttpClientRequest extends Inspectable.Inspectable, Pipeable {
56 readonly [TypeId]: typeof TypeId
57 readonly method: HttpMethod
58 readonly url: string
59 readonly urlParams: UrlParams.UrlParams
60 readonly hash: Option.Option<string>
61 readonly headers: Headers.Headers
62 readonly body: HttpBody.HttpBody
63}
64
65/**
66 * Options for constructing or modifying an `HttpClientRequest`.
67 *
68 * @stability unstable
69 * @category options
70 * @since 4.0.0
71 */
72export interface Options {
73 readonly method?: HttpMethod | undefined
74 readonly url?: string | URL | undefined
75 readonly urlParams?: UrlParams.Input | undefined
76 readonly hash?: string | undefined
77 readonly headers?: Headers.Input | undefined
78 readonly body?: HttpBody.HttpBody | undefined
79 readonly accept?: string | undefined
80 readonly acceptJson?: boolean | undefined
81}
82
83/**
84 * Namespace containing option types associated with `HttpClientRequest` construction.
85 *
86 * @stability unstable
87 * @since 4.0.0
88 */
89export declare namespace Options {
90 /**
91 * Request options that omit the method and URL for helpers that already receive those values separately.
92 *
93 * @stability unstable
94 * @category options
95 * @since 4.0.0
96 */
97 export interface NoUrl extends Omit<Options, "method" | "url"> {}
98}
99
100const Proto = {
101 [TypeId]: TypeId,
102 ...Inspectable.BaseProto,
103 toJSON(this: HttpClientRequest): unknown {
104 return {
105 _id: "HttpClientRequest",
106 method: this.method,
107 url: this.url,
108 urlParams: this.urlParams,
109 hash: this.hash,
110 headers: redact(this.headers),
111 body: this.body.toJSON()
112 }
113 },
114 pipe() {
115 return pipeArguments(this, arguments)
116 }
117}
118
119/**
120 * Constructs an `HttpClientRequest` from fully normalized request components.
121 *
122 * @stability unstable
123 * @category constructors
124 * @since 4.0.0
125 */
126export function makeWith(
127 method: HttpMethod,
128 url: string,
129 urlParams: UrlParams.Input,
130 hash: Option.Option<string>,
131 headers: Headers.Headers,
132 body: HttpBody.HttpBody
133): HttpClientRequest {
134 const self = Object.create(Proto)
135 self.method = method
136 self.url = url
137 self.urlParams = urlParams
138 self.hash = hash
139 self.headers = headers
140 self.body = body
141 return self
142}
143
144/**
145 * An empty `GET` request with no URL, query parameters, hash, headers, or body.
146 *
147 * @stability unstable
148 * @category constructors
149 * @since 4.0.0
150 */
151export const empty: HttpClientRequest = makeWith(
152 "GET",
153 "",
154 UrlParams.empty,
155 Option.none(),
156 Headers.empty,
157 HttpBody.empty
158)
159
160/**
161 * Creates a request constructor for the specified HTTP method.
162 *
163 * @stability unstable
164 * @category constructors
165 * @since 4.0.0
166 */
167export const make = <M extends HttpMethod>(
168 method: M
169) =>
170(
171 url: string | URL,
172 options?: Options.NoUrl | undefined
173): HttpClientRequest =>
174 modify(empty, {
175 method,
176 url,
177 ...(options ?? undefined)
178 })
179
180/**
181 * Creates a `GET` request for the specified URL.
182 *
183 * @stability unstable
184 * @category constructors
185 * @since 4.0.0
186 */
187export const get: (url: string | URL, options?: Options.NoUrl) => HttpClientRequest = make("GET")
188
189/**
190 * Creates a `QUERY` request for the specified URL.
191 *
192 * @stability unstable
193 * @category constructors
194 * @since 4.0.0
195 */
196export const query: (url: string | URL, options?: Options.NoUrl) => HttpClientRequest = make("QUERY")
197
198/**
199 * Creates a `POST` request for the specified URL.
200 *
201 * @stability unstable
202 * @category constructors
203 * @since 4.0.0
204 */
205export const post: (url: string | URL, options?: Options.NoUrl) => HttpClientRequest = make("POST")
206
207/**
208 * Creates a `PATCH` request for the specified URL.
209 *
210 * @stability unstable
211 * @category constructors
212 * @since 4.0.0
213 */
214export const patch: (url: string | URL, options?: Options.NoUrl) => HttpClientRequest = make("PATCH")
215
216/**
217 * Creates a `PUT` request for the specified URL.
218 *
219 * @stability unstable
220 * @category constructors
221 * @since 4.0.0
222 */
223export const put: (url: string | URL, options?: Options.NoUrl) => HttpClientRequest = make("PUT")
224
225const del: (url: string | URL, options?: Options.NoUrl) => HttpClientRequest = make("DELETE")
226
227/**
228 * @stability unstable
229 */
230export {
231 /**
232 * Creates a `DELETE` request for the specified URL.
233 *
234 * @stability unstable
235 * @category constructors
236 * @since 4.0.0
237 */
238 del as delete
239}
240
241/**
242 * Creates a `HEAD` request for the specified URL.
243 *
244 * @stability unstable
245 * @category constructors
246 * @since 4.0.0
247 */
248export const head: (url: string | URL, options?: Options.NoUrl) => HttpClientRequest = make("HEAD")
249
250/**
251 * Creates an `OPTIONS` request for the specified URL.
252 *
253 * @stability unstable
254 * @category constructors
255 * @since 4.0.0
256 */
257export const options: (url: string | URL, options?: Options.NoUrl) => HttpClientRequest = make("OPTIONS")
258
259/**
260 * Creates a `TRACE` request for the specified URL.
261 *
262 * @stability unstable
263 * @category constructors
264 * @since 4.0.0
265 */
266export const trace: (url: string | URL, options?: Options.NoUrl) => HttpClientRequest = make("TRACE")
267
268/**
269 * Applies request options to an `HttpClientRequest`, returning a new request.
270 *
271 * @stability unstable
272 * @category combinators
273 * @since 4.0.0
274 */
275export const modify: {
276 (options: Options): (self: HttpClientRequest) => HttpClientRequest
277 (self: HttpClientRequest, options: Options): HttpClientRequest
278} = dual(2, (self: HttpClientRequest, options: Options): HttpClientRequest => {
279 let result = self
280
281 if (options.method) {
282 result = setMethod(result, options.method)
283 }
284 if (options.url) {
285 result = setUrl(result, options.url)
286 }
287 if (options.headers) {
288 result = setHeaders(result, options.headers)
289 }
290 if (options.urlParams) {
291 result = setUrlParams(result, options.urlParams)
292 }
293 if (options.hash) {
294 result = setHash(result, options.hash)
295 }
296 if (options.body) {
297 result = setBody(result, options.body)
298 }
299 if (options.accept) {
300 result = accept(result, options.accept)
301 }
302 if (options.acceptJson) {
303 result = acceptJson(result)
304 }
305
306 return result
307})
308
309/**
310 * Sets the HTTP method on a request, returning a new request.
311 *
312 * @stability unstable
313 * @category combinators
314 * @since 4.0.0
315 */
316export const setMethod: {
317 (method: HttpMethod): (self: HttpClientRequest) => HttpClientRequest
318 (self: HttpClientRequest, method: HttpMethod): HttpClientRequest
319} = dual(
320 2,
321 (self: HttpClientRequest, method: HttpMethod): HttpClientRequest =>
322 makeWith(method, self.url, self.urlParams, self.hash, self.headers, self.body)
323)
324
325/**
326 * Sets a single request header, replacing any existing value for that header.
327 *
328 * @stability unstable
329 * @category combinators
330 * @since 4.0.0
331 */
332export const setHeader: {
333 (key: string, value: string): (self: HttpClientRequest) => HttpClientRequest
334 (self: HttpClientRequest, key: string, value: string): HttpClientRequest
335} = dual(3, (self: HttpClientRequest, key: string, value: string): HttpClientRequest =>
336 makeWith(
337 self.method,
338 self.url,
339 self.urlParams,
340 self.hash,
341 Headers.set(self.headers, key, value),
342 self.body
343 ))
344
345/**
346 * Sets multiple request headers from an input collection, replacing existing values with matching names.
347 *
348 * @stability unstable
349 * @category combinators
350 * @since 4.0.0
351 */
352export const setHeaders: {
353 (input: Headers.Input): (self: HttpClientRequest) => HttpClientRequest
354 (self: HttpClientRequest, input: Headers.Input): HttpClientRequest
355} = dual(2, (self: HttpClientRequest, input: Headers.Input): HttpClientRequest =>
356 makeWith(
357 self.method,
358 self.url,
359 self.urlParams,
360 self.hash,
361 Headers.setAll(self.headers, input),
362 self.body
363 ))
364
365/**
366 * Transforms the request headers with the provided function, returning a new request.
367 *
368 * @stability unstable
369 * @category combinators
370 * @since 4.0.0
371 */
372export const updateHeaders: {
373 (f: (headers: Headers.Headers) => Headers.Headers): (self: HttpClientRequest) => HttpClientRequest
374 (self: HttpClientRequest, f: (headers: Headers.Headers) => Headers.Headers): HttpClientRequest
375} = dual(2, (self: HttpClientRequest, f: (headers: Headers.Headers) => Headers.Headers): HttpClientRequest =>
376 makeWith(
377 self.method,
378 self.url,
379 self.urlParams,
380 self.hash,
381 f(self.headers),
382 self.body
383 ))
384
385/**
386 * Removes a single request header by name, returning a new request.
387 *
388 * @stability unstable
389 * @category combinators
390 * @since 4.0.0
391 */
392export const removeHeader: {
393 (key: string): (self: HttpClientRequest) => HttpClientRequest
394 (self: HttpClientRequest, key: string): HttpClientRequest
395} = dual(
396 2,
397 (self: HttpClientRequest, key: string): HttpClientRequest => updateHeaders(self, Headers.remove(key))
398)
399
400/**
401 * Sets the `Authorization` header using HTTP Basic authentication credentials.
402 *
403 * @stability unstable
404 * @category combinators
405 * @since 4.0.0
406 */
407export const basicAuth: {
408 (
409 username: string | Redacted.Redacted,
410 password: string | Redacted.Redacted
411 ): (self: HttpClientRequest) => HttpClientRequest
412 (
413 self: HttpClientRequest,
414 username: string | Redacted.Redacted,
415 password: string | Redacted.Redacted
416 ): HttpClientRequest
417} = dual(
418 3,
419 (
420 self: HttpClientRequest,
421 username: string | Redacted.Redacted,
422 password: string | Redacted.Redacted
423 ): HttpClientRequest =>
424 setHeader(self, "Authorization", `Basic ${btoa(`${stringOrRedacted(username)}:${stringOrRedacted(password)}`)}`)
425)
426
427/**
428 * Sets the `Authorization` header using a bearer token.
429 *
430 * @stability unstable
431 * @category combinators
432 * @since 4.0.0
433 */
434export const bearerToken: {
435 (token: string | Redacted.Redacted): (self: HttpClientRequest) => HttpClientRequest
436 (self: HttpClientRequest, token: string | Redacted.Redacted): HttpClientRequest
437} = dual(
438 2,
439 (self: HttpClientRequest, token: string | Redacted.Redacted): HttpClientRequest =>
440 setHeader(self, "Authorization", `Bearer ${stringOrRedacted(token)}`)
441)
442
443/**
444 * Sets the `Accept` header to the specified media type.
445 *
446 * @stability unstable
447 * @category combinators
448 * @since 4.0.0
449 */
450export const accept: {
451 (mediaType: string): (self: HttpClientRequest) => HttpClientRequest
452 (self: HttpClientRequest, mediaType: string): HttpClientRequest
453} = dual(2, (self: HttpClientRequest, mediaType: string): HttpClientRequest => setHeader(self, "Accept", mediaType))
454
455/**
456 * Sets the `Accept` header to `application/json`.
457 *
458 * @stability unstable
459 * @category combinators
460 * @since 4.0.0
461 */
462export const acceptJson: (self: HttpClientRequest) => HttpClientRequest = accept("application/json")
463
464/**
465 * Sets the request URL. When given a `URL`, its search parameters and hash are extracted into the request's structured fields.
466 *
467 * @stability unstable
468 * @category combinators
469 * @since 4.0.0
470 */
471export const setUrl: {
472 (url: string | URL): (self: HttpClientRequest) => HttpClientRequest
473 (self: HttpClientRequest, url: string | URL): HttpClientRequest
474} = dual(2, (self: HttpClientRequest, url: string | URL): HttpClientRequest => {
475 if (typeof url === "string") {
476 return makeWith(
477 self.method,
478 url,
479 self.urlParams,
480 self.hash,
481 self.headers,
482 self.body
483 )
484 }
485 const clone = new URL(url.toString())
486 const urlParams = UrlParams.fromInput(clone.searchParams)
487 const hash = Option.fromNullishOr(clone.hash === "" ? undefined : clone.hash.slice(1))
488 clone.search = ""
489 clone.hash = ""
490 return makeWith(
491 self.method,
492 clone.toString(),
493 urlParams,
494 hash,
495 self.headers,
496 self.body
497 )
498})
499
500/**
501 * Prepends a URL segment to the request URL, inserting or trimming one slash as needed.
502 *
503 * @stability unstable
504 * @category combinators
505 * @since 4.0.0
506 */
507export const prependUrl: {
508 (path: string): (self: HttpClientRequest) => HttpClientRequest
509 (self: HttpClientRequest, path: string): HttpClientRequest
510} = dual(2, (self: HttpClientRequest, path: string): HttpClientRequest => {
511 if (path === "") return self
512 return makeWith(
513 self.method,
514 joinSegments(path, self.url),
515 self.urlParams,
516 self.hash,
517 self.headers,
518 self.body
519 )
520})
521
522/**
523 * Appends a URL segment to the request URL, inserting or trimming one slash as needed.
524 *
525 * @stability unstable
526 * @category combinators
527 * @since 4.0.0
528 */
529export const appendUrl: {
530 (path: string): (self: HttpClientRequest) => HttpClientRequest
531 (self: HttpClientRequest, path: string): HttpClientRequest
532} = dual(2, (self: HttpClientRequest, path: string): HttpClientRequest => {
533 if (path === "") return self
534 return makeWith(
535 self.method,
536 joinSegments(self.url, path),
537 self.urlParams,
538 self.hash,
539 self.headers,
540 self.body
541 )
542})
543
544const joinSegments = (first: string, second: string): string => {
545 const endsWithSlash = first.endsWith("/")
546 const startsWithSlash = second.startsWith("/")
547 const needsTrim = endsWithSlash && startsWithSlash
548 const needsSlash = !endsWithSlash && !startsWithSlash
549 return needsTrim ?
550 first + second.slice(1) :
551 needsSlash ?
552 first + "/" + second :
553 first + second
554}
555
556/**
557 * Updates the request URL by applying a function to the current URL string.
558 *
559 * @stability unstable
560 * @category combinators
561 * @since 4.0.0
562 */
563export const updateUrl: {
564 (f: (url: string) => string): (self: HttpClientRequest) => HttpClientRequest
565 (self: HttpClientRequest, f: (url: string) => string): HttpClientRequest
566} = dual(2, (self: HttpClientRequest, f: (url: string) => string): HttpClientRequest =>
567 makeWith(
568 self.method,
569 f(self.url),
570 self.urlParams,
571 self.hash,
572 self.headers,
573 self.body
574 ))
575
576/**
577 * Sets one query parameter, replacing existing values for that parameter name.
578 *
579 * @stability unstable
580 * @category combinators
581 * @since 4.0.0
582 */
583export const setUrlParam: {
584 (key: string, value: string): (self: HttpClientRequest) => HttpClientRequest
585 (self: HttpClientRequest, key: string, value: string): HttpClientRequest
586} = dual(3, (self: HttpClientRequest, key: string, value: string): HttpClientRequest =>
587 makeWith(
588 self.method,
589 self.url,
590 UrlParams.set(self.urlParams, key, value),
591 self.hash,
592 self.headers,
593 self.body
594 ))
595
596/**
597 * Sets query parameters from an input collection, replacing existing values for matching names.
598 *
599 * @stability unstable
600 * @category combinators
601 * @since 4.0.0
602 */
603export const setUrlParams: {
604 (input: UrlParams.Input): (self: HttpClientRequest) => HttpClientRequest
605 (self: HttpClientRequest, input: UrlParams.Input): HttpClientRequest
606} = dual(2, (self: HttpClientRequest, input: UrlParams.Input): HttpClientRequest =>
607 makeWith(
608 self.method,
609 self.url,
610 UrlParams.setAll(self.urlParams, input),
611 self.hash,
612 self.headers,
613 self.body
614 ))
615
616/**
617 * Appends one query parameter value without removing existing values for the same name.
618 *
619 * @stability unstable
620 * @category combinators
621 * @since 4.0.0
622 */
623export const appendUrlParam: {
624 (key: string, value: string): (self: HttpClientRequest) => HttpClientRequest
625 (self: HttpClientRequest, key: string, value: string): HttpClientRequest
626} = dual(3, (self: HttpClientRequest, key: string, value: string): HttpClientRequest =>
627 makeWith(
628 self.method,
629 self.url,
630 UrlParams.append(self.urlParams, key, value),
631 self.hash,
632 self.headers,
633 self.body
634 ))
635
636/**
637 * Appends query parameters from an input collection without removing existing values for matching names.
638 *
639 * @stability unstable
640 * @category combinators
641 * @since 4.0.0
642 */
643export const appendUrlParams: {
644 (input: UrlParams.Input): (self: HttpClientRequest) => HttpClientRequest
645 (self: HttpClientRequest, input: UrlParams.Input): HttpClientRequest
646} = dual(2, (self: HttpClientRequest, input: UrlParams.Input): HttpClientRequest =>
647 makeWith(
648 self.method,
649 self.url,
650 UrlParams.appendAll(self.urlParams, input),
651 self.hash,
652 self.headers,
653 self.body
654 ))
655
656/**
657 * Sets the URL fragment on a request without the leading `#`.
658 *
659 * @stability unstable
660 * @category combinators
661 * @since 4.0.0
662 */
663export const setHash: {
664 (hash: string): (self: HttpClientRequest) => HttpClientRequest
665 (self: HttpClientRequest, hash: string): HttpClientRequest
666} = dual(2, (self: HttpClientRequest, hash: string): HttpClientRequest =>
667 makeWith(
668 self.method,
669 self.url,
670 self.urlParams,
671 Option.some(hash),
672 self.headers,
673 self.body
674 ))
675
676/**
677 * Removes the URL fragment from a request.
678 *
679 * @stability unstable
680 * @category combinators
681 * @since 4.0.0
682 */
683export const removeHash = (self: HttpClientRequest): HttpClientRequest =>
684 makeWith(
685 self.method,
686 self.url,
687 self.urlParams,
688 Option.none(),
689 self.headers,
690 self.body
691 )
692
693/**
694 * Sets the request body and updates `Content-Type` and `Content-Length` headers from the body metadata when available.
695 *
696 * @stability unstable
697 * @category combinators
698 * @since 4.0.0
699 */
700export const setBody: {
701 (body: HttpBody.HttpBody): (self: HttpClientRequest) => HttpClientRequest
702 (self: HttpClientRequest, body: HttpBody.HttpBody): HttpClientRequest
703} = dual(2, (self: HttpClientRequest, body: HttpBody.HttpBody): HttpClientRequest => {
704 return makeWith(
705 self.method,
706 self.url,
707 self.urlParams,
708 self.hash,
709 bodyInternal.updateHeaders(self.headers, body),
710 body
711 )
712})
713
714/**
715 * Sets a `Uint8Array` request body with an optional content type.
716 *
717 * @stability unstable
718 * @category combinators
719 * @since 4.0.0
720 */
721export const bodyUint8Array: {
722 (body: Uint8Array, contentType?: string): (self: HttpClientRequest) => HttpClientRequest
723 (self: HttpClientRequest, body: Uint8Array, contentType?: string): HttpClientRequest
724} = dual(
725 (args) => isHttpClientRequest(args[0]),
726 (self: HttpClientRequest, body: Uint8Array, contentType?: string): HttpClientRequest =>
727 setBody(self, HttpBody.uint8Array(body, contentType))
728)
729
730/**
731 * Sets a text request body with an optional content type.
732 *
733 * @stability unstable
734 * @category combinators
735 * @since 4.0.0
736 */
737export const bodyText: {
738 (body: string, contentType?: string): (self: HttpClientRequest) => HttpClientRequest
739 (self: HttpClientRequest, body: string, contentType?: string): HttpClientRequest
740} = dual(
741 (args) => isHttpClientRequest(args[0]),
742 (self: HttpClientRequest, body: string, contentType?: string): HttpClientRequest =>
743 setBody(self, HttpBody.text(body, contentType))
744)
745
746/**
747 * Encodes a value as a JSON request body and sets it on the request, failing with `HttpBodyError` if encoding fails.
748 *
749 * @stability unstable
750 * @category combinators
751 * @since 4.0.0
752 */
753export const bodyJson: {
754 (body: unknown): (self: HttpClientRequest) => Effect.Effect<HttpClientRequest, HttpBody.HttpBodyError>
755 (self: HttpClientRequest, body: unknown): Effect.Effect<HttpClientRequest, HttpBody.HttpBodyError>
756} = dual(
757 2,
758 (self: HttpClientRequest, body: unknown): Effect.Effect<HttpClientRequest, HttpBody.HttpBodyError> =>
759 Effect.map(HttpBody.json(body), (body) => setBody(self, body))
760)
761
762/**
763 * Sets a JSON request body using unsafe JSON encoding.
764 *
765 * **When to use**
766 *
767 * Use when the request body is known to be JSON-serializable and a synchronous
768 * `HttpClientRequest` result is needed.
769 *
770 * **Gotchas**
771 *
772 * JSON encoding may throw instead of failing in the Effect error channel.
773 *
774 * @stability unstable
775 * @category combinators
776 * @since 4.0.0
777 */
778export const bodyJsonUnsafe: {
779 (body: unknown): (self: HttpClientRequest) => HttpClientRequest
780 (self: HttpClientRequest, body: unknown): HttpClientRequest
781} = dual(2, (self: HttpClientRequest, body: unknown): HttpClientRequest => setBody(self, HttpBody.jsonUnsafe(body)))
782
783/**
784 * Creates a schema-based JSON body encoder that sets the encoded value on a request.
785 *
786 * @stability unstable
787 * @category combinators
788 * @since 4.0.0
789 */
790export const schemaBodyJson = <S extends Schema.Constraint>(
791 schema: S,
792 options?: ParseOptions | undefined
793): {
794 (
795 body: S["Type"]
796 ): (
797 self: HttpClientRequest
798 ) => Effect.Effect<HttpClientRequest, HttpBody.HttpBodyError, S["EncodingServices"]>
799 (
800 self: HttpClientRequest,
801 body: S["Type"]
802 ): Effect.Effect<HttpClientRequest, HttpBody.HttpBodyError, S["EncodingServices"]>
803} => {
804 const encode = HttpBody.jsonSchema(schema, options)
805 return dual(
806 2,
807 (
808 self: HttpClientRequest,
809 body: unknown
810 ): Effect.Effect<HttpClientRequest, HttpBody.HttpBodyError, S["EncodingServices"]> =>
811 Effect.map(encode(body), (body) => setBody(self, body))
812 )
813}
814
815/**
816 * Sets an `application/x-www-form-urlencoded` request body from URL parameter input.
817 *
818 * @stability unstable
819 * @category combinators
820 * @since 4.0.0
821 */
822export const bodyUrlParams: {
823 (input: UrlParams.Input): (self: HttpClientRequest) => HttpClientRequest
824 (self: HttpClientRequest, input: UrlParams.Input): HttpClientRequest
825} = dual(
826 2,
827 (self: HttpClientRequest, input: UrlParams.Input): HttpClientRequest =>
828 setBody(self, HttpBody.urlParams(UrlParams.fromInput(input)))
829)
830
831/**
832 * Sets a `FormData` request body.
833 *
834 * @stability unstable
835 * @category combinators
836 * @since 4.0.0
837 */
838export const bodyFormData: {
839 (body: FormData): (self: HttpClientRequest) => HttpClientRequest
840 (self: HttpClientRequest, body: FormData): HttpClientRequest
841} = dual(2, (self: HttpClientRequest, body: FormData): HttpClientRequest => setBody(self, HttpBody.formData(body)))
842
843/**
844 * Creates a `FormData` request body from record-style entries and sets it on the request.
845 *
846 * @stability unstable
847 * @category combinators
848 * @since 4.0.0
849 */
850export const bodyFormDataRecord: {
851 (entries: HttpBody.FormDataInput): (self: HttpClientRequest) => HttpClientRequest
852 (self: HttpClientRequest, entries: HttpBody.FormDataInput): HttpClientRequest
853} = dual(
854 2,
855 (self: HttpClientRequest, entries: HttpBody.FormDataInput): HttpClientRequest =>
856 setBody(self, HttpBody.formDataRecord(entries))
857)
858
859/**
860 * Sets a streaming `Uint8Array` request body with optional content type and content length metadata.
861 *
862 * @stability unstable
863 * @category combinators
864 * @since 4.0.0
865 */
866export const bodyStream: {
867 (
868 body: Stream.Stream<Uint8Array, unknown>,
869 options?: { readonly contentType?: string | undefined; readonly contentLength?: number | undefined } | undefined
870 ): (self: HttpClientRequest) => HttpClientRequest
871 (
872 self: HttpClientRequest,
873 body: Stream.Stream<Uint8Array, unknown>,
874 options?: { readonly contentType?: string | undefined; readonly contentLength?: number | undefined } | undefined
875 ): HttpClientRequest
876} = dual(
877 (args) => isHttpClientRequest(args[0]),
878 (
879 self: HttpClientRequest,
880 body: Stream.Stream<Uint8Array, unknown>,
881 options?: { readonly contentType?: string | undefined; readonly contentLength?: number | undefined } | undefined
882 ): HttpClientRequest =>
883 setBody(
884 self,
885 HttpBody.stream(body, options?.contentType, options?.contentLength)
886 )
887)
888
889/**
890 * Creates a file-backed request body from a filesystem path and sets it on the request.
891 *
892 * **Details**
893 *
894 * Uses {@link HttpBody.file} to validate ranges lazily and calculate the EOF-clamped content length with exact
895 * bigint arithmetic. Invalid range inputs or a final length above `Number.MAX_SAFE_INTEGER` fail with
896 * `PlatformError` / `BadArgument`. Larger sizes, offsets, and byte counts are valid when the final length is
897 * representable as a safe integer. The request's Content-Length header contains that exact length.
898 *
899 * @stability unstable
900 * @category combinators
901 * @since 4.0.0
902 */
903export const bodyFile: {
904 (
905 path: string,
906 options?: {
907 readonly bytesToRead?: ByteSize.Input | undefined
908 readonly chunkSize?: number | undefined
909 readonly offset?: ByteSize.Input | undefined
910 readonly contentType?: string
911 }
912 ): (self: HttpClientRequest) => Effect.Effect<HttpClientRequest, PlatformError.PlatformError, FileSystem.FileSystem>
913 (
914 self: HttpClientRequest,
915 path: string,
916 options?: {
917 readonly bytesToRead?: ByteSize.Input | undefined
918 readonly chunkSize?: number | undefined
919 readonly offset?: ByteSize.Input | undefined
920 readonly contentType?: string
921 }
922 ): Effect.Effect<HttpClientRequest, PlatformError.PlatformError, FileSystem.FileSystem>
923} = dual(
924 (args) => isHttpClientRequest(args[0]),
925 (
926 self: HttpClientRequest,
927 path: string,
928 options?: {
929 readonly bytesToRead?: ByteSize.Input | undefined
930 readonly chunkSize?: number | undefined
931 readonly offset?: ByteSize.Input | undefined
932 readonly contentType?: string
933 }
934 ): Effect.Effect<HttpClientRequest, PlatformError.PlatformError, FileSystem.FileSystem> =>
935 Effect.map(
936 HttpBody.file(path, options),
937 (body) => setBody(self, body)
938 )
939)
940
941/**
942 * Builds a `URL` from the request URL, query parameters, and hash, returning `Option.none()` if the URL is invalid.
943 *
944 * @stability unstable
945 * @category combinators
946 * @since 4.0.0
947 */
948export function toUrl(self: HttpClientRequest): Option.Option<URL> {
949 const r = Url.make(self.url, self.urlParams, Option.getOrUndefined(self.hash))
950 if (Result.isSuccess(r)) {
951 return Option.some(r.success)
952 }
953 return Option.none()
954}
955
956/**
957 * Converts a Web `Request` into an `HttpClientRequest`, preserving method, URL, headers, and supported request bodies.
958 *
959 * @stability unstable
960 * @category converting
961 * @since 4.0.0
962 */
963export const fromWeb = (request: globalThis.Request): HttpClientRequest => {
964 const method = request.method.toUpperCase() as HttpMethod
965 return modify(empty, {
966 method,
967 url: new URL(request.url),
968 headers: request.headers,
969 body: fromWebBody(request, method)
970 })
971}
972
973const fromWebBody = (request: globalThis.Request, method: HttpMethod): HttpBody.HttpBody => {
974 if (!hasBody(method) || request.body === null) {
975 return HttpBody.empty
976 }
977 return HttpBody.raw(request.body, {
978 contentType: request.headers.get("content-type") ?? undefined,
979 contentLength: bodyInternal.parseContentLength(request.headers.get("content-length"))
980 })
981}
982
983/**
984 * Converts an `HttpClientRequest` safely to a Web `Request` as a `Result`, failing when the request URL is invalid.
985 *
986 * @stability unstable
987 * @category converting
988 * @since 4.0.0
989 */
990export const toWebResult = (self: HttpClientRequest, options?: {
991 readonly signal?: AbortSignal | undefined
992 readonly context?: Context.Context<never> | undefined
993}): Result.Result<Request, Url.UrlError> => {
994 const url = Url.make(self.url, self.urlParams, Option.getOrUndefined(self.hash))
995 if (Result.isFailure(url)) {
996 return Result.fail(url.failure)
997 }
998 const requestInit: RequestInit = {
999 method: self.method,
1000 headers: self.headers
1001 }
1002 if (options?.signal) {
1003 requestInit.signal = options.signal
1004 }
1005 if (hasBody(self.method)) {
1006 switch (self.body._tag) {
1007 case "Empty": {
1008 break
1009 }
1010 case "Raw": {
1011 requestInit.body = self.body.body as any
1012 if (isReadableStream(self.body.body)) {
1013 ;(requestInit as any).duplex = "half"
1014 }
1015 break
1016 }
1017 case "Uint8Array": {
1018 requestInit.body = self.body.body as any
1019 break
1020 }
1021 case "FormData": {
1022 requestInit.body = self.body.formData
1023 break
1024 }
1025 case "Stream": {
1026 requestInit.body = Stream.toReadableStreamWith(self.body.stream, options?.context ?? Context.empty())
1027 ;(requestInit as any).duplex = "half"
1028 break
1029 }
1030 }
1031 }
1032 return Result.try({
1033 try: () => new Request(url.success, requestInit),
1034 catch: (cause) => new Url.UrlError({ cause })
1035 })
1036}
1037
1038const isReadableStream = (u: unknown): u is ReadableStream<Uint8Array> =>
1039 typeof ReadableStream !== "undefined" && u instanceof ReadableStream
1040
1041/**
1042 * Converts an `HttpClientRequest` to a Web `Request`, failing with `UrlError` when the request URL is invalid.
1043 *
1044 * @stability unstable
1045 * @category converting
1046 * @since 4.0.0
1047 */
1048export const toWeb = (self: HttpClientRequest, options?: {
1049 readonly signal?: AbortSignal | undefined
1050}): Effect.Effect<Request, Url.UrlError> =>
1051 Effect.contextWith((context) =>
1052 Effect.fromResult(toWebResult(self, {
1053 context: context,
1054 signal: options?.signal
1055 }))
1056 )