1/**
2 * Describes immutable responses returned by Effect HTTP handlers.
3 *
4 * An `HttpServerResponse` stores the status, optional status text, headers,
5 * cookies, and body that the server runtime later turns into a platform
6 * response such as a Web `Response`. This module includes constructors for
7 * common response bodies, helpers for updating response data, file response
8 * support through `HttpPlatform`, and conversions to or from Web and Effect
9 * HTTP client responses.
10 *
11 * @stability unstable
12 * @since 4.0.0
13 */
14import type * as ByteSize from "../ByteSize.ts"
15import * as Context from "../Context.ts"
16import * as Effect from "../Effect.ts"
17import * as ErrorReporter from "../ErrorReporter.ts"
18import { constVoid, dual } from "../Function.ts"
19import * as Inspectable from "../Inspectable.ts"
20import { PipeInspectableProto } from "../internal/core.ts"
21import * as Option from "../Option.ts"
22import { type Pipeable, pipeArguments } from "../Pipeable.ts"
23import type { PlatformError } from "../PlatformError.ts"
24import { hasProperty } from "../Predicate.ts"
25import { redact } from "../Redactable.ts"
26import type * as Schema from "../Schema.ts"
27import type { ParseOptions } from "../SchemaAST.ts"
28import * as Stream from "../Stream.ts"
29import type { Mutable } from "../Types.ts"
30import * as Cookies from "./Cookies.ts"
31import * as Headers from "./Headers.ts"
32import * as Body from "./HttpBody.ts"
33import * as HttpClientError from "./HttpClientError.ts"
34import * as HttpClientRequest from "./HttpClientRequest.ts"
35import * as HttpClientResponse from "./HttpClientResponse.ts"
36import * as HttpIncomingMessage from "./HttpIncomingMessage.ts"
37import type { HttpPlatform } from "./HttpPlatform.ts"
38import * as headersInternal from "./internal/headers.ts"
39import * as bodyInternal from "./internal/httpBody.ts"
40import * as Template from "./Template.ts"
41import * as UrlParams from "./UrlParams.ts"
42
43const TypeId = "~effect/http/HttpServerResponse"
44
45/**
46 * Server-side HTTP response model.
47 *
48 * **Details**
49 *
50 * A response contains a status, optional status text, headers, cookies, and an
51 * HTTP body that can later be converted to platform-specific response types.
52 *
53 * @stability unstable
54 * @category models
55 * @since 4.0.0
56 */
57export interface HttpServerResponse extends Inspectable.Inspectable, Pipeable, ErrorReporter.Reportable {
58 readonly [TypeId]: typeof TypeId
59 readonly status: number
60 readonly statusText?: string | undefined
61 readonly headers: Headers.Headers
62 readonly cookies: Cookies.Cookies
63 readonly body: Body.HttpBody
64}
65
66/**
67 * Common options accepted by HTTP server response constructors.
68 *
69 * @stability unstable
70 * @category options
71 * @since 4.0.0
72 */
73export interface Options {
74 readonly status?: number | undefined
75 readonly statusText?: string | undefined
76 readonly headers?: Headers.Input | undefined
77 readonly cookies?: Cookies.Cookies | undefined
78 readonly contentType?: string | undefined
79 readonly contentLength?: number | undefined
80}
81
82/**
83 * Option variants used by response constructors with different body metadata
84 * rules.
85 *
86 * @stability unstable
87 * @since 4.0.0
88 */
89export declare namespace Options {
90 /**
91 * Response options for constructors whose body determines its own content type
92 * and content length.
93 *
94 * @stability unstable
95 * @category options
96 * @since 4.0.0
97 */
98 export interface WithContent extends Omit<Options, "contentType" | "contentLength"> {}
99
100 /**
101 * Response options for constructors that allow overriding the content type while
102 * deriving the content length from the body.
103 *
104 * @stability unstable
105 * @category options
106 * @since 4.0.0
107 */
108 export interface WithContentType extends Omit<Options, "contentLength"> {}
109}
110
111/**
112 * Returns `true` when the supplied value is an `HttpServerResponse`.
113 *
114 * @stability unstable
115 * @category guards
116 * @since 4.0.0
117 */
118export const isHttpServerResponse = (u: unknown): u is HttpServerResponse => hasProperty(u, TypeId)
119
120/**
121 * Creates an empty HTTP response.
122 *
123 * **Details**
124 *
125 * The default status is `204`.
126 *
127 * @stability unstable
128 * @category constructors
129 * @since 4.0.0
130 */
131export const empty = (
132 options?: Options.WithContent | undefined
133): HttpServerResponse =>
134 makeResponse({
135 status: options?.status ?? 204,
136 statusText: options?.statusText,
137 headers: options?.headers ? Headers.fromInput(options.headers) : undefined,
138 cookies: options?.cookies
139 })
140
141/**
142 * Creates a redirect response with a `Location` header.
143 *
144 * **Details**
145 *
146 * The default status is `302`; custom headers are merged with the generated
147 * `Location` header.
148 *
149 * @stability unstable
150 * @category constructors
151 * @since 4.0.0
152 */
153export const redirect = (
154 location: string | URL,
155 options?: Options.WithContent | undefined
156): HttpServerResponse => {
157 const headers = Headers.fromRecordUnsafe({ location: location.toString() })
158 return makeResponse({
159 status: options?.status ?? 302,
160 statusText: options?.statusText,
161 headers: options?.headers
162 ? Headers.merge(headers, Headers.fromInput(options.headers))
163 : headers,
164 cookies: options?.cookies ?? Cookies.empty
165 })
166}
167
168/**
169 * Creates an HTTP response whose body is a `Uint8Array`.
170 *
171 * @stability unstable
172 * @category constructors
173 * @since 4.0.0
174 */
175export const uint8Array = (
176 body: Uint8Array,
177 options?: Options.WithContentType
178): HttpServerResponse => {
179 const headers = options?.headers
180 ? Headers.fromInput(options.headers)
181 : Headers.empty
182 return makeResponse({
183 status: options?.status ?? 200,
184 statusText: options?.statusText,
185 headers,
186 cookies: options?.cookies ?? Cookies.empty,
187 body: Body.uint8Array(body, getContentType(options, headers))
188 })
189}
190
191const getContentType = (
192 options: Options | undefined,
193 headers: Headers.Headers
194): string | undefined => {
195 if (options?.contentType) {
196 return options.contentType
197 } else if (options?.headers) {
198 return headers["content-type"]
199 }
200}
201
202/**
203 * Creates an HTTP response whose body is a string.
204 *
205 * @stability unstable
206 * @category constructors
207 * @since 4.0.0
208 */
209export const text = (
210 body: string,
211 options?: Options.WithContentType
212): HttpServerResponse => {
213 const headers = options?.headers
214 ? Headers.fromInput(options.headers)
215 : Headers.empty
216 return makeResponse({
217 status: options?.status ?? 200,
218 statusText: options?.statusText,
219 headers,
220 cookies: options?.cookies ?? Cookies.empty,
221 body: Body.text(body, getContentType(options, headers))
222 })
223}
224
225/**
226 * Creates an HTML response with the `text/html` content type.
227 *
228 * **Details**
229 *
230 * Passing a string returns a response directly. Using it as a template tag returns
231 * an effect so interpolated values can be rendered with their required services
232 * and errors.
233 *
234 * @stability unstable
235 * @category constructors
236 * @since 4.0.0
237 */
238export const html: {
239 <A extends ReadonlyArray<Template.Interpolated>>(
240 strings: TemplateStringsArray,
241 ...args: A
242 ): Effect.Effect<
243 HttpServerResponse,
244 Template.Interpolated.Error<A[number]>,
245 Template.Interpolated.Context<A[number]>
246 >
247 (html: string): HttpServerResponse
248} = (
249 strings: TemplateStringsArray | string,
250 ...args: ReadonlyArray<Template.Interpolated>
251) => {
252 if (typeof strings === "string") {
253 return text(strings, { contentType: "text/html" })
254 }
255
256 return Effect.map(Template.make(strings, ...args), (_) => text(_, { contentType: "text/html" })) as any
257}
258
259/**
260 * Creates a streaming HTML response from a template.
261 *
262 * **Details**
263 *
264 * The template is encoded as a byte stream and can use streaming interpolated
265 * values from the current context.
266 *
267 * @stability unstable
268 * @category constructors
269 * @since 4.0.0
270 */
271export const htmlStream = <
272 A extends ReadonlyArray<Template.InterpolatedWithStream>
273>(
274 strings: TemplateStringsArray,
275 ...args: A
276): Effect.Effect<
277 HttpServerResponse,
278 never,
279 Template.Interpolated.Context<A[number]>
280> =>
281 Effect.map(
282 Effect.context<Template.Interpolated.Context<A[number]>>(),
283 (context) =>
284 stream(
285 Stream.provideContext(
286 Stream.encodeText(Template.stream(strings, ...args)),
287 context
288 ),
289 { contentType: "text/html" }
290 )
291 )
292
293/**
294 * Creates a JSON HTTP response.
295 *
296 * **Details**
297 *
298 * The body is serialized with `JSON.stringify`; serialization errors are captured
299 * as `HttpBodyError` failures.
300 *
301 * @stability unstable
302 * @category constructors
303 * @since 4.0.0
304 */
305export const json = (
306 body: unknown,
307 options?: Options.WithContentType | undefined
308): Effect.Effect<HttpServerResponse, Body.HttpBodyError> => {
309 const headers = options?.headers ? Headers.fromInput(options.headers) : Headers.empty
310 return Effect.map(Body.json(body, getContentType(options, headers)), (body) =>
311 makeResponse({
312 status: options?.status ?? 200,
313 statusText: options?.statusText,
314 headers,
315 cookies: options?.cookies,
316 body
317 }))
318}
319
320/**
321 * Creates a JSON response constructor backed by a schema encoder.
322 *
323 * **Details**
324 *
325 * The returned function encodes the value with the supplied schema before
326 * serializing it as JSON, and can fail with `HttpBodyError` if schema encoding or
327 * JSON serialization fails.
328 *
329 * @stability unstable
330 * @category constructors
331 * @since 4.0.0
332 */
333export const schemaJson = <A, RE>(
334 schema: Schema.ConstraintCodec<A, unknown, unknown, RE>,
335 options?: ParseOptions | undefined
336) => {
337 const encode = Body.jsonSchema(schema, options)
338 return (
339 body: A,
340 options?: Options.WithContentType | undefined
341 ): Effect.Effect<HttpServerResponse, Body.HttpBodyError, RE> => {
342 const headers = options?.headers ? Headers.fromInput(options.headers) : Headers.empty
343 return Effect.map(encode(body, getContentType(options, headers)), (body) =>
344 makeResponse({
345 status: options?.status ?? 200,
346 statusText: options?.statusText,
347 headers,
348 cookies: options?.cookies,
349 body
350 }))
351 }
352}
353
354/**
355 * Creates a JSON HTTP response synchronously.
356 *
357 * **When to use**
358 *
359 * Use when the response body is known to be JSON-serializable and you need a
360 * synchronous `HttpServerResponse`.
361 *
362 * **Gotchas**
363 *
364 * Unlike `json`, serialization errors from `JSON.stringify` are not captured in
365 * `Effect`.
366 *
367 * @stability unstable
368 * @category constructors
369 * @since 4.0.0
370 */
371export const jsonUnsafe = (
372 body: unknown,
373 options?: Options.WithContentType | undefined
374): HttpServerResponse => {
375 const headers = options?.headers ? Headers.fromInput(options.headers) : Headers.empty
376 return makeResponse({
377 status: options?.status ?? 200,
378 statusText: options?.statusText,
379 headers,
380 cookies: options?.cookies,
381 body: Body.jsonUnsafe(body, getContentType(options, headers))
382 })
383}
384
385/**
386 * Creates a response from URL parameters using the
387 * `application/x-www-form-urlencoded` content type by default.
388 *
389 * @stability unstable
390 * @category constructors
391 * @since 4.0.0
392 */
393export const urlParams = (
394 body: UrlParams.Input,
395 options?: Options.WithContentType | undefined
396): HttpServerResponse => {
397 const headers = options?.headers ? Headers.fromInput(options.headers) : Headers.empty
398 return makeResponse({
399 status: options?.status ?? 200,
400 statusText: options?.statusText,
401 headers,
402 cookies: options?.cookies,
403 body: Body.text(
404 UrlParams.toString(UrlParams.fromInput(body)),
405 getContentType(options, headers) ?? "application/x-www-form-urlencoded"
406 )
407 })
408}
409
410/**
411 * Creates a response with a raw body value.
412 *
413 * **When to use**
414 *
415 * Use when you want to pass through a body value already understood by the
416 * underlying runtime, such as a Web `Response`, `Blob`, or `ReadableStream`,
417 * for later platform conversion.
418 *
419 * @stability unstable
420 * @category constructors
421 * @since 4.0.0
422 */
423export const raw = (
424 body: unknown,
425 options?: Options | undefined
426): HttpServerResponse =>
427 makeResponse({
428 status: options?.status ?? 200,
429 statusText: options?.statusText,
430 headers: options?.headers && Headers.fromInput(options.headers),
431 cookies: options?.cookies,
432 body: Body.raw(body, {
433 contentType: options?.contentType,
434 contentLength: options?.contentLength
435 })
436 })
437
438/**
439 * Creates a response whose body is a Web `FormData` value.
440 *
441 * @stability unstable
442 * @category constructors
443 * @since 4.0.0
444 */
445export const formData = (
446 body: FormData,
447 options?: Options.WithContent | undefined
448): HttpServerResponse =>
449 makeResponse({
450 status: options?.status ?? 200,
451 statusText: options?.statusText,
452 headers: options?.headers && Headers.fromInput(options.headers),
453 cookies: options?.cookies,
454 body: Body.formData(body)
455 })
456
457/**
458 * Creates a streaming response from a stream of byte chunks.
459 *
460 * **Details**
461 *
462 * Optional response metadata can supply the status, headers, content type, and
463 * content length.
464 *
465 * @stability unstable
466 * @category constructors
467 * @since 4.0.0
468 */
469export const stream = <E>(
470 body: Stream.Stream<Uint8Array, E>,
471 options?: Options | undefined
472): HttpServerResponse => {
473 const headers = options?.headers
474 ? Headers.fromInput(options.headers)
475 : Headers.empty
476 return makeResponse({
477 status: options?.status ?? 200,
478 statusText: options?.statusText,
479 headers,
480 cookies: options?.cookies,
481 body: Body.stream(
482 body,
483 getContentType(options, headers),
484 options?.contentLength
485 )
486 })
487}
488
489const HttpPlatformKey = Context.Service<
490 HttpPlatform,
491 HttpPlatform["Service"]
492>("effect/http/HttpPlatform" satisfies typeof HttpPlatform.key)
493
494/**
495 * Creates a streamed file response for a file system path.
496 *
497 * **Details**
498 *
499 * The effect requires `HttpPlatform`, can fail with a platform error, and supports
500 * options for status, headers, content type, offset, and byte range.
501 *
502 * @stability unstable
503 * @category constructors
504 * @since 4.0.0
505 */
506export const file = (
507 path: string,
508 options?:
509 | (Options.WithContentType & {
510 readonly bytesToRead?: ByteSize.Input | undefined
511 readonly chunkSize?: number | undefined
512 readonly offset?: ByteSize.Input | undefined
513 })
514 | undefined
515): Effect.Effect<HttpServerResponse, PlatformError, HttpPlatform> =>
516 Effect.flatMap(HttpPlatformKey, (platform) => platform.fileResponse(path, options))
517
518/**
519 * Creates a streamed file response for a Web `File`-like value.
520 *
521 * **Details**
522 *
523 * The effect requires `HttpPlatform` and supports options for status, headers,
524 * content type, offset, and byte range.
525 *
526 * @stability unstable
527 * @category constructors
528 * @since 4.0.0
529 */
530export const fileWeb = (
531 file: Body.HttpBody.FileLike,
532 options?:
533 | (Options.WithContentType & {
534 readonly bytesToRead?: number | undefined
535 readonly chunkSize?: number | undefined
536 readonly offset?: number | undefined
537 })
538 | undefined
539): Effect.Effect<HttpServerResponse, never, HttpPlatform> =>
540 Effect.flatMap(HttpPlatformKey, (platform) => platform.fileWebResponse(file, options))
541
542/**
543 * Returns a response with the specified header set to the supplied value.
544 *
545 * @stability unstable
546 * @category combinators
547 * @since 4.0.0
548 */
549export const setHeader: {
550 (
551 key: string,
552 value: string
553 ): (self: HttpServerResponse) => HttpServerResponse
554 (self: HttpServerResponse, key: string, value: string): HttpServerResponse
555} = dual(
556 3,
557 (self: HttpServerResponse, key: string, value: string): HttpServerResponse =>
558 makeResponse(self, Headers.set(self.headers, key, value))
559)
560
561/**
562 * Returns a response with the specified header removed.
563 *
564 * @stability unstable
565 * @category combinators
566 * @since 4.0.0
567 */
568export const removeHeader: {
569 (key: string): (self: HttpServerResponse) => HttpServerResponse
570 (self: HttpServerResponse, key: string): HttpServerResponse
571} = dual(
572 2,
573 (self: HttpServerResponse, key: string): HttpServerResponse =>
574 makeResponse({
575 ...self,
576 headers: Headers.remove(self.headers, key)
577 })
578)
579
580/**
581 * Returns a response with all supplied headers set on the existing header map.
582 *
583 * @stability unstable
584 * @category combinators
585 * @since 4.0.0
586 */
587export const setHeaders: {
588 (input: Headers.Input): (self: HttpServerResponse) => HttpServerResponse
589 (self: HttpServerResponse, input: Headers.Input): HttpServerResponse
590} = dual(
591 2,
592 (self: HttpServerResponse, input: Headers.Input): HttpServerResponse =>
593 makeResponse(self, Headers.setAll(self.headers, input))
594)
595
596/**
597 * Returns a response with the cookie of the specified name removed.
598 *
599 * @stability unstable
600 * @category combinators
601 * @since 4.0.0
602 */
603export const removeCookie: {
604 (name: string): (self: HttpServerResponse) => HttpServerResponse
605 (self: HttpServerResponse, name: string): HttpServerResponse
606} = dual(
607 2,
608 (self: HttpServerResponse, name: string): HttpServerResponse =>
609 makeResponse({
610 ...self,
611 cookies: Cookies.remove(self.cookies, name)
612 })
613)
614
615/**
616 * Returns a response with its cookie collection replaced by the supplied cookies.
617 *
618 * @stability unstable
619 * @category combinators
620 * @since 4.0.0
621 */
622export const replaceCookies: {
623 (cookies: Cookies.Cookies): (self: HttpServerResponse) => HttpServerResponse
624 (self: HttpServerResponse, cookies: Cookies.Cookies): HttpServerResponse
625} = dual(
626 2,
627 (self: HttpServerResponse, cookies: Cookies.Cookies): HttpServerResponse => makeResponse({ ...self, cookies })
628)
629
630/**
631 * Sets a cookie on the response.
632 *
633 * **Details**
634 *
635 * The effect fails with `CookiesError` if the cookie name, value, or options are
636 * invalid.
637 *
638 * @stability unstable
639 * @category combinators
640 * @since 4.0.0
641 */
642export const setCookie: {
643 (
644 name: string,
645 value: string,
646 options?: Cookies.Cookie["options"]
647 ): (
648 self: HttpServerResponse
649 ) => Effect.Effect<HttpServerResponse, Cookies.CookiesError>
650 (
651 self: HttpServerResponse,
652 name: string,
653 value: string,
654 options?: Cookies.Cookie["options"]
655 ): Effect.Effect<HttpServerResponse, Cookies.CookiesError>
656} = dual(
657 (args) => isHttpServerResponse(args[0]),
658 (
659 self: HttpServerResponse,
660 name: string,
661 value: string,
662 options?: Cookies.Cookie["options"]
663 ): Effect.Effect<HttpServerResponse, Cookies.CookiesError> =>
664 Effect.map(
665 Effect.fromResult(Cookies.set(self.cookies, name, value, options)),
666 (cookies) =>
667 makeResponse({
668 ...self,
669 cookies
670 })
671 )
672)
673
674/**
675 * Sets an expired cookie on an `HttpServerResponse`.
676 *
677 * **Details**
678 *
679 * Returns an effect because cookie encoding can fail. The original response is not
680 * mutated; the effect succeeds with a response containing the updated cookie set.
681 *
682 * @stability unstable
683 * @category combinators
684 * @since 4.0.0
685 */
686export const expireCookie: {
687 (
688 name: string,
689 options?: Omit<NonNullable<Cookies.Cookie["options"]>, "expires" | "maxAge">
690 ): (
691 self: HttpServerResponse
692 ) => Effect.Effect<HttpServerResponse, Cookies.CookiesError>
693 (
694 self: HttpServerResponse,
695 name: string,
696 options?: Omit<NonNullable<Cookies.Cookie["options"]>, "expires" | "maxAge">
697 ): Effect.Effect<HttpServerResponse, Cookies.CookiesError>
698} = dual(
699 (args) => isHttpServerResponse(args[0]),
700 (
701 self: HttpServerResponse,
702 name: string,
703 options?: Omit<NonNullable<Cookies.Cookie["options"]>, "expires" | "maxAge">
704 ): Effect.Effect<HttpServerResponse, Cookies.CookiesError> =>
705 Effect.map(
706 Effect.fromResult(Cookies.expireCookie(self.cookies, name, options)),
707 (cookies) =>
708 makeResponse({
709 ...self,
710 cookies
711 })
712 )
713)
714
715/**
716 * Sets a cookie on an `HttpServerResponse`, throwing if the cookie cannot be
717 * encoded.
718 *
719 * **When to use**
720 *
721 * Use when you need to set one trusted cookie and want encoding failures to
722 * throw instead of being represented as `CookiesError` failures.
723 *
724 * @stability unstable
725 * @category combinators
726 * @since 4.0.0
727 */
728export const setCookieUnsafe: {
729 (
730 name: string,
731 value: string,
732 options?: Cookies.Cookie["options"]
733 ): (self: HttpServerResponse) => HttpServerResponse
734 (
735 self: HttpServerResponse,
736 name: string,
737 value: string,
738 options?: Cookies.Cookie["options"]
739 ): HttpServerResponse
740} = dual(
741 (args) => isHttpServerResponse(args[0]),
742 (
743 self: HttpServerResponse,
744 name: string,
745 value: string,
746 options?: Cookies.Cookie["options"]
747 ): HttpServerResponse =>
748 makeResponse({
749 ...self,
750 cookies: Cookies.setUnsafe(self.cookies, name, value, options)
751 })
752)
753
754/**
755 * Sets an expired cookie on an `HttpServerResponse`, throwing if the expiration cookie
756 * cannot be encoded.
757 *
758 * **When to use**
759 *
760 * Use when you need to expire one trusted cookie and want encoding failures to
761 * throw instead of being represented as `CookiesError` failures.
762 *
763 * @stability unstable
764 * @category combinators
765 * @since 4.0.0
766 */
767export const expireCookieUnsafe: {
768 (
769 name: string,
770 options?: Omit<NonNullable<Cookies.Cookie["options"]>, "expires" | "maxAge">
771 ): (self: HttpServerResponse) => HttpServerResponse
772 (
773 self: HttpServerResponse,
774 name: string,
775 options?: Omit<NonNullable<Cookies.Cookie["options"]>, "expires" | "maxAge">
776 ): HttpServerResponse
777} = dual(
778 (args) => isHttpServerResponse(args[0]),
779 (
780 self: HttpServerResponse,
781 name: string,
782 options?: Omit<NonNullable<Cookies.Cookie["options"]>, "expires" | "maxAge">
783 ): HttpServerResponse =>
784 makeResponse({
785 ...self,
786 cookies: Cookies.expireCookieUnsafe(self.cookies, name, options)
787 })
788)
789
790/**
791 * Updates the cookies attached to an `HttpServerResponse` using the supplied
792 * function.
793 *
794 * **Details**
795 *
796 * The original response is not mutated; a new response is returned with the
797 * callback result as its cookie collection.
798 *
799 * @stability unstable
800 * @category combinators
801 * @since 4.0.0
802 */
803export const updateCookies: {
804 (
805 f: (cookies: Cookies.Cookies) => Cookies.Cookies
806 ): (self: HttpServerResponse) => HttpServerResponse
807 (
808 self: HttpServerResponse,
809 f: (cookies: Cookies.Cookies) => Cookies.Cookies
810 ): HttpServerResponse
811} = dual(
812 2,
813 (
814 self: HttpServerResponse,
815 f: (cookies: Cookies.Cookies) => Cookies.Cookies
816 ): HttpServerResponse =>
817 makeResponse({
818 ...self,
819 cookies: f(self.cookies)
820 })
821)
822
823/**
824 * Merges additional cookies into the cookies attached to an
825 * `HttpServerResponse`.
826 *
827 * **Details**
828 *
829 * The original response is not mutated; a new response is returned with the merged
830 * cookie collection.
831 *
832 * @stability unstable
833 * @category combinators
834 * @since 4.0.0
835 */
836export const mergeCookies: {
837 (cookies: Cookies.Cookies): (self: HttpServerResponse) => HttpServerResponse
838 (self: HttpServerResponse, cookies: Cookies.Cookies): HttpServerResponse
839} = dual(
840 2,
841 (self: HttpServerResponse, cookies: Cookies.Cookies): HttpServerResponse =>
842 makeResponse({ ...self, cookies: Cookies.merge(self.cookies, cookies) })
843)
844
845/**
846 * Sets multiple cookies on an `HttpServerResponse`.
847 *
848 * **Details**
849 *
850 * Each input entry contains a cookie name, value, and optional cookie options. The
851 * returned effect fails with `CookiesError` if any cookie cannot be encoded.
852 *
853 * @stability unstable
854 * @category combinators
855 * @since 4.0.0
856 */
857export const setCookies: {
858 (
859 cookies: Iterable<
860 readonly [
861 name: string,
862 value: string,
863 options?: Cookies.Cookie["options"]
864 ]
865 >
866 ): (
867 self: HttpServerResponse
868 ) => Effect.Effect<HttpServerResponse, Cookies.CookiesError, never>
869 (
870 self: HttpServerResponse,
871 cookies: Iterable<
872 readonly [
873 name: string,
874 value: string,
875 options?: Cookies.Cookie["options"]
876 ]
877 >
878 ): Effect.Effect<HttpServerResponse, Cookies.CookiesError, never>
879} = dual(
880 2,
881 (
882 self: HttpServerResponse,
883 cookies: Iterable<
884 readonly [
885 name: string,
886 value: string,
887 options?: Cookies.Cookie["options"]
888 ]
889 >
890 ): Effect.Effect<HttpServerResponse, Cookies.CookiesError> =>
891 Effect.map(Effect.fromResult(Cookies.setAll(self.cookies, cookies)), (cookies) =>
892 makeResponse({
893 ...self,
894 cookies
895 }))
896)
897
898/**
899 * Sets multiple cookies on an `HttpServerResponse`, throwing if any cookie cannot
900 * be encoded.
901 *
902 * **When to use**
903 *
904 * Use when you need to set multiple trusted cookies and want encoding failures
905 * to throw instead of being represented as `CookiesError` failures.
906 *
907 * @stability unstable
908 * @category combinators
909 * @since 4.0.0
910 */
911export const setCookiesUnsafe: {
912 (
913 cookies: Iterable<
914 readonly [
915 name: string,
916 value: string,
917 options?: Cookies.Cookie["options"]
918 ]
919 >
920 ): (self: HttpServerResponse) => HttpServerResponse
921 (
922 self: HttpServerResponse,
923 cookies: Iterable<
924 readonly [
925 name: string,
926 value: string,
927 options?: Cookies.Cookie["options"]
928 ]
929 >
930 ): HttpServerResponse
931} = dual(
932 2,
933 (
934 self: HttpServerResponse,
935 cookies: Iterable<
936 readonly [
937 name: string,
938 value: string,
939 options?: Cookies.Cookie["options"]
940 ]
941 >
942 ): HttpServerResponse =>
943 makeResponse({
944 ...self,
945 cookies: Cookies.setAllUnsafe(self.cookies, cookies)
946 })
947)
948
949/**
950 * Replaces the body of an `HttpServerResponse`.
951 *
952 * **Details**
953 *
954 * When the body carries a content type or content length, the returned response
955 * includes the corresponding headers.
956 *
957 * @stability unstable
958 * @category combinators
959 * @since 4.0.0
960 */
961export const setBody: {
962 (body: Body.HttpBody): (self: HttpServerResponse) => HttpServerResponse
963 (self: HttpServerResponse, body: Body.HttpBody): HttpServerResponse
964} = dual(
965 2,
966 (self: HttpServerResponse, body: Body.HttpBody): HttpServerResponse =>
967 makeResponse({ ...self, body }, bodyInternal.updateHeaders(self.headers, body))
968)
969
970/**
971 * Replaces the body without updating headers. Use when content metadata is unchanged.
972 *
973 * @internal
974 */
975export const setBodyKeepHeaders = (self: HttpServerResponse, body: Body.HttpBody): HttpServerResponse => {
976 const response = Object.create(Proto) as Mutable<HttpServerResponse>
977 response.status = self.status
978 response.statusText = self.statusText
979 response.headers = self.headers
980 response.cookies = self.cookies
981 response.body = body
982 return response
983}
984
985/**
986 * Sets the HTTP status code of an `HttpServerResponse`.
987 *
988 * **Details**
989 *
990 * When `statusText` is omitted, the existing status text is preserved.
991 *
992 * @stability unstable
993 * @category combinators
994 * @since 4.0.0
995 */
996export const setStatus: {
997 (
998 status: number,
999 statusText?: string | undefined
1000 ): (self: HttpServerResponse) => HttpServerResponse
1001 (
1002 self: HttpServerResponse,
1003 status: number,
1004 statusText?: string | undefined
1005 ): HttpServerResponse
1006} = dual(
1007 (args) => isHttpServerResponse(args[0]),
1008 (
1009 self: HttpServerResponse,
1010 status: number,
1011 statusText?: string | undefined
1012 ): HttpServerResponse =>
1013 makeResponse({
1014 ...self,
1015 status,
1016 statusText: statusText ?? self.statusText
1017 })
1018)
1019
1020/**
1021 * Returns whether a response must omit its body because its status is 204, 205,
1022 * or 304, or `withoutBody` is true.
1023 *
1024 * @stability unstable
1025 * @category predicates
1026 * @since 4.0.0
1027 */
1028export const omitsBody = (response: HttpServerResponse, withoutBody = false): boolean =>
1029 withoutBody || response.status === 204 || response.status === 205 || response.status === 304
1030
1031/**
1032 * Converts an `HttpServerResponse` to a Web `Response`.
1033 *
1034 * **Details**
1035 *
1036 * Cookies are appended as `Set-Cookie` headers. Stream bodies are converted using
1037 * the supplied context.
1038 *
1039 * Bodies are omitted for statuses 204, 205, and 304, or when `withoutBody` is true
1040 * (for example, for HEAD responses). Omitted raw `ReadableStream` bodies are
1041 * cancelled without awaiting completion, and cancellation errors are ignored.
1042 *
1043 * A raw `Response` body is returned as-is after the outer headers are merged
1044 * into it: outer headers replace native ones, and cookies are appended. When
1045 * its body is omitted, the bodyless `Response` keeps those merged headers and
1046 * uses the outer status for 204, 205 and 304, or the raw `Response` status
1047 * otherwise.
1048 *
1049 * @stability unstable
1050 * @category converting
1051 * @since 4.0.0
1052 */
1053export const toWeb = (
1054 response: HttpServerResponse,
1055 options?: {
1056 readonly withoutBody?: boolean | undefined
1057 readonly context?: Context.Context<never> | undefined
1058 }
1059): Response => {
1060 const headers = new globalThis.Headers(response.headers)
1061 if (!Cookies.isEmpty(response.cookies)) {
1062 const toAdd = Cookies.toSetCookieHeaders(response.cookies)
1063 for (const header of toAdd) {
1064 headers.append("set-cookie", header)
1065 }
1066 }
1067 const body = response.body
1068 const outerOmitsBody = omitsBody(response)
1069 const withoutBody = outerOmitsBody || options?.withoutBody === true
1070 if (body._tag === "Raw" && body.body instanceof Response) {
1071 const raw = body.body
1072 for (const [key, value] of headers as any) {
1073 if (key === "set-cookie") {
1074 raw.headers.append(key, value)
1075 } else {
1076 raw.headers.set(key, value)
1077 }
1078 }
1079 if (!withoutBody) {
1080 return raw
1081 }
1082 // A bodyless outer status is what the caller chose to send; otherwise the
1083 // body is only omitted for HEAD and the raw Response describes the resource.
1084 const source = outerOmitsBody ? response : raw
1085 return new Response(undefined, {
1086 status: source.status,
1087 statusText: source.statusText as string,
1088 headers: raw.headers
1089 })
1090 }
1091 if (withoutBody) {
1092 if (body._tag === "Raw" && isReadableStream(body.body)) {
1093 body.body.cancel().catch(constVoid)
1094 }
1095 return new Response(undefined, {
1096 status: response.status,
1097 statusText: response.statusText as string,
1098 headers
1099 })
1100 }
1101 switch (body._tag) {
1102 case "Empty": {
1103 return new Response(undefined, {
1104 status: response.status,
1105 statusText: response.statusText as string,
1106 headers
1107 })
1108 }
1109 case "Uint8Array": {
1110 return new Response(body.text ?? body.body as any, {
1111 status: response.status,
1112 statusText: response.statusText!,
1113 headers
1114 })
1115 }
1116 case "Raw": {
1117 return new Response(body.body as any, {
1118 status: response.status,
1119 statusText: response.statusText!,
1120 headers
1121 })
1122 }
1123 case "FormData": {
1124 return new Response(body.formData as any, {
1125 status: response.status,
1126 statusText: response.statusText!,
1127 headers
1128 })
1129 }
1130 case "Stream": {
1131 return new Response(
1132 Stream.toReadableStreamWith(
1133 body.stream,
1134 options?.context ?? Context.empty()
1135 ),
1136 {
1137 status: response.status,
1138 statusText: response.statusText!,
1139 headers
1140 }
1141 )
1142 }
1143 }
1144}
1145
1146/**
1147 * Wraps an `HttpServerResponse` as an `HttpClientResponse`, using the optional
1148 * request for metadata and decode errors. Without a request, `url` is empty.
1149 *
1150 * @stability unstable
1151 * @category converting
1152 * @since 4.0.0
1153 */
1154export const toClientResponse = (
1155 response: HttpServerResponse,
1156 options?: {
1157 readonly request?: HttpClientRequest.HttpClientRequest | undefined
1158 }
1159): HttpClientResponse.HttpClientResponse =>
1160 new ServerHttpClientResponse(
1161 options?.request ?? HttpClientRequest.empty,
1162 response
1163 )
1164
1165class ServerHttpClientResponse extends Inspectable.Class implements HttpClientResponse.HttpClientResponse, Pipeable {
1166 readonly [HttpIncomingMessage.TypeId]: typeof HttpIncomingMessage.TypeId
1167 readonly [HttpClientResponse.TypeId]: typeof HttpClientResponse.TypeId
1168
1169 readonly request: HttpClientRequest.HttpClientRequest
1170 private readonly response: HttpServerResponse
1171
1172 constructor(
1173 request: HttpClientRequest.HttpClientRequest,
1174 response: HttpServerResponse
1175 ) {
1176 super()
1177 this.request = request
1178 this.response = response
1179 this[HttpIncomingMessage.TypeId] = HttpIncomingMessage.TypeId
1180 this[HttpClientResponse.TypeId] = HttpClientResponse.TypeId
1181 }
1182
1183 toJSON(): unknown {
1184 return HttpIncomingMessage.inspect(this, {
1185 _id: "HttpClientResponse",
1186 request: this.request.toJSON(),
1187 status: this.status
1188 })
1189 }
1190
1191 get status(): number {
1192 return this.response.status
1193 }
1194
1195 get url(): string {
1196 if (this.request === HttpClientRequest.empty) return ""
1197 const url = HttpClientRequest.toUrl(this.request)
1198 if (Option.isNone(url)) return ""
1199 url.value.hash = ""
1200 return url.value.href
1201 }
1202
1203 private cachedHeaders?: Headers.Headers
1204 get headers(): Headers.Headers {
1205 return this.cachedHeaders ??= this.response.body._tag === "FormData"
1206 ? Headers.merge(this.response.headers, Headers.fromInput(this.getFormDataResponse().headers))
1207 : this.response.headers
1208 }
1209
1210 get cookies(): Cookies.Cookies {
1211 return this.response.cookies
1212 }
1213
1214 get remoteAddress(): Option.Option<string> {
1215 return Option.none()
1216 }
1217
1218 get stream(): Stream.Stream<Uint8Array, HttpClientError.HttpClientError> {
1219 const body = this.response.body
1220 switch (body._tag) {
1221 case "Empty": {
1222 return Stream.empty
1223 }
1224 case "Stream": {
1225 return Stream.mapError(body.stream, (cause) => this.decodeError(cause))
1226 }
1227 case "Uint8Array": {
1228 return Stream.succeed(body.body)
1229 }
1230 case "Raw": {
1231 const rawBody = body.body
1232 if (rawBody instanceof Response) {
1233 return rawBody.body
1234 ? Stream.fromReadableStream({
1235 evaluate: () => rawBody.body!,
1236 onError: (cause) => this.decodeError(cause)
1237 })
1238 : Stream.empty
1239 }
1240 if (isReadableStream(rawBody)) {
1241 return Stream.fromReadableStream({
1242 evaluate: () => rawBody,
1243 onError: (cause) => this.decodeError(cause)
1244 })
1245 }
1246 if (rawBody instanceof Blob) {
1247 return Stream.fromReadableStream({
1248 evaluate: () => rawBody.stream(),
1249 onError: (cause) => this.decodeError(cause)
1250 })
1251 }
1252 return Stream.unwrap(Effect.map(this.bytes, Stream.succeed))
1253 }
1254 case "FormData": {
1255 const response = this.getFormDataResponse()
1256 return Stream.fromReadableStream({
1257 evaluate: () => response.body!,
1258 onError: (cause) => this.decodeError(cause)
1259 })
1260 }
1261 }
1262 }
1263
1264 get json(): Effect.Effect<Schema.Json, HttpClientError.HttpClientError> {
1265 return Effect.flatMap(this.text, (text) =>
1266 Effect.try({
1267 try: () => text === "" ? null : JSON.parse(text),
1268 catch: (cause) =>
1269 new HttpClientError.HttpClientError({
1270 reason: new HttpClientError.DecodeError({
1271 request: this.request,
1272 response: this,
1273 cause
1274 })
1275 })
1276 }))
1277 }
1278
1279 private get bytes(): Effect.Effect<Uint8Array, HttpClientError.HttpClientError> {
1280 const body = this.response.body
1281 switch (body._tag) {
1282 case "Empty": {
1283 return Effect.succeed(new Uint8Array(0))
1284 }
1285 case "Uint8Array": {
1286 return Effect.succeed(body.body)
1287 }
1288 case "Stream": {
1289 return Stream.mkUint8Array(this.stream)
1290 }
1291 case "Raw": {
1292 const rawBody = body.body
1293 if (rawBody instanceof Response) {
1294 return Effect.tryPromise({
1295 try: () => rawBody.clone().arrayBuffer().then((buffer) => new Uint8Array(buffer)),
1296 catch: (cause) => this.decodeError(cause)
1297 })
1298 }
1299 return Effect.tryPromise({
1300 try: () => new Response(rawBody as any).arrayBuffer().then((buffer) => new Uint8Array(buffer)),
1301 catch: (cause) => this.decodeError(cause)
1302 })
1303 }
1304 case "FormData": {
1305 return Effect.tryPromise({
1306 try: () => new Response(body.formData).arrayBuffer().then((buffer) => new Uint8Array(buffer)),
1307 catch: (cause) => this.decodeError(cause)
1308 })
1309 }
1310 }
1311 }
1312
1313 get text(): Effect.Effect<string, HttpClientError.HttpClientError> {
1314 return Effect.map(this.bytes, (bytes) => textDecoder.decode(bytes))
1315 }
1316
1317 get urlParamsBody(): Effect.Effect<UrlParams.UrlParams, HttpClientError.HttpClientError> {
1318 return Effect.flatMap(this.text, (_) =>
1319 Effect.try({
1320 try: () => UrlParams.fromInput(new URLSearchParams(_)),
1321 catch: (cause) =>
1322 new HttpClientError.HttpClientError({
1323 reason: new HttpClientError.DecodeError({
1324 request: this.request,
1325 response: this,
1326 cause
1327 })
1328 })
1329 }))
1330 }
1331
1332 get formData(): Effect.Effect<FormData, HttpClientError.HttpClientError> {
1333 const body = this.response.body
1334 if (body._tag === "FormData") {
1335 return Effect.succeed(body.formData)
1336 }
1337 return Effect.contextWith((context: Context.Context<never>) => {
1338 const readableStream = Stream.toReadableStreamWith(this.stream, context)
1339 return Effect.tryPromise({
1340 try: () => new Response(readableStream, { headers: this.headers }).formData(),
1341 catch: (cause) => this.decodeError(cause)
1342 })
1343 })
1344 }
1345
1346 get arrayBuffer(): Effect.Effect<ArrayBuffer, HttpClientError.HttpClientError> {
1347 return Effect.map(
1348 this.bytes,
1349 // Copy this view because Buffer views may share a larger ArrayBuffer.
1350 (bytes) => bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer
1351 )
1352 }
1353
1354 private decodeError(cause: unknown): HttpClientError.HttpClientError {
1355 return new HttpClientError.HttpClientError({
1356 reason: new HttpClientError.DecodeError({
1357 request: this.request,
1358 response: this,
1359 cause
1360 })
1361 })
1362 }
1363
1364 private formDataResponse?: Response
1365 private getFormDataResponse(): Response {
1366 return this.formDataResponse ??= new Response((this.response.body as Body.FormData).formData)
1367 }
1368
1369 pipe() {
1370 return pipeArguments(this, arguments)
1371 }
1372}
1373
1374const textDecoder = new TextDecoder()
1375
1376/**
1377 * Converts an `HttpClientResponse` to an `HttpServerResponse`.
1378 *
1379 * **Details**
1380 *
1381 * The response body is streamed from the client response. `Set-Cookie` headers are
1382 * removed from the header map and represented in the response cookie collection.
1383 *
1384 * @stability unstable
1385 * @category converting
1386 * @since 4.0.0
1387 */
1388export const fromClientResponse = (
1389 response: HttpClientResponse.HttpClientResponse
1390): HttpServerResponse => {
1391 const headers = Headers.remove(response.headers, "set-cookie")
1392 return makeResponse({
1393 status: response.status,
1394 headers,
1395 cookies: response.cookies,
1396 body: Body.stream(
1397 Stream.catchIf(response.stream, isEmptyBodyError, () => Stream.empty),
1398 Option.getOrUndefined(Headers.get(headers, "content-type")),
1399 bodyInternal.parseContentLength(headers["content-length"])
1400 )
1401 })
1402}
1403
1404const isReadableStream = (u: unknown): u is ReadableStream<Uint8Array> =>
1405 typeof ReadableStream !== "undefined" && u instanceof ReadableStream
1406
1407const isEmptyBodyError = (
1408 error: HttpClientError.HttpClientError
1409): error is HttpClientError.HttpClientError =>
1410 HttpClientError.isHttpClientError(error) && error.reason._tag === "EmptyBodyError"
1411
1412const Proto: Omit<
1413 HttpServerResponse,
1414 "status" | "statusText" | "headers" | "cookies" | "body"
1415> = {
1416 ...PipeInspectableProto,
1417 [TypeId]: TypeId,
1418 [ErrorReporter.ignore]: true,
1419 toJSON(this: HttpServerResponse) {
1420 return {
1421 _id: "HttpServerResponse",
1422 status: this.status,
1423 statusText: this.statusText,
1424 headers: redact(this.headers),
1425 cookies: this.cookies.toJSON(),
1426 body: this.body.toJSON()
1427 }
1428 }
1429}
1430
1431const makeResponse = (options: {
1432 readonly status: number
1433 readonly statusText?: string | undefined
1434 readonly headers?: Headers.Headers | undefined
1435 readonly cookies?: Cookies.Cookies | undefined
1436 readonly body?: Body.HttpBody | undefined
1437}, ownedHeaders?: Headers.Headers) => {
1438 // ownedHeaders is a freshly created map that nothing else references, so it
1439 // can be completed in place instead of copied. It supersedes options.headers,
1440 // and explicit content headers in it take precedence over body-derived ones.
1441 const self = Object.create(Proto) as Mutable<HttpServerResponse>
1442 self.status = options.status
1443 self.statusText = options.statusText
1444 self.cookies = options.cookies ?? Cookies.empty
1445 self.body = options.body ?? Body.empty
1446 if (
1447 self.body._tag !== "Empty" &&
1448 (self.body.contentType || self.body.contentLength !== undefined)
1449 ) {
1450 const owned = ownedHeaders !== undefined
1451 const newHeaders = owned
1452 ? ownedHeaders as any
1453 : options.headers === undefined || options.headers === Headers.empty
1454 ? headersInternal.emptyMutableUnsafe() as any
1455 : Headers.fromRecordUnsafe({ ...options.headers }) as any
1456 if (self.body.contentType && (!owned || newHeaders["content-type"] === undefined)) {
1457 newHeaders["content-type"] = self.body.contentType
1458 }
1459 if (self.body.contentLength !== undefined && (!owned || newHeaders["content-length"] === undefined)) {
1460 newHeaders["content-length"] = self.body.contentLength.toString()
1461 }
1462 self.headers = newHeaders
1463 } else {
1464 self.headers = ownedHeaders ?? options.headers ?? Headers.empty
1465 }
1466 return self
1467}
1468
1469/**
1470 * Converts a Web `Response` to an `HttpServerResponse`.
1471 *
1472 * **Details**
1473 *
1474 * `Set-Cookie` headers are parsed into the response cookie collection and removed
1475 * from the header map. A present Web body is exposed as a stream body.
1476 *
1477 * @stability unstable
1478 * @category converting
1479 * @since 4.0.0
1480 */
1481export const fromWeb = (response: Response): HttpServerResponse => {
1482 const headers = new globalThis.Headers(response.headers)
1483 const setCookieHeaders = headers.getSetCookie()
1484 headers.delete("set-cookie")
1485 let self = empty({
1486 status: response.status,
1487 statusText: response.statusText,
1488 headers: headers as any,
1489 cookies: Cookies.fromSetCookie(setCookieHeaders)
1490 })
1491 if (response.body) {
1492 const contentType = response.headers.get("content-type")
1493 self = setBody(
1494 self,
1495 Body.stream(
1496 Stream.fromReadableStream({
1497 evaluate: () => response.body!,
1498 onError: (e) => e
1499 }),
1500 contentType ?? undefined,
1501 bodyInternal.parseContentLength(response.headers.get("content-length"))
1502 )
1503 )
1504 }
1505 return self
1506}