Skip to content

platform: HttpApp.toWebHandlerLayerWith memoises the layer build inside the first request — an aborted first request permanently hangs the isolate on Cloudflare workerd (error 1101) #6319

Description

@maninjwa

What version of Effect is running?

effect@3.21.2, @effect/platform@0.96.1

What steps can reproduce the bug?

On Cloudflare Workers (workerd), HttpApp.toWebHandlerLayerWith (and everything built on it: HttpApp.toWebHandler, RpcServer.toWebHandler, …) memoises its layer build in a promise that is created inside the first request:

// packages/platform/src/HttpApp.ts (current main, introduced in #5206/#5208)
function handler(request: Request, context?: Context.Context<never> | undefined): Promise<Response> {
  if (handlerCache) {
    return handlerCache(request, context)
  }
  handlerPromise ??= Effect.gen(function*() {
    const runtime = yield* (options.memoMap
      ? Layer.toRuntimeWithMemoMap(layer, options.memoMap)
      : Layer.toRuntime(layer))
    return handlerCache = toWebHandlerRuntime(runtime)(
      yield* options.toHandler(runtime),
      options.middleware
    )
  }).pipe(
    Scope.extend(scope),
    Effect.runPromise
  )
  return handlerPromise.then((f) => f(request, context))
}

workerd cancels a request's IoContext when the client disconnects, and pending promises created inside a cancelled request's context never settle — they are not rejected, they simply stay pending forever. So if the first request into a cold isolate is client-aborted while the layer build is in flight:

  1. handlerPromise is created inside that request's IoContext and never settles.
  2. handlerCache is never assigned.
  3. Every subsequent request takes the handlerPromise.then(...) path and chains onto a promise that will never settle.
  4. The isolate is permanently wedged; Cloudflare eventually reports error 1101 "Worker's code had hung and would never generate a response" for every request until the isolate is evicted (which can take a long time under steady traffic, since hung requests keep the isolate alive).

Reproduction

Deploy a minimal worker whose fetch handler delegates to a toWebHandler-built handler (any non-trivial layer so the first build takes a few ms), then from a cold isolate:

# burst of client-aborted requests against a cold isolate
for i in $(seq 1 20); do curl -m 0.05 -s https://your-worker.example.workers.dev/ & done; wait
# now a normal request:
curl -s https://your-worker.example.workers.dev/   # hangs → eventually 1101

We hit this in production (Cloudflare Workers app using @effect/rpc's RpcServer.toWebHandler, which routes through toWebHandlerLayerWith): a single aborted first request (e.g. a browser aborting an SSR navigation) permanently hung the isolate. Confirmed via wrangler tail and reproduced deterministically with the curl burst above.

What is the expected behavior?

Either the memoised build survives an aborted first request, or the failure is recoverable — one aborted request should never permanently poison an isolate.

Possible directions:

  • Guard the memo: clear handlerPromise when the build promise rejects, and/or race the build against a timeout so a never-settling promise is abandoned and rebuilt by the next request.
  • Detach the build from the request context: on runtimes where a "waitUntil"-style API exists, anchoring the build promise to it prevents cancellation; alternatively document that the build should be kicked off eagerly at module scope.
  • Expose eager init: an explicit way to start (and await) the layer build outside any request (e.g. returning the build promise, or an init() handle), plus a docs note for workerd users. Before lazily build HttpLayerRouter web handlers #5206/consolidate Http web handler layer apis #5208 the build ran eagerly via Effect.runPromise at construction time, which did not have this failure mode on workerd.

Additional context

Metadata

Metadata

Assignees

No one assigned

    Labels

    3.0Used for issues, pull requests, etc. that are relevant for the `v3` branch targeting Effect v3.

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions