1/**
2 * Service for serving Effect HTTP responses on a concrete HTTP server.
3 *
4 * Platform adapters provide `HttpServer`, and routers or applications consume
5 * it to run an `HttpServerResponse` effect for each incoming request. The
6 * service exposes the listening address, while this module also includes helpers
7 * for address formatting, server logging, and clients that target the current
8 * server in tests.
9 *
10 * @stability unstable
11 * @since 4.0.0
12 */
13import * as Context from "../Context.ts"
14import * as Effect from "../Effect.ts"
15import * as FileSystem from "../FileSystem.ts"
16import { dual } from "../Function.ts"
17import * as Layer from "../Layer.ts"
18import * as NetAddress from "../net/NetAddress.ts"
19import * as Path from "../Path.ts"
20import type * as Scope from "../Scope.ts"
21import * as Etag from "./Etag.ts"
22import * as HttpClient from "./HttpClient.ts"
23import * as ClientRequest from "./HttpClientRequest.ts"
24import type * as Middleware from "./HttpMiddleware.ts"
25import * as HttpPlatform from "./HttpPlatform.ts"
26import type { HttpServerRequest } from "./HttpServerRequest.ts"
27import type { HttpServerResponse } from "./HttpServerResponse.ts"
28
29/**
30 * Service tag for an HTTP server runtime.
31 *
32 * **Details**
33 *
34 * The service can serve an HTTP response effect and exposes the address where the
35 * server is listening.
36 *
37 * @stability unstable
38 * @category services
39 * @since 4.0.0
40 */
41export class HttpServer extends Context.Service<HttpServer, {
42 readonly serve: {
43 <E, R>(effect: Effect.Effect<HttpServerResponse, E, R>): Effect.Effect<
44 void,
45 never,
46 Exclude<R, HttpServerRequest> | Scope.Scope
47 >
48 <E, R, App extends Effect.Effect<HttpServerResponse, any, any>>(
49 effect: Effect.Effect<HttpServerResponse, E, R>,
50 middleware: Middleware.HttpMiddleware.Applied<App, E, R>
51 ): Effect.Effect<
52 void,
53 never,
54 Exclude<R, HttpServerRequest> | Scope.Scope
55 >
56 }
57
58 readonly address: NetAddress.SocketAddress
59}>()("effect/http/HttpServer") {}
60
61/**
62 * Constructs an `HttpServer` service from a serving implementation and listening
63 * address.
64 *
65 * @stability unstable
66 * @category constructors
67 * @since 4.0.0
68 */
69export const make = (
70 options: {
71 readonly serve: (
72 httpEffect: Effect.Effect<HttpServerResponse, unknown, HttpServerRequest | Scope.Scope>,
73 middleware?: Middleware.HttpMiddleware
74 ) => Effect.Effect<void, never, Scope.Scope>
75 readonly address: NetAddress.SocketAddress
76 }
77): HttpServer["Service"] => options
78
79/**
80 * Creates a layer that starts serving an HTTP response effect with the current
81 * `HttpServer`.
82 *
83 * **Details**
84 *
85 * The request service is supplied by the server for each request; the returned
86 * layer still requires the server, a scope, and any non-request dependencies of
87 * the response effect or middleware.
88 *
89 * @stability unstable
90 * @category layers
91 * @since 4.0.0
92 */
93export const serve: {
94 (): <E, R>(
95 effect: Effect.Effect<HttpServerResponse, E, R>
96 ) => Layer.Layer<never, never, HttpServer | Exclude<R, HttpServerRequest | Scope.Scope>>
97 <E, R, App extends Effect.Effect<HttpServerResponse, any, any>>(
98 middleware: Middleware.HttpMiddleware.Applied<App, E, R>
99 ): (
100 effect: Effect.Effect<HttpServerResponse, E, R>
101 ) => Layer.Layer<
102 never,
103 never,
104 HttpServer | Exclude<Effect.Services<App>, HttpServerRequest | Scope.Scope>
105 >
106 <E, R>(
107 effect: Effect.Effect<HttpServerResponse, E, R>
108 ): Layer.Layer<never, never, HttpServer | Exclude<R, HttpServerRequest | Scope.Scope>>
109 <E, R, App extends Effect.Effect<HttpServerResponse, any, any>>(
110 effect: Effect.Effect<HttpServerResponse, E, R>,
111 middleware: Middleware.HttpMiddleware.Applied<App, E, R>
112 ): Layer.Layer<
113 never,
114 never,
115 HttpServer | Exclude<Effect.Services<App>, HttpServerRequest | Scope.Scope>
116 >
117} = dual((args) => Effect.isEffect(args[0]), <E, R, App extends Effect.Effect<HttpServerResponse, any, any>>(
118 effect: Effect.Effect<HttpServerResponse, E, R>,
119 middleware?: Middleware.HttpMiddleware.Applied<App, E, R>
120): Layer.Layer<
121 never,
122 never,
123 HttpServer | Exclude<Effect.Services<App>, HttpServerRequest | Scope.Scope>
124> =>
125 Layer.effectDiscard(
126 HttpServer.use((server) => server.serve(effect, middleware!))
127 ) as any)
128
129/**
130 * Effect that starts serving an HTTP response effect with the current
131 * `HttpServer`.
132 *
133 * **Details**
134 *
135 * The request service is supplied by the server for each request; the effect
136 * requires a scope and any non-request dependencies of the response effect or
137 * middleware.
138 *
139 * @stability unstable
140 * @category accessors
141 * @since 4.0.0
142 */
143export const serveEffect: {
144 (): <E, R>(
145 effect: Effect.Effect<HttpServerResponse, E, R>
146 ) => Effect.Effect<void, never, Scope.Scope | HttpServer | Exclude<R, HttpServerRequest>>
147 <E, R, App extends Effect.Effect<HttpServerResponse, any, any>>(
148 middleware: Middleware.HttpMiddleware.Applied<App, E, R>
149 ): (
150 effect: Effect.Effect<HttpServerResponse, E, R>
151 ) => Effect.Effect<
152 void,
153 never,
154 Scope.Scope | HttpServer | Exclude<Effect.Services<App>, HttpServerRequest>
155 >
156 <E, R>(
157 effect: Effect.Effect<HttpServerResponse, E, R>
158 ): Effect.Effect<void, never, Scope.Scope | HttpServer | Exclude<R, HttpServerRequest>>
159 <E, R, App extends Effect.Effect<HttpServerResponse, any, any>>(
160 effect: Effect.Effect<HttpServerResponse, E, R>,
161 middleware: Middleware.HttpMiddleware.Applied<App, E, R>
162 ): Effect.Effect<
163 void,
164 never,
165 Scope.Scope | HttpServer | Exclude<Effect.Services<App>, HttpServerRequest>
166 >
167} = dual((args) => Effect.isEffect(args[0]), <E, R, App extends Effect.Effect<HttpServerResponse, any, any>>(
168 effect: Effect.Effect<HttpServerResponse, E, R>,
169 middleware?: Middleware.HttpMiddleware.Applied<App, E, R>
170): Effect.Effect<
171 void,
172 never,
173 Scope.Scope | HttpServer | Exclude<Effect.Services<App>, HttpServerRequest>
174> => HttpServer.use((server) => server.serve(effect, middleware!)) as any)
175
176/**
177 * Formats a server address as a display string using {@link NetAddress.formatUrlUnsafe}.
178 *
179 * **Gotchas**
180 *
181 * Throws a `NetAddressError` when URL conversion fails, including for scoped
182 * IPv6 addresses. Unix socket paths are displayed with a `unix://` prefix.
183 *
184 * @stability unstable
185 * @category converting
186 * @since 4.0.0
187 */
188export const formatAddress: (address: NetAddress.SocketAddress) => string = NetAddress.formatUrlUnsafe
189
190/**
191 * Reads the current server address, formats it with `formatAddress`, and passes
192 * the formatted address to the supplied effectful function.
193 *
194 * @stability unstable
195 * @category accessors
196 * @since 4.0.0
197 */
198export const addressFormattedWith = <A, E, R>(
199 f: (address: string) => Effect.Effect<A, E, R>
200): Effect.Effect<A, E, HttpServer | R> =>
201 Effect.flatMap(
202 HttpServer,
203 (server) => f(formatAddress(server.address))
204 )
205
206/**
207 * Logs the formatted address of the current HTTP server.
208 *
209 * @stability unstable
210 * @category logging
211 * @since 4.0.0
212 */
213export const logAddress: Effect.Effect<void, never, HttpServer> = addressFormattedWith((_) =>
214 Effect.log(`Listening on ${_}`)
215)
216
217/**
218 * Adds address logging to a layer that provides an `HttpServer`.
219 *
220 * @stability unstable
221 * @category layers
222 * @since 4.0.0
223 */
224export const withLogAddress = <A, E, R>(
225 layer: Layer.Layer<A, E, R>
226): Layer.Layer<A, E, R | Exclude<HttpServer, A>> =>
227 Layer.effectDiscard(logAddress).pipe(
228 Layer.provideMerge(layer)
229 )
230
231/**
232 * Builds an `HttpClient` that sends requests to the current test HTTP server.
233 *
234 * **Details**
235 *
236 * For internet servers, requests are prefixed with the server URL and unspecified
237 * addresses are replaced by IPv4 loopback, including dual-stack `::` listeners.
238 *
239 * **Gotchas**
240 *
241 * Unix socket addresses and scoped IPv6 addresses are not supported.
242 *
243 * @stability unstable
244 * @category testing
245 * @since 4.0.0
246 */
247export const makeTestClient: Effect.Effect<
248 HttpClient.HttpClient,
249 never,
250 HttpServer | HttpClient.HttpClient
251> = Effect.gen(function*() {
252 const server = yield* HttpServer
253 const client = yield* HttpClient.HttpClient
254 const address = server.address
255 if (NetAddress.isUnixPathAddress(address)) {
256 return yield* Effect.die(new Error("HttpServer.layerTestClient: UnixPathAddress not supported"))
257 }
258 const url = yield* Effect.fromResult(NetAddress.toUrl(address)).pipe(Effect.orDie)
259 if (NetAddress.isUnspecified(address.address)) {
260 url.hostname = NetAddress.formatIp(NetAddress.ipv4Loopback)
261 }
262 return HttpClient.mapRequest(client, ClientRequest.prependUrl(url.origin))
263})
264
265/**
266 * Layer that provides the test `HttpClient` created by `makeTestClient`.
267 *
268 * @stability unstable
269 * @category testing
270 * @since 4.0.0
271 */
272export const layerTestClient: Layer.Layer<
273 HttpClient.HttpClient,
274 never,
275 HttpServer | HttpClient.HttpClient
276> = Layer.effect(HttpClient.HttpClient)(makeTestClient)
277
278/**
279 * Layer that provides the platform services commonly needed by HTTP
280 * server tests.
281 *
282 * **Details**
283 *
284 * It includes `HttpPlatform`, `Path`, a weak ETag generator, and a no-op
285 * `FileSystem`.
286 *
287 * @stability unstable
288 * @category testing
289 * @since 4.0.0
290 */
291export const layerServices: Layer.Layer<
292 | Path.Path
293 | HttpPlatform.HttpPlatform
294 | FileSystem.FileSystem
295 | Etag.Generator
296> = Layer.mergeAll(
297 HttpPlatform.layer,
298 Path.layer,
299 Etag.layerWeak
300).pipe(
301 Layer.provideMerge(FileSystem.layerNoop({}))
302)