Skip to content

API Fetch: Respect caller-provided Content-Type in httpV1 middleware#76285

Merged
Mamaduka merged 1 commit intoWordPress:trunkfrom
chubes4:fix/api-fetch-content-type-override
Mar 9, 2026
Merged

API Fetch: Respect caller-provided Content-Type in httpV1 middleware#76285
Mamaduka merged 1 commit intoWordPress:trunkfrom
chubes4:fix/api-fetch-content-type-override

Conversation

@chubes4
Copy link
Copy Markdown
Contributor

@chubes4 chubes4 commented Mar 7, 2026

What?

Reorders the header spread in httpV1Middleware so that Content-Type: application/json acts as a default rather than an unconditional overwrite. Caller-provided Content-Type headers are now preserved.

Why?

The middleware currently places Content-Type: 'application/json' after ...options.headers in the spread, which means any custom Content-Type set by the caller is silently lost:

// Before (broken): caller's Content-Type is always overwritten
headers: {
    ...options.headers,
    'X-HTTP-Method-Override': method,
    'Content-Type': 'application/json',  // wins every time
},

This makes it impossible to send non-JSON content (e.g. raw markdown, plain text, XML) via apiFetch using PUT, PATCH, or DELETE methods. The server receives raw content labeled as application/json and returns 400 Invalid JSON body passed.

How?

One-line reorder — move Content-Type before the caller's header spread so it acts as a default:

// After (fixed): Content-Type is a default, caller can override
headers: {
    'Content-Type': 'application/json',  // default
    ...options.headers,                  // caller wins
    'X-HTTP-Method-Override': method,    // always forced
},
  • Callers who don't set Content-Type still get application/json (backwards compatible)
  • Callers who do set a custom Content-Type have their value respected
  • X-HTTP-Method-Override remains after the spread and cannot be overridden by callers (correct security behavior)

How was this discovered?

While building a WordPress plugin (Data Machine) that saves markdown files (agent memory files — SOUL.md, MEMORY.md) via a custom REST endpoint. The admin page used apiFetch with method: 'PUT', headers: { 'Content-Type': 'text/plain' }, and body: rawMarkdownContent. Saves failed with 400 Invalid JSON body passed because the middleware silently overwrote the Content-Type.

Testing

Automated

  • 11 new unit tests added to packages/api-fetch/src/middlewares/test/http-v1.ts
  • All 60 tests across the entire @wordpress/api-fetch package pass (9 suites, 0 failures)
  • Tests cover: Content-Type preservation, default behavior, all override methods (PUT/PATCH/DELETE), header preservation, non-override methods untouched, body integrity, case-insensitive method matching

Manual (production site)

  • Patched api-fetch.min.js on a live WordPress 6.9 site (chubes.net)
  • Confirmed Content-Type: text/plain survives through the middleware to the server (verified via Chrome DevTools Request Headers)
  • Successfully saved raw markdown content via PUT request through apiFetch
  • Confirmed X-HTTP-Method-Override: PUT is still correctly set and forced

Fixes #76284

AI Disclosure

Per the WordPress AI Guidelines, this contribution was made with meaningful AI assistance:

  • Tool used: Claude Code (Claude claude-opus-4-6 via Anthropic's CLI, running through Kimaki Discord integration)
  • How AI was used: The bug was discovered and diagnosed collaboratively — I (Chris Huber) encountered the 400 error while building the Data Machine plugin, and Claude Code helped trace the root cause through the httpV1Middleware source code, identify the spread order issue, propose the fix, write the comprehensive test suite, and draft the PR description. The fix itself (reordering one line) was straightforward once the root cause was identified.
  • Human verification: I reviewed all code changes, confirmed the fix on my production WordPress site via browser DevTools (verifying the actual HTTP headers sent), and approved the PR submission. The root cause analysis, fix logic, and test coverage were all validated by me before submission.
  • The full debugging session included: reading the Gutenberg source, tracing the middleware chain, patching the built JS on a live site, working around a Cloudflare cache issue (the version hash is hardcoded in script-loader-packages.min.php), and confirming the fix with real browser requests saving real markdown files.

…ware

The httpV1 middleware unconditionally overwrites the Content-Type header
to 'application/json' for PUT, PATCH, and DELETE requests. Because the
spread order places Content-Type after ...options.headers, any custom
Content-Type set by the caller is silently lost.

This makes it impossible to send non-JSON content (e.g. raw markdown,
plain text) via apiFetch for these HTTP methods. The server receives raw
content with a JSON content type and returns 400 Invalid JSON body.

Fix: Reorder the spread so Content-Type: application/json acts as a
default that callers can override, while X-HTTP-Method-Override remains
forced after the spread (cannot be overridden by callers).

Also adds 11 new tests covering Content-Type preservation, default
behavior, all override methods, header preservation, and body integrity.

Fixes WordPress#76284
@chubes4 chubes4 requested review from mmtr and nerrad as code owners March 7, 2026 18:39
@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 7, 2026

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: chubes4 <extrachill@git.wordpress.org>
Co-authored-by: Mamaduka <mamaduka@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions github-actions bot added [Package] API fetch /packages/api-fetch First-time Contributor Pull request opened by a first-time contributor to Gutenberg repository labels Mar 7, 2026
@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 7, 2026

👋 Thanks for your first Pull Request and for helping build the future of Gutenberg and WordPress, @chubes4! In case you missed it, we'd love to have you join us in our Slack community.

If you want to learn more about WordPress development in general, check out the Core Handbook full of helpful information.

@Mamaduka Mamaduka added the [Type] Bug An existing feature does not function as intended label Mar 9, 2026
Copy link
Copy Markdown
Member

@Mamaduka Mamaduka left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @chubes4!

The fix makes sense, and as far as I can tell from the Git history, this was just an overlooked case.

@Mamaduka Mamaduka added the props-bot Manually triggers Props Bot to ensure the list of props is up to date. label Mar 9, 2026
@github-actions github-actions bot removed the props-bot Manually triggers Props Bot to ensure the list of props is up to date. label Mar 9, 2026
@Mamaduka Mamaduka merged commit 129f293 into WordPress:trunk Mar 9, 2026
50 of 52 checks passed
@github-actions github-actions bot added this to the Gutenberg 22.8 milestone Mar 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

First-time Contributor Pull request opened by a first-time contributor to Gutenberg repository [Package] API fetch /packages/api-fetch [Type] Bug An existing feature does not function as intended

Projects

None yet

Development

Successfully merging this pull request may close these issues.

api-fetch: httpV1 middleware unconditionally overwrites Content-Type header

2 participants