nextjs - ✅(Solved) Fix Bug: `expected non-null body source` when passing a `Request` object to fetch AND receiving an error response (Node >= 24.14.0) [2 pull requests, 2 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
vercel/next.js#90826Fetched 2026-04-08 00:19:27
View on GitHub
Comments
2
Participants
2
Timeline
6
Reactions
4
Author
Participants
Timeline (top)
cross-referenced ×2comment_deleted ×1commented ×1issue_type_added ×1

Error Message

The reproduction is very simple. Execute this inside an App Router handler (e.g., Server Action or Route Handler) targeting an endpoint that returns a 4xx or 5xx error: // 💥 This crashes immediately on Node >= 24.14.0 when the response is an error The server responds with an error status (e.g., 401 or 500). It appears that when Next.js receives a JSON error response, its internal patch-fetch logic attempts to read or clone the original Request object (likely for error logging, tracing, or cache evaluation). Because the stream has already been consumed by the actual network request, attempting to access it again throws this error. // ❌ Fails on error response

Root Cause

This issue surfaces specifically on newer Node.js versions (>= v24.14.0 and probably later v25.x) where undici strictly enforces stream locking. It appears that when Next.js receives a JSON error response, its internal patch-fetch logic attempts to read or clone the original Request object (likely for error logging, tracing, or cache evaluation). Because the stream has already been consumed by the actual network request, attempting to access it again throws this error.

Fix Action

Fix / Workaround

Expected Behavior:

The patched fetch should return the Response object with res.ok === false without crashing, just as it does in a pure Node.js environment.

In the App Router environment, using the global patched fetch crashes with TypeError: expected non-null body source under a very specific set of conditions:

This issue surfaces specifically on newer Node.js versions (>= v24.14.0 and probably later v25.x) where undici strictly enforces stream locking. It appears that when Next.js receives a JSON error response, its internal patch-fetch logic attempts to read or clone the original Request object (likely for error logging, tracing, or cache evaluation). Because the stream has already been consumed by the actual network request, attempting to access it again throws this error.

PR fix notes

PR #90886: fix: preserve Request body source in patch-fetch

Description (problem / solution / changelog)

Fixes #90826 Related #83001

What?

We should preserve the original Request's internal body source when patch-fetch reconstructs the Request for uncached POST requests.

Why?

When fetch(new Request(url, { method: 'POST', body: JSON.stringify({}) })) is called in a Server Action or Route Handler on Node >= 24.14.0, it throws TypeError: expected non-null body source.

patch-fetch.ts was extracting reqInput.body (a ReadableStream) and passing it to new Request(reqInput.url, { body: stream }). Node 24.14+ (via undici) validates that the body has an internal "source", ReadableStreams extracted via .body don't carry that source.

The existing _ogBody mechanism only helps when caching is enabled (it's set during generateCacheKey()). For uncached POST requests, _ogBody is never set, so the fallback to reqInput.body triggers the crash.

How?

Split the isRequestInput block into two branches:

  • if _ogBody exists the body was consumed by generateCacheKey(), Request is disturbed. Reconstruct from URL string with the preserved body (thats the existing behavior).
  • if _ogBody is absent the body stream is intact. Use new Request(reqInput, reqOptions) instead of new Request(reqInput.url, reqOptions) to preserve the internal body source. This follows the same pattern as dedupe-fetch.ts which already reuses the original Request to avoid disturbing the body.

I also added a unit test verifying uncached POST requests preserve method and non-null body through to originFetch. Verified with reproduction repo on Node 24.14.0.

Changed files

  • packages/next/src/server/lib/patch-fetch.ts (modified, +16/-4)
  • test/e2e/app-dir/patch-fetch-request-body/app/api/test-post/route.ts (added, +16/-0)
  • test/e2e/app-dir/patch-fetch-request-body/app/layout.tsx (added, +8/-0)
  • test/e2e/app-dir/patch-fetch-request-body/app/page.tsx (added, +3/-0)
  • test/e2e/app-dir/patch-fetch-request-body/next.config.js (added, +6/-0)
  • test/e2e/app-dir/patch-fetch-request-body/patch-fetch-request-body.test.ts (added, +56/-0)

PR #360: Add dashboard permission access controls

Description (problem / solution / changelog)

Summary

This PR tightens and clarifies dashboard access behavior across the bot API and web app.

Changes

  • restore migration ordering compatibility by preserving the historical 014 migration and moving perf indexes to 015
  • fix Windows ESM command loading by importing command files via file:// URLs
  • add explicit guild access evaluation so configured moderator/admin roles are reflected in dashboard access
  • limit moderator dashboard access to the Moderation, Members, and Tickets sections
  • enforce those moderator/admin permissions on both the web proxy layer and bot API routes
  • improve Discord/bot API rate-limit handling so long retry windows do not stall dashboard requests

Changed files

  • migrations/014_audit_logs_user_tag_backfill.cjs (added, +38/-0)
  • src/api/middleware/auth.js (modified, +9/-0)
  • src/api/middleware/rateLimit.js (modified, +6/-0)
  • src/api/middleware/redisRateLimit.js (modified, +5/-0)
  • src/api/middleware/trustedInternalRequest.js (added, +21/-0)
  • src/api/routes/guilds.js (modified, +181/-23)
  • src/api/routes/members.js (modified, +7/-7)
  • src/api/routes/tickets.js (modified, +4/-4)
  • src/utils/loadCommands.js (modified, +2/-1)
  • tests/api/middleware/auth.test.js (modified, +55/-0)
  • tests/api/middleware/rateLimit.test.js (modified, +16/-0)
  • tests/api/middleware/redisRateLimit.test.js (modified, +23/-2)
  • tests/api/routes/guilds.coverage.test.js (modified, +66/-1)
  • tests/api/routes/guilds.test.js (modified, +74/-3)
  • tests/api/routes/members.test.js (modified, +27/-0)
  • tests/commands.test.js (modified, +2/-2)
  • tests/modules/ai.test.js (modified, +19/-5)
  • tests/modules/cli-process.test.js (modified, +7/-4)
  • vitest.config.js (modified, +2/-1)
  • web/src/app/api/guilds/[guildId]/members/[userId]/cases/route.ts (modified, +3/-3)
  • web/src/app/api/guilds/[guildId]/members/[userId]/route.ts (modified, +3/-3)
  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts (modified, +14/-3)
  • web/src/app/api/guilds/[guildId]/members/export/route.ts (modified, +3/-3)
  • web/src/app/api/guilds/[guildId]/members/route.ts (modified, +3/-3)
  • web/src/app/api/guilds/[guildId]/tickets/[ticketId]/route.ts (modified, +2/-2)
  • web/src/app/api/guilds/[guildId]/tickets/route.ts (modified, +2/-2)
  • web/src/app/api/guilds/[guildId]/tickets/stats/route.ts (modified, +2/-2)
  • web/src/app/api/guilds/route.ts (modified, +81/-1)
  • web/src/app/api/moderation/cases/[id]/route.ts (modified, +2/-2)
  • web/src/app/api/moderation/cases/route.ts (modified, +3/-3)
  • web/src/app/api/moderation/stats/route.ts (modified, +3/-3)
  • web/src/app/api/moderation/user/[userId]/history/route.ts (modified, +2/-2)
  • web/src/components/dashboard/config-editor.tsx (modified, +12/-176)
  • web/src/components/layout/dashboard-shell.tsx (modified, +23/-20)
  • web/src/components/layout/guild-directory-context.tsx (added, +101/-0)
  • web/src/components/layout/server-selector.tsx (modified, +24/-66)
  • web/src/components/layout/sidebar.tsx (modified, +40/-22)
  • web/src/components/ui/discord-markdown-editor.tsx (modified, +25/-4)
  • web/src/components/ui/embed-builder.tsx (modified, +67/-12)
  • web/src/hooks/use-guild-role.ts (modified, +14/-5)
  • web/src/lib/bot-api-proxy.ts (modified, +171/-21)
  • web/src/lib/discord.server.ts (modified, +60/-8)
  • web/src/types/discord.ts (modified, +1/-0)
  • web/tests/api/guilds.test.ts (modified, +151/-0)
  • web/tests/api/xp-route.test.ts (modified, +42/-4)
  • web/tests/components/dashboard/analytics-dashboard.test.tsx (modified, +30/-26)
  • web/tests/components/dashboard/config-diff-modal.test.tsx (modified, +8/-4)
  • web/tests/components/dashboard/config-editor-autosave.test.tsx (modified, +95/-18)
  • web/tests/components/layout/server-selector.test.tsx (modified, +51/-13)
  • web/tests/components/layout/sidebar.test.tsx (modified, +61/-5)
  • web/tests/components/ui/discord-markdown-editor.test.tsx (modified, +41/-0)
  • web/tests/components/ui/embed-builder.test.tsx (modified, +59/-0)
  • web/tests/hooks/use-guild-role.test.ts (modified, +12/-2)
  • web/tests/lib/bot-api-proxy-branches.test.ts (modified, +40/-0)
  • web/tests/lib/discord.test.ts (modified, +90/-0)

Code Example

// 1. Create a Request object with a body
const req = new Request('https://httpstat.us/401', { // An endpoint that returns 401
  method: 'POST',
  body: JSON.stringify({ test: true }),
  headers: { 'Content-Type': 'application/json' }
});

// 2. Pass the Request object directly to global fetch
// 💥 This crashes immediately on Node >= 24.14.0 when the response is an error
const res = await fetch(req);

---

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 25.3.0: Wed Jan 28 20:53:31 PST 2026; root:xnu-12377.81.4~5/RELEASE_ARM64_T8122
  Available memory (MB): 16384
  Available CPU cores: 8
Binaries:
  Node: 24.14.0
  npm: 11.9.0
  Yarn: N/A
  pnpm: 10.29.3
Relevant Packages:
  next: 16.1.6 // Latest available version is detected (16.1.6).
  eslint-config-next: N/A
  react: 19.2.4
  react-dom: 19.2.4
  typescript: 5.9.3
Next.js Config:
  output: standalone

---

// ❌ Fails on error response
fetch(new Request(url, init)); 

// ✅ Works perfectly
fetch(url, init);
RAW_BUFFERClick to expand / collapse

Link to the code that reproduces this issue

https://github.com/masakij/nextjs-expected-non-null-body-source-repro

To Reproduce

The reproduction is very simple. Execute this inside an App Router handler (e.g., Server Action or Route Handler) targeting an endpoint that returns a 4xx or 5xx error:

// 1. Create a Request object with a body
const req = new Request('https://httpstat.us/401', { // An endpoint that returns 401
  method: 'POST',
  body: JSON.stringify({ test: true }),
  headers: { 'Content-Type': 'application/json' }
});

// 2. Pass the Request object directly to global fetch
// 💥 This crashes immediately on Node >= 24.14.0 when the response is an error
const res = await fetch(req);

Current vs. Expected behavior

Expected Behavior:

The patched fetch should return the Response object with res.ok === false without crashing, just as it does in a pure Node.js environment.

Actual Behavior:

The application crashes with TypeError: expected non-null body source at ignore-listed frames.

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 25.3.0: Wed Jan 28 20:53:31 PST 2026; root:xnu-12377.81.4~5/RELEASE_ARM64_T8122
  Available memory (MB): 16384
  Available CPU cores: 8
Binaries:
  Node: 24.14.0
  npm: 11.9.0
  Yarn: N/A
  pnpm: 10.29.3
Relevant Packages:
  next: 16.1.6 // Latest available version is detected (16.1.6).
  eslint-config-next: N/A
  react: 19.2.4
  react-dom: 19.2.4
  typescript: 5.9.3
Next.js Config:
  output: standalone

Which area(s) are affected? (Select all that apply)

Not sure

Which stage(s) are affected? (Select all that apply)

next dev (local)

Additional context

Description:

In the App Router environment, using the global patched fetch crashes with TypeError: expected non-null body source under a very specific set of conditions:

You pass a Request object (that contains a body, e.g., POST/PUT) directly to fetch().

The request has Content-Type: application/json.

The server responds with an error status (e.g., 401 or 500).

This issue surfaces specifically on newer Node.js versions (>= v24.14.0 and probably later v25.x) where undici strictly enforces stream locking. It appears that when Next.js receives a JSON error response, its internal patch-fetch logic attempts to read or clone the original Request object (likely for error logging, tracing, or cache evaluation). Because the stream has already been consumed by the actual network request, attempting to access it again throws this error.

This commonly affects users of wrapper libraries like openapi-fetch that construct a Request object internally and pass it to the global fetch.

Current Workarounds:

Until this is fixed in the framework, developers can use one of the following workarounds depending on their use case:

  1. Deconstruct the Request: Pass the URL and RequestInit separately instead of the Request object.
// ❌ Fails on error response
fetch(new Request(url, init)); 

// ✅ Works perfectly
fetch(url, init);
  1. Use Native Undici Fetch: Bypass the Next.js patched fetch completely for non-GET requests by importing the native fetch: import { fetch as undiciFetch } from 'undici';

  2. Downgrade Node.js: Use Node.js <= v24.13.1 where undici's stream handling was less strict.

  3. Change Content-Type: If the API allows, using a Content-Type other than application/json seems to bypass the internal cloning logic causing the crash.


(Note: The investigation and debugging were done manually, but I used an AI assistant to help structure and translate this issue report into English.)

extent analysis

Quick Fix

Replace the Next.js‑patched fetch with the native Undici fetch for any request that carries a body (POST/PUT/PATCH) and may return a non‑2xx status.
You can do this with a tiny wrapper that automatically falls back to Undici when the request has a body, eliminating the “expected non‑null body source” crash on Node ≥ 24.14.


Step‑by‑Step Fix Plan

StepActionCode
1️⃣Install Undici (if not already a dependency).bash npm i undici # or yarn add undici
2️⃣

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING