From c401b1208246eba0e9db6c5afee0ff3d441c6b22 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Sat, 16 May 2026 13:51:52 -0700 Subject: [PATCH] fix(platform): document and surface redirect behavior for HttpClient.followRedirects Fixes #6231 --- .changeset/fuzzy-chairs-taste.md | 5 +++++ packages/platform/src/FetchHttpClient.ts | 5 +++++ packages/platform/src/HttpClient.ts | 4 ++++ packages/platform/test/HttpClient.test.ts | 25 +++++++++++++++++++---- 4 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 .changeset/fuzzy-chairs-taste.md diff --git a/.changeset/fuzzy-chairs-taste.md b/.changeset/fuzzy-chairs-taste.md new file mode 100644 index 00000000000..1a8656bf68d --- /dev/null +++ b/.changeset/fuzzy-chairs-taste.md @@ -0,0 +1,5 @@ +--- +"@effect/platform": patch +--- + +Allow `HttpClient.followRedirects` to take effect on `FetchHttpClient` by exposing `redirect` through the requestInit FiberRef and documenting the interaction. diff --git a/packages/platform/src/FetchHttpClient.ts b/packages/platform/src/FetchHttpClient.ts index 0f1a54650ff..34aae9cb3df 100644 --- a/packages/platform/src/FetchHttpClient.ts +++ b/packages/platform/src/FetchHttpClient.ts @@ -13,6 +13,11 @@ import * as internal from "./internal/fetchHttpClient.js" export class Fetch extends Context.Tag(internal.fetchTagKey)() {} /** + * The `RequestInit` options provided to `fetch`. + * + * Set `redirect` to `"manual"` when using `HttpClient.followRedirects`, so that + * fetch does not follow redirects before the client decorator can observe them. + * * @since 1.0.0 * @category tags */ diff --git a/packages/platform/src/HttpClient.ts b/packages/platform/src/HttpClient.ts index 628e9e52296..9dcb5dcfdd8 100644 --- a/packages/platform/src/HttpClient.ts +++ b/packages/platform/src/HttpClient.ts @@ -621,6 +621,10 @@ export const withCookiesRef: { /** * Follows HTTP redirects up to a specified number of times. * + * When used with `FetchHttpClient`, provide `FetchHttpClient.RequestInit` with + * `{ redirect: "manual" }` so that `fetch` exposes redirect responses to this + * decorator instead of following them internally. + * * @since 1.0.0 * @category redirects */ diff --git a/packages/platform/test/HttpClient.test.ts b/packages/platform/test/HttpClient.test.ts index ca1c6067343..98e4e885fc6 100644 --- a/packages/platform/test/HttpClient.test.ts +++ b/packages/platform/test/HttpClient.test.ts @@ -295,16 +295,33 @@ describe("HttpClient", () => { it.effect("followRedirects", () => Effect.gen(function*() { + const requests: Array<{ readonly url: string; readonly init: RequestInit | undefined }> = [] + const fetch: typeof globalThis.fetch = (input, init) => { + const url = typeof input === "string" || input instanceof URL ? input.toString() : input.url + requests.push({ url, init }) + if (requests.length === 1) { + return Promise.resolve(new Response(null, { + status: 302, + headers: { location: "/redirected" } + })) + } + return Promise.resolve(new Response("ok")) + } const defaultClient = yield* HttpClient.HttpClient const client = defaultClient.pipe(HttpClient.followRedirects()) - const response = yield* client.get("https://google.com/") - strictEqual(response.request.url, "https://www.google.com/") + const response = yield* client.get("https://example.com/") + strictEqual(response.request.url, "https://example.com/redirected") + strictEqual(requests.length, 2) + strictEqual(requests[0]!.url, "https://example.com/") + strictEqual(requests[0]!.init?.redirect, "manual") + strictEqual(requests[1]!.url, "https://example.com/redirected") + strictEqual(requests[1]!.init?.redirect, "manual") }).pipe( - it.flakyTest, Effect.provide(FetchHttpClient.layer), + Effect.provideService(FetchHttpClient.Fetch, fetch), Effect.provideService(FetchHttpClient.RequestInit, { redirect: "manual" }) - ), 30000) + )) describe("retryTransient", () => { const makeTestClient = (status: number) => {