Description
Issue Description
I've identified an issue where POST requests made with ky lose their payload in certain WebKit-based environments, particularly in iOS AlipayClient WebView and some Safari versions. The error message observed on the server side is "readablestream uploading is not supported".
Environment Information
- Browser/WebView: Safari, iOS WebView, particularly AlipayClient WebView
- Sample User Agent:
Mozilla/5.0 (iPhone; CPU iPhone Os 18 3 2 like Mac Os X)AppleWebKit/605.1.15(KHTML.like Gecko) Mobile/22D82 Channelld(23) Ariver/1.1:0 AIAPD(AP/10.7.16.6000) Nebula WkRVKType(1)AlipayDefined(nt:WIFl,ws:440|8923.0)AlipayClient/10.7.16.6000 Alipay Language/zh-Hans Region/CNNebulax/1.0.0
- ky version: 1.8.0
Reproduction Steps
- Create a POST request with a JSON payload using ky
- Set headers including Content-Type: 'application/json'
- Send the request from a WebKit-based environment (especially iOS WebView)
- Observe that the server does not receive the payload
Root Cause Analysis
After thorough investigation, I believe this issue stems from the following:
-
Incomplete ReadableStream Support: While many WebKit-based browsers claim to support ReadableStream API, their implementation is incomplete or buggy when used as request bodies.
-
Feature Detection Limitations: The current feature detection in ky (
supportsRequestStreams
) only checks ifduplex
property is accessed, but doesn't verify if the browser can actually process stream requests correctly. -
Lack of Graceful Degradation: When stream requests aren't fully supported, ky doesn't automatically fall back to a more compatible approach.
The key issue is that setting duplex: 'half'
and using ReadableStream in these environments leads to request body serialization failures, even though basic feature detection suggests these features should work.
Proposed Solution
I propose enhancing the browser compatibility by implementing a more robust feature detection and graceful degradation approach:
-
Comprehensive Feature Testing: Test not just the presence of APIs but their actual working capability by creating a small test stream and attempt to use it.
-
Multi-level Safety Checks: Implement checks at multiple points (initial detection, constructor, stream creation, and request sending).
-
Automatic Fallback Mechanism: For POST requests in environments where streams aren't properly supported, automatically fall back to simpler request formats.
Code Suggestions
Below are some code snippets demonstrating the proposed approach:
- Enhanced feature detection in
constants.ts
:
export const supportsRequestStreams = (() => {
// Check basic API support
if (typeof globalThis.ReadableStream !== 'function' ||
typeof globalThis.Request !== 'function') {
return false;
}
// Try creating a request with a stream and test browser support
try {
const stream = new globalThis.ReadableStream({
start(controller) {
controller.close();
}
});
let duplexSupported = false;
let requestBodyStreamSupported = true;
try {
const request = new globalThis.Request('https://example.com', {
method: 'POST',
body: stream,
// @ts-expect-error - Types are outdated.
get duplex() {
duplexSupported = true;
return 'half';
}
});
// Further validation with a tiny stream
if (duplexSupported) {
// Additional validation logic...
}
} catch (error) {
// Error handling logic...
requestBodyStreamSupported = false;
}
return duplexSupported && requestBodyStreamSupported;
} catch (error) {
return false;
}
})();
- Self-contained feature testing in
streamRequest
function:
export const streamRequest = (request, onUploadProgress) => {
// Internal feature detection
const canUseStreams = (() => {
try {
// Feature testing logic...
return true;
} catch (error) {
return false;
}
})();
// Graceful degradation if streams not supported
if (!canUseStreams) {
// Fallback logic...
return request;
}
// Stream handling logic...
};
- Enhanced POST request handling in
_fetch
method:
if (!supportsRequestStreams && this.request.method.toLowerCase() === "post") {
// Create a simple request object for compatibility
const simpleOptions = {
method: this.request.method,
headers: this.request.headers,
body: await this.request.clone().text(),
// Other request properties...
};
return this._options.fetch(this.request.url, simpleOptions);
}
Benefits of This Approach
- Broader Compatibility: Works across all browsers including those with partial ReadableStream support
- Future-Proof: Relies on actual capability testing rather than user agent detection
- Resilient Design: Multiple layers of checks and fallbacks ensure requests succeed even in edge cases
- Maintainable Code: Self-contained testing reduces cross-module dependencies
I'd be happy to provide more information or assist with implementing these changes if needed.