1/**
2 * Adapter base for exposing Node `http.IncomingMessage` values as Effect HTTP
3 * incoming messages.
4 *
5 * Server requests and Node client responses both arrive as Node readable
6 * streams with raw header objects, socket metadata, and one-shot body
7 * consumption. This module's `NodeHttpIncomingMessage` class keeps the original
8 * Node message available while presenting Effect's `HttpIncomingMessage` shape:
9 * typed headers, remote address lookup, stream access, and text, JSON,
10 * URL-encoded, and array-buffer body readers.
11 *
12 * @since 4.0.0
13 */
14import * as Effect from "effect/Effect"
15import * as Headers from "effect/http/Headers"
16import * as IncomingMessage from "effect/http/HttpIncomingMessage"
17import * as UrlParams from "effect/http/UrlParams"
18import * as Inspectable from "effect/Inspectable"
19import * as Option from "effect/Option"
20import type * as Schema from "effect/Schema"
21import type * as Stream from "effect/Stream"
22import type * as Http from "node:http"
23import * as NodeStream from "./NodeStream.ts"
24
25/**
26 * Adapts a Node `IncomingMessage` to Effect HTTP incoming messages.
27 *
28 * **When to use**
29 *
30 * Use to implement Node HTTP request or response adapters that expose the
31 * Effect HTTP incoming-message interface.
32 *
33 * **Details**
34 *
35 * The adapter exposes headers, remote address, stream access, and cached body
36 * decoders. Subclasses provide the error mapping for unknown Node errors.
37 *
38 * @category constructors
39 * @since 4.0.0
40 */
41export abstract class NodeHttpIncomingMessage<E> extends Inspectable.Class
42 implements IncomingMessage.HttpIncomingMessage<E>
43{
44 /**
45 * Marks this value as an HTTP incoming message for runtime guards.
46 *
47 * @since 4.0.0
48 */
49 readonly [IncomingMessage.TypeId]: typeof IncomingMessage.TypeId
50 readonly source: Http.IncomingMessage
51 readonly onError: (error: unknown) => E
52 readonly remoteAddressOverride?: Option.Option<string> | undefined
53
54 constructor(
55 source: Http.IncomingMessage,
56 onError: (error: unknown) => E,
57 remoteAddressOverride?: Option.Option<string>
58 ) {
59 super()
60 this[IncomingMessage.TypeId] = IncomingMessage.TypeId
61 this.source = source
62 this.onError = onError
63 this.remoteAddressOverride = remoteAddressOverride
64 }
65
66 get headers() {
67 return Headers.fromInput(this.source.headers as any)
68 }
69
70 get remoteAddress() {
71 return this.remoteAddressOverride ?? Option.fromNullishOr(this.source.socket?.remoteAddress)
72 }
73
74 private textEffect: Effect.Effect<string, E> | undefined
75 get text(): Effect.Effect<string, E> {
76 if (this.textEffect) {
77 return this.textEffect
78 }
79 this.textEffect = Effect.map(this.arrayBuffer, (_) => Buffer.from(_).toString("utf8"))
80 return this.textEffect
81 }
82
83 get textUnsafe(): string {
84 return Effect.runSync(this.text)
85 }
86
87 get json(): Effect.Effect<Schema.Json, E> {
88 return Effect.flatMap(this.text, (text) =>
89 Effect.try({
90 try: () => text === "" ? null : JSON.parse(text),
91 catch: this.onError
92 }))
93 }
94
95 get jsonUnsafe(): Schema.Json {
96 return Effect.runSync(this.json)
97 }
98
99 get urlParamsBody(): Effect.Effect<UrlParams.UrlParams, E> {
100 return Effect.flatMap(this.text, (_) =>
101 Effect.try({
102 try: () => UrlParams.fromInput(new URLSearchParams(_)),
103 catch: this.onError
104 }))
105 }
106
107 get stream(): Stream.Stream<Uint8Array, E> {
108 return NodeStream.fromReadable({
109 evaluate: () => this.source,
110 onError: this.onError
111 })
112 }
113
114 private arrayBufferEffect: Effect.Effect<ArrayBuffer, E> | undefined
115 get arrayBuffer(): Effect.Effect<ArrayBuffer, E> {
116 if (this.arrayBufferEffect) {
117 return this.arrayBufferEffect
118 }
119 this.arrayBufferEffect = Effect.withFiber((fiber) =>
120 NodeStream.toArrayBuffer(() => this.source, {
121 onError: this.onError,
122 maxBytes: fiber.getRef(IncomingMessage.MaxBodySize)
123 })
124 ).pipe(
125 Effect.cached,
126 Effect.runSync
127 )
128 this.textEffect = Effect.map(this.arrayBufferEffect, (_) => new TextDecoder().decode(_))
129 return this.arrayBufferEffect
130 }
131}