1/**
2 * Common model for incoming HTTP messages.
3 *
4 * `HttpIncomingMessage` is used by server requests and client responses to
5 * expose headers, an optional remote address, and body accessors. This module
6 * provides decoders for JSON bodies, URL-encoded bodies, and headers, plus the
7 * shared body-size setting and inspection helper used by request and response
8 * implementations.
9 *
10 * @stability unstable
11 * @since 4.0.0
12 */
13import type * as ByteSize from "../ByteSize.ts"
14import * as Context from "../Context.ts"
15import * as Effect from "../Effect.ts"
16import type * as Inspectable from "../Inspectable.ts"
17import type * as Option from "../Option.ts"
18import { hasProperty } from "../Predicate.ts"
19import { redact } from "../Redactable.ts"
20import * as Schema from "../Schema.ts"
21import type { ParseOptions } from "../SchemaAST.ts"
22import type * as Stream from "../Stream.ts"
23import type * as Headers from "./Headers.ts"
24import type * as UrlParams from "./UrlParams.ts"
25
26/**
27 * Type identifier for `HttpIncomingMessage` values.
28 *
29 * @stability unstable
30 * @category type IDs
31 * @since 4.0.0
32 */
33export const TypeId = "~effect/http/HttpIncomingMessage"
34
35/**
36 * Returns `true` when a value is an `HttpIncomingMessage`.
37 *
38 * @stability unstable
39 * @category guards
40 * @since 4.0.0
41 */
42export const isHttpIncomingMessage = (u: unknown): u is HttpIncomingMessage => hasProperty(u, TypeId)
43
44/**
45 * Options for parsing an incoming HTTP message body as JSON.
46 *
47 * @stability unstable
48 * @category models
49 * @since 4.0.0
50 */
51export interface JsonOptions {
52 readonly reviver?: Parameters<typeof JSON.parse>[1] | undefined
53}
54
55/**
56 * Common model for incoming HTTP messages, with headers, remote address, and effectful body accessors.
57 *
58 * @stability unstable
59 * @category models
60 * @since 4.0.0
61 */
62export interface HttpIncomingMessage<E = unknown> extends Inspectable.Inspectable {
63 readonly [TypeId]: typeof TypeId
64 readonly headers: Headers.Headers
65 readonly remoteAddress: Option.Option<string>
66 readonly json: Effect.Effect<Schema.Json, E>
67 readonly text: Effect.Effect<string, E>
68 readonly urlParamsBody: Effect.Effect<UrlParams.UrlParams, E>
69 readonly arrayBuffer: Effect.Effect<ArrayBuffer, E>
70 readonly stream: Stream.Stream<Uint8Array, E>
71}
72
73/**
74 * Creates a decoder that reads an incoming message's JSON body and decodes it with the supplied schema.
75 *
76 * @stability unstable
77 * @category schemas
78 * @since 4.0.0
79 */
80export const schemaBodyJson = <S extends Schema.Constraint>(
81 schema: S,
82 options?: (ParseOptions & JsonOptions) | undefined
83) => {
84 const decode = Schema.decodeEffect(Schema.toCodecJson(schema))
85 const decodeJson = options?.reviver === undefined
86 ? undefined
87 : Schema.decodeEffect(Schema.fromJsonString(Schema.toCodecJson(schema), options))
88 return <E>(
89 self: HttpIncomingMessage<E>
90 ): Effect.Effect<S["Type"], E | Schema.SchemaError, S["DecodingServices"]> =>
91 decodeJson === undefined
92 ? Effect.flatMap(self.json, (u) => decode(u, options))
93 : Effect.flatMap(self.text, (body) =>
94 body === "" ? Effect.flatMap(self.json, (u) => decode(u, options)) : decodeJson(body, options))
95}
96
97/**
98 * Creates a decoder that reads an incoming message's URL-encoded body parameters and decodes them with the supplied schema.
99 *
100 * @stability unstable
101 * @category schemas
102 * @since 4.0.0
103 */
104export const schemaBodyUrlParams = <
105 A,
106 I extends Readonly<Record<string, string | ReadonlyArray<string> | undefined>>,
107 RD
108>(
109 schema: Schema.ConstraintCodec<A, I, RD, unknown>,
110 options?: ParseOptions | undefined
111) => {
112 const decode = Schema.RecordFromUrlParams.pipe(
113 Schema.decodeTo(schema),
114 Schema.decodeEffect
115 )
116 return <E>(self: HttpIncomingMessage<E>): Effect.Effect<A, E | Schema.SchemaError, RD> =>
117 Effect.flatMap(self.urlParamsBody, (u) => decode(u, options))
118}
119
120/**
121 * Creates a decoder that validates and decodes an incoming message's headers with the supplied schema.
122 *
123 * @stability unstable
124 * @category schemas
125 * @since 4.0.0
126 */
127export const schemaHeaders = <A, I extends Readonly<Record<string, string | undefined>>, RD>(
128 schema: Schema.ConstraintCodec<A, I, RD, unknown>,
129 options?: ParseOptions | undefined
130) => {
131 const decode = Schema.decodeUnknownEffect(schema)
132 return <E>(self: HttpIncomingMessage<E>): Effect.Effect<A, Schema.SchemaError, RD> => decode(self.headers, options)
133}
134
135/**
136 * Context reference for the optional maximum size allowed when reading an incoming message body.
137 *
138 * @stability unstable
139 * @category references
140 * @since 4.0.0
141 */
142export const MaxBodySize = Context.Reference<ByteSize.ByteSize | undefined>(
143 "effect/http/HttpIncomingMessage/MaxBodySize",
144 { defaultValue: () => undefined }
145)
146
147/**
148 * Builds an inspectable object for an incoming message, redacting headers and including a synchronously readable JSON or text body when available.
149 *
150 * @stability unstable
151 * @category converting
152 * @since 4.0.0
153 */
154export const inspect = <E>(self: HttpIncomingMessage<E>, that: object): object => {
155 const contentType = self.headers["content-type"] ?? ""
156 let body: unknown
157 if (contentType.includes("application/json")) {
158 try {
159 body = Effect.runSync(self.json)
160 // oxlint-disable-next-line @typescript-eslint/no-unused-vars
161 } catch (_) {
162 //
163 }
164 } else if (contentType.includes("text/") || contentType.includes("urlencoded")) {
165 try {
166 body = Effect.runSync(self.text)
167 // oxlint-disable-next-line @typescript-eslint/no-unused-vars
168 } catch (_) {
169 //
170 }
171 }
172 const obj: any = {
173 ...that,
174 headers: redact(self.headers),
175 remoteAddress: self.remoteAddress
176 }
177 if (body !== undefined) {
178 obj.body = body
179 }
180 return obj
181}