EdgeAIG's picture
download
raw
22.9 kB
import * as Channel from "../../Channel.js";
import * as Context from "../../Context.js";
import * as Effect from "../../Effect.js";
import * as Inspectable from "../../Inspectable.js";
import * as Option from "../../Option.js";
import * as Result from "../../Result.js";
import * as Schema from "../../Schema.js";
import * as Stream from "../../Stream.js";
import * as Socket from "../socket/Socket.js";
import * as Cookies from "./Cookies.js";
import * as Headers from "./Headers.js";
import * as HttpBody from "./HttpBody.js";
import * as HttpClientRequest from "./HttpClientRequest.js";
import * as HttpIncomingMessage from "./HttpIncomingMessage.js";
import { hasBody } from "./HttpMethod.js";
import { HttpServerError, RequestParseError } from "./HttpServerError.js";
import * as Multipart from "./Multipart.js";
import * as UrlParams from "./UrlParams.js";
export {
/**
* Provides the `MaxBodySize` fiber reference for configuring request body limits.
*
* **When to use**
*
* Use to configure the maximum body size accepted while reading server
* request bodies.
*
* @category fiber refs
* @since 4.0.0
*/
MaxBodySize } from "./HttpIncomingMessage.js";
/**
* Runtime type identifier for `HttpServerRequest` values.
*
* @category type IDs
* @since 4.0.0
*/
export const TypeId = "~effect/http/HttpServerRequest";
/**
* Service tag for the active server-side HTTP request.
*
* **When to use**
*
* Use to access the request currently being handled by HTTP server routes and
* middleware.
*
* @category context
* @since 4.0.0
*/
export const HttpServerRequest = /*#__PURE__*/Context.Service("effect/http/HttpServerRequest");
/**
* Service that contains decoded URL query parameters for the current request.
*
* **When to use**
*
* Use to access query parameters that have already been parsed for the current
* server request.
*
* **Details**
*
* Each key maps to a string value, or to an array when the parameter appears more
* than once.
*
* @category search params
* @since 4.0.0
*/
export class ParsedSearchParams extends /*#__PURE__*/Context.Service()("effect/http/ParsedSearchParams") {}
/**
* Converts a `URL` object's search parameters into a record.
*
* **Details**
*
* Repeated parameters are represented as arrays in insertion order.
*
* @category search params
* @since 4.0.0
*/
export const searchParamsFromURL = url => {
const out = {};
for (const [key, value] of url.searchParams.entries()) {
const entry = out[key];
if (entry !== undefined) {
if (Array.isArray(entry)) {
entry.push(value);
} else {
out[key] = [entry, value];
}
} else {
out[key] = value;
}
}
return out;
};
/**
* Creates a channel backed by the current request's upgraded socket.
*
* **Details**
*
* The channel reads incoming socket messages and writes byte chunks to the
* socket, failing if the request cannot be upgraded or the socket fails.
*
* @category accessors
* @since 4.0.0
*/
export const upgradeChannel = () => HttpServerRequest.pipe(Effect.flatMap(_ => _.upgrade), Effect.map(Socket.toChannelWith()), Channel.unwrap);
/**
* Decodes a schema from the cookies of the current request.
*
* @category schemas
* @since 4.0.0
*/
export const schemaCookies = (schema, options) => {
const parse = Schema.decodeUnknownEffect(schema);
return Effect.flatMap(HttpServerRequest, req => parse(req.cookies, options));
};
/**
* Decodes a schema from the headers of the current request.
*
* @category schemas
* @since 4.0.0
*/
export const schemaHeaders = (schema, options) => {
const parse = Schema.decodeUnknownEffect(schema);
return Effect.flatMap(HttpServerRequest, req => parse(req.headers, options));
};
/**
* Decodes a schema from the parsed search parameters of the current request.
*
* @category schemas
* @since 4.0.0
*/
export const schemaSearchParams = (schema, options) => {
const parse = Schema.decodeUnknownEffect(schema);
return Effect.flatMap(ParsedSearchParams, params => parse(params, options));
};
/**
* Reads the current request body as JSON and decodes it with the supplied schema.
*
* **Details**
*
* The effect can fail if the body cannot be read or parsed, or if schema decoding
* fails.
*
* @category schemas
* @since 4.0.0
*/
export const schemaBodyJson = (schema, options) => {
const parse = HttpIncomingMessage.schemaBodyJson(schema, options);
return Effect.flatMap(HttpServerRequest, parse);
};
const isMultipart = request => request.headers["content-type"]?.toLowerCase().includes("multipart/form-data") === true || getFormDataBody(request) !== undefined;
/**
* Decodes the current request body as form data.
*
* **Details**
*
* Multipart requests are persisted and decoded as multipart data; other form
* requests are decoded from URL-encoded body parameters.
*
* @category schemas
* @since 4.0.0
*/
export const schemaBodyForm = (schema, options) => {
const parseMultipart = Multipart.schemaPersisted(schema);
const parseUrlParams = HttpIncomingMessage.schemaBodyUrlParams(schema, options);
return Effect.flatMap(HttpServerRequest, request => {
if (isMultipart(request)) {
return Effect.flatMap(request.multipart, _ => parseMultipart(_, options));
}
return parseUrlParams(request);
});
};
/**
* Reads the current request body as URL-encoded parameters and decodes them with
* the supplied schema.
*
* @category schemas
* @since 4.0.0
*/
export const schemaBodyUrlParams = (schema, options) => {
const parse = HttpIncomingMessage.schemaBodyUrlParams(schema, options);
return Effect.flatMap(HttpServerRequest, parse);
};
/**
* Persists the current multipart request body and decodes it with the supplied
* schema.
*
* **Details**
*
* The effect requires the services needed to persist multipart files, including a
* scope, file system, and path service.
*
* @category schemas
* @since 4.0.0
*/
export const schemaBodyMultipart = (schema, options) => {
const parse = Multipart.schemaPersisted(schema);
return HttpServerRequest.pipe(Effect.flatMap(_ => _.multipart), Effect.flatMap(_ => parse(_, options)));
};
/**
* Creates a decoder for a JSON value stored in a form field.
*
* **Details**
*
* For multipart requests, the named multipart field is decoded as JSON. For
* URL-encoded requests, the named parameter is decoded as JSON and then decoded
* with the supplied schema.
*
* @category schemas
* @since 4.0.0
*/
export const schemaBodyFormJson = (schema, options) => {
const parseMultipart = Multipart.schemaJson(schema, options);
return field => {
const parseUrlParams = UrlParams.schemaJsonField(field).pipe(Schema.decodeTo(schema), Schema.decodeEffect);
return Effect.flatMap(HttpServerRequest, request => {
if (isMultipart(request)) {
return Effect.flatMap(Effect.mapError(request.multipart, cause => new HttpServerError({
reason: new RequestParseError({
request,
cause
})
})), parseMultipart(field));
}
return Effect.flatMap(request.urlParamsBody, _ => parseUrlParams(_, options));
});
};
};
/**
* Creates an `HttpServerRequest` view of an `HttpClientRequest`.
*
* **Details**
*
* If the client request can be converted to an absolute URL, that URL is used as
* the original URL.
*
* @category converting
* @since 4.0.0
*/
export const fromClientRequest = request => {
const url = Option.match(HttpClientRequest.toUrl(request), {
onNone: () => request.url,
onSome: url => url.toString()
});
return new ClientRequestImpl(request, url);
};
/**
* Wraps a Web `Request` as an `HttpServerRequest`.
*
* **Details**
*
* The request's current URL is stored without the scheme and host, while the
* original Web URL remains available as `originalUrl`.
*
* @category converting
* @since 4.0.0
*/
export const fromWeb = request => new ServerRequestImpl(request, removeHost(request.url));
/**
* Converts an `HttpServerRequest` into an `HttpClientRequest`.
*
* **Details**
*
* The converted request preserves the method, headers, body stream, and a URL
* derived from the request when possible.
*
* @category converting
* @since 4.0.0
*/
export const toClientRequest = request => HttpClientRequest.setUrl(HttpClientRequest.makeWith(request.method, "", UrlParams.empty, Option.none(), request.headers, toClientBody(request)), Option.getOrElse(toURL(request), () => request.url));
const toClientBody = request => hasBody(request.method) ? HttpBody.stream(request.stream, request.headers["content-type"], parseContentLength(request.headers["content-length"])) : HttpBody.empty;
const parseContentLength = contentLength => {
if (contentLength === undefined) {
return undefined;
}
const parsed = Number.parseInt(contentLength, 10);
return Number.isNaN(parsed) ? undefined : parsed;
};
const removeHost = url => {
if (url[0] === "/") {
return url;
}
const index = url.indexOf("/", url.indexOf("//") + 2);
return index === -1 ? "/" : url.slice(index);
};
class ServerRequestImpl extends Inspectable.Class {
[TypeId];
[HttpIncomingMessage.TypeId];
source;
url;
headersOverride;
remoteAddressOverride;
constructor(source, url, headersOverride, remoteAddressOverride) {
super();
this[TypeId] = TypeId;
this[HttpIncomingMessage.TypeId] = HttpIncomingMessage.TypeId;
this.source = source;
this.url = url;
this.headersOverride = headersOverride;
this.remoteAddressOverride = remoteAddressOverride;
}
toJSON() {
return HttpIncomingMessage.inspect(this, {
_id: "HttpServerRequest",
method: this.method,
url: this.originalUrl
});
}
modify(options) {
return new ServerRequestImpl(this.source, options.url ?? this.url, options.headers ?? this.headersOverride, "remoteAddress" in options ? options.remoteAddress : this.remoteAddressOverride);
}
get method() {
return this.source.method.toUpperCase();
}
get originalUrl() {
return this.source.url;
}
get remoteAddress() {
return this.remoteAddressOverride ?? Option.none();
}
get headers() {
this.headersOverride ??= Headers.fromInput(this.source.headers);
return this.headersOverride;
}
cachedCookies;
get cookies() {
if (this.cachedCookies) {
return this.cachedCookies;
}
return this.cachedCookies = Cookies.parseHeader(this.headers.cookie ?? "");
}
get stream() {
return this.source.body ? Stream.fromReadableStream({
evaluate: () => this.source.body,
onError: cause => new HttpServerError({
reason: new RequestParseError({
request: this,
cause
})
})
}) : Stream.fail(new HttpServerError({
reason: new RequestParseError({
request: this,
description: "can not create stream from empty body"
})
}));
}
textEffect;
get text() {
if (this.textEffect) {
return this.textEffect;
}
this.textEffect = Effect.runSync(Effect.cached(Effect.tryPromise({
try: () => this.source.text(),
catch: cause => new HttpServerError({
reason: new RequestParseError({
request: this,
cause
})
})
})));
return this.textEffect;
}
get json() {
return Effect.flatMap(this.text, text => Effect.try({
try: () => JSON.parse(text),
catch: cause => new HttpServerError({
reason: new RequestParseError({
request: this,
cause
})
})
}));
}
get urlParamsBody() {
return Effect.flatMap(this.text, _ => Effect.try({
try: () => UrlParams.fromInput(new URLSearchParams(_)),
catch: cause => new HttpServerError({
reason: new RequestParseError({
request: this,
cause
})
})
}));
}
multipartEffect;
get multipart() {
if (this.multipartEffect) {
return this.multipartEffect;
}
this.multipartEffect = Effect.runSync(Effect.cached(Multipart.toPersisted(this.multipartStream)));
return this.multipartEffect;
}
get multipartStream() {
return Stream.pipeThroughChannel(Stream.mapError(this.stream, cause => Multipart.MultipartError.fromReason("InternalError", cause)), Multipart.makeChannel(this.headers));
}
arrayBufferEffect;
get arrayBuffer() {
if (this.arrayBufferEffect) {
return this.arrayBufferEffect;
}
this.arrayBufferEffect = Effect.runSync(Effect.cached(Effect.tryPromise({
try: () => this.source.arrayBuffer(),
catch: cause => new HttpServerError({
reason: new RequestParseError({
request: this,
cause
})
})
})));
return this.arrayBufferEffect;
}
get upgrade() {
return Effect.fail(new HttpServerError({
reason: new RequestParseError({
request: this,
description: "Not an upgradeable ServerRequest"
})
}));
}
}
class ClientRequestImpl extends Inspectable.Class {
[TypeId];
[HttpIncomingMessage.TypeId];
source;
originalUrl;
headersOverride;
remoteAddressOverride;
urlOverride;
constructor(source, originalUrl, urlOverride, headersOverride, remoteAddressOverride) {
super();
this[TypeId] = TypeId;
this[HttpIncomingMessage.TypeId] = HttpIncomingMessage.TypeId;
this.source = source;
this.originalUrl = originalUrl;
this.urlOverride = urlOverride;
this.headersOverride = headersOverride;
this.remoteAddressOverride = remoteAddressOverride;
}
toJSON() {
return HttpIncomingMessage.inspect(this, {
_id: "HttpServerRequest",
method: this.method,
url: this.originalUrl
});
}
modify(options) {
return new ClientRequestImpl(this.source, this.originalUrl, options.url ?? this.url, options.headers ?? this.headersOverride, "remoteAddress" in options ? options.remoteAddress : this.remoteAddressOverride);
}
get method() {
return this.source.method;
}
get url() {
return this.urlOverride ?? removeHost(this.originalUrl);
}
get remoteAddress() {
return this.remoteAddressOverride ?? Option.none();
}
get headers() {
return this.headersOverride ??= this.source.headers;
}
cachedCookies;
get cookies() {
if (this.cachedCookies) {
return this.cachedCookies;
}
return this.cachedCookies = Cookies.parseHeader(this.headers.cookie ?? "");
}
get stream() {
const body = this.source.body;
switch (body._tag) {
case "Empty":
{
return Stream.empty;
}
case "Uint8Array":
{
return Stream.succeed(body.body);
}
case "Stream":
{
return Stream.mapError(body.stream, cause => requestParseError(this, undefined, cause));
}
case "FormData":
{
return streamFromReadable(this, new Response(body.formData).body);
}
case "Raw":
{
return rawBodyStream(this, body.body);
}
}
}
bytesEffect;
get bytes() {
if (this.bytesEffect) {
return this.bytesEffect;
}
const body = this.source.body;
let effect;
switch (body._tag) {
case "Empty":
{
effect = Effect.succeed(new Uint8Array(0));
break;
}
case "Uint8Array":
{
effect = Effect.succeed(body.body);
break;
}
case "FormData":
{
effect = bytesFromBodyInit(this, body.formData);
break;
}
case "Stream":
{
effect = Stream.mkUint8Array(this.stream);
break;
}
case "Raw":
{
effect = rawBodyBytes(this, body.body);
break;
}
}
this.bytesEffect = Effect.runSync(Effect.cached(effect));
return this.bytesEffect;
}
get text() {
return Effect.map(this.bytes, bytes => textDecoder.decode(bytes));
}
get json() {
return Effect.flatMap(this.text, text => Effect.try({
try: () => text === "" ? null : JSON.parse(text),
catch: cause => requestParseError(this, undefined, cause)
}));
}
get urlParamsBody() {
return Effect.flatMap(this.text, _ => Effect.try({
try: () => UrlParams.fromInput(new URLSearchParams(_)),
catch: cause => requestParseError(this, undefined, cause)
}));
}
multipartEffect;
get multipart() {
if (this.multipartEffect) {
return this.multipartEffect;
}
this.multipartEffect = Effect.runSync(Effect.cached(Multipart.toPersisted(this.multipartStream)));
return this.multipartEffect;
}
get multipartStream() {
const formData = this.source.body._tag === "FormData" && this.source.body.formData;
if (formData) {
return Stream.fromIterable(formDataToParts(formData));
}
return Stream.pipeThroughChannel(Stream.mapError(this.stream, cause => Multipart.MultipartError.fromReason("InternalError", cause)), Multipart.makeChannel(this.headers));
}
get arrayBuffer() {
return Effect.map(this.bytes, bytes => bytes.slice().buffer);
}
get upgrade() {
return Effect.fail(requestParseError(this, "Not an upgradeable ServerRequest"));
}
}
const getFormDataBody = request => {
if (!HttpClientRequest.isHttpClientRequest(request.source)) {
return undefined;
}
const body = request.source.body;
if (body._tag === "FormData") {
return body.formData;
}
if (body._tag === "Raw" && isFormData(body.body)) {
return body.body;
}
return undefined;
};
const rawBodyStream = (request, body) => {
if (body instanceof Request) {
return streamFromReadable(request, body.body);
}
if (isFormData(body)) {
return streamFromReadable(request, new Response(body).body);
}
if (isReadableStream(body)) {
return streamFromReadable(request, body);
}
return Stream.fail(requestParseError(request, "Unsupported body type"));
};
const rawBodyBytes = (request, body) => {
if (body instanceof Blob) {
return bytesFromBodyInit(request, body);
}
if (body instanceof Request) {
return Effect.tryPromise({
try: () => body.arrayBuffer().then(buffer => new Uint8Array(buffer)),
catch: cause => requestParseError(request, undefined, cause)
});
}
return Effect.fail(requestParseError(request, "Unsupported body type"));
};
const bytesFromBodyInit = (request, body) => Effect.tryPromise({
try: () => new Response(body).arrayBuffer().then(buffer => new Uint8Array(buffer)),
catch: cause => requestParseError(request, undefined, cause)
});
const streamFromReadable = (request, body) => body ? Stream.fromReadableStream({
evaluate: () => body,
onError: cause => requestParseError(request, undefined, cause)
}) : Stream.empty;
const requestParseError = (request, description, cause) => new HttpServerError({
reason: new RequestParseError({
request,
...(description === undefined ? undefined : {
description
}),
...(cause === undefined ? undefined : {
cause
})
})
});
const formDataToParts = formData => {
const parts = [];
for (const [key, value] of formData.entries()) {
parts.push(typeof value === "string" ? new MultipartFieldPart(key, value) : new MultipartFilePart(key, value));
}
return parts;
};
class MultipartFieldPart extends Inspectable.Class {
[Multipart.TypeId];
_tag = "Field";
contentType = "text/plain";
key;
value;
constructor(key, value) {
super();
this[Multipart.TypeId] = Multipart.TypeId;
this.key = key;
this.value = value;
}
toJSON() {
return {
_id: "@effect/platform/Multipart/Part",
_tag: "Field",
key: this.key,
contentType: this.contentType,
value: this.value
};
}
}
class MultipartFilePart extends Inspectable.Class {
[Multipart.TypeId];
_tag = "File";
key;
name;
contentType;
content;
contentEffect;
constructor(key, file) {
super();
this[Multipart.TypeId] = Multipart.TypeId;
this.key = key;
this.name = file.name;
this.contentType = file.type;
this.content = Stream.fromReadableStream({
evaluate: () => file.stream(),
onError: cause => Multipart.MultipartError.fromReason("InternalError", cause)
});
this.contentEffect = Effect.tryPromise({
try: () => file.arrayBuffer().then(buffer => new Uint8Array(buffer)),
catch: cause => Multipart.MultipartError.fromReason("InternalError", cause)
});
}
toJSON() {
return {
_id: "@effect/platform/Multipart/Part",
_tag: "File",
key: this.key,
name: this.name,
contentType: this.contentType
};
}
}
const isReadableStream = u => typeof ReadableStream !== "undefined" && u instanceof ReadableStream;
const isFormData = u => typeof FormData !== "undefined" && u instanceof FormData;
const textDecoder = /*#__PURE__*/new TextDecoder();
/**
* Attempts to construct an absolute `URL` for a server request safely.
*
* **Details**
*
* The host comes from the `host` header, defaulting to `localhost`, and the
* protocol is `https` only when `x-forwarded-proto` is `https`; invalid URLs
* return `Option.none`.
*
* @category converting
* @since 4.0.0
*/
export const toURL = self => {
const host = self.headers.host ?? "localhost";
const protocol = self.headers["x-forwarded-proto"] === "https" ? "https" : "http";
try {
return Option.some(new URL(self.url, `${protocol}://${host}`));
} catch {
return Option.none();
}
};
/**
* Converts an `HttpServerRequest` safely to a Web `Request` as a `Result`.
*
* **Details**
*
* If the source is already a Web `Request`, it is returned unchanged. Otherwise
* an absolute URL is derived from the request; invalid URLs fail with a
* `RequestParseError`.
*
* @category converting
* @since 4.0.0
*/
export const toWebResult = (self, options) => {
if (self.source instanceof Request) {
return Result.succeed(self.source);
}
const url = toURL(self);
if (Option.isNone(url)) {
return Result.fail(new RequestParseError({
request: self,
description: "Invalid URL"
}));
}
const requestInit = {
method: self.method,
headers: self.headers
};
if (options?.signal) {
requestInit.signal = options.signal;
}
if (hasBody(self.method)) {
requestInit.body = Stream.toReadableStreamWith(self.stream, options?.context ?? Context.empty());
requestInit.duplex = "half";
}
return Result.succeed(new Request(url.value, requestInit));
};
/**
* Converts an `HttpServerRequest` to a Web `Request` in `Effect`.
*
* **Details**
*
* The current context is used when streaming the request body into the Web
* request.
*
* @category converting
* @since 4.0.0
*/
export const toWeb = (self, options) => Effect.contextWith(context => Effect.fromResult(toWebResult(self, {
context,
signal: options?.signal
})));
//# sourceMappingURL=HttpServerRequest.js.map

Xet Storage Details

Size:
22.9 kB
·
Xet hash:
7ac4124f8813f0785d40a71dcf869c27329df96ef0c3e8e392fee73964e08342

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