Skip to content

Enhance browser compatibility for ReadableStream in POST requests #691

Open
@yilishan

Description

@yilishan

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

  1. Create a POST request with a JSON payload using ky
  2. Set headers including Content-Type: 'application/json'
  3. Send the request from a WebKit-based environment (especially iOS WebView)
  4. Observe that the server does not receive the payload

Root Cause Analysis

After thorough investigation, I believe this issue stems from the following:

  1. Incomplete ReadableStream Support: While many WebKit-based browsers claim to support ReadableStream API, their implementation is incomplete or buggy when used as request bodies.

  2. Feature Detection Limitations: The current feature detection in ky (supportsRequestStreams) only checks if duplex property is accessed, but doesn't verify if the browser can actually process stream requests correctly.

  3. 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:

  1. 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.

  2. Multi-level Safety Checks: Implement checks at multiple points (initial detection, constructor, stream creation, and request sending).

  3. 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:

  1. 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;
    }
})();
  1. 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...
};
  1. 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

  1. Broader Compatibility: Works across all browsers including those with partial ReadableStream support
  2. Future-Proof: Relies on actual capability testing rather than user agent detection
  3. Resilient Design: Multiple layers of checks and fallbacks ensure requests succeed even in edge cases
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesthelp wantedExtra attention is needed

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions