Skip to content

fix: send cross-origin writes with their real method - #841

Merged
dcalhoun merged 6 commits into
trunkfrom
fix/cross-origin-method-override
Oct 30, 2026
Merged

dcalhoun merged 6 commits into
trunkfrom
fix/cross-origin-method-override

Conversation

@dcalhoun

@dcalhoun dcalhoun commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

Description

Close CMM-2553. Send the real fetch method rather than a method-override header for cross-origin requests.

Core places PUT and DELETE methods as headers to mitigate disruption from servers that block these methods. However, the header is not allowed cross-origin, which causes these requests to fail.

This prepares for editor-driven saving, where the editor will send these requests to save posts. It also fixes plugins that send these requests, which fail cross-origin today.

Add logic to the GutenbergKit api-fetch handler to restore the real method for cross-origin requests. Same-origin requests keep the header.

Testing instructions

Tip

The Chrome inspector seems to often timeout when using GutenbergKit's local dev server with an emulator. Consider using a physical device and/or a local production build instead (make build).

The editor doesn't send writes yet, so these steps send them from the WebView console.

  1. Run make build, then build and install the Android demo app. Use the production build, not the Vite dev server: the dev server makes every request cross-origin.
  2. Sign the app in to a WordPress.com Simple site, an Atomic site, and a self-hosted site with an application password, e.g. make login-android-app SITE=<site>.
  3. For each site, open the editor, then inspect the WebView from chrome://inspect.
  4. In the console, create, update and trash a draft:
    const { id } = await wp.apiFetch( { path: '/wp/v2/posts', method: 'POST', data: { title: 'Method override test', status: 'draft' } } );
    await wp.apiFetch( { path: `/wp/v2/posts/${ id }`, method: 'PUT', data: { title: 'Method override test (updated)' } } );
    await wp.apiFetch( { path: `/wp/v2/posts/${ id }`, method: 'DELETE' } );
  5. Verify that each call resolves, and that the draft is in the site's trash with the updated title.
  6. In the Network panel, verify the update and trash requests:
    • Simple site: a PUT and a DELETE to public-api.wordpress.com, with no X-HTTP-Method-Override header.
    • Atomic and self-hosted sites: POSTs to the site's own /wp-json/ with the X-HTTP-Method-Override header.

On trunk, the PUT fails on the Simple site, because public-api.wordpress.com's CORS preflight doesn't allow the X-HTTP-Method-Override header.


Additional AI-generated details

api-fetch's built-in httpV1Middleware sends PUT, PATCH and DELETE as POST with an X-HTTP-Method-Override header. Core WordPress's and public-api.wordpress.com's CORS preflight responses allow those methods but not that header, so a cross-origin write fails before it's sent. On Android, that covers Simple sites, which the editor reaches through public-api.wordpress.com. Atomic and Jetpack sites use their own /wp-json/, so they're same-origin. Editor writes don't exist yet, but editor-driven saving needs them.

A new fetch-handler wrapper, withCrossOriginRealMethod, restores the real method and drops the header when the page is HTTP(S) and the request is cross-origin. httpV1Middleware runs after every GBK middleware, so the change has to happen in the fetch handler.

  • Same-origin requests keep the header, as in WP Admin: Android sites with application passwords, and iOS, whose file: page is never preflighted.
  • _method doesn't work. The first commit used the _method query parameter. WordPress applies it to the preflight too, which carries no credentials, so the preflight returns 401. That happens on both public-api.wordpress.com and core.
  • Trade-off: cross-origin writes lose the protection against servers that block PUT and DELETE. Both preflight responses allow those methods.
  • Tested on an Android emulator (Pixel 8 Pro) with the demo app, using the steps above:
    • Simple site: the trunk-style request fails with HeaderDisallowedByPreflightResponse; the branch's PUT and DELETE return 200.
    • Atomic site: keeps the header and works.
    • Self-hosted site (Cloudways, WordPress core only): keeps the header and works.
  • The wp-env CORS mu-plugin is unchanged. It already allows PUT, PATCH and DELETE.

🤖 Generated with Claude Code

@dcalhoun dcalhoun added [Type] Bug An existing feature does not function as intended Android labels Oct 28, 2026
@wpmobilebot

wpmobilebot commented Oct 28, 2026 •

Copy link
Copy Markdown

XCFramework Build

This PR's XCFramework is available for testing. Add the following to your Package.swift:

.package(url: "https://github.com/wordpress-mobile/GutenbergKit", branch: "pr-build/841")

Built from f8daefd

api-fetch sends PUT, PATCH and DELETE as POST with an
X-HTTP-Method-Override header. WordPress's CORS preflight doesn't allow
that header, so cross-origin writes fail, e.g. from Android to sites
whose REST API lives on another host. WordPress reads `_method` as the
same override and it needs no preflight approval.

Refs CMM-2553

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dcalhoun
dcalhoun force-pushed the fix/cross-origin-method-override branch from 4555521 to 1132375 Compare October 30, 2026 12:39
WordPress applies `_method` to the CORS preflight too, which carries no
credentials, so the preflight fails authentication with a 401. The real
PUT, PATCH and DELETE methods are allowed by the preflight.

Refs CMM-2553

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dcalhoun dcalhoun changed the title fix: send cross-origin method overrides as a query parameter fix: send cross-origin writes with their real method Oct 30, 2026
dcalhoun and others added 3 commits October 30, 2026 09:23
`fetch` normalizes `put` and `delete` but not `patch`, and the CORS
preflight compares methods case-sensitively, so a lowercase `patch`
was still blocked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`isPreflighted` returned true for the iOS dev server, whose requests
go through the relay without a preflight; the new name says what the
check actually tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The relay's comments said a cross-origin editor cannot send DELETE,
which is no longer true now that cross-origin writes use their real
method. Removing the relay is left for a follow-up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Whether to remove the relay is still undecided, so the comments point
to the issue tracking it rather than presuming the outcome.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dcalhoun
dcalhoun marked this pull request as ready for review October 30, 2026 13:40
@dcalhoun
dcalhoun requested a review from nbradbury October 30, 2026 13:40

@nbradbury nbradbury left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good! :shipit:

@dcalhoun
dcalhoun merged commit 28ff458 into trunk Oct 30, 2026
25 checks passed
@dcalhoun
dcalhoun deleted the fix/cross-origin-method-override branch October 30, 2026 15:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Android [Type] Bug An existing feature does not function as intended

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants