1/**
2 * Provides server-side access to the current incoming HTTP request.
3 *
4 * `HttpServerRequest` is the context service used by handlers, middleware,
5 * schema decoders, multipart parsers, WebSocket upgrades, and adapters. A
6 * request stores its method, URL, original URL, headers, cookies, remote
7 * address, body stream, and platform source object. This module also includes
8 * request conversions and schema decoders for cookies, headers, search
9 * parameters, JSON, forms, URL-encoded bodies, and multipart bodies.
10 *
11 * @stability unstable
12 * @since 4.0.0
13 */
14import type * as Arr from "../Array.ts"
15import * as Channel from "../Channel.ts"
16import * as Context from "../Context.ts"
17import * as Effect from "../Effect.ts"
18import type * as FileSystem from "../FileSystem.ts"
19import * as Inspectable from "../Inspectable.ts"
20import * as InternalRecord from "../internal/record.ts"
21import * as Option from "../Option.ts"
22import type * as Path from "../Path.ts"
23import type { ReadonlyRecord } from "../Record.ts"
24import * as Result from "../Result.ts"
25import * as Schema from "../Schema.ts"
26import type { ParseOptions } from "../SchemaAST.ts"
27import type * as Scope from "../Scope.ts"
28import * as Socket from "../socket/Socket.ts"
29import * as Stream from "../Stream.ts"
30import * as Cookies from "./Cookies.ts"
31import * as Headers from "./Headers.ts"
32import * as HttpBody from "./HttpBody.ts"
33import * as HttpClientRequest from "./HttpClientRequest.ts"
34import * as HttpIncomingMessage from "./HttpIncomingMessage.ts"
35import { hasBody, type HttpMethod } from "./HttpMethod.ts"
36import { HttpServerError, type RequestError, RequestParseError } from "./HttpServerError.ts"
37import * as bodyInternal from "./internal/httpBody.ts"
38import * as Multipart from "./Multipart.ts"
39import * as UrlParams from "./UrlParams.ts"
40
41/**
42 * @stability unstable
43 */
44export {
45 /**
46 * Provides the `MaxBodySize` fiber reference for configuring request body limits.
47 *
48 * **When to use**
49 *
50 * Use to configure the maximum body size accepted while reading server
51 * request bodies.
52 *
53 * @stability unstable
54 * @category references
55 * @since 4.0.0
56 */
57 MaxBodySize
58} from "./HttpIncomingMessage.ts"
59
60/**
61 * Runtime type identifier for `HttpServerRequest` values.
62 *
63 * @stability unstable
64 * @category type IDs
65 * @since 4.0.0
66 */
67export const TypeId = "~effect/http/HttpServerRequest"
68
69/**
70 * Server-side representation of an incoming HTTP request.
71 *
72 * **Details**
73 *
74 * It extends `HttpIncomingMessage` with request metadata, parsed cookies,
75 * multipart accessors, WebSocket upgrade support, and a `modify` method for
76 * creating adjusted request views.
77 *
78 * @stability unstable
79 * @category models
80 * @since 4.0.0
81 */
82export interface HttpServerRequest extends HttpIncomingMessage.HttpIncomingMessage<HttpServerError> {
83 readonly [TypeId]: typeof TypeId
84 readonly source: object
85 readonly url: string
86 readonly originalUrl: string
87 readonly method: HttpMethod
88 readonly cookies: ReadonlyRecord<string, string>
89
90 readonly multipart: Effect.Effect<
91 Multipart.Persisted,
92 Multipart.MultipartError,
93 Scope.Scope | FileSystem.FileSystem | Path.Path
94 >
95 readonly multipartStream: Stream.Stream<Multipart.Part, Multipart.MultipartError>
96
97 readonly upgrade: Effect.Effect<Socket.Socket, HttpServerError>
98
99 readonly modify: (
100 options: {
101 readonly url?: string
102 readonly headers?: Headers.Headers
103 readonly remoteAddress?: Option.Option<string>
104 }
105 ) => HttpServerRequest
106}
107
108/**
109 * Service tag for the active server-side HTTP request.
110 *
111 * **When to use**
112 *
113 * Use to access the request currently being handled by HTTP server routes and
114 * middleware.
115 *
116 * @stability unstable
117 * @category services
118 * @since 4.0.0
119 */
120export const HttpServerRequest: Context.Service<HttpServerRequest, HttpServerRequest> = Context.Service(
121 "effect/http/HttpServerRequest"
122)
123
124/**
125 * Service that contains decoded URL query parameters for the current request.
126 *
127 * **When to use**
128 *
129 * Use to access query parameters that have already been parsed for the current
130 * server request.
131 *
132 * **Details**
133 *
134 * Each key maps to a string value, or to an array when the parameter appears more
135 * than once.
136 *
137 * @stability unstable
138 * @category services
139 * @since 4.0.0
140 */
141export class ParsedSearchParams extends Context.Service<
142 ParsedSearchParams,
143 ReadonlyRecord<string, string | Array<string>>
144>()("effect/http/ParsedSearchParams") {}
145
146/**
147 * Converts a `URL` object's search parameters into a record.
148 *
149 * **Details**
150 *
151 * Repeated parameters are represented as arrays in insertion order.
152 *
153 * @stability unstable
154 * @category parsing
155 * @since 4.0.0
156 */
157export const searchParamsFromURL = (url: URL): ReadonlyRecord<string, string | Array<string>> => {
158 const out: Record<string, string | Array<string>> = {}
159 for (const [key, value] of url.searchParams.entries()) {
160 if (Object.hasOwn(out, key)) {
161 const entry = out[key]
162 if (Array.isArray(entry)) {
163 entry.push(value)
164 } else {
165 InternalRecord.assignProperty(out, key, [entry, value])
166 }
167 } else {
168 InternalRecord.assignProperty(out, key, value)
169 }
170 }
171 return out
172}
173
174/**
175 * Creates a channel backed by the current request's upgraded socket.
176 *
177 * **Details**
178 *
179 * The channel reads incoming socket messages and writes byte chunks to the
180 * socket, failing if the request cannot be upgraded or the socket fails.
181 *
182 * @stability unstable
183 * @category accessors
184 * @since 4.0.0
185 */
186export const upgradeChannel = <IE = never>(): Channel.Channel<
187 Arr.NonEmptyReadonlyArray<Uint8Array>,
188 HttpServerError | IE | Socket.SocketError,
189 void,
190 Arr.NonEmptyReadonlyArray<string | Uint8Array | Socket.CloseEvent>,
191 IE,
192 unknown,
193 HttpServerRequest
194> =>
195 HttpServerRequest.pipe(
196 Effect.flatMap((_) => _.upgrade),
197 Effect.map(Socket.toChannelWith<IE>()),
198 Channel.unwrap
199 )
200
201/**
202 * Decodes a schema from the cookies of the current request.
203 *
204 * @stability unstable
205 * @category schemas
206 * @since 4.0.0
207 */
208export const schemaCookies = <A, I extends Readonly<Record<string, string | undefined>>, RD>(
209 schema: Schema.ConstraintCodec<A, I, RD, unknown>,
210 options?: ParseOptions | undefined
211): Effect.Effect<A, Schema.SchemaError, RD | HttpServerRequest> => {
212 const parse = Schema.decodeUnknownEffect(schema)
213 return Effect.flatMap(HttpServerRequest, (req) => parse(req.cookies, options))
214}
215
216/**
217 * Decodes a schema from the headers of the current request.
218 *
219 * @stability unstable
220 * @category schemas
221 * @since 4.0.0
222 */
223export const schemaHeaders = <A, I extends Readonly<Record<string, string | undefined>>, RD>(
224 schema: Schema.ConstraintCodec<A, I, RD, unknown>,
225 options?: ParseOptions | undefined
226): Effect.Effect<A, Schema.SchemaError, HttpServerRequest | RD> => {
227 const parse = Schema.decodeUnknownEffect(schema)
228 return Effect.flatMap(HttpServerRequest, (req) => parse(req.headers, options))
229}
230
231/**
232 * Decodes a schema from the parsed search parameters of the current request.
233 *
234 * @stability unstable
235 * @category schemas
236 * @since 4.0.0
237 */
238export const schemaSearchParams = <
239 A,
240 I extends Readonly<Record<string, string | ReadonlyArray<string> | undefined>>,
241 RD
242>(
243 schema: Schema.ConstraintCodec<A, I, RD, unknown>,
244 options?: ParseOptions | undefined
245): Effect.Effect<A, Schema.SchemaError, ParsedSearchParams | RD> => {
246 const parse = Schema.decodeUnknownEffect(schema)
247 return Effect.flatMap(ParsedSearchParams, (params) => parse(params, options))
248}
249/**
250 * Reads the current request body as JSON and decodes it with the supplied schema.
251 *
252 * **Details**
253 *
254 * The effect can fail if the body cannot be read or parsed, or if schema decoding
255 * fails.
256 *
257 * @stability unstable
258 * @category schemas
259 * @since 4.0.0
260 */
261export const schemaBodyJson = <A, RD>(
262 schema: Schema.ConstraintDecoder<A, RD>,
263 options?: (ParseOptions & HttpIncomingMessage.JsonOptions) | undefined
264): Effect.Effect<A, HttpServerError | Schema.SchemaError, HttpServerRequest | RD> => {
265 const parse = HttpIncomingMessage.schemaBodyJson(schema, options)
266 return Effect.flatMap(HttpServerRequest, parse)
267}
268
269const isMultipart = (request: HttpServerRequest) =>
270 request.headers["content-type"]?.toLowerCase().includes("multipart/form-data") === true ||
271 getFormDataBody(request) !== undefined
272
273/**
274 * Decodes the current request body as form data.
275 *
276 * **Details**
277 *
278 * Multipart requests are persisted and decoded as multipart data; other form
279 * requests are decoded from URL-encoded body parameters.
280 *
281 * @stability unstable
282 * @category schemas
283 * @since 4.0.0
284 */
285export const schemaBodyForm = <A, I extends Partial<Multipart.Persisted>, RD>(
286 schema: Schema.ConstraintCodec<A, I, RD, unknown>,
287 options?: ParseOptions | undefined
288) => {
289 const parseMultipart = Multipart.schemaPersisted(schema)
290 const parseUrlParams = HttpIncomingMessage.schemaBodyUrlParams(
291 schema as Schema.ConstraintCodec<A, any, RD, unknown>,
292 options
293 )
294 return Effect.flatMap(HttpServerRequest, (request): Effect.Effect<
295 A,
296 Multipart.MultipartError | Schema.SchemaError | HttpServerError,
297 RD | HttpServerRequest | Scope.Scope | FileSystem.FileSystem | Path.Path
298 > => {
299 if (isMultipart(request)) {
300 return Effect.flatMap(request.multipart, (_) => parseMultipart(_, options))
301 }
302 return parseUrlParams(request)
303 })
304}
305
306/**
307 * Reads the current request body as URL-encoded parameters and decodes them with
308 * the supplied schema.
309 *
310 * @stability unstable
311 * @category schemas
312 * @since 4.0.0
313 */
314export const schemaBodyUrlParams = <
315 A,
316 I extends Readonly<Record<string, string | ReadonlyArray<string> | undefined>>,
317 RD
318>(
319 schema: Schema.ConstraintCodec<A, I, RD, unknown>,
320 options?: ParseOptions | undefined
321): Effect.Effect<A, HttpServerError | Schema.SchemaError, HttpServerRequest | RD> => {
322 const parse = HttpIncomingMessage.schemaBodyUrlParams(schema, options)
323 return Effect.flatMap(HttpServerRequest, parse)
324}
325
326/**
327 * Persists the current multipart request body and decodes it with the supplied
328 * schema.
329 *
330 * **Details**
331 *
332 * The effect requires the services needed to persist multipart files, including a
333 * scope, file system, and path service.
334 *
335 * @stability unstable
336 * @category schemas
337 * @since 4.0.0
338 */
339export const schemaBodyMultipart = <A, I extends Partial<Multipart.Persisted>, RD>(
340 schema: Schema.ConstraintCodec<A, I, RD, unknown>,
341 options?: ParseOptions | undefined
342): Effect.Effect<
343 A,
344 Multipart.MultipartError | Schema.SchemaError,
345 HttpServerRequest | Scope.Scope | FileSystem.FileSystem | Path.Path | RD
346> => {
347 const parse = Multipart.schemaPersisted(schema)
348 return HttpServerRequest.pipe(
349 Effect.flatMap((_) => _.multipart),
350 Effect.flatMap((_) => parse(_, options))
351 )
352}
353
354/**
355 * Creates a decoder for a JSON value stored in a form field.
356 *
357 * **Details**
358 *
359 * For multipart requests, the named multipart field is decoded as JSON. For
360 * URL-encoded requests, the named parameter is decoded as JSON and then decoded
361 * with the supplied schema.
362 *
363 * @stability unstable
364 * @category schemas
365 * @since 4.0.0
366 */
367export const schemaBodyFormJson = <A, RD>(
368 schema: Schema.ConstraintDecoder<A, RD>,
369 options?: (ParseOptions & HttpIncomingMessage.JsonOptions) | undefined
370) => {
371 const parseMultipart = Multipart.schemaJson(schema, options)
372 return (field: string) => {
373 const parseUrlParams = Schema.JsonFromUrlParamsField(field, options).pipe(
374 Schema.decodeTo(schema),
375 Schema.decodeEffect
376 )
377 return Effect.flatMap(
378 HttpServerRequest,
379 (request): Effect.Effect<
380 A,
381 Schema.SchemaError | HttpServerError,
382 RD | FileSystem.FileSystem | Path.Path | Scope.Scope | HttpServerRequest
383 > => {
384 if (isMultipart(request)) {
385 return Effect.flatMap(
386 Effect.mapError(request.multipart, (cause) =>
387 new HttpServerError({
388 reason: new RequestParseError({
389 request,
390 cause
391 })
392 })),
393 parseMultipart(field)
394 )
395 }
396 return Effect.flatMap(request.urlParamsBody, (_) => parseUrlParams(_, options))
397 }
398 )
399 }
400}
401
402/**
403 * Creates an `HttpServerRequest` view of an `HttpClientRequest`.
404 *
405 * **Details**
406 *
407 * If the client request can be converted to an absolute URL, that URL is used as
408 * the original URL.
409 *
410 * @stability unstable
411 * @category converting
412 * @since 4.0.0
413 */
414export const fromClientRequest = (request: HttpClientRequest.HttpClientRequest): HttpServerRequest => {
415 const url = Option.match(HttpClientRequest.toUrl(request), {
416 onNone: () => request.url,
417 onSome: (url) => url.toString()
418 })
419 return new ClientRequestImpl(request, url)
420}
421
422/**
423 * Wraps a Web `Request` as an `HttpServerRequest`.
424 *
425 * **Details**
426 *
427 * The request's current URL is stored without the scheme and host, while the
428 * original Web URL remains available as `originalUrl`.
429 *
430 * @stability unstable
431 * @category converting
432 * @since 4.0.0
433 */
434export const fromWeb = (request: globalThis.Request): HttpServerRequest =>
435 new ServerRequestImpl(request, removeHost(request.url))
436
437/**
438 * Converts an `HttpServerRequest` into an `HttpClientRequest`.
439 *
440 * **Details**
441 *
442 * The converted request preserves the method, headers, body stream, and a URL
443 * derived from the request when possible.
444 *
445 * @stability unstable
446 * @category converting
447 * @since 4.0.0
448 */
449export const toClientRequest = (request: HttpServerRequest): HttpClientRequest.HttpClientRequest => {
450 const body = toClientBody(request)
451 const headers = body._tag !== "Empty" && body.contentLength === undefined
452 ? Headers.remove(request.headers, "content-length")
453 : request.headers
454 return HttpClientRequest.setUrl(
455 HttpClientRequest.makeWith(
456 request.method,
457 "",
458 UrlParams.empty,
459 Option.none(),
460 headers,
461 body
462 ),
463 Option.getOrElse(toURL(request), () => request.url)
464 )
465}
466
467const toClientBody = (request: HttpServerRequest): HttpBody.HttpBody => {
468 if (!hasBody(request.method)) {
469 return HttpBody.empty
470 }
471 const formData = getFormDataBody(request)
472 return formData === undefined
473 ? HttpBody.stream(
474 request.stream,
475 request.headers["content-type"],
476 bodyInternal.parseContentLength(request.headers["content-length"])
477 )
478 : HttpBody.formData(formData)
479}
480
481const removeHost = (url: string) => {
482 if (url[0] === "/") {
483 return url
484 }
485 const index = url.indexOf("/", url.indexOf("//") + 2)
486 return index === -1 ? "/" : url.slice(index)
487}
488
489class ServerRequestImpl extends Inspectable.Class implements HttpServerRequest {
490 readonly [TypeId]: typeof TypeId
491 readonly [HttpIncomingMessage.TypeId]: typeof HttpIncomingMessage.TypeId
492 readonly source: Request
493 readonly url: string
494 public headersOverride?: Headers.Headers | undefined
495 private remoteAddressOverride?: Option.Option<string> | undefined
496
497 constructor(
498 source: Request,
499 url: string,
500 headersOverride?: Headers.Headers,
501 remoteAddressOverride?: Option.Option<string>
502 ) {
503 super()
504 this[TypeId] = TypeId
505 this[HttpIncomingMessage.TypeId] = HttpIncomingMessage.TypeId
506 this.source = source
507 this.url = url
508 this.headersOverride = headersOverride
509 this.remoteAddressOverride = remoteAddressOverride
510 }
511 toJSON(): unknown {
512 return HttpIncomingMessage.inspect(this, {
513 _id: "HttpServerRequest",
514 method: this.method,
515 url: this.originalUrl
516 })
517 }
518 modify(
519 options: {
520 readonly url?: string | undefined
521 readonly headers?: Headers.Headers | undefined
522 readonly remoteAddress?: Option.Option<string> | undefined
523 }
524 ) {
525 return new ServerRequestImpl(
526 this.source,
527 options.url ?? this.url,
528 options.headers ?? this.headersOverride,
529 "remoteAddress" in options ? options.remoteAddress : this.remoteAddressOverride
530 )
531 }
532 get method(): HttpMethod {
533 return this.source.method.toUpperCase() as HttpMethod
534 }
535 get originalUrl() {
536 return this.source.url
537 }
538 get remoteAddress(): Option.Option<string> {
539 return this.remoteAddressOverride ?? Option.none()
540 }
541 get headers(): Headers.Headers {
542 this.headersOverride ??= Headers.fromInput(this.source.headers as any)
543 return this.headersOverride
544 }
545
546 private cachedCookies: ReadonlyRecord<string, string> | undefined
547 get cookies() {
548 if (this.cachedCookies) {
549 return this.cachedCookies
550 }
551 return this.cachedCookies = Cookies.parseHeader(this.headers.cookie ?? "")
552 }
553
554 get stream(): Stream.Stream<Uint8Array, HttpServerError> {
555 return this.source.body
556 ? Stream.fromReadableStream({
557 evaluate: () => this.source.body as any,
558 onError: (cause) =>
559 new HttpServerError({
560 reason: new RequestParseError({
561 request: this,
562 cause
563 })
564 })
565 })
566 : Stream.fail(
567 new HttpServerError({
568 reason: new RequestParseError({
569 request: this,
570 description: "can not create stream from empty body"
571 })
572 })
573 )
574 }
575
576 private textEffect: Effect.Effect<string, HttpServerError> | undefined
577 get text(): Effect.Effect<string, HttpServerError> {
578 if (this.textEffect) {
579 return this.textEffect
580 }
581 this.textEffect = Effect.runSync(Effect.cached(
582 Effect.tryPromise({
583 try: () => this.source.text(),
584 catch: (cause) =>
585 new HttpServerError({
586 reason: new RequestParseError({
587 request: this,
588 cause
589 })
590 })
591 })
592 ))
593 return this.textEffect
594 }
595
596 get json(): Effect.Effect<Schema.Json, HttpServerError> {
597 return Effect.flatMap(this.text, (text) =>
598 Effect.try({
599 try: () => JSON.parse(text) as Schema.Json,
600 catch: (cause) =>
601 new HttpServerError({
602 reason: new RequestParseError({
603 request: this,
604 cause
605 })
606 })
607 }))
608 }
609
610 get urlParamsBody(): Effect.Effect<UrlParams.UrlParams, HttpServerError> {
611 return Effect.flatMap(this.text, (_) =>
612 Effect.try({
613 try: () => UrlParams.fromInput(new URLSearchParams(_)),
614 catch: (cause) =>
615 new HttpServerError({
616 reason: new RequestParseError({
617 request: this,
618 cause
619 })
620 })
621 }))
622 }
623
624 private multipartEffect:
625 | Effect.Effect<
626 Multipart.Persisted,
627 Multipart.MultipartError,
628 Scope.Scope | FileSystem.FileSystem | Path.Path
629 >
630 | undefined
631 get multipart(): Effect.Effect<
632 Multipart.Persisted,
633 Multipart.MultipartError,
634 Scope.Scope | FileSystem.FileSystem | Path.Path
635 > {
636 if (this.multipartEffect) {
637 return this.multipartEffect
638 }
639 this.multipartEffect = Effect.runSync(Effect.cached(
640 Multipart.toPersisted(this.multipartStream)
641 ))
642 return this.multipartEffect
643 }
644
645 get multipartStream(): Stream.Stream<Multipart.Part, Multipart.MultipartError> {
646 return Stream.pipeThroughChannel(
647 Stream.mapError(this.stream, (cause) => Multipart.MultipartError.fromReason("InternalError", cause)),
648 Multipart.makeChannel(this.headers)
649 )
650 }
651
652 private arrayBufferEffect: Effect.Effect<ArrayBuffer, HttpServerError> | undefined
653 get arrayBuffer(): Effect.Effect<ArrayBuffer, HttpServerError> {
654 if (this.arrayBufferEffect) {
655 return this.arrayBufferEffect
656 }
657 this.arrayBufferEffect = Effect.runSync(Effect.cached(
658 Effect.tryPromise({
659 try: () => this.source.arrayBuffer(),
660 catch: (cause) =>
661 new HttpServerError({
662 reason: new RequestParseError({
663 request: this,
664 cause
665 })
666 })
667 })
668 ))
669 return this.arrayBufferEffect
670 }
671
672 get upgrade(): Effect.Effect<Socket.Socket, HttpServerError> {
673 return Effect.fail(
674 new HttpServerError({
675 reason: new RequestParseError({
676 request: this,
677 description: "Not an upgradeable ServerRequest"
678 })
679 })
680 )
681 }
682}
683
684class ClientRequestImpl extends Inspectable.Class implements HttpServerRequest {
685 readonly [TypeId]: typeof TypeId
686 readonly [HttpIncomingMessage.TypeId]: typeof HttpIncomingMessage.TypeId
687 readonly source: HttpClientRequest.HttpClientRequest
688 public originalUrl: string
689 public headersOverride?: Headers.Headers | undefined
690 private remoteAddressOverride?: Option.Option<string> | undefined
691 private urlOverride?: string | undefined
692
693 constructor(
694 source: HttpClientRequest.HttpClientRequest,
695 originalUrl: string,
696 urlOverride?: string,
697 headersOverride?: Headers.Headers,
698 remoteAddressOverride?: Option.Option<string>
699 ) {
700 super()
701 this[TypeId] = TypeId
702 this[HttpIncomingMessage.TypeId] = HttpIncomingMessage.TypeId
703 this.source = source
704 this.originalUrl = originalUrl
705 this.urlOverride = urlOverride
706 this.headersOverride = headersOverride
707 this.remoteAddressOverride = remoteAddressOverride
708 }
709
710 toJSON(): unknown {
711 return HttpIncomingMessage.inspect(this, {
712 _id: "HttpServerRequest",
713 method: this.method,
714 url: this.originalUrl
715 })
716 }
717
718 modify(
719 options: {
720 readonly url?: string | undefined
721 readonly headers?: Headers.Headers | undefined
722 readonly remoteAddress?: Option.Option<string> | undefined
723 }
724 ) {
725 return new ClientRequestImpl(
726 this.source,
727 this.originalUrl,
728 options.url ?? this.url,
729 options.headers ?? this.headersOverride,
730 "remoteAddress" in options ? options.remoteAddress : this.remoteAddressOverride
731 )
732 }
733
734 get method(): HttpMethod {
735 return this.source.method
736 }
737
738 get url(): string {
739 return this.urlOverride ?? removeHost(this.originalUrl)
740 }
741
742 get remoteAddress(): Option.Option<string> {
743 return this.remoteAddressOverride ?? Option.none()
744 }
745
746 get headers(): Headers.Headers {
747 return this.headersOverride ??= this.source.headers
748 }
749
750 private cachedCookies: ReadonlyRecord<string, string> | undefined
751 get cookies() {
752 if (this.cachedCookies) {
753 return this.cachedCookies
754 }
755 return this.cachedCookies = Cookies.parseHeader(this.headers.cookie ?? "")
756 }
757
758 get stream(): Stream.Stream<Uint8Array, HttpServerError> {
759 const body = this.source.body
760 switch (body._tag) {
761 case "Empty": {
762 return Stream.empty
763 }
764 case "Uint8Array": {
765 return Stream.succeed(body.body)
766 }
767 case "Stream": {
768 return Stream.mapError(body.stream, (cause) => requestParseError(this, undefined, cause))
769 }
770 case "FormData": {
771 return streamFromReadable(this, new Response(body.formData).body)
772 }
773 case "Raw": {
774 return rawBodyStream(this, body.body)
775 }
776 }
777 }
778
779 private bytesEffect: Effect.Effect<Uint8Array, HttpServerError> | undefined
780 private get bytes(): Effect.Effect<Uint8Array, HttpServerError> {
781 if (this.bytesEffect) {
782 return this.bytesEffect
783 }
784 const body = this.source.body
785 let effect: Effect.Effect<Uint8Array, HttpServerError>
786 switch (body._tag) {
787 case "Empty": {
788 effect = Effect.succeed(new Uint8Array(0))
789 break
790 }
791 case "Uint8Array": {
792 effect = Effect.succeed(body.body)
793 break
794 }
795 case "FormData": {
796 effect = bytesFromBodyInit(this, body.formData)
797 break
798 }
799 case "Stream": {
800 effect = Stream.mkUint8Array(this.stream)
801 break
802 }
803 case "Raw": {
804 effect = rawBodyBytes(this, body.body)
805 break
806 }
807 }
808 this.bytesEffect = Effect.runSync(Effect.cached(effect))
809 return this.bytesEffect
810 }
811
812 get text(): Effect.Effect<string, HttpServerError> {
813 return Effect.map(this.bytes, (bytes) => textDecoder.decode(bytes))
814 }
815
816 get json(): Effect.Effect<Schema.Json, HttpServerError> {
817 return Effect.flatMap(this.text, (text) =>
818 Effect.try({
819 try: () => text === "" ? null : JSON.parse(text),
820 catch: (cause) => requestParseError(this, undefined, cause)
821 }))
822 }
823
824 get urlParamsBody(): Effect.Effect<UrlParams.UrlParams, HttpServerError> {
825 return Effect.flatMap(this.text, (_) =>
826 Effect.try({
827 try: () => UrlParams.fromInput(new URLSearchParams(_)),
828 catch: (cause) => requestParseError(this, undefined, cause)
829 }))
830 }
831
832 private multipartEffect:
833 | Effect.Effect<
834 Multipart.Persisted,
835 Multipart.MultipartError,
836 Scope.Scope | FileSystem.FileSystem | Path.Path
837 >
838 | undefined
839 get multipart(): Effect.Effect<
840 Multipart.Persisted,
841 Multipart.MultipartError,
842 Scope.Scope | FileSystem.FileSystem | Path.Path
843 > {
844 if (this.multipartEffect) {
845 return this.multipartEffect
846 }
847 this.multipartEffect = Effect.runSync(Effect.cached(
848 Multipart.toPersisted(this.multipartStream)
849 ))
850 return this.multipartEffect
851 }
852
853 get multipartStream(): Stream.Stream<Multipart.Part, Multipart.MultipartError> {
854 const formData = this.source.body._tag === "FormData" && this.source.body.formData
855 if (formData) {
856 return Stream.fromIterable(formDataToParts(formData))
857 }
858 return Stream.pipeThroughChannel(
859 Stream.mapError(this.stream, (cause) => Multipart.MultipartError.fromReason("InternalError", cause)),
860 Multipart.makeChannel(this.headers)
861 )
862 }
863
864 get arrayBuffer(): Effect.Effect<ArrayBuffer, HttpServerError> {
865 return Effect.map(
866 this.bytes,
867 // Copy this view because Buffer views may share a larger ArrayBuffer.
868 (bytes) => bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer
869 )
870 }
871
872 get upgrade(): Effect.Effect<Socket.Socket, HttpServerError> {
873 return Effect.fail(requestParseError(this, "Not an upgradeable ServerRequest"))
874 }
875}
876
877const getFormDataBody = (request: HttpServerRequest): FormData | undefined => {
878 if (!HttpClientRequest.isHttpClientRequest(request.source)) {
879 return undefined
880 }
881 const body = request.source.body
882 if (body._tag === "FormData") {
883 return body.formData
884 }
885 if (body._tag === "Raw" && isFormData(body.body)) {
886 return body.body
887 }
888 return undefined
889}
890
891const rawBodyStream = (request: HttpServerRequest, body: unknown): Stream.Stream<Uint8Array, HttpServerError> => {
892 if (body instanceof Request) {
893 return streamFromReadable(request, body.body)
894 }
895 if (isReadableStream(body)) {
896 return streamFromReadable(request, body)
897 }
898 if (isBodyInit(body)) {
899 return streamFromReadable(request, new Response(body).body)
900 }
901 return Stream.fail(requestParseError(request, "Unsupported body type"))
902}
903
904const rawBodyBytes = (request: HttpServerRequest, body: unknown): Effect.Effect<Uint8Array, HttpServerError> => {
905 if (body instanceof Request) {
906 return Effect.tryPromise({
907 try: () => body.arrayBuffer().then((buffer) => new Uint8Array(buffer)),
908 catch: (cause) => requestParseError(request, undefined, cause)
909 })
910 }
911 if (isBodyInit(body)) {
912 return bytesFromBodyInit(request, body)
913 }
914 return Effect.fail(requestParseError(request, "Unsupported body type"))
915}
916
917const bytesFromBodyInit = (request: HttpServerRequest, body: BodyInit): Effect.Effect<Uint8Array, HttpServerError> =>
918 Effect.tryPromise({
919 try: () => new Response(body).arrayBuffer().then((buffer) => new Uint8Array(buffer)),
920 catch: (cause) => requestParseError(request, undefined, cause)
921 })
922
923const streamFromReadable = (
924 request: HttpServerRequest,
925 body: ReadableStream<Uint8Array> | null | undefined
926): Stream.Stream<Uint8Array, HttpServerError> =>
927 body
928 ? Stream.fromReadableStream({
929 evaluate: () => body,
930 onError: (cause) => requestParseError(request, undefined, cause)
931 })
932 : Stream.empty
933
934const requestParseError = (
935 request: HttpServerRequest,
936 description?: string,
937 cause?: unknown
938) =>
939 new HttpServerError({
940 reason: new RequestParseError({
941 request,
942 ...(description === undefined ? undefined : { description }),
943 ...(cause === undefined ? undefined : { cause })
944 })
945 })
946
947const formDataToParts = (formData: FormData): Array<Multipart.Part> => {
948 const parts: Array<Multipart.Part> = []
949 for (const [key, value] of formData.entries()) {
950 parts.push(typeof value === "string" ? new MultipartFieldPart(key, value) : new MultipartFilePart(key, value))
951 }
952 return parts
953}
954
955class MultipartFieldPart extends Inspectable.Class implements Multipart.Field {
956 readonly [Multipart.TypeId]: typeof Multipart.TypeId
957 readonly _tag = "Field"
958 readonly contentType = "text/plain"
959 readonly key: string
960 readonly value: string
961
962 constructor(
963 key: string,
964 value: string
965 ) {
966 super()
967 this[Multipart.TypeId] = Multipart.TypeId
968 this.key = key
969 this.value = value
970 }
971
972 toJSON(): unknown {
973 return {
974 _id: "@effect/platform/Multipart/Part",
975 _tag: "Field",
976 key: this.key,
977 contentType: this.contentType,
978 value: this.value
979 }
980 }
981}
982
983class MultipartFilePart extends Inspectable.Class implements Multipart.File {
984 readonly [Multipart.TypeId]: typeof Multipart.TypeId
985 readonly _tag = "File"
986 readonly key: string
987 readonly name: string
988 readonly contentType: string
989 readonly content: Stream.Stream<Uint8Array, Multipart.MultipartError>
990 readonly contentEffect: Effect.Effect<Uint8Array, Multipart.MultipartError>
991
992 constructor(
993 key: string,
994 file: File
995 ) {
996 super()
997 this[Multipart.TypeId] = Multipart.TypeId
998 this.key = key
999 this.name = file.name
1000 this.contentType = file.type
1001 this.content = Stream.fromReadableStream({
1002 evaluate: () => file.stream() as ReadableStream<Uint8Array>,
1003 onError: (cause) => Multipart.MultipartError.fromReason("InternalError", cause)
1004 })
1005 this.contentEffect = Effect.tryPromise({
1006 try: () => file.arrayBuffer().then((buffer) => new Uint8Array(buffer)),
1007 catch: (cause) => Multipart.MultipartError.fromReason("InternalError", cause)
1008 })
1009 }
1010
1011 toJSON(): unknown {
1012 return {
1013 _id: "@effect/platform/Multipart/Part",
1014 _tag: "File",
1015 key: this.key,
1016 name: this.name,
1017 contentType: this.contentType
1018 }
1019 }
1020}
1021
1022const isReadableStream = (u: unknown): u is ReadableStream<Uint8Array> =>
1023 typeof ReadableStream !== "undefined" && u instanceof ReadableStream
1024
1025const isFormData = (u: unknown): u is FormData => typeof FormData !== "undefined" && u instanceof FormData
1026
1027const isBodyInit = (u: unknown): u is BodyInit =>
1028 typeof u === "string" ||
1029 (typeof ArrayBuffer !== "undefined" && (u instanceof ArrayBuffer || ArrayBuffer.isView(u))) ||
1030 (typeof Blob !== "undefined" && u instanceof Blob) ||
1031 isFormData(u) ||
1032 (typeof URLSearchParams !== "undefined" && u instanceof URLSearchParams) ||
1033 isReadableStream(u)
1034
1035const textDecoder = new TextDecoder()
1036
1037/**
1038 * Attempts to construct an absolute `URL` for a server request safely.
1039 *
1040 * **Details**
1041 *
1042 * The host comes from the `host` header, defaulting to `localhost`, and the
1043 * protocol is `https` only when `x-forwarded-proto` is `https`; invalid URLs
1044 * return `Option.none`.
1045 *
1046 * @stability unstable
1047 * @category converting
1048 * @since 4.0.0
1049 */
1050export const toURL = (self: HttpServerRequest): Option.Option<URL> => {
1051 const host = self.headers.host ?? "localhost"
1052 const protocol = self.headers["x-forwarded-proto"] === "https" ? "https" : "http"
1053 try {
1054 return Option.some(new URL(self.url, `${protocol}://${host}`))
1055 } catch {
1056 return Option.none()
1057 }
1058}
1059
1060/**
1061 * Converts an `HttpServerRequest` safely to a Web `Request` as a `Result`.
1062 *
1063 * **Details**
1064 *
1065 * If the source is already a Web `Request`, it is returned unchanged. Otherwise
1066 * an absolute URL is derived from the request; invalid URLs fail with a
1067 * `RequestParseError`.
1068 *
1069 * @stability unstable
1070 * @category converting
1071 * @since 4.0.0
1072 */
1073export const toWebResult = (self: HttpServerRequest, options?: {
1074 readonly signal?: AbortSignal | undefined
1075 readonly context?: Context.Context<never> | undefined
1076}): Result.Result<Request, RequestError> => {
1077 if (self.source instanceof Request) {
1078 return Result.succeed(self.source)
1079 }
1080 const url = toURL(self)
1081 if (Option.isNone(url)) {
1082 return Result.fail(
1083 new RequestParseError({
1084 request: self,
1085 description: "Invalid URL"
1086 })
1087 )
1088 }
1089 const requestInit: RequestInit = {
1090 method: self.method,
1091 headers: self.headers
1092 }
1093 if (options?.signal) {
1094 requestInit.signal = options.signal
1095 }
1096 if (hasBody(self.method)) {
1097 requestInit.body = Stream.toReadableStreamWith(self.stream, options?.context ?? Context.empty())
1098 ;(requestInit as any).duplex = "half"
1099 }
1100 return Result.succeed(new Request(url.value, requestInit))
1101}
1102
1103/**
1104 * Converts an `HttpServerRequest` to a Web `Request` in `Effect`.
1105 *
1106 * **Details**
1107 *
1108 * The current context is used when streaming the request body into the Web
1109 * request.
1110 *
1111 * @stability unstable
1112 * @category converting
1113 * @since 4.0.0
1114 */
1115export const toWeb = (self: HttpServerRequest, options?: {
1116 readonly signal?: AbortSignal | undefined
1117}): Effect.Effect<Request, RequestError> =>
1118 Effect.contextWith((context) =>
1119 Effect.fromResult(toWebResult(self, {
1120 context,
1121 signal: options?.signal
1122 }))
1123 )