| 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 | */ |
| 14 | import * as Effect from "effect/Effect" |
| 15 | import * as Headers from "effect/http/Headers" |
| 16 | import * as IncomingMessage from "effect/http/HttpIncomingMessage" |
| 17 | import * as UrlParams from "effect/http/UrlParams" |
| 18 | import * as Inspectable from "effect/Inspectable" |
| 19 | import * as Option from "effect/Option" |
| 20 | import type * as Schema from "effect/Schema" |
| 21 | import type * as Stream from "effect/Stream" |
| 22 | import type * as Http from "node:http" |
| 23 | import * 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 | */ |
| 41 | export 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 | } |