EdgeAIG's picture
download
raw
26.5 kB
/**
* Describes immutable responses returned by Effect HTTP handlers.
*
* An `HttpServerResponse` stores the status, optional status text, headers,
* cookies, and body that the server runtime later turns into a platform
* response such as a Web `Response`. This module includes constructors for
* common response bodies, helpers for updating response data, file response
* support through `HttpPlatform`, and conversions to or from Web and Effect
* HTTP client responses.
*
* @since 4.0.0
*/
import * as Context from "../../Context.js";
import * as Effect from "../../Effect.js";
import * as ErrorReporter from "../../ErrorReporter.js";
import { dual } from "../../Function.js";
import * as Inspectable from "../../Inspectable.js";
import { PipeInspectableProto } from "../../internal/core.js";
import * as Option from "../../Option.js";
import { pipeArguments } from "../../Pipeable.js";
import { hasProperty } from "../../Predicate.js";
import { redact } from "../../Redactable.js";
import * as Stream from "../../Stream.js";
import * as Cookies from "./Cookies.js";
import * as Headers from "./Headers.js";
import * as Body from "./HttpBody.js";
import * as HttpClientError from "./HttpClientError.js";
import * as HttpClientRequest from "./HttpClientRequest.js";
import * as HttpClientResponse from "./HttpClientResponse.js";
import * as HttpIncomingMessage from "./HttpIncomingMessage.js";
import * as Template from "./Template.js";
import * as UrlParams from "./UrlParams.js";
const TypeId = "~effect/http/HttpServerResponse";
/**
* Returns `true` when the supplied value is an `HttpServerResponse`.
*
* @category guards
* @since 4.0.0
*/
export const isHttpServerResponse = u => hasProperty(u, TypeId);
/**
* Creates an empty HTTP response.
*
* **Details**
*
* The default status is `204`.
*
* @category constructors
* @since 4.0.0
*/
export const empty = options => makeResponse({
status: options?.status ?? 204,
statusText: options?.statusText,
headers: options?.headers ? Headers.fromInput(options.headers) : undefined,
cookies: options?.cookies
});
/**
* Creates a redirect response with a `Location` header.
*
* **Details**
*
* The default status is `302`; custom headers are merged with the generated
* `Location` header.
*
* @category constructors
* @since 4.0.0
*/
export const redirect = (location, options) => {
const headers = Headers.fromRecordUnsafe({
location: location.toString()
});
return makeResponse({
status: options?.status ?? 302,
statusText: options?.statusText,
headers: options?.headers ? Headers.merge(headers, Headers.fromInput(options.headers)) : headers,
cookies: options?.cookies ?? Cookies.empty
});
};
/**
* Creates an HTTP response whose body is a `Uint8Array`.
*
* @category constructors
* @since 4.0.0
*/
export const uint8Array = (body, options) => {
const headers = options?.headers ? Headers.fromInput(options.headers) : Headers.empty;
return makeResponse({
status: options?.status ?? 200,
statusText: options?.statusText,
headers,
cookies: options?.cookies ?? Cookies.empty,
body: Body.uint8Array(body, getContentType(options, headers))
});
};
const getContentType = (options, headers) => {
if (options?.contentType) {
return options.contentType;
} else if (options?.headers) {
return headers["content-type"];
}
};
/**
* Creates an HTTP response whose body is a string.
*
* @category constructors
* @since 4.0.0
*/
export const text = (body, options) => {
const headers = options?.headers ? Headers.fromInput(options.headers) : Headers.empty;
return makeResponse({
status: options?.status ?? 200,
statusText: options?.statusText,
headers,
cookies: options?.cookies ?? Cookies.empty,
body: Body.text(body, getContentType(options, headers))
});
};
/**
* Creates an HTML response with the `text/html` content type.
*
* **Details**
*
* Passing a string returns a response directly. Using it as a template tag returns
* an effect so interpolated values can be rendered with their required services
* and errors.
*
* @category constructors
* @since 4.0.0
*/
export const html = (strings, ...args) => {
if (typeof strings === "string") {
return text(strings, {
contentType: "text/html"
});
}
return Effect.map(Template.make(strings, ...args), _ => text(_, {
contentType: "text/html"
}));
};
/**
* Creates a streaming HTML response from a template.
*
* **Details**
*
* The template is encoded as a byte stream and can use streaming interpolated
* values from the current context.
*
* @category constructors
* @since 4.0.0
*/
export const htmlStream = (strings, ...args) => Effect.map(Effect.context(), context => stream(Stream.provideContext(Stream.encodeText(Template.stream(strings, ...args)), context), {
contentType: "text/html"
}));
/**
* Creates a JSON HTTP response.
*
* **Details**
*
* The body is serialized with `JSON.stringify`; serialization errors are captured
* as `HttpBodyError` failures.
*
* @category constructors
* @since 4.0.0
*/
export const json = (body, options) => {
const headers = options?.headers ? Headers.fromInput(options.headers) : Headers.empty;
return Effect.map(Body.json(body, getContentType(options, headers)), body => makeResponse({
status: options?.status ?? 200,
statusText: options?.statusText,
headers,
cookies: options?.cookies,
body
}));
};
/**
* Creates a JSON response constructor backed by a schema encoder.
*
* **Details**
*
* The returned function encodes the value with the supplied schema before
* serializing it as JSON, and can fail with `HttpBodyError` if schema encoding or
* JSON serialization fails.
*
* @category constructors
* @since 4.0.0
*/
export const schemaJson = (schema, options) => {
const encode = Body.jsonSchema(schema, options);
return (body, options) => {
const headers = options?.headers ? Headers.fromInput(options.headers) : Headers.empty;
return Effect.map(encode(body, getContentType(options, headers)), body => makeResponse({
status: options?.status ?? 200,
statusText: options?.statusText,
headers,
cookies: options?.cookies,
body
}));
};
};
/**
* Creates a JSON HTTP response synchronously.
*
* **When to use**
*
* Use when the response body is known to be JSON-serializable and you need a
* synchronous `HttpServerResponse`.
*
* **Gotchas**
*
* Unlike `json`, serialization errors from `JSON.stringify` are not captured in
* `Effect`.
*
* @category constructors
* @since 4.0.0
*/
export const jsonUnsafe = (body, options) => {
const headers = options?.headers ? Headers.fromInput(options.headers) : Headers.empty;
return makeResponse({
status: options?.status ?? 200,
statusText: options?.statusText,
headers,
cookies: options?.cookies,
body: Body.jsonUnsafe(body, getContentType(options, headers))
});
};
/**
* Creates a response from URL parameters using the
* `application/x-www-form-urlencoded` content type by default.
*
* @category constructors
* @since 4.0.0
*/
export const urlParams = (body, options) => {
const headers = options?.headers ? Headers.fromInput(options.headers) : Headers.empty;
return makeResponse({
status: options?.status ?? 200,
statusText: options?.statusText,
headers,
cookies: options?.cookies,
body: Body.text(UrlParams.toString(UrlParams.fromInput(body)), getContentType(options, headers) ?? "application/x-www-form-urlencoded")
});
};
/**
* Creates a response with a raw body value.
*
* **When to use**
*
* Use when you want to pass through a body value already understood by the
* underlying runtime, such as a Web `Response`, `Blob`, or `ReadableStream`,
* for later platform conversion.
*
* @category constructors
* @since 4.0.0
*/
export const raw = (body, options) => makeResponse({
status: options?.status ?? 200,
statusText: options?.statusText,
headers: options?.headers && Headers.fromInput(options.headers),
cookies: options?.cookies,
body: Body.raw(body, {
contentType: options?.contentType,
contentLength: options?.contentLength
})
});
/**
* Creates a response whose body is a Web `FormData` value.
*
* @category constructors
* @since 4.0.0
*/
export const formData = (body, options) => makeResponse({
status: options?.status ?? 200,
statusText: options?.statusText,
headers: options?.headers && Headers.fromInput(options.headers),
cookies: options?.cookies,
body: Body.formData(body)
});
/**
* Creates a streaming response from a stream of byte chunks.
*
* **Details**
*
* Optional response metadata can supply the status, headers, content type, and
* content length.
*
* @category constructors
* @since 4.0.0
*/
export const stream = (body, options) => {
const headers = options?.headers ? Headers.fromInput(options.headers) : Headers.empty;
return makeResponse({
status: options?.status ?? 200,
statusText: options?.statusText,
headers,
cookies: options?.cookies,
body: Body.stream(body, getContentType(options, headers), options?.contentLength)
});
};
const HttpPlatformKey = /*#__PURE__*/Context.Service("effect/http/HttpPlatform");
/**
* Creates a streamed file response for a file system path.
*
* **Details**
*
* The effect requires `HttpPlatform`, can fail with a platform error, and supports
* options for status, headers, offset, and byte range.
*
* @category constructors
* @since 4.0.0
*/
export const file = (path, options) => Effect.flatMap(HttpPlatformKey, platform => platform.fileResponse(path, options));
/**
* Creates a streamed file response for a Web `File`-like value.
*
* **Details**
*
* The effect requires `HttpPlatform` and supports options for status, headers,
* offset, and byte range.
*
* @category constructors
* @since 4.0.0
*/
export const fileWeb = (file, options) => Effect.flatMap(HttpPlatformKey, platform => platform.fileWebResponse(file, options));
/**
* Returns a response with the specified header set to the supplied value.
*
* @category combinators
* @since 4.0.0
*/
export const setHeader = /*#__PURE__*/dual(3, (self, key, value) => makeResponse({
...self,
headers: Headers.set(self.headers, key, value)
}));
/**
* Returns a response with all supplied headers set on the existing header map.
*
* @category combinators
* @since 4.0.0
*/
export const setHeaders = /*#__PURE__*/dual(2, (self, input) => makeResponse({
...self,
headers: Headers.setAll(self.headers, input)
}));
/**
* Returns a response with the cookie of the specified name removed.
*
* @category combinators
* @since 4.0.0
*/
export const removeCookie = /*#__PURE__*/dual(2, (self, name) => makeResponse({
...self,
cookies: Cookies.remove(self.cookies, name)
}));
/**
* Returns a response with its cookie collection replaced by the supplied cookies.
*
* @category combinators
* @since 4.0.0
*/
export const replaceCookies = /*#__PURE__*/dual(2, (self, cookies) => makeResponse({
...self,
cookies
}));
/**
* Sets a cookie on the response.
*
* **Details**
*
* The effect fails with `CookiesError` if the cookie name, value, or options are
* invalid.
*
* @category combinators
* @since 4.0.0
*/
export const setCookie = /*#__PURE__*/dual(args => isHttpServerResponse(args[0]), (self, name, value, options) => Effect.map(Effect.fromResult(Cookies.set(self.cookies, name, value, options)), cookies => makeResponse({
...self,
cookies
})));
/**
* Sets an expired cookie on an `HttpServerResponse`.
*
* **Details**
*
* Returns an effect because cookie encoding can fail. The original response is not
* mutated; the effect succeeds with a response containing the updated cookie set.
*
* @category combinators
* @since 4.0.0
*/
export const expireCookie = /*#__PURE__*/dual(args => isHttpServerResponse(args[0]), (self, name, options) => Effect.map(Effect.fromResult(Cookies.expireCookie(self.cookies, name, options)), cookies => makeResponse({
...self,
cookies
})));
/**
* Sets a cookie on an `HttpServerResponse`, throwing if the cookie cannot be
* encoded.
*
* **When to use**
*
* Use when you need to set one trusted cookie and want encoding failures to
* throw instead of being represented as `CookiesError` failures.
*
* @category combinators
* @since 4.0.0
*/
export const setCookieUnsafe = /*#__PURE__*/dual(args => isHttpServerResponse(args[0]), (self, name, value, options) => makeResponse({
...self,
cookies: Cookies.setUnsafe(self.cookies, name, value, options)
}));
/**
* Sets an expired cookie on an `HttpServerResponse`, throwing if the expiration cookie
* cannot be encoded.
*
* **When to use**
*
* Use when you need to expire one trusted cookie and want encoding failures to
* throw instead of being represented as `CookiesError` failures.
*
* @category combinators
* @since 4.0.0
*/
export const expireCookieUnsafe = /*#__PURE__*/dual(args => isHttpServerResponse(args[0]), (self, name, options) => makeResponse({
...self,
cookies: Cookies.expireCookieUnsafe(self.cookies, name, options)
}));
/**
* Updates the cookies attached to an `HttpServerResponse` using the supplied
* function.
*
* **Details**
*
* The original response is not mutated; a new response is returned with the
* callback result as its cookie collection.
*
* @category combinators
* @since 4.0.0
*/
export const updateCookies = /*#__PURE__*/dual(2, (self, f) => makeResponse({
...self,
cookies: f(self.cookies)
}));
/**
* Merges additional cookies into the cookies attached to an
* `HttpServerResponse`.
*
* **Details**
*
* The original response is not mutated; a new response is returned with the merged
* cookie collection.
*
* @category combinators
* @since 4.0.0
*/
export const mergeCookies = /*#__PURE__*/dual(2, (self, cookies) => makeResponse({
...self,
cookies: Cookies.merge(self.cookies, cookies)
}));
/**
* Sets multiple cookies on an `HttpServerResponse`.
*
* **Details**
*
* Each input entry contains a cookie name, value, and optional cookie options. The
* returned effect fails with `CookiesError` if any cookie cannot be encoded.
*
* @category combinators
* @since 4.0.0
*/
export const setCookies = /*#__PURE__*/dual(2, (self, cookies) => Effect.map(Effect.fromResult(Cookies.setAll(self.cookies, cookies)), cookies => makeResponse({
...self,
cookies
})));
/**
* Sets multiple cookies on an `HttpServerResponse`, throwing if any cookie cannot
* be encoded.
*
* **When to use**
*
* Use when you need to set multiple trusted cookies and want encoding failures
* to throw instead of being represented as `CookiesError` failures.
*
* @category combinators
* @since 4.0.0
*/
export const setCookiesUnsafe = /*#__PURE__*/dual(2, (self, cookies) => makeResponse({
...self,
cookies: Cookies.setAllUnsafe(self.cookies, cookies)
}));
/**
* Replaces the body of an `HttpServerResponse`.
*
* **Details**
*
* When the body carries a content type or content length, the returned response
* includes the corresponding headers.
*
* @category combinators
* @since 4.0.0
*/
export const setBody = /*#__PURE__*/dual(2, (self, body) => makeResponse({
...self,
body
}));
/**
* Sets the HTTP status code of an `HttpServerResponse`.
*
* **Details**
*
* When `statusText` is omitted, the existing status text is preserved.
*
* @category combinators
* @since 4.0.0
*/
export const setStatus = /*#__PURE__*/dual(args => isHttpServerResponse(args[0]), (self, status, statusText) => makeResponse({
...self,
status,
statusText: statusText ?? self.statusText
}));
/**
* Converts an `HttpServerResponse` to a Web `Response`.
*
* **Details**
*
* Cookies are appended as `Set-Cookie` headers. Stream bodies are converted using
* the supplied context, and `withoutBody` can be used for responses such as HEAD
* responses.
*
* @category converting
* @since 4.0.0
*/
export const toWeb = (response, options) => {
const headers = new globalThis.Headers(response.headers);
if (!Cookies.isEmpty(response.cookies)) {
const toAdd = Cookies.toSetCookieHeaders(response.cookies);
for (const header of toAdd) {
headers.append("set-cookie", header);
}
}
if (options?.withoutBody) {
return new Response(undefined, {
status: response.status,
statusText: response.statusText,
headers
});
}
const body = response.body;
switch (body._tag) {
case "Empty":
{
return new Response(undefined, {
status: response.status,
statusText: response.statusText,
headers
});
}
case "Uint8Array":
case "Raw":
{
if (body.body instanceof Response) {
for (const [key, value] of headers) {
body.body.headers.set(key, value);
}
return body.body;
}
return new Response(body.body, {
status: response.status,
statusText: response.statusText,
headers
});
}
case "FormData":
{
return new Response(body.formData, {
status: response.status,
statusText: response.statusText,
headers
});
}
case "Stream":
{
return new Response(Stream.toReadableStreamWith(body.stream, options?.context ?? Context.empty()), {
status: response.status,
statusText: response.statusText,
headers
});
}
}
};
/**
* Wraps an `HttpServerResponse` as an `HttpClientResponse`.
*
* **Details**
*
* An optional request can be supplied for client-response metadata and decode
* errors.
*
* @category converting
* @since 4.0.0
*/
export const toClientResponse = (response, options) => new ServerHttpClientResponse(options?.request ?? HttpClientRequest.empty, response);
class ServerHttpClientResponse extends Inspectable.Class {
[HttpIncomingMessage.TypeId];
[HttpClientResponse.TypeId];
request;
response;
constructor(request, response) {
super();
this.request = request;
this.response = response;
this[HttpIncomingMessage.TypeId] = HttpIncomingMessage.TypeId;
this[HttpClientResponse.TypeId] = HttpClientResponse.TypeId;
}
toJSON() {
return HttpIncomingMessage.inspect(this, {
_id: "HttpClientResponse",
request: this.request.toJSON(),
status: this.status
});
}
get status() {
return this.response.status;
}
cachedHeaders;
get headers() {
return this.cachedHeaders ??= this.response.body._tag === "FormData" ? Headers.merge(this.response.headers, Headers.fromInput(this.getFormDataResponse().headers)) : this.response.headers;
}
get cookies() {
return this.response.cookies;
}
get remoteAddress() {
return Option.none();
}
get stream() {
const body = this.response.body;
switch (body._tag) {
case "Empty":
{
return Stream.empty;
}
case "Stream":
{
return Stream.mapError(body.stream, cause => this.decodeError(cause));
}
case "Uint8Array":
{
return Stream.succeed(body.body);
}
case "Raw":
{
const rawBody = body.body;
if (rawBody instanceof Response) {
return rawBody.body ? Stream.fromReadableStream({
evaluate: () => rawBody.body,
onError: cause => this.decodeError(cause)
}) : Stream.empty;
}
if (isReadableStream(rawBody)) {
return Stream.fromReadableStream({
evaluate: () => rawBody,
onError: cause => this.decodeError(cause)
});
}
if (rawBody instanceof Blob) {
return Stream.fromReadableStream({
evaluate: () => rawBody.stream(),
onError: cause => this.decodeError(cause)
});
}
return Stream.unwrap(Effect.map(this.bytes, Stream.succeed));
}
case "FormData":
{
const response = this.getFormDataResponse();
return Stream.fromReadableStream({
evaluate: () => response.body,
onError: cause => this.decodeError(cause)
});
}
}
}
get json() {
return Effect.flatMap(this.text, text => Effect.try({
try: () => text === "" ? null : JSON.parse(text),
catch: cause => new HttpClientError.HttpClientError({
reason: new HttpClientError.DecodeError({
request: this.request,
response: this,
cause
})
})
}));
}
get bytes() {
const body = this.response.body;
switch (body._tag) {
case "Empty":
{
return Effect.succeed(new Uint8Array(0));
}
case "Uint8Array":
{
return Effect.succeed(body.body);
}
case "Stream":
{
return Stream.mkUint8Array(this.stream);
}
case "Raw":
{
const rawBody = body.body;
if (rawBody instanceof Response) {
return Effect.tryPromise({
try: () => rawBody.arrayBuffer().then(buffer => new Uint8Array(buffer)),
catch: cause => this.decodeError(cause)
});
}
return Effect.tryPromise({
try: () => new Response(rawBody).arrayBuffer().then(buffer => new Uint8Array(buffer)),
catch: cause => this.decodeError(cause)
});
}
case "FormData":
{
return Effect.tryPromise({
try: () => new Response(body.formData).arrayBuffer().then(buffer => new Uint8Array(buffer)),
catch: cause => this.decodeError(cause)
});
}
}
}
get text() {
return Effect.map(this.bytes, bytes => textDecoder.decode(bytes));
}
get urlParamsBody() {
return Effect.flatMap(this.text, _ => Effect.try({
try: () => UrlParams.fromInput(new URLSearchParams(_)),
catch: cause => new HttpClientError.HttpClientError({
reason: new HttpClientError.DecodeError({
request: this.request,
response: this,
cause
})
})
}));
}
get formData() {
const body = this.response.body;
if (body._tag === "FormData") {
return Effect.succeed(body.formData);
}
return Effect.contextWith(context => {
const readableStream = Stream.toReadableStreamWith(this.stream, context);
return Effect.tryPromise({
try: () => new Response(readableStream, {
headers: this.headers
}).formData(),
catch: cause => this.decodeError(cause)
});
});
}
get arrayBuffer() {
return Effect.map(this.bytes, bytes => bytes.slice().buffer);
}
decodeError(cause) {
return new HttpClientError.HttpClientError({
reason: new HttpClientError.DecodeError({
request: this.request,
response: this,
cause
})
});
}
formDataResponse;
getFormDataResponse() {
return this.formDataResponse ??= new Response(this.response.body.formData);
}
pipe() {
return pipeArguments(this, arguments);
}
}
const textDecoder = /*#__PURE__*/new TextDecoder();
/**
* Converts an `HttpClientResponse` to an `HttpServerResponse`.
*
* **Details**
*
* The response body is streamed from the client response. `Set-Cookie` headers are
* removed from the header map and represented in the response cookie collection.
*
* @category converting
* @since 4.0.0
*/
export const fromClientResponse = response => {
const headers = Headers.remove(response.headers, "set-cookie");
return makeResponse({
status: response.status,
headers,
cookies: response.cookies,
body: Body.stream(Stream.catchIf(response.stream, isEmptyBodyError, () => Stream.empty), Option.getOrUndefined(Headers.get(headers, "content-type")), getContentLength(headers))
});
};
const isReadableStream = u => typeof ReadableStream !== "undefined" && u instanceof ReadableStream;
const isEmptyBodyError = error => HttpClientError.isHttpClientError(error) && error.reason._tag === "EmptyBodyError";
const getContentLength = headers => {
const contentLength = Option.getOrUndefined(Headers.get(headers, "content-length"));
if (contentLength === undefined) {
return undefined;
}
const parsed = Number(contentLength);
return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
};
const Proto = {
...PipeInspectableProto,
[TypeId]: TypeId,
[ErrorReporter.ignore]: true,
toJSON() {
return {
_id: "HttpServerResponse",
status: this.status,
statusText: this.statusText,
headers: redact(this.headers),
cookies: this.cookies.toJSON(),
body: this.body.toJSON()
};
}
};
const makeResponse = options => {
const self = Object.create(Proto);
self.status = options.status;
self.statusText = options.statusText;
self.cookies = options.cookies ?? Cookies.empty;
self.body = options.body ?? Body.empty;
if (self.body._tag !== "Empty" && (self.body.contentType || self.body.contentLength)) {
const newHeaders = Headers.fromRecordUnsafe({
...options.headers
});
if (self.body.contentType) {
newHeaders["content-type"] = self.body.contentType;
}
if (self.body.contentLength) {
newHeaders["content-length"] = self.body.contentLength.toString();
}
self.headers = newHeaders;
} else {
self.headers = options.headers ?? Headers.empty;
}
return self;
};
/**
* Converts a Web `Response` to an `HttpServerResponse`.
*
* **Details**
*
* `Set-Cookie` headers are parsed into the response cookie collection and removed
* from the header map. A present Web body is exposed as a stream body.
*
* @category converting
* @since 4.0.0
*/
export const fromWeb = response => {
const headers = new globalThis.Headers(response.headers);
const setCookieHeaders = headers.getSetCookie();
headers.delete("set-cookie");
let self = empty({
status: response.status,
statusText: response.statusText,
headers: headers,
cookies: Cookies.fromSetCookie(setCookieHeaders)
});
if (response.body) {
const contentType = response.headers.get("content-type");
self = setBody(self, Body.stream(Stream.fromReadableStream({
evaluate: () => response.body,
onError: e => e
}), contentType ?? undefined));
}
return self;
};
//# sourceMappingURL=HttpServerResponse.js.map

Xet Storage Details

Size:
26.5 kB
·
Xet hash:
ff41fd65799a2efdeefa07e68929d9592282486fcad1703d9410c04c3ff338ce

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.