Fix Safari failing to make cross-origin HTTP requests#3440
Fix Safari failing to make cross-origin HTTP requests#3440
Conversation
|
Let's still use the body stream when supported, no need to make it worse for other browsers. |
|
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"). |
There was a problem hiding this comment.
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:
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
b427a3f to
4367a2f
Compare
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
There was a problem hiding this comment.
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
ReadableStreamas afetch()request body and cache the result. - For non-supporting browsers, buffer the request body into an
ArrayBufferto 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.
packages/php-wasm/web-service-worker/src/fetch-with-cors-proxy.ts
Outdated
Show resolved
Hide resolved
packages/php-wasm/web-service-worker/src/fetch-with-cors-proxy.ts
Outdated
Show resolved
Hide resolved
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
There was a problem hiding this comment.
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.
packages/php-wasm/web-service-worker/src/fetch-with-cors-proxy.ts
Outdated
Show resolved
Hide resolved
Reduces public API surface by grouping test utilities under a clearly internal __testing export object. Made-with: Cursor
| controller.close(); | ||
| }, | ||
| }); | ||
| await fetch('data:a/a,', { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
You are right. I didn't realise mime type was optional. Good to know. Tested 👍
| * fetch attempt and the CORS proxy fallback. Safari does not | ||
| * support ReadableStream as a fetch() request body | ||
| * ("ReadableStream uploading is not supported"). |
There was a problem hiding this comment.
Wherever we note this, let's:
- prefix the comment about Safari with a date
- clarify to "does not currently support"
- mention that Safari support is in the works for Interop 2026
- include a reference link: https://web.dev/blog/interop-2026#fetch_uploads_and_ranges
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Very good point! I can refactor it in a follow up PR so that the fix is not still waiting to land. Sounds good?
| const rootUrl = new URL(import.meta.url); | ||
| rootUrl.pathname = ''; | ||
| rootUrl.search = ''; | ||
| rootUrl.hash = ''; | ||
| const corsProxyUrlObj = new URL(corsProxyUrl, rootUrl.toString()); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Yeah, seems alright to do. I have made the change.
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
ReadableStreamas afetch()request body, throwing"NotSupportedError: ReadableStream uploading is not supported".fetchWithCorsProxyusedReadableStream.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 (noReadableStreamupload support), buffer the request body into anArrayBufferso 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:
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:
http://localhost:7777/?plugin=wordpress-beta-testerin Safari browser.Bleeding-edgeand thenBeta/RC, save.7.0-RC2will be offered for upgrade.