970 lines
27 KiB
TypeScript
970 lines
27 KiB
TypeScript
/**
|
|
* Describes immutable outgoing HTTP client requests.
|
|
*
|
|
* `HttpClientRequest` is the request model shared by Effect HTTP clients and
|
|
* platform adapters. A request stores its method, URL, query parameters, hash,
|
|
* headers, and body as structured data. This module includes constructors,
|
|
* helpers for updating requests, body encoders for common payloads, and
|
|
* conversions to and from Web `Request` values.
|
|
*
|
|
* @since 4.0.0
|
|
*/
|
|
import * as Context from "../../Context.ts"
|
|
import * as Effect from "../../Effect.ts"
|
|
import type * as FileSystem from "../../FileSystem.ts"
|
|
import { dual } from "../../Function.ts"
|
|
import * as Inspectable from "../../Inspectable.ts"
|
|
import { stringOrRedacted } from "../../internal/redacted.ts"
|
|
import * as Option from "../../Option.ts"
|
|
import { type Pipeable, pipeArguments } from "../../Pipeable.ts"
|
|
import type * as PlatformError from "../../PlatformError.ts"
|
|
import { hasProperty } from "../../Predicate.ts"
|
|
import { redact } from "../../Redactable.ts"
|
|
import type * as Redacted from "../../Redacted.ts"
|
|
import * as Result from "../../Result.ts"
|
|
import type * as Schema from "../../Schema.ts"
|
|
import type { ParseOptions } from "../../SchemaAST.ts"
|
|
import * as Stream from "../../Stream.ts"
|
|
import * as Headers from "./Headers.ts"
|
|
import * as HttpBody from "./HttpBody.ts"
|
|
import { hasBody, type HttpMethod } from "./HttpMethod.ts"
|
|
import * as Url from "./Url.ts"
|
|
import * as UrlParams from "./UrlParams.ts"
|
|
|
|
const TypeId = "~effect/http/HttpClientRequest"
|
|
|
|
/**
|
|
* Returns `true` when a value is an `HttpClientRequest`.
|
|
*
|
|
* @category guards
|
|
* @since 4.0.0
|
|
*/
|
|
export const isHttpClientRequest = (u: unknown): u is HttpClientRequest => hasProperty(u, TypeId)
|
|
|
|
/**
|
|
* Immutable model of an outgoing HTTP client request, including its method, URL components, headers, and body.
|
|
*
|
|
* @category models
|
|
* @since 4.0.0
|
|
*/
|
|
export interface HttpClientRequest extends Inspectable.Inspectable, Pipeable {
|
|
readonly [TypeId]: typeof TypeId
|
|
readonly method: HttpMethod
|
|
readonly url: string
|
|
readonly urlParams: UrlParams.UrlParams
|
|
readonly hash: Option.Option<string>
|
|
readonly headers: Headers.Headers
|
|
readonly body: HttpBody.HttpBody
|
|
}
|
|
|
|
/**
|
|
* Options for constructing or modifying an `HttpClientRequest`.
|
|
*
|
|
* @category options
|
|
* @since 4.0.0
|
|
*/
|
|
export interface Options {
|
|
readonly method?: HttpMethod | undefined
|
|
readonly url?: string | URL | undefined
|
|
readonly urlParams?: UrlParams.Input | undefined
|
|
readonly hash?: string | undefined
|
|
readonly headers?: Headers.Input | undefined
|
|
readonly body?: HttpBody.HttpBody | undefined
|
|
readonly accept?: string | undefined
|
|
readonly acceptJson?: boolean | undefined
|
|
}
|
|
|
|
/**
|
|
* Namespace containing option types associated with `HttpClientRequest` construction.
|
|
*
|
|
* @since 4.0.0
|
|
*/
|
|
export declare namespace Options {
|
|
/**
|
|
* Request options that omit the method and URL for helpers that already receive those values separately.
|
|
*
|
|
* @category options
|
|
* @since 4.0.0
|
|
*/
|
|
export interface NoUrl extends Omit<Options, "method" | "url"> {}
|
|
}
|
|
|
|
const Proto = {
|
|
[TypeId]: TypeId,
|
|
...Inspectable.BaseProto,
|
|
toJSON(this: HttpClientRequest): unknown {
|
|
return {
|
|
_id: "HttpClientRequest",
|
|
method: this.method,
|
|
url: this.url,
|
|
urlParams: this.urlParams,
|
|
hash: this.hash,
|
|
headers: redact(this.headers),
|
|
body: this.body.toJSON()
|
|
}
|
|
},
|
|
pipe() {
|
|
return pipeArguments(this, arguments)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Constructs an `HttpClientRequest` from fully normalized request components.
|
|
*
|
|
* @category constructors
|
|
* @since 4.0.0
|
|
*/
|
|
export function makeWith(
|
|
method: HttpMethod,
|
|
url: string,
|
|
urlParams: UrlParams.Input,
|
|
hash: Option.Option<string>,
|
|
headers: Headers.Headers,
|
|
body: HttpBody.HttpBody
|
|
): HttpClientRequest {
|
|
const self = Object.create(Proto)
|
|
self.method = method
|
|
self.url = url
|
|
self.urlParams = urlParams
|
|
self.hash = hash
|
|
self.headers = headers
|
|
self.body = body
|
|
return self
|
|
}
|
|
|
|
/**
|
|
* An empty `GET` request with no URL, query parameters, hash, headers, or body.
|
|
*
|
|
* @category constructors
|
|
* @since 4.0.0
|
|
*/
|
|
export const empty: HttpClientRequest = makeWith(
|
|
"GET",
|
|
"",
|
|
UrlParams.empty,
|
|
Option.none(),
|
|
Headers.empty,
|
|
HttpBody.empty
|
|
)
|
|
|
|
/**
|
|
* Creates a request constructor for the specified HTTP method.
|
|
*
|
|
* @category constructors
|
|
* @since 4.0.0
|
|
*/
|
|
export const make = <M extends HttpMethod>(
|
|
method: M
|
|
) =>
|
|
(
|
|
url: string | URL,
|
|
options?: Options.NoUrl | undefined
|
|
): HttpClientRequest =>
|
|
modify(empty, {
|
|
method,
|
|
url,
|
|
...(options ?? undefined)
|
|
})
|
|
|
|
/**
|
|
* Creates a `GET` request for the specified URL.
|
|
*
|
|
* @category constructors
|
|
* @since 4.0.0
|
|
*/
|
|
export const get: (url: string | URL, options?: Options.NoUrl) => HttpClientRequest = make("GET")
|
|
|
|
/**
|
|
* Creates a `POST` request for the specified URL.
|
|
*
|
|
* @category constructors
|
|
* @since 4.0.0
|
|
*/
|
|
export const post: (url: string | URL, options?: Options.NoUrl) => HttpClientRequest = make("POST")
|
|
|
|
/**
|
|
* Creates a `PATCH` request for the specified URL.
|
|
*
|
|
* @category constructors
|
|
* @since 4.0.0
|
|
*/
|
|
export const patch: (url: string | URL, options?: Options.NoUrl) => HttpClientRequest = make("PATCH")
|
|
|
|
/**
|
|
* Creates a `PUT` request for the specified URL.
|
|
*
|
|
* @category constructors
|
|
* @since 4.0.0
|
|
*/
|
|
export const put: (url: string | URL, options?: Options.NoUrl) => HttpClientRequest = make("PUT")
|
|
|
|
const del: (url: string | URL, options?: Options.NoUrl) => HttpClientRequest = make("DELETE")
|
|
|
|
export {
|
|
/**
|
|
* Creates a `DELETE` request for the specified URL.
|
|
*
|
|
* @category constructors
|
|
* @since 4.0.0
|
|
*/
|
|
del as delete
|
|
}
|
|
|
|
/**
|
|
* Creates a `HEAD` request for the specified URL.
|
|
*
|
|
* @category constructors
|
|
* @since 4.0.0
|
|
*/
|
|
export const head: (url: string | URL, options?: Options.NoUrl) => HttpClientRequest = make("HEAD")
|
|
|
|
/**
|
|
* Creates an `OPTIONS` request for the specified URL.
|
|
*
|
|
* @category constructors
|
|
* @since 4.0.0
|
|
*/
|
|
export const options: (url: string | URL, options?: Options.NoUrl) => HttpClientRequest = make("OPTIONS")
|
|
|
|
/**
|
|
* Creates a `TRACE` request for the specified URL.
|
|
*
|
|
* @category constructors
|
|
* @since 4.0.0
|
|
*/
|
|
export const trace: (url: string | URL, options?: Options.NoUrl) => HttpClientRequest = make("TRACE")
|
|
|
|
/**
|
|
* Applies request options to an `HttpClientRequest`, returning a new request.
|
|
*
|
|
* @category combinators
|
|
* @since 4.0.0
|
|
*/
|
|
export const modify: {
|
|
(options: Options): (self: HttpClientRequest) => HttpClientRequest
|
|
(self: HttpClientRequest, options: Options): HttpClientRequest
|
|
} = dual(2, (self: HttpClientRequest, options: Options): HttpClientRequest => {
|
|
let result = self
|
|
|
|
if (options.method) {
|
|
result = setMethod(result, options.method)
|
|
}
|
|
if (options.url) {
|
|
result = setUrl(result, options.url)
|
|
}
|
|
if (options.headers) {
|
|
result = setHeaders(result, options.headers)
|
|
}
|
|
if (options.urlParams) {
|
|
result = setUrlParams(result, options.urlParams)
|
|
}
|
|
if (options.hash) {
|
|
result = setHash(result, options.hash)
|
|
}
|
|
if (options.body) {
|
|
result = setBody(result, options.body)
|
|
}
|
|
if (options.accept) {
|
|
result = accept(result, options.accept)
|
|
}
|
|
if (options.acceptJson) {
|
|
result = acceptJson(result)
|
|
}
|
|
|
|
return result
|
|
})
|
|
|
|
/**
|
|
* Sets the HTTP method on a request, returning a new request.
|
|
*
|
|
* @category combinators
|
|
* @since 4.0.0
|
|
*/
|
|
export const setMethod: {
|
|
(method: HttpMethod): (self: HttpClientRequest) => HttpClientRequest
|
|
(self: HttpClientRequest, method: HttpMethod): HttpClientRequest
|
|
} = dual(
|
|
2,
|
|
(self: HttpClientRequest, method: HttpMethod): HttpClientRequest =>
|
|
makeWith(method, self.url, self.urlParams, self.hash, self.headers, self.body)
|
|
)
|
|
|
|
/**
|
|
* Sets a single request header, replacing any existing value for that header.
|
|
*
|
|
* @category combinators
|
|
* @since 4.0.0
|
|
*/
|
|
export const setHeader: {
|
|
(key: string, value: string): (self: HttpClientRequest) => HttpClientRequest
|
|
(self: HttpClientRequest, key: string, value: string): HttpClientRequest
|
|
} = dual(3, (self: HttpClientRequest, key: string, value: string): HttpClientRequest =>
|
|
makeWith(
|
|
self.method,
|
|
self.url,
|
|
self.urlParams,
|
|
self.hash,
|
|
Headers.set(self.headers, key, value),
|
|
self.body
|
|
))
|
|
|
|
/**
|
|
* Sets multiple request headers from an input collection, replacing existing values with matching names.
|
|
*
|
|
* @category combinators
|
|
* @since 4.0.0
|
|
*/
|
|
export const setHeaders: {
|
|
(input: Headers.Input): (self: HttpClientRequest) => HttpClientRequest
|
|
(self: HttpClientRequest, input: Headers.Input): HttpClientRequest
|
|
} = dual(2, (self: HttpClientRequest, input: Headers.Input): HttpClientRequest =>
|
|
makeWith(
|
|
self.method,
|
|
self.url,
|
|
self.urlParams,
|
|
self.hash,
|
|
Headers.setAll(self.headers, input),
|
|
self.body
|
|
))
|
|
|
|
/**
|
|
* Sets the `Authorization` header using HTTP Basic authentication credentials.
|
|
*
|
|
* @category combinators
|
|
* @since 4.0.0
|
|
*/
|
|
export const basicAuth: {
|
|
(
|
|
username: string | Redacted.Redacted,
|
|
password: string | Redacted.Redacted
|
|
): (self: HttpClientRequest) => HttpClientRequest
|
|
(
|
|
self: HttpClientRequest,
|
|
username: string | Redacted.Redacted,
|
|
password: string | Redacted.Redacted
|
|
): HttpClientRequest
|
|
} = dual(
|
|
3,
|
|
(
|
|
self: HttpClientRequest,
|
|
username: string | Redacted.Redacted,
|
|
password: string | Redacted.Redacted
|
|
): HttpClientRequest =>
|
|
setHeader(self, "Authorization", `Basic ${btoa(`${stringOrRedacted(username)}:${stringOrRedacted(password)}`)}`)
|
|
)
|
|
|
|
/**
|
|
* Sets the `Authorization` header using a bearer token.
|
|
*
|
|
* @category combinators
|
|
* @since 4.0.0
|
|
*/
|
|
export const bearerToken: {
|
|
(token: string | Redacted.Redacted): (self: HttpClientRequest) => HttpClientRequest
|
|
(self: HttpClientRequest, token: string | Redacted.Redacted): HttpClientRequest
|
|
} = dual(
|
|
2,
|
|
(self: HttpClientRequest, token: string | Redacted.Redacted): HttpClientRequest =>
|
|
setHeader(self, "Authorization", `Bearer ${stringOrRedacted(token)}`)
|
|
)
|
|
|
|
/**
|
|
* Sets the `Accept` header to the specified media type.
|
|
*
|
|
* @category combinators
|
|
* @since 4.0.0
|
|
*/
|
|
export const accept: {
|
|
(mediaType: string): (self: HttpClientRequest) => HttpClientRequest
|
|
(self: HttpClientRequest, mediaType: string): HttpClientRequest
|
|
} = dual(2, (self: HttpClientRequest, mediaType: string): HttpClientRequest => setHeader(self, "Accept", mediaType))
|
|
|
|
/**
|
|
* Sets the `Accept` header to `application/json`.
|
|
*
|
|
* @category combinators
|
|
* @since 4.0.0
|
|
*/
|
|
export const acceptJson: (self: HttpClientRequest) => HttpClientRequest = accept("application/json")
|
|
|
|
/**
|
|
* Sets the request URL. When given a `URL`, its search parameters and hash are extracted into the request's structured fields.
|
|
*
|
|
* @category combinators
|
|
* @since 4.0.0
|
|
*/
|
|
export const setUrl: {
|
|
(url: string | URL): (self: HttpClientRequest) => HttpClientRequest
|
|
(self: HttpClientRequest, url: string | URL): HttpClientRequest
|
|
} = dual(2, (self: HttpClientRequest, url: string | URL): HttpClientRequest => {
|
|
if (typeof url === "string") {
|
|
return makeWith(
|
|
self.method,
|
|
url,
|
|
self.urlParams,
|
|
self.hash,
|
|
self.headers,
|
|
self.body
|
|
)
|
|
}
|
|
const clone = new URL(url.toString())
|
|
const urlParams = UrlParams.fromInput(clone.searchParams)
|
|
const hash = Option.fromNullishOr(clone.hash === "" ? undefined : clone.hash.slice(1))
|
|
clone.search = ""
|
|
clone.hash = ""
|
|
return makeWith(
|
|
self.method,
|
|
clone.toString(),
|
|
urlParams,
|
|
hash,
|
|
self.headers,
|
|
self.body
|
|
)
|
|
})
|
|
|
|
/**
|
|
* Prepends a URL segment to the request URL, inserting or trimming one slash as needed.
|
|
*
|
|
* @category combinators
|
|
* @since 4.0.0
|
|
*/
|
|
export const prependUrl: {
|
|
(path: string): (self: HttpClientRequest) => HttpClientRequest
|
|
(self: HttpClientRequest, path: string): HttpClientRequest
|
|
} = dual(2, (self: HttpClientRequest, path: string): HttpClientRequest => {
|
|
if (path === "") return self
|
|
return makeWith(
|
|
self.method,
|
|
joinSegments(path, self.url),
|
|
self.urlParams,
|
|
self.hash,
|
|
self.headers,
|
|
self.body
|
|
)
|
|
})
|
|
|
|
/**
|
|
* Appends a URL segment to the request URL, inserting or trimming one slash as needed.
|
|
*
|
|
* @category combinators
|
|
* @since 4.0.0
|
|
*/
|
|
export const appendUrl: {
|
|
(path: string): (self: HttpClientRequest) => HttpClientRequest
|
|
(self: HttpClientRequest, path: string): HttpClientRequest
|
|
} = dual(2, (self: HttpClientRequest, path: string): HttpClientRequest => {
|
|
if (path === "") return self
|
|
return makeWith(
|
|
self.method,
|
|
joinSegments(self.url, path),
|
|
self.urlParams,
|
|
self.hash,
|
|
self.headers,
|
|
self.body
|
|
)
|
|
})
|
|
|
|
const joinSegments = (first: string, second: string): string => {
|
|
const endsWithSlash = first.endsWith("/")
|
|
const startsWithSlash = second.startsWith("/")
|
|
const needsTrim = endsWithSlash && startsWithSlash
|
|
const needsSlash = !endsWithSlash && !startsWithSlash
|
|
return needsTrim ?
|
|
first + second.slice(1) :
|
|
needsSlash ?
|
|
first + "/" + second :
|
|
first + second
|
|
}
|
|
|
|
/**
|
|
* Updates the request URL by applying a function to the current URL string.
|
|
*
|
|
* @category combinators
|
|
* @since 4.0.0
|
|
*/
|
|
export const updateUrl: {
|
|
(f: (url: string) => string): (self: HttpClientRequest) => HttpClientRequest
|
|
(self: HttpClientRequest, f: (url: string) => string): HttpClientRequest
|
|
} = dual(2, (self: HttpClientRequest, f: (url: string) => string): HttpClientRequest =>
|
|
makeWith(
|
|
self.method,
|
|
f(self.url),
|
|
self.urlParams,
|
|
self.hash,
|
|
self.headers,
|
|
self.body
|
|
))
|
|
|
|
/**
|
|
* Sets one query parameter, replacing existing values for that parameter name.
|
|
*
|
|
* @category combinators
|
|
* @since 4.0.0
|
|
*/
|
|
export const setUrlParam: {
|
|
(key: string, value: string): (self: HttpClientRequest) => HttpClientRequest
|
|
(self: HttpClientRequest, key: string, value: string): HttpClientRequest
|
|
} = dual(3, (self: HttpClientRequest, key: string, value: string): HttpClientRequest =>
|
|
makeWith(
|
|
self.method,
|
|
self.url,
|
|
UrlParams.set(self.urlParams, key, value),
|
|
self.hash,
|
|
self.headers,
|
|
self.body
|
|
))
|
|
|
|
/**
|
|
* Sets query parameters from an input collection, replacing existing values for matching names.
|
|
*
|
|
* @category combinators
|
|
* @since 4.0.0
|
|
*/
|
|
export const setUrlParams: {
|
|
(input: UrlParams.Input): (self: HttpClientRequest) => HttpClientRequest
|
|
(self: HttpClientRequest, input: UrlParams.Input): HttpClientRequest
|
|
} = dual(2, (self: HttpClientRequest, input: UrlParams.Input): HttpClientRequest =>
|
|
makeWith(
|
|
self.method,
|
|
self.url,
|
|
UrlParams.setAll(self.urlParams, input),
|
|
self.hash,
|
|
self.headers,
|
|
self.body
|
|
))
|
|
|
|
/**
|
|
* Appends one query parameter value without removing existing values for the same name.
|
|
*
|
|
* @category combinators
|
|
* @since 4.0.0
|
|
*/
|
|
export const appendUrlParam: {
|
|
(key: string, value: string): (self: HttpClientRequest) => HttpClientRequest
|
|
(self: HttpClientRequest, key: string, value: string): HttpClientRequest
|
|
} = dual(3, (self: HttpClientRequest, key: string, value: string): HttpClientRequest =>
|
|
makeWith(
|
|
self.method,
|
|
self.url,
|
|
UrlParams.append(self.urlParams, key, value),
|
|
self.hash,
|
|
self.headers,
|
|
self.body
|
|
))
|
|
|
|
/**
|
|
* Appends query parameters from an input collection without removing existing values for matching names.
|
|
*
|
|
* @category combinators
|
|
* @since 4.0.0
|
|
*/
|
|
export const appendUrlParams: {
|
|
(input: UrlParams.Input): (self: HttpClientRequest) => HttpClientRequest
|
|
(self: HttpClientRequest, input: UrlParams.Input): HttpClientRequest
|
|
} = dual(2, (self: HttpClientRequest, input: UrlParams.Input): HttpClientRequest =>
|
|
makeWith(
|
|
self.method,
|
|
self.url,
|
|
UrlParams.appendAll(self.urlParams, input),
|
|
self.hash,
|
|
self.headers,
|
|
self.body
|
|
))
|
|
|
|
/**
|
|
* Sets the URL fragment on a request without the leading `#`.
|
|
*
|
|
* @category combinators
|
|
* @since 4.0.0
|
|
*/
|
|
export const setHash: {
|
|
(hash: string): (self: HttpClientRequest) => HttpClientRequest
|
|
(self: HttpClientRequest, hash: string): HttpClientRequest
|
|
} = dual(2, (self: HttpClientRequest, hash: string): HttpClientRequest =>
|
|
makeWith(
|
|
self.method,
|
|
self.url,
|
|
self.urlParams,
|
|
Option.some(hash),
|
|
self.headers,
|
|
self.body
|
|
))
|
|
|
|
/**
|
|
* Removes the URL fragment from a request.
|
|
*
|
|
* @category combinators
|
|
* @since 4.0.0
|
|
*/
|
|
export const removeHash = (self: HttpClientRequest): HttpClientRequest =>
|
|
makeWith(
|
|
self.method,
|
|
self.url,
|
|
self.urlParams,
|
|
Option.none(),
|
|
self.headers,
|
|
self.body
|
|
)
|
|
|
|
/**
|
|
* Sets the request body and updates `Content-Type` and `Content-Length` headers from the body metadata when available.
|
|
*
|
|
* @category combinators
|
|
* @since 4.0.0
|
|
*/
|
|
export const setBody: {
|
|
(body: HttpBody.HttpBody): (self: HttpClientRequest) => HttpClientRequest
|
|
(self: HttpClientRequest, body: HttpBody.HttpBody): HttpClientRequest
|
|
} = dual(2, (self: HttpClientRequest, body: HttpBody.HttpBody): HttpClientRequest => {
|
|
let headers = self.headers
|
|
if (body._tag === "Empty" || body._tag === "FormData") {
|
|
headers = Headers.remove(Headers.remove(headers, "Content-Type"), "Content-length")
|
|
} else {
|
|
if (body.contentType) {
|
|
headers = Headers.set(headers, "content-type", body.contentType)
|
|
}
|
|
if (body.contentLength !== undefined) {
|
|
headers = Headers.set(headers, "content-length", body.contentLength.toString())
|
|
}
|
|
}
|
|
return makeWith(
|
|
self.method,
|
|
self.url,
|
|
self.urlParams,
|
|
self.hash,
|
|
headers,
|
|
body
|
|
)
|
|
})
|
|
|
|
/**
|
|
* Sets a `Uint8Array` request body with an optional content type.
|
|
*
|
|
* @category combinators
|
|
* @since 4.0.0
|
|
*/
|
|
export const bodyUint8Array: {
|
|
(body: Uint8Array, contentType?: string): (self: HttpClientRequest) => HttpClientRequest
|
|
(self: HttpClientRequest, body: Uint8Array, contentType?: string): HttpClientRequest
|
|
} = dual(
|
|
(args) => isHttpClientRequest(args[0]),
|
|
(self: HttpClientRequest, body: Uint8Array, contentType?: string): HttpClientRequest =>
|
|
setBody(self, HttpBody.uint8Array(body, contentType))
|
|
)
|
|
|
|
/**
|
|
* Sets a text request body with an optional content type.
|
|
*
|
|
* @category combinators
|
|
* @since 4.0.0
|
|
*/
|
|
export const bodyText: {
|
|
(body: string, contentType?: string): (self: HttpClientRequest) => HttpClientRequest
|
|
(self: HttpClientRequest, body: string, contentType?: string): HttpClientRequest
|
|
} = dual(
|
|
(args) => isHttpClientRequest(args[0]),
|
|
(self: HttpClientRequest, body: string, contentType?: string): HttpClientRequest =>
|
|
setBody(self, HttpBody.text(body, contentType))
|
|
)
|
|
|
|
/**
|
|
* Encodes a value as a JSON request body and sets it on the request, failing with `HttpBodyError` if encoding fails.
|
|
*
|
|
* @category combinators
|
|
* @since 4.0.0
|
|
*/
|
|
export const bodyJson: {
|
|
(body: unknown): (self: HttpClientRequest) => Effect.Effect<HttpClientRequest, HttpBody.HttpBodyError>
|
|
(self: HttpClientRequest, body: unknown): Effect.Effect<HttpClientRequest, HttpBody.HttpBodyError>
|
|
} = dual(
|
|
2,
|
|
(self: HttpClientRequest, body: unknown): Effect.Effect<HttpClientRequest, HttpBody.HttpBodyError> =>
|
|
Effect.map(HttpBody.json(body), (body) => setBody(self, body))
|
|
)
|
|
|
|
/**
|
|
* Sets a JSON request body using unsafe JSON encoding.
|
|
*
|
|
* **When to use**
|
|
*
|
|
* Use when the request body is known to be JSON-serializable and a synchronous
|
|
* `HttpClientRequest` result is needed.
|
|
*
|
|
* **Gotchas**
|
|
*
|
|
* JSON encoding may throw instead of failing in the Effect error channel.
|
|
*
|
|
* @category combinators
|
|
* @since 4.0.0
|
|
*/
|
|
export const bodyJsonUnsafe: {
|
|
(body: unknown): (self: HttpClientRequest) => HttpClientRequest
|
|
(self: HttpClientRequest, body: unknown): HttpClientRequest
|
|
} = dual(2, (self: HttpClientRequest, body: unknown): HttpClientRequest => setBody(self, HttpBody.jsonUnsafe(body)))
|
|
|
|
/**
|
|
* Creates a schema-based JSON body encoder that sets the encoded value on a request.
|
|
*
|
|
* @category combinators
|
|
* @since 4.0.0
|
|
*/
|
|
export const schemaBodyJson = <S extends Schema.Constraint>(
|
|
schema: S,
|
|
options?: ParseOptions | undefined
|
|
): {
|
|
(
|
|
body: S["Type"]
|
|
): (
|
|
self: HttpClientRequest
|
|
) => Effect.Effect<HttpClientRequest, HttpBody.HttpBodyError, S["EncodingServices"]>
|
|
(
|
|
self: HttpClientRequest,
|
|
body: S["Type"]
|
|
): Effect.Effect<HttpClientRequest, HttpBody.HttpBodyError, S["EncodingServices"]>
|
|
} => {
|
|
const encode = HttpBody.jsonSchema(schema, options)
|
|
return dual(
|
|
2,
|
|
(
|
|
self: HttpClientRequest,
|
|
body: unknown
|
|
): Effect.Effect<HttpClientRequest, HttpBody.HttpBodyError, S["EncodingServices"]> =>
|
|
Effect.map(encode(body), (body) => setBody(self, body))
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Sets an `application/x-www-form-urlencoded` request body from URL parameter input.
|
|
*
|
|
* @category combinators
|
|
* @since 4.0.0
|
|
*/
|
|
export const bodyUrlParams: {
|
|
(input: UrlParams.Input): (self: HttpClientRequest) => HttpClientRequest
|
|
(self: HttpClientRequest, input: UrlParams.Input): HttpClientRequest
|
|
} = dual(
|
|
2,
|
|
(self: HttpClientRequest, input: UrlParams.Input): HttpClientRequest =>
|
|
setBody(self, HttpBody.urlParams(UrlParams.fromInput(input)))
|
|
)
|
|
|
|
/**
|
|
* Sets a `FormData` request body.
|
|
*
|
|
* @category combinators
|
|
* @since 4.0.0
|
|
*/
|
|
export const bodyFormData: {
|
|
(body: FormData): (self: HttpClientRequest) => HttpClientRequest
|
|
(self: HttpClientRequest, body: FormData): HttpClientRequest
|
|
} = dual(2, (self: HttpClientRequest, body: FormData): HttpClientRequest => setBody(self, HttpBody.formData(body)))
|
|
|
|
/**
|
|
* Creates a `FormData` request body from record-style entries and sets it on the request.
|
|
*
|
|
* @category combinators
|
|
* @since 4.0.0
|
|
*/
|
|
export const bodyFormDataRecord: {
|
|
(entries: HttpBody.FormDataInput): (self: HttpClientRequest) => HttpClientRequest
|
|
(self: HttpClientRequest, entries: HttpBody.FormDataInput): HttpClientRequest
|
|
} = dual(
|
|
2,
|
|
(self: HttpClientRequest, entries: HttpBody.FormDataInput): HttpClientRequest =>
|
|
setBody(self, HttpBody.formDataRecord(entries))
|
|
)
|
|
|
|
/**
|
|
* Sets a streaming `Uint8Array` request body with optional content type and content length metadata.
|
|
*
|
|
* @category combinators
|
|
* @since 4.0.0
|
|
*/
|
|
export const bodyStream: {
|
|
(
|
|
body: Stream.Stream<Uint8Array, unknown>,
|
|
options?: { readonly contentType?: string | undefined; readonly contentLength?: number | undefined } | undefined
|
|
): (self: HttpClientRequest) => HttpClientRequest
|
|
(
|
|
self: HttpClientRequest,
|
|
body: Stream.Stream<Uint8Array, unknown>,
|
|
options?: { readonly contentType?: string | undefined; readonly contentLength?: number | undefined } | undefined
|
|
): HttpClientRequest
|
|
} = dual(
|
|
(args) => isHttpClientRequest(args[0]),
|
|
(
|
|
self: HttpClientRequest,
|
|
body: Stream.Stream<Uint8Array, unknown>,
|
|
options?: { readonly contentType?: string | undefined; readonly contentLength?: number | undefined } | undefined
|
|
): HttpClientRequest =>
|
|
setBody(
|
|
self,
|
|
HttpBody.stream(body, options?.contentType, options?.contentLength)
|
|
)
|
|
)
|
|
|
|
/**
|
|
* Creates a file-backed request body from a filesystem path and sets it on the request.
|
|
*
|
|
* @category combinators
|
|
* @since 4.0.0
|
|
*/
|
|
export const bodyFile: {
|
|
(
|
|
path: string,
|
|
options?: {
|
|
readonly bytesToRead?: FileSystem.SizeInput | undefined
|
|
readonly chunkSize?: FileSystem.SizeInput | undefined
|
|
readonly offset?: FileSystem.SizeInput | undefined
|
|
readonly contentType?: string
|
|
}
|
|
): (self: HttpClientRequest) => Effect.Effect<HttpClientRequest, PlatformError.PlatformError, FileSystem.FileSystem>
|
|
(
|
|
self: HttpClientRequest,
|
|
path: string,
|
|
options?: {
|
|
readonly bytesToRead?: FileSystem.SizeInput | undefined
|
|
readonly chunkSize?: FileSystem.SizeInput | undefined
|
|
readonly offset?: FileSystem.SizeInput | undefined
|
|
readonly contentType?: string
|
|
}
|
|
): Effect.Effect<HttpClientRequest, PlatformError.PlatformError, FileSystem.FileSystem>
|
|
} = dual(
|
|
(args) => isHttpClientRequest(args[0]),
|
|
(
|
|
self: HttpClientRequest,
|
|
path: string,
|
|
options?: {
|
|
readonly bytesToRead?: FileSystem.SizeInput | undefined
|
|
readonly chunkSize?: FileSystem.SizeInput | undefined
|
|
readonly offset?: FileSystem.SizeInput | undefined
|
|
readonly contentType?: string
|
|
}
|
|
): Effect.Effect<HttpClientRequest, PlatformError.PlatformError, FileSystem.FileSystem> =>
|
|
Effect.map(
|
|
HttpBody.file(path, options),
|
|
(body) => setBody(self, body)
|
|
)
|
|
)
|
|
|
|
/**
|
|
* Builds a `URL` from the request URL, query parameters, and hash, returning `Option.none()` if the URL is invalid.
|
|
*
|
|
* @category combinators
|
|
* @since 4.0.0
|
|
*/
|
|
export function toUrl(self: HttpClientRequest): Option.Option<URL> {
|
|
const r = Url.make(self.url, self.urlParams, Option.getOrUndefined(self.hash))
|
|
if (Result.isSuccess(r)) {
|
|
return Option.some(r.success)
|
|
}
|
|
return Option.none()
|
|
}
|
|
|
|
/**
|
|
* Converts a Web `Request` into an `HttpClientRequest`, preserving method, URL, headers, and supported request bodies.
|
|
*
|
|
* @category converting
|
|
* @since 4.0.0
|
|
*/
|
|
export const fromWeb = (request: globalThis.Request): HttpClientRequest => {
|
|
const method = request.method.toUpperCase() as HttpMethod
|
|
return modify(empty, {
|
|
method,
|
|
url: new URL(request.url),
|
|
headers: request.headers,
|
|
body: fromWebBody(request, method)
|
|
})
|
|
}
|
|
|
|
const fromWebBody = (request: globalThis.Request, method: HttpMethod): HttpBody.HttpBody => {
|
|
if (!hasBody(method) || request.body === null) {
|
|
return HttpBody.empty
|
|
}
|
|
return HttpBody.raw(request.body, {
|
|
contentType: request.headers.get("content-type") ?? undefined,
|
|
contentLength: parseContentLength(request.headers.get("content-length"))
|
|
})
|
|
}
|
|
|
|
const parseContentLength = (contentLength: string | null): number | undefined => {
|
|
if (contentLength === null) {
|
|
return undefined
|
|
}
|
|
const parsed = Number.parseInt(contentLength, 10)
|
|
return Number.isNaN(parsed) ? undefined : parsed
|
|
}
|
|
|
|
/**
|
|
* Converts an `HttpClientRequest` safely to a Web `Request` as a `Result`, failing when the request URL is invalid.
|
|
*
|
|
* @category converting
|
|
* @since 4.0.0
|
|
*/
|
|
export const toWebResult = (self: HttpClientRequest, options?: {
|
|
readonly signal?: AbortSignal | undefined
|
|
readonly context?: Context.Context<never> | undefined
|
|
}): Result.Result<Request, Url.UrlError> => {
|
|
const url = Url.make(self.url, self.urlParams, Option.getOrUndefined(self.hash))
|
|
if (Result.isFailure(url)) {
|
|
return Result.fail(url.failure)
|
|
}
|
|
const requestInit: RequestInit = {
|
|
method: self.method,
|
|
headers: self.headers
|
|
}
|
|
if (options?.signal) {
|
|
requestInit.signal = options.signal
|
|
}
|
|
if (hasBody(self.method)) {
|
|
switch (self.body._tag) {
|
|
case "Empty": {
|
|
break
|
|
}
|
|
case "Raw": {
|
|
requestInit.body = self.body.body as any
|
|
if (isReadableStream(self.body.body)) {
|
|
;(requestInit as any).duplex = "half"
|
|
}
|
|
break
|
|
}
|
|
case "Uint8Array": {
|
|
requestInit.body = self.body.body as any
|
|
break
|
|
}
|
|
case "FormData": {
|
|
requestInit.body = self.body.formData
|
|
break
|
|
}
|
|
case "Stream": {
|
|
requestInit.body = Stream.toReadableStreamWith(self.body.stream, options?.context ?? Context.empty())
|
|
;(requestInit as any).duplex = "half"
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return Result.try({
|
|
try: () => new Request(url.success, requestInit),
|
|
catch: (cause) => new Url.UrlError({ cause })
|
|
})
|
|
}
|
|
|
|
const isReadableStream = (u: unknown): u is ReadableStream<Uint8Array> =>
|
|
typeof ReadableStream !== "undefined" && u instanceof ReadableStream
|
|
|
|
/**
|
|
* Converts an `HttpClientRequest` to a Web `Request`, failing with `UrlError` when the request URL is invalid.
|
|
*
|
|
* @category converting
|
|
* @since 4.0.0
|
|
*/
|
|
export const toWeb = (self: HttpClientRequest, options?: {
|
|
readonly signal?: AbortSignal | undefined
|
|
}): Effect.Effect<Request, Url.UrlError> =>
|
|
Effect.contextWith((context) =>
|
|
Effect.fromResult(toWebResult(self, {
|
|
context: context,
|
|
signal: options?.signal
|
|
}))
|
|
)
|