diff --git a/packages/alchemy/src/Cloudflare/Workers/HttpServer.ts b/packages/alchemy/src/Cloudflare/Workers/HttpServer.ts index 69bb7bf3..876be7ea 100644 --- a/packages/alchemy/src/Cloudflare/Workers/HttpServer.ts +++ b/packages/alchemy/src/Cloudflare/Workers/HttpServer.ts @@ -1,10 +1,10 @@ import type * as cf from "@cloudflare/workers-types"; -import * as Cause from "effect/Cause"; import * as Effect from "effect/Effect"; import * as Option from "effect/Option"; import type { Scope } from "effect/Scope"; import type { HttpBodyError } from "effect/unstable/http/HttpBody"; -import * as HttpServerError from "effect/unstable/http/HttpServerError"; +import * as HttpEffectModule from "effect/unstable/http/HttpEffect"; +import type * as HttpServerError from "effect/unstable/http/HttpServerError"; import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest"; import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; import * as Http from "../../Http.ts"; @@ -47,41 +47,40 @@ export const serveWebRequest = ( never, Exclude > => - Effect.gen(function* () { - const request = HttpServerRequest.fromWeb( - webRequest as any as globalThis.Request, - ).modify({ - remoteAddress: Option.fromUndefinedOr(options.remoteAddress), - }); - - Object.defineProperty(request, "raw", { - get: () => - Object.assign(request.stream, { - raw: webRequest.body, - }), - }); + Effect.flatMap(Effect.context(), (parentContext) => + Effect.callback((resume) => { + const request = HttpServerRequest.fromWeb( + webRequest as any as globalThis.Request, + ).modify({ + remoteAddress: Option.fromUndefinedOr(options.remoteAddress), + }); - const response = yield* handler.pipe( - Effect.provideService(HttpServerRequest.HttpServerRequest, request), - Effect.provideService(Request, webRequest as any), - Effect.catchCause((cause) => { - const message = Option.match(Cause.findErrorOption(cause), { - onNone: () => "Internal Server Error", - onSome: (error) => - error instanceof Error && error.message - ? error.message - : "Internal Server Error", - }); - return Effect.succeed( - HttpServerResponse.text(message, { - status: 500, - statusText: message, + Object.defineProperty(request, "raw", { + get: () => + Object.assign(request.stream, { + raw: webRequest.body, }), + }); + + const httpApp = HttpEffectModule.toHandled(handler, (_req, response) => { + const transferred = HttpEffectModule.scopeTransferToStream(response); + resume( + Effect.succeed( + HttpServerResponse.toWeb(transferred, { context: parentContext }), + ), ); - }), - ); + return Effect.void; + }); + + const fiber = httpApp.pipe( + Effect.provideService(HttpServerRequest.HttpServerRequest, request), + Effect.provideService(Request, webRequest as any), + Effect.provide(parentContext), + Effect.runFork, + ); - return HttpServerResponse.toWeb(response, { - context: yield* Effect.context(), - }); - }) as any; + return Effect.sync(() => { + fiber.interruptUnsafe(); + }); + }), + ) as any; diff --git a/packages/alchemy/test/Cloudflare/Workers/HttpServer.test.ts b/packages/alchemy/test/Cloudflare/Workers/HttpServer.test.ts new file mode 100644 index 00000000..6802382a --- /dev/null +++ b/packages/alchemy/test/Cloudflare/Workers/HttpServer.test.ts @@ -0,0 +1,71 @@ +import { serveWebRequest } from "@/Cloudflare/Workers/HttpServer"; +import { describe, expect, it } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import * as HttpEffect from "effect/unstable/http/HttpEffect"; +import * as HttpMiddleware from "effect/unstable/http/HttpMiddleware"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; + +const helloHandler = Effect.succeed( + HttpServerResponse.text("hello", { status: 200 }), +); + +describe("serveWebRequest", () => { + it.effect( + "drains preResponseHandlers so HttpMiddleware.cors() tags GET responses", + () => + Effect.gen(function* () { + const request = new Request("https://worker.test/hello", { + method: "GET", + headers: { Origin: "https://example.test" }, + }); + + const response = yield* serveWebRequest( + request as any, + HttpMiddleware.cors()(helloHandler), + ); + + expect(response.status).toBe(200); + expect(response.headers.get("access-control-allow-origin")).toBe("*"); + expect(yield* Effect.promise(() => response.text())).toBe("hello"); + }), + ); + + it.effect( + "lets HttpMiddleware.cors() answer OPTIONS preflight with CORS headers", + () => + Effect.gen(function* () { + const request = new Request("https://worker.test/hello", { + method: "OPTIONS", + headers: { + Origin: "https://example.test", + "Access-Control-Request-Method": "GET", + }, + }); + + const response = yield* serveWebRequest( + request as any, + HttpMiddleware.cors()(helloHandler), + ); + + expect(response.status).toBe(204); + expect(response.headers.get("access-control-allow-origin")).toBe("*"); + }), + ); + + it.effect( + "applies a manually-registered preResponseHandler to the response", + () => + Effect.gen(function* () { + const handler = helloHandler.pipe( + HttpEffect.withPreResponseHandler((_req, res) => + Effect.succeed(HttpServerResponse.setHeader(res, "x-tagged", "yes")), + ), + ); + + const request = new Request("https://worker.test/", { method: "GET" }); + const response = yield* serveWebRequest(request as any, handler); + + expect(response.headers.get("x-tagged")).toBe("yes"); + }), + ); +});