diff --git a/packages/platform/src/FetchHttpClient.ts b/packages/platform/src/FetchHttpClient.ts index 0f1a54650ff..190195893d3 100644 --- a/packages/platform/src/FetchHttpClient.ts +++ b/packages/platform/src/FetchHttpClient.ts @@ -4,22 +4,28 @@ import * as Context from "effect/Context" import type * as Layer from "effect/Layer" import type { HttpClient } from "./HttpClient.js" -import * as internal from "./internal/fetchHttpClient.js" +import { layer, layerWithFetch } from "./internal/fetchHttpClient.js" /** * @since 1.0.0 * @category tags */ -export class Fetch extends Context.Tag(internal.fetchTagKey)() {} +export class Fetch extends Context.Tag("@effect/platform/FetchHttpClient/Fetch")() {} /** * @since 1.0.0 * @category tags */ -export class RequestInit extends Context.Tag(internal.requestInitTagKey)() {} +export class RequestInit extends Context.Tag("@effect/platform/FetchHttpClient/FetchOptions")() {} /** * @since 1.0.0 * @category layers + * Default FetchHttpClient Layer using global fetch. + * + * @example + * import { FetchHttpClient } from "@effect/platform/FetchHttpClient" + * + * const defaultLayer = FetchHttpClient.layer */ -export const layer: Layer.Layer = internal.layer +export { layer, layerWithFetch } diff --git a/packages/platform/src/internal/fetchHttpClient.ts b/packages/platform/src/internal/fetchHttpClient.ts index 434c143d809..50af21b6efb 100644 --- a/packages/platform/src/internal/fetchHttpClient.ts +++ b/packages/platform/src/internal/fetchHttpClient.ts @@ -6,6 +6,8 @@ import type * as Client from "../HttpClient.js" import * as Error from "../HttpClientError.js" import * as client from "./httpClient.js" import * as internalResponse from "./httpClientResponse.js" +import * as Layer from "effect/Layer" +import * as Context from "effect/Context" /** @internal */ export const fetchTagKey = "@effect/platform/FetchHttpClient/Fetch" @@ -29,14 +31,14 @@ const fetch: Client.HttpClient = client.make((request, url, signal, fiber) => { duplex: request.body._tag === "Stream" ? "half" : undefined, signal } as any), - catch: (cause) => + catch: (cause: unknown) => new Error.RequestError({ request, reason: "Transport", cause }) }), - (response) => internalResponse.fromWeb(request, response) + (response: Response) => internalResponse.fromWeb(request, response) ) switch (request.body._tag) { case "Raw": @@ -50,5 +52,56 @@ const fetch: Client.HttpClient = client.make((request, url, signal, fiber) => { return send(undefined) }) -/** @internal */ +/** + * @internal + * Default FetchHttpClient Layer using global fetch. + */ export const layer = client.layerMergedContext(Effect.succeed(fetch)) + +/** + * @internal + * Creates a FetchHttpClient Layer using a custom fetch implementation (e.g., Next.js's fetch). + */ +export function layerWithFetch(customFetch: typeof globalThis.fetch) { + return client.layerMergedContext( + Effect.sync(() => + client.make((request, url, signal, fiber) => { + const context = fiber.getFiberRef(FiberRef.currentContext) + const fetch: typeof globalThis.fetch = customFetch + const options: RequestInit = context.unsafeMap.get(requestInitTagKey) ?? {} + const headers = options.headers ? Headers.merge(Headers.fromInput(options.headers), request.headers) : request.headers + const send = (body: BodyInit | undefined) => + Effect.map( + Effect.tryPromise({ + try: () => + fetch(url, { + ...options, + method: request.method, + headers, + body, + duplex: request.body._tag === "Stream" ? "half" : undefined, + signal + } as any), + catch: (cause: unknown) => + new Error.RequestError({ + request, + reason: "Transport", + cause + }) + }), + (response: Response) => internalResponse.fromWeb(request, response) + ) + switch (request.body._tag) { + case "Raw": + case "Uint8Array": + return send(request.body.body as any) + case "FormData": + return send(request.body.formData) + case "Stream": + return Effect.flatMap(Stream.toReadableStreamEffect(request.body.stream), send) + } + return send(undefined) + }) + ) + ) +} diff --git a/packages/platform/test/HttpClient.test.ts b/packages/platform/test/HttpClient.test.ts index c9f63339eaf..2679165bd0e 100644 --- a/packages/platform/test/HttpClient.test.ts +++ b/packages/platform/test/HttpClient.test.ts @@ -237,3 +237,27 @@ describe("HttpClient", () => { Effect.runPromise )) }) + +describe("FetchHttpClient.layerWithFetch", () => { + it.effect("uses the injected fetch implementation", () => + Effect.gen(function*() { + let called = false; + // Mock fetch implementation + const mockFetch: typeof globalThis.fetch = (input, init) => { + called = true; + // Return a minimal Response object + return Promise.resolve(new Response(JSON.stringify({ ok: true }), { + status: 200, + headers: { "Content-Type": "application/json" } + })); + }; + const client = yield* HttpClient.HttpClient; + const response = yield* pipe( + HttpClient.get("https://mocked-url.com/"), + Effect.flatMap((_) => _.json) + ).pipe(Effect.provide(FetchHttpClient.layerWithFetch(mockFetch))); + deepStrictEqual(response, { ok: true }); + strictEqual(called, true); + }) + ); +});