1import { expect, layer } from "@effect/vitest";
2import * as Effect from "effect/Effect";
3import * as Exit from "effect/Exit";
4import * as Schedule from "effect/Schedule";
5import * as Scope from "effect/Scope";
6import { createServer } from "node:http";
7import { WebSocket, WebSocketServer } from "ws";
8import * as Service from "../bindings/Service.ts";
9import { registerHttpServer } from "../HttpServer.ts";
10import { localRuntimeLayer, startTestWorker } from "./helpers/runtime.ts";
11
12layer(localRuntimeLayer, { excludeTestServices: true })(
13 "HTTP dev server bindings",
14 (it) => {
15 it.effect(
16 "forwards HTTP and WebSockets, unregisters, and reconnects after restart",
17 () =>
18 Effect.gen(function* () {
19 const server = createServer(async (request, response) => {
20 const chunks: Buffer[] = [];
21 for await (const chunk of request) chunks.push(Buffer.from(chunk));
22 response.writeHead(201, {
23 "set-cookie": ["first=1", "second=2"],
24 "x-upstream": "dev",
25 });
26 response.end(
27 JSON.stringify({
28 method: request.method,
29 url: request.url,
30 host: request.headers.host,
31 proto: request.headers["x-forwarded-proto"],
32 cookie: request.headers.cookie,
33 body: Buffer.concat(chunks).toString(),
34 }),
35 );
36 });
37 const sockets = new WebSocketServer({ server });
38 sockets.on("connection", (socket) =>
39 socket.on("message", (message) => socket.send(message.toString())),
40 );
41 const upstream = yield* Effect.acquireRelease(
42 Effect.promise(
43 () =>
44 new Promise<URL>((resolve) => {
45 server.listen(0, "127.0.0.1", () => {
46 const address = server.address();
47 if (!address || typeof address === "string")
48 throw new Error("Expected TCP address");
49 resolve(new URL(`http://127.0.0.1:${address.port}`));
50 });
51 }),
52 ),
53 () =>
54 Effect.promise(
55 () =>
56 new Promise<void>((resolve) => {
57 for (const client of sockets.clients) client.terminate();
58 sockets.close();
59 server.closeAllConnections();
60 server.close(() => resolve());
61 }),
62 ),
63 );
64 const gateway = yield* startTestWorker({
65 name: "http-server-gateway",
66 compatibilityDate: "2026-03-10",
67 compatibilityFlags: [],
68 bindings: [
69 Service.local({
70 binding: "WEBSITE",
71 scriptName: "http-server-website",
72 }),
73 ],
74 modules: [
75 {
76 name: "main.js",
77 type: "ESModule",
78 content: `export default {
79 fetch(request, env) {
80 const url = new URL(request.url);
81 url.host = "website.example";
82 url.port = "";
83 url.protocol = "https:";
84 return env.WEBSITE.fetch(new Request(url, request));
85 }
86 };`,
87 },
88 ],
89 });
90 const checkStatus = (status: number) =>
91 Effect.tryPromise(async () => {
92 const response = await fetch(gateway.baseUrl, {
93 signal: AbortSignal.timeout(3000),
94 });
95 await response.text();
96 expect(response.status).toBe(status);
97 }).pipe(
98 Effect.retry({
99 times: 8,
100 schedule: Schedule.spaced("250 millis"),
101 }),
102 );
103 yield* checkStatus(503);
104 const parent = yield* Effect.scope;
105 const registration = yield* Scope.fork(parent);
106 yield* registerHttpServer("http-server-website", upstream).pipe(
107 Scope.provide(registration),
108 );
109 yield* checkStatus(201);
110 const response = yield* gateway.fetch("/submit?q=hello%20world", {
111 method: "POST",
112 body: "request body",
113 headers: { cookie: "session=abc" },
114 });
115 expect(response.status).toBe(201);
116 expect(response.headers.get("x-upstream")).toBe("dev");
117 expect(response.headers.getSetCookie()).toEqual([
118 "first=1",
119 "second=2",
120 ]);
121 expect(yield* Effect.promise(() => response.json())).toEqual({
122 method: "POST",
123 url: "/submit?q=hello%20world",
124 host: "website.example",
125 proto: "https",
126 cookie: "session=abc",
127 body: "request body",
128 });
129 yield* Effect.tryPromise(
130 () =>
131 new Promise<void>((resolve, reject) => {
132 const url = new URL("/socket", gateway.baseUrl);
133 url.protocol = "ws:";
134 const socket = new WebSocket(url);
135 const timer = setTimeout(() => {
136 socket.terminate();
137 reject(new Error("WebSocket timed out"));
138 }, 3000);
139 socket.on("error", (error) => {
140 clearTimeout(timer);
141 reject(error);
142 });
143 socket.on("open", () => socket.send("hello"));
144 socket.on("message", (message) => {
145 clearTimeout(timer);
146 socket.close();
147 if (message.toString() === "hello") resolve();
148 else
149 reject(
150 new Error(`Unexpected WebSocket message: ${message}`),
151 );
152 });
153 }),
154 );
155 yield* Scope.close(registration, Exit.void);
156 yield* checkStatus(503);
157 yield* registerHttpServer("http-server-website", upstream);
158 yield* checkStatus(201);
159 }),
160 );
161 },
162);