[PR #126] [MERGED] fix: wire ctx.waitUntil for middleware fetch event background tasks #332

Closed
opened 2026-05-06 12:39:15 +02:00 by BreizhHardware · 0 comments

📋 Pull Request Information

Original PR: https://github.com/cloudflare/vinext/pull/126
Author: @nbardy
Created: 2/26/2026
Status: Merged
Merged: 3/28/2026
Merged by: @james-elicx

Base: mainHead: fix-clerk-middleware


📝 Commits (10+)

  • 0cbbe57 fix: pass NextFetchEvent to middleware and bubble up waitUntil promises to Cloudflare Worker ctx
  • a8ee44d Merge remote-tracking branch 'origin/main' into fix-clerk-middleware
  • f07f226 fix: address review feedback for waitUntil/NextFetchEvent support
  • 0f4e82d fix: move waitUntil settlement before early returns in prod-server
  • bf04872 Merge main into fix-clerk-middleware: resolve conflicts, fix waitUntil plumbing
  • 3a9975e fix(middleware): remove double drainWaitUntil, include waitUntilPromises on error path
  • 6fd374b fix: remove dead __vinextWaitUntil read code with no corresponding setter
  • ced210e fix: remove no-op .catch on Promise.allSettled, strengthen waitUntil redirect test
  • a98855f test: add waitUntil continue:true path coverage
  • a8517db fix: settle waitUntil promises in Pages Router dev server (index.ts)

📊 Changes

11 files changed (+125 additions, -17 deletions)

View changed files

📝 packages/vinext/src/deploy.ts (+7 -0)
📝 packages/vinext/src/entries/app-rsc-entry.ts (+8 -4)
📝 packages/vinext/src/entries/pages-server-entry.ts (+2 -0)
📝 packages/vinext/src/index.ts (+6 -0)
📝 packages/vinext/src/server/middleware.ts (+8 -7)
📝 packages/vinext/src/server/prod-server.ts (+7 -0)
📝 packages/vinext/src/shims/server.ts (+4 -0)
📝 tests/__snapshots__/entry-templates.test.ts.snap (+10 -4)
📝 tests/app-router.test.ts (+7 -0)
📝 tests/fixtures/app-basic/middleware.ts (+12 -2)
📝 tests/shims.test.ts (+54 -0)

📄 Description

Description

This PR makes vinext fully compatible with @clerk/nextjs by implementing the NextFetchEvent.waitUntil background task API across the middleware and Worker layers.

Dependency: This PR pairs with clerk/javascript#7954, which fixes ESM compatibility in @clerk/nextjs itself (replacing require() calls that crash in Vite/Workers). That PR makes Clerk's package loadable in ESM runtimes. This PR provides the runtime API surface Clerk needs once it loads. Both are required for end-to-end Clerk support on Cloudflare Workers via vinext.

What changed

  • Pass NextFetchEvent to middleware: Construct the event with a waitUntil array and pass it as the second argument to middlewareFn. This is the standard Next.js middleware signature — Clerk uses event.waitUntil() to schedule session sync and telemetry as background tasks.
  • Bubble up promises: Collect waitUntilPromises from the middleware result and attach them to the Response object via a non-enumerable __vinextWaitUntil property so they survive the routing pipeline.
  • Cloudflare Worker integration (App Router & Pages Router): Add ctx: ExecutionContext to the generated Worker fetch signature, collect promises from both runMiddleware and __vinextWaitUntil, and delegate to Cloudflare's native ctx.waitUntil(). This ensures background work survives the serverless response lifecycle.
  • Update checks: vinext check now reports @clerk/nextjs as supported.
  • Tests: Add app-router.test.ts coverage verifying event.waitUntil is injected correctly.

Architectural note: from workaround to native API support

The original framing of this work was "making Clerk not crash." The paired Clerk PR changes that picture: once Clerk's ESM imports are fixed, @clerk/nextjs loads cleanly in Vite/Workers. What vinext needs to do is provide the correct API surface — specifically, a real NextFetchEvent with a functioning waitUntil(). This is not a shim to paper over bugs; it is the standard Next.js middleware contract that Clerk (and any other middleware library) legitimately depends on.


Architectural Decision: The 'Bubble Up' Pattern

Background promises need to travel from the middleware layer up to the Cloudflare Worker's ctx.waitUntil(). This PR uses a hidden property (Object.defineProperty(response, '__vinextWaitUntil', ...)).

Alternatives considered:

  1. AsyncLocalStorage (ALS): Risky in streaming responses. The primary execution thread finishes when the ReadableStream is returned; the ALS scope can tear down before background tasks are safely handed off to Cloudflare.
  2. Structured return types ({ response, tasks }): Changes the core router return type from Response to a tuple — massive blast radius across every caching, proxy, and error boundary layer.
  3. Global WeakMap keyed to Request: Fragile because the framework frequently clones requests when manipulating headers and URLs, which drops the reference.

Attaching promises directly to the Response as a non-enumerable property lets them travel safely up the execution chain, survive streaming, and require zero signature changes across the broader codebase.


Clerk API contact points

API Status
NextFetchEvent + event.waitUntil() This PR
NextResponse.next({ request: { headers } }) / x-middleware-request-* Already implemented
req.cookies / res.cookies.set() Already implemented
req.nextUrl / NextURL Already implemented
NextResponse.redirect() early return Already implemented
ESM-safe @clerk/nextjs package clerk/javascript#7954

🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/cloudflare/vinext/pull/126 **Author:** [@nbardy](https://github.com/nbardy) **Created:** 2/26/2026 **Status:** ✅ Merged **Merged:** 3/28/2026 **Merged by:** [@james-elicx](https://github.com/james-elicx) **Base:** `main` ← **Head:** `fix-clerk-middleware` --- ### 📝 Commits (10+) - [`0cbbe57`](https://github.com/cloudflare/vinext/commit/0cbbe577dfa837fd74a4ec50dcffb01717b8e332) fix: pass NextFetchEvent to middleware and bubble up waitUntil promises to Cloudflare Worker ctx - [`a8ee44d`](https://github.com/cloudflare/vinext/commit/a8ee44d69a0bba82eb6016d0554e590594c0f98a) Merge remote-tracking branch 'origin/main' into fix-clerk-middleware - [`f07f226`](https://github.com/cloudflare/vinext/commit/f07f226feb1290c978803f59dec59b4639587ad0) fix: address review feedback for waitUntil/NextFetchEvent support - [`0f4e82d`](https://github.com/cloudflare/vinext/commit/0f4e82d8977e2f7ef4f58115d3a9afb74706e77b) fix: move waitUntil settlement before early returns in prod-server - [`bf04872`](https://github.com/cloudflare/vinext/commit/bf0487255853f75ebfad47aa43d187ae2b04fdde) Merge main into fix-clerk-middleware: resolve conflicts, fix waitUntil plumbing - [`3a9975e`](https://github.com/cloudflare/vinext/commit/3a9975eb81f0c3aa67371e26713ef745e5e5f9b3) fix(middleware): remove double drainWaitUntil, include waitUntilPromises on error path - [`6fd374b`](https://github.com/cloudflare/vinext/commit/6fd374bd31168512618d8ab2c5ace7d5bc212540) fix: remove dead __vinextWaitUntil read code with no corresponding setter - [`ced210e`](https://github.com/cloudflare/vinext/commit/ced210e42fa25d72fb0006e3b8f1d632deaaf8b8) fix: remove no-op .catch on Promise.allSettled, strengthen waitUntil redirect test - [`a98855f`](https://github.com/cloudflare/vinext/commit/a98855f0818238d20f915bd5f0c3ad3bf192c7d7) test: add waitUntil continue:true path coverage - [`a8517db`](https://github.com/cloudflare/vinext/commit/a8517db07c64758de6577c9008c36f994772390c) fix: settle waitUntil promises in Pages Router dev server (index.ts) ### 📊 Changes **11 files changed** (+125 additions, -17 deletions) <details> <summary>View changed files</summary> 📝 `packages/vinext/src/deploy.ts` (+7 -0) 📝 `packages/vinext/src/entries/app-rsc-entry.ts` (+8 -4) 📝 `packages/vinext/src/entries/pages-server-entry.ts` (+2 -0) 📝 `packages/vinext/src/index.ts` (+6 -0) 📝 `packages/vinext/src/server/middleware.ts` (+8 -7) 📝 `packages/vinext/src/server/prod-server.ts` (+7 -0) 📝 `packages/vinext/src/shims/server.ts` (+4 -0) 📝 `tests/__snapshots__/entry-templates.test.ts.snap` (+10 -4) 📝 `tests/app-router.test.ts` (+7 -0) 📝 `tests/fixtures/app-basic/middleware.ts` (+12 -2) 📝 `tests/shims.test.ts` (+54 -0) </details> ### 📄 Description ## Description This PR makes `vinext` fully compatible with `@clerk/nextjs` by implementing the `NextFetchEvent.waitUntil` background task API across the middleware and Worker layers. > **Dependency:** This PR pairs with [clerk/javascript#7954](https://github.com/clerk/javascript/pull/7954), which fixes ESM compatibility in `@clerk/nextjs` itself (replacing `require()` calls that crash in Vite/Workers). That PR makes Clerk's package loadable in ESM runtimes. This PR provides the runtime API surface Clerk needs once it loads. Both are required for end-to-end Clerk support on Cloudflare Workers via vinext. ### What changed - **Pass `NextFetchEvent` to middleware**: Construct the event with a `waitUntil` array and pass it as the second argument to `middlewareFn`. This is the standard Next.js middleware signature — Clerk uses `event.waitUntil()` to schedule session sync and telemetry as background tasks. - **Bubble up promises**: Collect `waitUntilPromises` from the middleware result and attach them to the `Response` object via a non-enumerable `__vinextWaitUntil` property so they survive the routing pipeline. - **Cloudflare Worker integration (App Router & Pages Router)**: Add `ctx: ExecutionContext` to the generated Worker `fetch` signature, collect promises from both `runMiddleware` and `__vinextWaitUntil`, and delegate to Cloudflare's native `ctx.waitUntil()`. This ensures background work survives the serverless response lifecycle. - **Update checks**: `vinext check` now reports `@clerk/nextjs` as supported. - **Tests**: Add `app-router.test.ts` coverage verifying `event.waitUntil` is injected correctly. ### Architectural note: from workaround to native API support The original framing of this work was "making Clerk not crash." The paired Clerk PR changes that picture: once Clerk's ESM imports are fixed, `@clerk/nextjs` loads cleanly in Vite/Workers. What vinext needs to do is provide the **correct API surface** — specifically, a real `NextFetchEvent` with a functioning `waitUntil()`. This is not a shim to paper over bugs; it is the standard Next.js middleware contract that Clerk (and any other middleware library) legitimately depends on. --- ### Architectural Decision: The 'Bubble Up' Pattern Background promises need to travel from the middleware layer up to the Cloudflare Worker's `ctx.waitUntil()`. This PR uses a **hidden property** (`Object.defineProperty(response, '__vinextWaitUntil', ...)`). Alternatives considered: 1. **AsyncLocalStorage (ALS):** Risky in streaming responses. The primary execution thread finishes when the `ReadableStream` is returned; the ALS scope can tear down before background tasks are safely handed off to Cloudflare. 2. **Structured return types (`{ response, tasks }`):** Changes the core router return type from `Response` to a tuple — massive blast radius across every caching, proxy, and error boundary layer. 3. **Global `WeakMap` keyed to `Request`:** Fragile because the framework frequently clones requests when manipulating headers and URLs, which drops the reference. Attaching promises directly to the `Response` as a non-enumerable property lets them travel safely up the execution chain, survive streaming, and require zero signature changes across the broader codebase. --- ### Clerk API contact points | API | Status | |---|---| | `NextFetchEvent` + `event.waitUntil()` | ✅ This PR | | `NextResponse.next({ request: { headers } })` / `x-middleware-request-*` | ✅ Already implemented | | `req.cookies` / `res.cookies.set()` | ✅ Already implemented | | `req.nextUrl` / `NextURL` | ✅ Already implemented | | `NextResponse.redirect()` early return | ✅ Already implemented | | ESM-safe `@clerk/nextjs` package | ✅ clerk/javascript#7954 | --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
BreizhHardware 2026-05-06 12:39:15 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/vinext#332
No description provided.