From 11323754bf7363824b32aabd1140515ce791f73b Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Tue, 27 Oct 2026 16:05:54 -0400 Subject: [PATCH 1/6] fix: send cross-origin method overrides as a query parameter 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 --- src/utils/api-fetch.js | 65 ++++++++++++++++++++++- src/utils/api-fetch.test.js | 100 ++++++++++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+), 1 deletion(-) diff --git a/src/utils/api-fetch.js b/src/utils/api-fetch.js index c60044365..17d2f9e5a 100644 --- a/src/utils/api-fetch.js +++ b/src/utils/api-fetch.js @@ -2,6 +2,7 @@ import apiFetch from '@wordpress/api-fetch'; import { store as coreStore } from '@wordpress/core-data'; import { select } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; +import { addQueryArgs } from '@wordpress/url'; import { getGBKit, getPostPath, getPostReference } from './bridge'; import { info, warn, error as logError } from './logger'; import { ensureTrailingSlash, stripTrailingSlash } from './url'; @@ -37,6 +38,9 @@ const RETRY_DELAYS_MS = [ 500, 2000 ]; /** Longest `Retry-After` delay worth waiting for; longer ones fail at once. */ const MAX_RETRY_AFTER_MS = 10_000; +/** Header api-fetch's `httpV1Middleware` sets when sending a write as POST. */ +const METHOD_OVERRIDE_HEADER = 'X-HTTP-Method-Override'; + /** * Initializes the API fetch configuration and middleware. * @@ -93,7 +97,9 @@ export function configureApiFetch() { // Registered last so it runs first, covering preloaded responses too. apiFetch.use( persistedPostOverlayMiddleware ); apiFetch.setFetchHandler( - withRateLimitRetry( apiFetch.defaultFetchHandler ) + withRateLimitRetry( + withCrossOriginMethodOverride( apiFetch.defaultFetchHandler ) + ) ); } @@ -1313,6 +1319,63 @@ async function parseResponse( response, isOk ) { return body; } +/** + * Wraps a fetch handler to move the method override of cross-origin requests + * from a header to the `_method` query parameter. + * + * api-fetch sends PUT, PATCH and DELETE as POST with an override header, for + * servers that block those methods. WordPress's CORS preflight response does + * not allow that header, so a cross-origin write would fail before it is sent. + * WordPress reads `_method` as the same override, and a query parameter needs + * no preflight approval. Same-origin requests keep the header, as in WP Admin. + * + * As a fetch handler, it sees the final URL, after every middleware has run, + * including api-fetch's own `httpV1Middleware`. + * + * Exported for testing only. + * + * @param {FetchHandler} fetchHandler The handler performing the request. + * @return {FetchHandler} The handler with cross-origin overrides moved. + */ +export function withCrossOriginMethodOverride( fetchHandler ) { + return ( options ) => { + const method = options.headers?.[ METHOD_OVERRIDE_HEADER ]; + if ( ! method || ! options.url || ! isPreflighted( options.url ) ) { + return fetchHandler( options ); + } + + const { [ METHOD_OVERRIDE_HEADER ]: _header, ...headers } = + options.headers; + return fetchHandler( { + ...options, + url: addQueryArgs( options.url, { _method: method } ), + headers, + } ); + }; +} + +/** + * Whether the browser sends a CORS preflight for a request to the given URL. + * + * Only pages served over HTTP(S) are subject to CORS here: the iOS editor is a + * `file:` page with universal access, and its requests are never preflighted. + * + * @param {string} url The request URL. + * @return {boolean} True when the page is HTTP(S) and the URL is cross-origin. + */ +function isPreflighted( url ) { + const { protocol, origin, href } = window.location; + if ( protocol !== 'http:' && protocol !== 'https:' ) { + return false; + } + + try { + return new URL( url, href ).origin !== origin; + } catch { + return false; + } +} + const defaultPreloadData = { [ POST_TYPES_PATH ]: { body: { diff --git a/src/utils/api-fetch.test.js b/src/utils/api-fetch.test.js index e6dacba6c..70eded62a 100644 --- a/src/utils/api-fetch.test.js +++ b/src/utils/api-fetch.test.js @@ -13,6 +13,7 @@ import { configureApiFetch, persistedPostOverlayMiddleware, postTypesFallbackMiddleware, + withCrossOriginMethodOverride, withEditedPostType, withRateLimitRetry, } from './api-fetch'; @@ -1028,6 +1029,105 @@ describe( 'api-fetch credentials handling', () => { } ); } ); + describe( 'withCrossOriginMethodOverride', () => { + // The editor page is `http://localhost:3000` in jsdom, so the configured + // `https://example.com` root is cross-origin. + beforeEach( () => { + bridge.getGBKit.mockReturnValue( { + siteApiRoot: 'https://example.com/wp-json/', + siteApiNamespace: [], + namespaceExcludedPaths: [], + } ); + } ); + + const overriddenPut = ( url ) => ( { + url, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-HTTP-Method-Override': 'PUT', + }, + } ); + + it.each( [ 'PUT', 'PATCH', 'DELETE' ] )( + 'sends a cross-origin %s override as a query parameter', + async ( method ) => { + await apiFetch( { path: '/wp/v2/posts/1', method } ); + + const [ url, options ] = global.fetch.mock.calls[ 0 ]; + const { origin, pathname, searchParams } = new URL( url ); + expect( `${ origin }${ pathname }` ).toBe( + 'https://example.com/wp-json/wp/v2/posts/1' + ); + expect( searchParams.get( '_method' ) ).toBe( method ); + expect( options.method ).toBe( 'POST' ); + expect( options.headers ).not.toHaveProperty( + 'X-HTTP-Method-Override' + ); + } + ); + + it( 'appends to an existing query string', () => { + const fetchHandler = vi.fn(); + const handler = withCrossOriginMethodOverride( fetchHandler ); + + handler( + overriddenPut( + 'https://example.com/?rest_route=%2Fwp%2Fv2%2Fposts%2F1' + ) + ); + + const [ { url } ] = fetchHandler.mock.calls[ 0 ]; + const { searchParams } = new URL( url ); + expect( searchParams.get( 'rest_route' ) ).toBe( '/wp/v2/posts/1' ); + expect( searchParams.get( '_method' ) ).toBe( 'PUT' ); + } ); + + it( 'keeps the header on a same-origin request', () => { + const fetchHandler = vi.fn(); + const handler = withCrossOriginMethodOverride( fetchHandler ); + const options = overriddenPut( + `${ window.location.origin }/wp-json/wp/v2/posts/1` + ); + + handler( options ); + + expect( fetchHandler ).toHaveBeenCalledWith( options ); + } ); + + it.each( [ 'GET', 'POST' ] )( + 'leaves a cross-origin %s request unchanged', + async ( method ) => { + await apiFetch( { path: '/wp/v2/posts', method } ); + + const [ url, options ] = global.fetch.mock.calls[ 0 ]; + expect( new URL( url ).searchParams.has( '_method' ) ).toBe( + false + ); + expect( options.method ).toBe( method ); + } + ); + + it( 'keeps the header when the page is not served over HTTP', () => { + const fetchHandler = vi.fn(); + const handler = withCrossOriginMethodOverride( fetchHandler ); + const options = overriddenPut( + 'https://example.com/wp-json/wp/v2/posts/1' + ); + vi.spyOn( window, 'location', 'get' ).mockReturnValue( + new URL( 'file:///editor/index.html' ) + ); + + try { + handler( options ); + } finally { + vi.restoreAllMocks(); + } + + expect( fetchHandler ).toHaveBeenCalledWith( options ); + } ); + } ); + it( 'should preserve other headers when adding Authorization', async () => { bridge.getGBKit.mockReturnValue( { siteApiRoot: 'https://example.com/wp-json/', From ea19d898b2c49125d156f35a8b19fa29353e5fcd Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Fri, 30 Oct 2026 08:49:37 -0400 Subject: [PATCH 2/6] fix: send cross-origin writes with their real method 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 --- src/utils/api-fetch.js | 25 +++++++++------------ src/utils/api-fetch.test.js | 45 +++++++++++++------------------------ 2 files changed, 25 insertions(+), 45 deletions(-) diff --git a/src/utils/api-fetch.js b/src/utils/api-fetch.js index 17d2f9e5a..bbd8de922 100644 --- a/src/utils/api-fetch.js +++ b/src/utils/api-fetch.js @@ -2,7 +2,6 @@ import apiFetch from '@wordpress/api-fetch'; import { store as coreStore } from '@wordpress/core-data'; import { select } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; -import { addQueryArgs } from '@wordpress/url'; import { getGBKit, getPostPath, getPostReference } from './bridge'; import { info, warn, error as logError } from './logger'; import { ensureTrailingSlash, stripTrailingSlash } from './url'; @@ -98,7 +97,7 @@ export function configureApiFetch() { apiFetch.use( persistedPostOverlayMiddleware ); apiFetch.setFetchHandler( withRateLimitRetry( - withCrossOriginMethodOverride( apiFetch.defaultFetchHandler ) + withCrossOriginRealMethod( apiFetch.defaultFetchHandler ) ) ); } @@ -1320,14 +1319,14 @@ async function parseResponse( response, isOk ) { } /** - * Wraps a fetch handler to move the method override of cross-origin requests - * from a header to the `_method` query parameter. + * Wraps a fetch handler to send cross-origin writes with their real method. * * api-fetch sends PUT, PATCH and DELETE as POST with an override header, for - * servers that block those methods. WordPress's CORS preflight response does - * not allow that header, so a cross-origin write would fail before it is sent. - * WordPress reads `_method` as the same override, and a query parameter needs - * no preflight approval. Same-origin requests keep the header, as in WP Admin. + * servers that block those methods. WordPress's CORS preflight response allows + * those methods but not that header, so a cross-origin write would fail before + * it is sent. The `_method` query parameter is no alternative: WordPress also + * applies it to the preflight, which then fails authentication. Same-origin + * requests keep the header, as in WP Admin. * * As a fetch handler, it sees the final URL, after every middleware has run, * including api-fetch's own `httpV1Middleware`. @@ -1335,9 +1334,9 @@ async function parseResponse( response, isOk ) { * Exported for testing only. * * @param {FetchHandler} fetchHandler The handler performing the request. - * @return {FetchHandler} The handler with cross-origin overrides moved. + * @return {FetchHandler} The handler with cross-origin methods restored. */ -export function withCrossOriginMethodOverride( fetchHandler ) { +export function withCrossOriginRealMethod( fetchHandler ) { return ( options ) => { const method = options.headers?.[ METHOD_OVERRIDE_HEADER ]; if ( ! method || ! options.url || ! isPreflighted( options.url ) ) { @@ -1346,11 +1345,7 @@ export function withCrossOriginMethodOverride( fetchHandler ) { const { [ METHOD_OVERRIDE_HEADER ]: _header, ...headers } = options.headers; - return fetchHandler( { - ...options, - url: addQueryArgs( options.url, { _method: method } ), - headers, - } ); + return fetchHandler( { ...options, method, headers } ); }; } diff --git a/src/utils/api-fetch.test.js b/src/utils/api-fetch.test.js index 70eded62a..8d0d3fdb1 100644 --- a/src/utils/api-fetch.test.js +++ b/src/utils/api-fetch.test.js @@ -13,7 +13,7 @@ import { configureApiFetch, persistedPostOverlayMiddleware, postTypesFallbackMiddleware, - withCrossOriginMethodOverride, + withCrossOriginRealMethod, withEditedPostType, withRateLimitRetry, } from './api-fetch'; @@ -1029,7 +1029,7 @@ describe( 'api-fetch credentials handling', () => { } ); } ); - describe( 'withCrossOriginMethodOverride', () => { + describe( 'withCrossOriginRealMethod', () => { // The editor page is `http://localhost:3000` in jsdom, so the configured // `https://example.com` root is cross-origin. beforeEach( () => { @@ -1050,42 +1050,27 @@ describe( 'api-fetch credentials handling', () => { } ); it.each( [ 'PUT', 'PATCH', 'DELETE' ] )( - 'sends a cross-origin %s override as a query parameter', + 'sends a cross-origin %s with its real method', async ( method ) => { await apiFetch( { path: '/wp/v2/posts/1', method } ); const [ url, options ] = global.fetch.mock.calls[ 0 ]; - const { origin, pathname, searchParams } = new URL( url ); - expect( `${ origin }${ pathname }` ).toBe( - 'https://example.com/wp-json/wp/v2/posts/1' + expect( new URL( url ).pathname ).toBe( + '/wp-json/wp/v2/posts/1' ); - expect( searchParams.get( '_method' ) ).toBe( method ); - expect( options.method ).toBe( 'POST' ); + expect( options.method ).toBe( method ); expect( options.headers ).not.toHaveProperty( 'X-HTTP-Method-Override' ); + expect( options.headers[ 'Content-Type' ] ).toBe( + 'application/json' + ); } ); - it( 'appends to an existing query string', () => { - const fetchHandler = vi.fn(); - const handler = withCrossOriginMethodOverride( fetchHandler ); - - handler( - overriddenPut( - 'https://example.com/?rest_route=%2Fwp%2Fv2%2Fposts%2F1' - ) - ); - - const [ { url } ] = fetchHandler.mock.calls[ 0 ]; - const { searchParams } = new URL( url ); - expect( searchParams.get( 'rest_route' ) ).toBe( '/wp/v2/posts/1' ); - expect( searchParams.get( '_method' ) ).toBe( 'PUT' ); - } ); - it( 'keeps the header on a same-origin request', () => { const fetchHandler = vi.fn(); - const handler = withCrossOriginMethodOverride( fetchHandler ); + const handler = withCrossOriginRealMethod( fetchHandler ); const options = overriddenPut( `${ window.location.origin }/wp-json/wp/v2/posts/1` ); @@ -1100,17 +1085,17 @@ describe( 'api-fetch credentials handling', () => { async ( method ) => { await apiFetch( { path: '/wp/v2/posts', method } ); - const [ url, options ] = global.fetch.mock.calls[ 0 ]; - expect( new URL( url ).searchParams.has( '_method' ) ).toBe( - false - ); + const [ , options ] = global.fetch.mock.calls[ 0 ]; expect( options.method ).toBe( method ); + expect( options.headers ).not.toHaveProperty( + 'X-HTTP-Method-Override' + ); } ); it( 'keeps the header when the page is not served over HTTP', () => { const fetchHandler = vi.fn(); - const handler = withCrossOriginMethodOverride( fetchHandler ); + const handler = withCrossOriginRealMethod( fetchHandler ); const options = overriddenPut( 'https://example.com/wp-json/wp/v2/posts/1' ); From bdf8f7365482793022e6baf1111d06bf270caf33 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Fri, 30 Oct 2026 09:23:26 -0400 Subject: [PATCH 3/6] fix: uppercase restored cross-origin methods `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 --- src/utils/api-fetch.js | 8 +++++++- src/utils/api-fetch.test.js | 10 ++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/utils/api-fetch.js b/src/utils/api-fetch.js index bbd8de922..c03bff4f3 100644 --- a/src/utils/api-fetch.js +++ b/src/utils/api-fetch.js @@ -1345,7 +1345,13 @@ export function withCrossOriginRealMethod( fetchHandler ) { const { [ METHOD_OVERRIDE_HEADER ]: _header, ...headers } = options.headers; - return fetchHandler( { ...options, method, headers } ); + // `fetch` uppercases `put` and `delete` but not `patch`, and the + // preflight compares methods case-sensitively. + return fetchHandler( { + ...options, + method: method.toUpperCase(), + headers, + } ); }; } diff --git a/src/utils/api-fetch.test.js b/src/utils/api-fetch.test.js index 8d0d3fdb1..9f6d77b88 100644 --- a/src/utils/api-fetch.test.js +++ b/src/utils/api-fetch.test.js @@ -1068,6 +1068,16 @@ describe( 'api-fetch credentials handling', () => { } ); + it.each( [ 'put', 'patch', 'delete' ] )( + 'uppercases a cross-origin %s method', + async ( method ) => { + await apiFetch( { path: '/wp/v2/posts/1', method } ); + + const [ , options ] = global.fetch.mock.calls[ 0 ]; + expect( options.method ).toBe( method.toUpperCase() ); + } + ); + it( 'keeps the header on a same-origin request', () => { const fetchHandler = vi.fn(); const handler = withCrossOriginRealMethod( fetchHandler ); From 30208d1983ae8cf110cac15a7198bd8c39ea7526 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Fri, 30 Oct 2026 09:23:45 -0400 Subject: [PATCH 4/6] refactor: trim the cross-origin method docs and rename its helper `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 --- src/utils/api-fetch.js | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/src/utils/api-fetch.js b/src/utils/api-fetch.js index c03bff4f3..19a55c6e0 100644 --- a/src/utils/api-fetch.js +++ b/src/utils/api-fetch.js @@ -1319,17 +1319,10 @@ async function parseResponse( response, isOk ) { } /** - * Wraps a fetch handler to send cross-origin writes with their real method. - * - * api-fetch sends PUT, PATCH and DELETE as POST with an override header, for - * servers that block those methods. WordPress's CORS preflight response allows - * those methods but not that header, so a cross-origin write would fail before - * it is sent. The `_method` query parameter is no alternative: WordPress also - * applies it to the preflight, which then fails authentication. Same-origin - * requests keep the header, as in WP Admin. - * - * As a fetch handler, it sees the final URL, after every middleware has run, - * including api-fetch's own `httpV1Middleware`. + * Wraps a fetch handler to send cross-origin writes with their real method, as + * WordPress's CORS preflight rejects api-fetch's `X-HTTP-Method-Override`. The + * `_method` query parameter fails too: WordPress applies it to the preflight, + * which carries no credentials. * * Exported for testing only. * @@ -1339,7 +1332,11 @@ async function parseResponse( response, isOk ) { export function withCrossOriginRealMethod( fetchHandler ) { return ( options ) => { const method = options.headers?.[ METHOD_OVERRIDE_HEADER ]; - if ( ! method || ! options.url || ! isPreflighted( options.url ) ) { + if ( + ! method || + ! options.url || + ! isCrossOriginFromHttpPage( options.url ) + ) { return fetchHandler( options ); } @@ -1356,15 +1353,13 @@ export function withCrossOriginRealMethod( fetchHandler ) { } /** - * Whether the browser sends a CORS preflight for a request to the given URL. - * - * Only pages served over HTTP(S) are subject to CORS here: the iOS editor is a - * `file:` page with universal access, and its requests are never preflighted. + * Whether a request from this page to the URL is cross-origin under CORS; + * `file:` pages, like the iOS editor's, are exempt. * * @param {string} url The request URL. * @return {boolean} True when the page is HTTP(S) and the URL is cross-origin. */ -function isPreflighted( url ) { +function isCrossOriginFromHttpPage( url ) { const { protocol, origin, href } = window.location; if ( protocol !== 'http:' && protocol !== 'https:' ) { return false; From d242d9ccf5fae87144b2287c1a3115178e01bec2 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Fri, 30 Oct 2026 09:24:42 -0400 Subject: [PATCH 5/6] docs: mark the native media delete relay as redundant 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 --- .../java/org/wordpress/gutenberg/MediaUploadServer.kt | 6 ++---- src/utils/api-fetch-upload-middleware.test.js | 4 +--- src/utils/api-fetch.js | 11 +++++------ 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/android/Gutenberg/src/main/java/org/wordpress/gutenberg/MediaUploadServer.kt b/android/Gutenberg/src/main/java/org/wordpress/gutenberg/MediaUploadServer.kt index 389e4a912..a296e7c3f 100644 --- a/android/Gutenberg/src/main/java/org/wordpress/gutenberg/MediaUploadServer.kt +++ b/android/Gutenberg/src/main/java/org/wordpress/gutenberg/MediaUploadServer.kt @@ -336,10 +336,8 @@ internal class MediaUploadServer( * Relays the editor's orphan cleanup to WordPress. * * Core's media upload middleware deletes the attachment when every - * `post-process` retry fails. A cross-origin editor cannot issue that - * request directly — api-fetch tunnels `DELETE` as a `POST` carrying - * `X-HTTP-Method-Override`, which core's CORS allow-list omits, so the - * browser blocks it at preflight. Relaying it here lets the cleanup run. + * `post-process` retry fails. The editor can now send that request directly, + * even cross-origin, so this route is redundant and slated for removal. */ private suspend fun handleDelete(attachmentId: String, query: String): HttpResponse { val client = internalClient ?: return errorResponse(500, "No internal media client configured") diff --git a/src/utils/api-fetch-upload-middleware.test.js b/src/utils/api-fetch-upload-middleware.test.js index 4f954afcd..32238f4d7 100644 --- a/src/utils/api-fetch-upload-middleware.test.js +++ b/src/utils/api-fetch-upload-middleware.test.js @@ -528,9 +528,7 @@ describe( 'nativeMediaUploadMiddleware', () => { } ); it( 'routes an attachment deletion through the native server', async () => { - // api-fetch tunnels DELETE as POST + `X-HTTP-Method-Override`, which - // core's CORS allow-list omits, so a cross-origin editor cannot make - // this request directly. Relaying it keeps the orphan cleanup working. + // The native server relays the deletion, as it does the upload. global.fetch = vi.fn( () => Promise.resolve( new Response( '{"deleted":true}', { status: 200 } ) diff --git a/src/utils/api-fetch.js b/src/utils/api-fetch.js index 19a55c6e0..dccd408dc 100644 --- a/src/utils/api-fetch.js +++ b/src/utils/api-fetch.js @@ -650,12 +650,8 @@ function rejectUnreachableUpload( connectionError, options ) { * through to its normal handling. * * Core's media upload middleware deletes the orphaned attachment when every - * `post-process` retry fails. That request cannot be made directly from a - * cross-origin editor: `@wordpress/api-fetch` tunnels `DELETE` as a `POST` - * carrying `X-HTTP-Method-Override`, and core's `rest_allowed_cors_headers` - * does not list that header, so the browser blocks it at preflight and the - * orphan survives. Relaying through native code — which sets its own CORS - * policy — is what lets the cleanup complete. + * `post-process` retry fails. Native code relays that deletion, as it does the + * upload. * * This runs before `X-HTTP-Method-Override` exists: api-fetch's `httpV1` * middleware adds it further down the chain, so the method here is still a @@ -670,6 +666,9 @@ function rejectUnreachableUpload( connectionError, options ) { * @param {NativeUploadTransport} transport How to reach native code. * @param {(options: Object) => Promise} next The next middleware, for a refused deletion. * @return {?Promise} The relayed deletion, or `null` if not applicable. + * + * @todo Remove: `withCrossOriginRealMethod` lets the editor send this deletion + * directly, even cross-origin. */ function nativeMediaDelete( options, transport, next ) { if ( options.method?.toUpperCase() !== 'DELETE' || ! options.path ) { From f8daefdb96eafc6e8f38a5ad3dd1273cb441d13b Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Fri, 30 Oct 2026 09:30:46 -0400 Subject: [PATCH 6/6] docs: frame the media delete relay removal as an opportunity 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 --- .../main/java/org/wordpress/gutenberg/MediaUploadServer.kt | 3 ++- src/utils/api-fetch.js | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/android/Gutenberg/src/main/java/org/wordpress/gutenberg/MediaUploadServer.kt b/android/Gutenberg/src/main/java/org/wordpress/gutenberg/MediaUploadServer.kt index a296e7c3f..db1933797 100644 --- a/android/Gutenberg/src/main/java/org/wordpress/gutenberg/MediaUploadServer.kt +++ b/android/Gutenberg/src/main/java/org/wordpress/gutenberg/MediaUploadServer.kt @@ -337,7 +337,8 @@ internal class MediaUploadServer( * * Core's media upload middleware deletes the attachment when every * `post-process` retry fails. The editor can now send that request directly, - * even cross-origin, so this route is redundant and slated for removal. + * even cross-origin, so this route may no longer be needed. See + * https://github.com/wordpress-mobile/GutenbergKit/issues/860. */ private suspend fun handleDelete(attachmentId: String, query: String): HttpResponse { val client = internalClient ?: return errorResponse(500, "No internal media client configured") diff --git a/src/utils/api-fetch.js b/src/utils/api-fetch.js index dccd408dc..dae0b958d 100644 --- a/src/utils/api-fetch.js +++ b/src/utils/api-fetch.js @@ -667,8 +667,9 @@ function rejectUnreachableUpload( connectionError, options ) { * @param {(options: Object) => Promise} next The next middleware, for a refused deletion. * @return {?Promise} The relayed deletion, or `null` if not applicable. * - * @todo Remove: `withCrossOriginRealMethod` lets the editor send this deletion - * directly, even cross-origin. + * @todo Consider removing, as `withCrossOriginRealMethod` lets the editor send + * this deletion directly, even cross-origin. See + * https://github.com/wordpress-mobile/GutenbergKit/issues/860. */ function nativeMediaDelete( options, transport, next ) { if ( options.method?.toUpperCase() !== 'DELETE' || ! options.path ) {