Skip to content

@effect/platform HttpApi: middleware is skipped #6121

@adamalfredsson

Description

@adamalfredsson

What version of Effect is running?

3.19.18

What steps can reproduce the bug?

It seems like middleware is being ignored entirely under certain conditions.

This is as minimally I could reproduce the issue:

import { createServer } from "node:http";
import {
  HttpApi,
  HttpApiBuilder,
  HttpApiEndpoint,
  HttpApiError,
  HttpApiGroup,
  HttpApiMiddleware,
  HttpApiSecurity,
  HttpLayerRouter,
} from "@effect/platform";
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node";
import { Context, Effect, Layer, Schema } from "effect";

export class Session extends Context.Tag("app/Session")<
  Session,
  {
    id: string;
  }
>() {}

class InternalAuthorization extends HttpApiMiddleware.Tag<InternalAuthorization>()(
  "InternalAuthorization",
  {
    failure: HttpApiError.Unauthorized,
    provides: Session,
    security: {
      apiKey: HttpApiSecurity.bearer,
    },
  }
) {}

const externalApi = HttpApi.make("external-api").add(
  HttpApiGroup.make("Posts")
    .add(
      HttpApiEndpoint.get("posts", "/")
        .addSuccess(Schema.String)
        .addError(HttpApiError.BadRequest)
    )
    .prefix("/posts")
);

const internalApi = HttpApi.make("internal-api")
  .add(
    HttpApiGroup.make("Customers")
      .add(
        HttpApiEndpoint.get("customers", "/").addSuccess(
          Schema.Array(Schema.String)
        )
      )
      .prefix("/customers")
  )
  .add(
    HttpApiGroup.make("Users")
      .add(
        HttpApiEndpoint.get("users", "/").addSuccess(
          Schema.Array(Schema.String)
        )
      )
      .prefix("/users")
  )
  .middleware(InternalAuthorization);

const InternalAuthorizationLive = Layer.succeed(InternalAuthorization, {
  apiKey: () => Effect.fail(new HttpApiError.Unauthorized()),
});

const PostsLive = HttpApiBuilder.group(externalApi, "Posts", (handlers) =>
  handlers.handle("posts", () => Effect.succeed("ok"))
);

const CustomersLive = HttpApiBuilder.group(
  internalApi,
  "Customers",
  (handlers) =>
    handlers.handle(
      "customers",
      Effect.fnUntraced(function* () {
        const session = yield* Session;
        return ["customer1", "customer2", session.id];
      })
    )
);

const UsersLive = HttpApiBuilder.group(internalApi, "Users", (handlers) =>
  handlers.handle("users", () => Effect.succeed(["user1", "user2"]))
);

const ExternalRoutes = HttpLayerRouter.addHttpApi(externalApi).pipe(
  Layer.provide([PostsLive])
);

const InternalRoutes = HttpLayerRouter.addHttpApi(internalApi).pipe(
  Layer.provide([UsersLive, CustomersLive]),
  Layer.provide(InternalAuthorizationLive)
);

const Routes = Layer.mergeAll(ExternalRoutes, InternalRoutes);

const HttpLive = HttpLayerRouter.serve(Routes).pipe(
  Layer.provide(NodeHttpServer.layer(createServer, { port: 3001 }))
);

NodeRuntime.runMain(Layer.launch(HttpLive));

I would then hit the internal API:

curl -i http://localhost:3001/users

What is the expected behavior?

The users endpoint should be protected by the internal authorization middleware. Expected status code 401.

What do you see instead?

⨯ curl -i http://localhost:3001/users                                                                                                   
HTTP/1.1 200 OK
content-type: application/json
content-length: 17
Date: Wed, 11 Mar 2026 15:41:16 GMT
Connection: keep-alive
Keep-Alive: timeout=5

["user1","user2"]           

Additional information

Hitting the customers endpoint defects due to "Service not found: app/Session".

Applying the middleware to the group or route instead of the http api results in another unexpected response of 500 Internal Server Error.

@effect/platform: 0.94.5

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions