From 11d88916e673ac1720ec35e8db9ba227c651c306 Mon Sep 17 00:00:00 2001 From: "omegent-app[bot]" <306514130+omegent-app[bot]@users.noreply.github.com> Date: Fri, 23 Oct 2026 08:13:43 +0000 Subject: [PATCH 1/2] fix(effect-app): keep xForwardedHeaders error and request types Effect 4.0.6 types HttpMiddleware.xForwardedHeaders as HttpMiddleware, so the result is Effect. effect-app/http now re-exports that middleware with the app's error and HttpServerRequest requirement preserved. Co-authored-by: Patrick Roza <42661+patroza@users.noreply.github.com> --- .changeset/x-forwarded-headers.md | 7 ++ .../effect-app/src/http/HttpMiddleware.ts | 17 +++++ packages/effect-app/src/http/internal/lib.ts | 2 +- .../test/x-forwarded-headers.test.ts | 64 +++++++++++++++++++ 4 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 .changeset/x-forwarded-headers.md create mode 100644 packages/effect-app/src/http/HttpMiddleware.ts create mode 100644 packages/effect-app/test/x-forwarded-headers.test.ts diff --git a/.changeset/x-forwarded-headers.md b/.changeset/x-forwarded-headers.md new file mode 100644 index 000000000..6914aa9b7 --- /dev/null +++ b/.changeset/x-forwarded-headers.md @@ -0,0 +1,7 @@ +--- +"effect-app": patch +--- + +Re-export `HttpMiddleware` from `effect-app/http` with `xForwardedHeaders` keeping the app's error and `HttpServerRequest` requirement. + +Effect 4.0.6 types that factory as `HttpMiddleware`, so the result is `Effect`. Import `HttpMiddleware` from `effect-app/http`. The copy from `effect/http` is unchanged. diff --git a/packages/effect-app/src/http/HttpMiddleware.ts b/packages/effect-app/src/http/HttpMiddleware.ts new file mode 100644 index 000000000..7e319fddf --- /dev/null +++ b/packages/effect-app/src/http/HttpMiddleware.ts @@ -0,0 +1,17 @@ +import type { Effect } from "effect/Effect" +import { type TrustedProxyOptions, xForwardedHeaders as xForwardedHeadersUpstream } from "effect/http/HttpMiddleware" +import type { HttpServerRequest } from "effect/http/HttpServerRequest" +import type { HttpServerResponse } from "effect/http/HttpServerResponse" + +export type { HttpMiddleware, TrustedProxyOptions } from "effect/http/HttpMiddleware" + +export { compression, cors, decompression, layerTracerDisabledForUrls, logger, make, searchParamsParser, SpanNameGenerator, tracer, TracerDisabledWhen, TracerHeaderFilter, withLoggerDisabled } from "effect/http/HttpMiddleware" + +type ForwardedApp = Effect + +// Effect 4.0.6 types this factory as `HttpMiddleware`, whose call signature +// returns `Effect`. The middleware only updates +// the current `HttpServerRequest`, so the app's error and requirements stay. +export const xForwardedHeaders = + (options: TrustedProxyOptions) => (httpApp: ForwardedApp): ForwardedApp => + xForwardedHeadersUpstream(options)(httpApp) as ForwardedApp diff --git a/packages/effect-app/src/http/internal/lib.ts b/packages/effect-app/src/http/internal/lib.ts index 6c035c281..c6e1311d7 100644 --- a/packages/effect-app/src/http/internal/lib.ts +++ b/packages/effect-app/src/http/internal/lib.ts @@ -5,9 +5,9 @@ export * as HttpClientError from "effect/http/HttpClientError" export * as HttpClientRequest from "effect/http/HttpClientRequest" export * as HttpClientResponse from "effect/http/HttpClientResponse" // HttpLayerRouter removed in v4 — use HttpRouter instead -export * as HttpMiddleware from "effect/http/HttpMiddleware" export * as HttpRouter from "effect/http/HttpRouter" export * as HttpServer from "effect/http/HttpServer" export * as HttpServerError from "effect/http/HttpServerError" export * as HttpServerRequest from "effect/http/HttpServerRequest" export * as HttpServerResponse from "effect/http/HttpServerResponse" +export * as HttpMiddleware from "../HttpMiddleware.ts" diff --git a/packages/effect-app/test/x-forwarded-headers.test.ts b/packages/effect-app/test/x-forwarded-headers.test.ts new file mode 100644 index 000000000..58d620ab1 --- /dev/null +++ b/packages/effect-app/test/x-forwarded-headers.test.ts @@ -0,0 +1,64 @@ +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import type * as Upstream from "effect/http/HttpMiddleware" +import { IpNetwork } from "effect/net" +import * as Option from "effect/Option" +import { expect, expectTypeOf, test } from "vitest" +import { HttpMiddleware, HttpRouter, HttpServerRequest, HttpServerResponse } from "../src/http.js" + +class Svc extends Context.Service()("Svc") {} + +const trustedProxies = [IpNetwork.fromStringUnsafe("10.0.0.0/8")] + +const addressText = (request: HttpServerRequest.HttpServerRequest) => + Option.getOrElse(request.remoteAddress, () => "unknown") + +test("xForwardedHeaders keeps the app error and HttpServerRequest", () => { + const app = HttpServerRequest + .HttpServerRequest + .use((request) => Effect.succeed(HttpServerResponse.text(addressText(request)))) + .pipe( + HttpMiddleware.xForwardedHeaders({ trustedProxies }) + ) + + expectTypeOf(app).toEqualTypeOf< + Effect.Effect + >() + + const rich: Effect.Effect< + HttpServerResponse.HttpServerResponse, + "Boom", + HttpServerRequest.HttpServerRequest | Svc + > = Effect.fail("Boom") + const wrapped = rich.pipe(HttpMiddleware.xForwardedHeaders({ trustedProxies })) + + expectTypeOf(wrapped).toEqualTypeOf< + Effect.Effect< + HttpServerResponse.HttpServerResponse, + "Boom", + HttpServerRequest.HttpServerRequest | Svc + > + >() + + const layer = HttpRouter.middleware(HttpMiddleware.xForwardedHeaders({ trustedProxies })) + const corsLayer = HttpRouter.middleware(HttpMiddleware.cors()) + expectTypeOf(layer).toEqualTypeOf(corsLayer) + + expectTypeOf().toEqualTypeOf() +}) + +test("xForwardedHeaders still applies a trusted proxy hop", async () => { + const app = HttpServerRequest + .HttpServerRequest + .use((request) => Effect.succeed(HttpServerResponse.text(addressText(request)))) + .pipe( + HttpMiddleware.xForwardedHeaders({ trustedProxies }) + ) + const request = HttpServerRequest + .fromWeb( + new Request("http://app.test/", { headers: { "x-forwarded-for": "6.6.6.6, 198.51.100.7" } }) + ) + .modify({ remoteAddress: Option.some("10.0.0.1") }) + const response = await Effect.runPromise(Effect.provideService(app, HttpServerRequest.HttpServerRequest, request)) + expect(await HttpServerResponse.toWeb(response).text()).toBe("198.51.100.7") +}) From 01932f2c7ff8cb22d5a0fb811ff1951b62e1a8d5 Mon Sep 17 00:00:00 2001 From: "omegent-app[bot]" <306514130+omegent-app[bot]@users.noreply.github.com> Date: Fri, 23 Oct 2026 08:14:22 +0000 Subject: [PATCH 2/2] fix(effect-app): publish xForwardedHeaders channels inline Co-authored-by: Patrick Roza <42661+patroza@users.noreply.github.com> --- packages/effect-app/src/http/HttpMiddleware.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/effect-app/src/http/HttpMiddleware.ts b/packages/effect-app/src/http/HttpMiddleware.ts index 7e319fddf..897349143 100644 --- a/packages/effect-app/src/http/HttpMiddleware.ts +++ b/packages/effect-app/src/http/HttpMiddleware.ts @@ -12,6 +12,10 @@ type ForwardedApp = Effect // Effect 4.0.6 types this factory as `HttpMiddleware`, whose call signature // returns `Effect`. The middleware only updates // the current `HttpServerRequest`, so the app's error and requirements stay. -export const xForwardedHeaders = - (options: TrustedProxyOptions) => (httpApp: ForwardedApp): ForwardedApp => +export const xForwardedHeaders: ( + options: TrustedProxyOptions +) => ( + httpApp: Effect +) => Effect = + (options) => (httpApp: ForwardedApp): ForwardedApp => xForwardedHeadersUpstream(options)(httpApp) as ForwardedApp