Skip to content

Commit 27fea0f

Browse files
authored
EFF-700: Improve HttpApi addHttpApi / middleware missing-service failures (#1725)
1 parent b38de88 commit 27fea0f

File tree

3 files changed

+174
-3
lines changed

3 files changed

+174
-3
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"effect": patch
3+
---
4+
5+
Improve unstable HttpApi runtime failures for missing server middleware and missing group implementations.
6+
7+
- HttpApiBuilder.applyMiddleware now resolves middleware services via ServiceMap.getUnsafe, so missing middleware fails with a clear "Service not found: <middleware>" error instead of an opaque is not a function TypeError.
8+
- HttpApiBuilder.layer now reports missing groups with actionable context (group identifier, service key, suggested HttpApiBuilder.group(...) call, and available group keys).
9+
- Added regression tests in packages/platform-node/test/HttpApi.test.ts covering:
10+
- addHttpApi + API-level middleware applied across merged groups
11+
- missing middleware service diagnostics
12+
- missing addHttpApi group layer diagnostics

packages/effect/src/unstable/httpapi/HttpApiBuilder.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,16 @@ export const layer = <Id extends string, Groups extends HttpApiGroup.Any>(
7272
| Path
7373
>()
7474
const routes: Array<HttpRouter.Route<any, any>> = []
75+
const availableGroups = Array.from(services.mapUnsafe.keys()).filter((key) =>
76+
key.startsWith("effect/httpapi/HttpApiGroup/")
77+
)
7578
for (const group of Object.values(api.groups)) {
7679
const groupRoutes = services.mapUnsafe.get(group.key) as Array<HttpRouter.Route<any, any>>
7780
if (groupRoutes === undefined) {
78-
return yield* Effect.die(`HttpApiGroup "${group.key}" not found`)
81+
const available = availableGroups.length === 0 ? "none" : availableGroups.join(", ")
82+
return yield* Effect.die(
83+
`HttpApiGroup "${group.identifier}" not found (key: "${group.key}"). Did you forget to provide HttpApiBuilder.group(api, "${group.identifier}", ...)? Available groups: ${available}`
84+
)
7985
}
8086
routes.push(...groupRoutes)
8187
}
@@ -630,13 +636,13 @@ const getRequestMediaType = (request: HttpServerRequest): string => {
630636
const applyMiddleware = <A extends Effect.Effect<any, any, any>>(
631637
group: HttpApiGroup.AnyWithProps,
632638
endpoint: HttpApiEndpoint.AnyWithProps,
633-
services: ServiceMap.ServiceMap<never>,
639+
services: ServiceMap.ServiceMap<any>,
634640
handler: A
635641
) => {
636642
const options = { group, endpoint }
637643
for (const key_ of endpoint.middlewares) {
638644
const key = key_ as any as HttpApiMiddleware.AnyService
639-
const service = services.mapUnsafe.get(key_.key) as HttpApiMiddleware.HttpApiMiddleware<any, any, any>
645+
const service = ServiceMap.getUnsafe(services, key as any) as HttpApiMiddleware.HttpApiMiddleware<any, any, any>
640646
const apply = HttpApiMiddleware.isSecurity(key)
641647
? makeSecurityMiddleware(key, service as any)
642648
: service

packages/platform-node/test/HttpApi.test.ts

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { NodeHttpServer } from "@effect/platform-node"
22
import { assert, describe, it } from "@effect/vitest"
33
import {
44
Array,
5+
Cause,
56
DateTime,
67
Effect,
78
Equal,
@@ -434,6 +435,158 @@ describe("HttpApi", () => {
434435
assert.strictEqual(yield* authedClient.group.a(), "token")
435436
}).pipe(Effect.provide(ApiLive))
436437
})
438+
439+
it.effect("addHttpApi + middleware works across merged groups", () => {
440+
class M1 extends HttpApiMiddleware.Service<M1>()("Http/M1") {}
441+
class M2 extends HttpApiMiddleware.Service<M2>()("Http/M2") {}
442+
443+
const calls: Array<string> = []
444+
445+
const V0 = HttpApi.make("v0").add(
446+
HttpApiGroup.make("users").add(
447+
HttpApiEndpoint.get("list", "/users", {
448+
success: Schema.String
449+
})
450+
)
451+
)
452+
const Api = HttpApi.make("api")
453+
.add(
454+
HttpApiGroup.make("health").add(
455+
HttpApiEndpoint.get("health", "/health", {
456+
success: Schema.String
457+
})
458+
)
459+
)
460+
.addHttpApi(V0)
461+
.middleware(M1)
462+
.middleware(M2)
463+
464+
const HealthLive = HttpApiBuilder.group(
465+
Api,
466+
"health",
467+
(handlers) => handlers.handle("health", () => Effect.succeed("ok"))
468+
)
469+
const UsersLive = HttpApiBuilder.group(
470+
Api,
471+
"users",
472+
(handlers) => handlers.handle("list", () => Effect.succeed("ok"))
473+
)
474+
const M1Live = Layer.succeed(
475+
M1,
476+
(effect, { endpoint, group }) =>
477+
Effect.sync(() => calls.push(`m1:${group.identifier}.${endpoint.name}`)).pipe(
478+
Effect.flatMap(() => effect)
479+
)
480+
)
481+
const M2Live = Layer.succeed(
482+
M2,
483+
(effect, { endpoint, group }) =>
484+
Effect.sync(() => calls.push(`m2:${group.identifier}.${endpoint.name}`)).pipe(
485+
Effect.flatMap(() => effect)
486+
)
487+
)
488+
489+
const ApiLive = HttpRouter.serve(
490+
HttpApiBuilder.layer(Api).pipe(
491+
Layer.provide(HealthLive),
492+
Layer.provide(UsersLive),
493+
Layer.provide(M1Live),
494+
Layer.provide(M2Live)
495+
),
496+
{ disableListenLog: true, disableLogger: true }
497+
).pipe(Layer.provideMerge(NodeHttpServer.layerTest))
498+
499+
return Effect.gen(function*() {
500+
yield* assertServerJson(yield* HttpClient.get("/health"), 200, "ok")
501+
yield* assertServerJson(yield* HttpClient.get("/users"), 200, "ok")
502+
assert.deepStrictEqual(calls, [
503+
"m2:health.health",
504+
"m1:health.health",
505+
"m2:users.list",
506+
"m1:users.list"
507+
])
508+
}).pipe(Effect.provide(ApiLive))
509+
})
510+
511+
it.effect("missing middleware layer fails with service not found error", () => {
512+
class M extends HttpApiMiddleware.Service<M>()("Server/MissingMiddleware") {}
513+
514+
const Api = HttpApi.make("api").add(
515+
HttpApiGroup.make("group")
516+
.add(
517+
HttpApiEndpoint.get("a", "/a", {
518+
success: Schema.String
519+
})
520+
)
521+
.middleware(M)
522+
)
523+
const GroupLive = HttpApiBuilder.group(
524+
Api,
525+
"group",
526+
(handlers) => handlers.handle("a", () => Effect.succeed("ok"))
527+
)
528+
const ApiLive = HttpRouter.serve(
529+
HttpApiBuilder.layer(Api).pipe(Layer.provide(GroupLive)),
530+
{ disableListenLog: true, disableLogger: true }
531+
).pipe(Layer.provideMerge(NodeHttpServer.layerTest))
532+
533+
return HttpClient.get("/a").pipe(
534+
Effect.provide(ApiLive),
535+
Effect.sandbox,
536+
Effect.flip,
537+
Effect.flatMap((cause) =>
538+
Effect.sync(() => {
539+
const defect = Cause.squash(cause)
540+
assert.instanceOf(defect, Error)
541+
assert.include(defect.message, "Service not found: Server/MissingMiddleware")
542+
assert.isFalse(defect.message.includes("is not a function"))
543+
})
544+
)
545+
) as Effect.Effect<void, HttpClientResponse.HttpClientResponse>
546+
})
547+
})
548+
549+
it.effect("missing addHttpApi group layer has actionable error", () => {
550+
const HealthApi = HttpApiGroup.make("health").add(
551+
HttpApiEndpoint.get("health", "/health", {
552+
success: Schema.String
553+
})
554+
)
555+
const V0 = HttpApi.make("v0").add(
556+
HttpApiGroup.make("users").add(
557+
HttpApiEndpoint.get("list", "/users", {
558+
success: Schema.String
559+
})
560+
)
561+
)
562+
const Api = HttpApi.make("api")
563+
.add(HealthApi)
564+
.addHttpApi(V0)
565+
566+
const UsersLive = HttpApiBuilder.group(
567+
Api,
568+
"users",
569+
(handlers) => handlers.handle("list", () => Effect.succeed("ok"))
570+
)
571+
const ApiLive = HttpRouter.serve(
572+
HttpApiBuilder.layer(Api).pipe(Layer.provide(UsersLive)),
573+
{ disableListenLog: true, disableLogger: true }
574+
).pipe(Layer.provideMerge(NodeHttpServer.layerTest))
575+
576+
return HttpClient.get("/users").pipe(
577+
Effect.provide(ApiLive),
578+
Effect.sandbox,
579+
Effect.flip,
580+
Effect.flatMap((cause) =>
581+
Effect.sync(() => {
582+
const defect = Cause.squash(cause)
583+
assert.strictEqual(typeof defect, "string")
584+
assert.include(defect, "HttpApiGroup \"health\" not found")
585+
assert.include(defect, "HttpApiBuilder.group(api, \"health\", ...)")
586+
assert.include(defect, "Available groups: effect/httpapi/HttpApiGroup/users")
587+
})
588+
)
589+
) as Effect.Effect<void, HttpClientResponse.HttpClientResponse>
437590
})
438591

439592
describe("payload option", () => {

0 commit comments

Comments
 (0)