Skip to content

Fix Safari failing to make cross-origin HTTP requests#3440

Open
ashfame wants to merge 13 commits intotrunkfrom
beta_tester_safari_prod_only
Open

Fix Safari failing to make cross-origin HTTP requests#3440
ashfame wants to merge 13 commits intotrunkfrom
beta_tester_safari_prod_only

Conversation

@ashfame
Copy link
Copy Markdown
Member

@ashfame ashfame commented Mar 28, 2026

Motivation for the change, related issues

Problem:
In Safari, WordPress beta tester plugin fails to offer the correct updates that are available after updating the channels to Beta/RCs in its settings. This happen because, the request to api.wordpress.org doesn't go through.

Root cause:
Safari does not support ReadableStream as a fetch() request body, throwing "NotSupportedError: ReadableStream uploading is not supported". fetchWithCorsProxy used ReadableStream.tee() to duplicate the body for the direct-fetch + CORS-proxy-fallback pattern, which broke all POST requests (e.g. wp_version_check to api.wordpress.org) in Safari.

Implementation details

For browsers that support it, keep the original streaming teeRequest() direct-fetch + CORS-proxy-fallback flow. For Safari (no ReadableStream upload support), buffer the request body into an ArrayBuffer so it can be reused for both the direct fetch attempt and the proxy retry.

The issue is confirmed to fix with this PR, but the issue only surfaces locally when ran like the following:

npx nx build playground-website
cd dist/packages/playground/wasm-wordpress-net
php -S 127.0.0.1:7777 -t .

The issue doesn't surface when running npm run dev, not sure why.

Testing Instructions (or ideally a Blueprint)

Checkout this branch locally & run with:

npx nx build playground-website
cd dist/packages/playground/wasm-wordpress-net
php -S 127.0.0.1:7777 -t .
  • Open http://localhost:7777/?plugin=wordpress-beta-tester in Safari browser.
  • Go to Dashboard > Updates, only Nightly is offered.
  • Go to Tools > Beta Testing
  • Select Bleeding-edge and then Beta/RC, save.
  • Now go to Dashboard > Updates, 7.0-RC2 will be offered for upgrade.

@ashfame ashfame self-assigned this Mar 28, 2026
@adamziel
Copy link
Copy Markdown
Collaborator

Let's still use the body stream when supported, no need to make it worse for other browsers.

@adamziel
Copy link
Copy Markdown
Collaborator

Alternatively, we could try to emulate stream support using XHR. This matters when uploading big files.

* attempt and the CORS proxy fallback. We buffer into an ArrayBuffer
* instead of using ReadableStream.tee() because Safari does not support
* ReadableStream as a fetch() request body ("ReadableStream uploading
* is not supported").
Copy link
Copy Markdown
Member

@brandonpayton brandonpayton Mar 28, 2026

Choose a reason for hiding this comment

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

Fortunately, it looks like "ReadableStream request bodies" are part the WebKit Interop 2026 plans:
https://webkit.org/blog/17818/announcing-interop-2026/#fetch-uploads-and-ranges

ReadableStream request bodies enable true streaming uploads, letting you upload large files or real-time data without loading everything into memory first:

ashfame added 2 commits March 31, 2026 21:18
Safari does not support ReadableStream as a fetch() request body,
throwing "NotSupportedError: ReadableStream uploading is not supported".
fetchWithCorsProxy used ReadableStream.tee() to duplicate the body for
the direct-fetch + CORS-proxy-fallback pattern, which broke all POST
requests (e.g. wp_version_check to api.wordpress.org) in Safari.

Buffer the request body into an ArrayBuffer instead so it can be reused
for both fetch attempts.

Made-with: Cursor
…pport

Instead of unconditionally buffering the request body into an ArrayBuffer
for all browsers, detect at runtime whether the browser supports
ReadableStream as a fetch() request body. Use the streaming teeRequest()
approach when supported (Chrome, Firefox) and fall back to ArrayBuffer
buffering only when not (Safari).

Addresses review feedback to avoid degrading streaming performance for
browsers that already support it.

Made-with: Cursor
@ashfame ashfame force-pushed the beta_tester_safari_prod_only branch from b427a3f to 4367a2f Compare March 31, 2026 15:33
ashfame added 2 commits March 31, 2026 22:27
Add a beforeAll hook that triggers the one-time ReadableStream body
support detection before any per-test fetch mocks are installed, so
the detection's internal fetch() call doesn't consume mock responses.

Made-with: Cursor
@ashfame ashfame marked this pull request as ready for review March 31, 2026 18:43
@ashfame ashfame requested review from a team, Copilot and zaerl March 31, 2026 18:43
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses Safari failures for cross-origin POST requests by avoiding ReadableStream request bodies when unsupported, enabling the direct-fetch + CORS-proxy-fallback flow to work in Safari.

Changes:

  • Add runtime feature detection for ReadableStream as a fetch() request body and cache the result.
  • For non-supporting browsers, buffer the request body into an ArrayBuffer to reuse it across direct fetch and proxy retry.
  • Update tests to “pre-warm” the feature detection cache to avoid consuming per-test mocked fetch responses.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
packages/php-wasm/web-service-worker/src/fetch-with-cors-proxy.ts Adds ReadableStream upload support detection and an ArrayBuffer buffering fallback for Safari before retrying via CORS proxy.
packages/php-wasm/web-service-worker/src/fetch-with-cors-proxy.spec.ts Adds suite-level warmup to keep feature detection from interfering with per-test fetch mocks.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

ashfame and others added 4 commits April 1, 2026 01:54
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
After teeRequest/buffering, requestObject's body stream may be
locked. Defensively use directRequest (which carries identical
metadata) in the catch block to avoid relying on the original
request's body state.

Made-with: Cursor
Export a test-only resetStreamBodySupportedForTesting() function so
the cached feature detection can be cleared between test runs. Add
three tests that force the non-streaming path by rejecting the
detection probe, covering buffered POST bodies for both the direct
fetch and CORS proxy fallback, as well as bodyless GET requests.

Made-with: Cursor
@ashfame ashfame requested a review from Copilot March 31, 2026 20:25
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Reduces public API surface by grouping test utilities under a
clearly internal __testing export object.

Made-with: Cursor
@ashfame ashfame requested a review from brandonpayton March 31, 2026 20:39
controller.close();
},
});
await fetch('data:a/a,', {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is there a reason we need to use a meaningless MIME type here? If so, let's note why in a comment. If not, maybe we can remove the type altogether. The data URL spec says it is totally optional, and fetch('data:,') works fine in Firefox, Chrome, and Safari for me.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

You are right. I didn't realise mime type was optional. Good to know. Tested 👍

Comment on lines +100 to +102
* fetch attempt and the CORS proxy fallback. Safari does not
* support ReadableStream as a fetch() request body
* ("ReadableStream uploading is not supported").
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Wherever we note this, let's:

This way we can feel comfortable removing the workaround when it is no longer needed.

const [teedDirect, teedProxy] = await teeRequest(requestObject);
directRequest = teedDirect;
corsProxyBody = teedProxy.body;
} else {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is there a reason we shouldn't move this if/else into the teeRequest() utility?

If we solve this problem in the utility function, it will be solved wherever teeRequest() is used. teeRequest() is not currently used elsewhere in this repo, but I think the workaround belongs there, unless there is a reason not to move it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Very good point! I can refactor it in a follow up PR so that the fix is not still waiting to land. Sounds good?

Comment on lines +164 to +168
const rootUrl = new URL(import.meta.url);
rootUrl.pathname = '';
rootUrl.search = '';
rootUrl.hash = '';
const corsProxyUrlObj = new URL(corsProxyUrl, rootUrl.toString());
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why do we need to clear all the fields on the rootUrl? Wouldn't the URL() constructor be able to make sense of the root URL on its own?

For example, in the browser dev tools console, new URL('a', new URL('http://happycode.net/?stuff#huzzah')).href evaluates to 'http://happycode.net/a'.

I think this can be as simple as:

if (
	useStreaming &&
	body &&
	new URL(corsProxyUrlObj, import.meta.url).protocol === 'http:'
) {

I know the rootUrl changes are pre-existing code that was moved. But I think we can either simplify while we're moving around what I think is unnecessary code, or maybe we should just leave the code alone except for adding useStreaming to the original if.

There may be something I'm missing, but that is my read here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah, seems alright to do. I have made the change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants