- 
          
 - 
                Notifications
    
You must be signed in to change notification settings  - Fork 436
 
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 ifduplexproperty 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 
streamRequestfunction: 
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 
_fetchmethod: 
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.