You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Cordova中EventSource/fetch处理SseEmitter为何与Chrome行为不同?

问题

在Google Chrome浏览器中,EventSource和fetch可以逐个获取SseEmitter端点返回的数据包,实现类似ChatGPT的流式token生成效果。但在Android和iOS平台的Cordova应用中,EventSource与fetch请求会缓冲后端响应,直到服务器停止发送后才将文本一次性推送给JavaScript代码,无法实现流式效果。请问该行为的原因是什么?能否不使用WebSocket连接在Cordova中实现相同的流式token生成效果?


一、Cordova中响应被缓冲的原因

  • WebView默认策略:Cordova基于Android Chrome WebView、iOS WKWebView/UIWebView,这些WebView为优化普通HTTP请求性能,默认会缓冲完整响应后再传递给JavaScript,直接破坏了SSE的流式传输逻辑。
  • 头部标识缺失:若后端未正确返回Content-Type: text/event-stream、Cache-Control: no-cache、Connection: keep-alive等头部,WebView会判定这是普通响应并启用缓冲机制。
  • 中间层干扰:Cordova应用可能经过应用内HTTP拦截插件、系统代理等中间层,这些组件会主动缓冲响应内容,导致流式数据无法实时传递到前端。

二、不使用WebSocket实现流式效果的方案

1. 强制关闭WebView响应缓冲

  • Android端配置:修改Cordova Android平台代码,针对SSE接口禁用缓冲:
    webView.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);
    webView.setWebViewClient(new WebViewClient() {
        @Override
        public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
            String url = request.getUrl().toString();
            if (url.contains("/your-sse-endpoint")) {
                try {
                    HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
                    conn.setRequestProperty("Cache-Control", "no-cache");
                    conn.setRequestProperty("Connection", "keep-alive");
                    return new WebResourceResponse(
                        "text/event-stream",
                        "UTF-8",
                        conn.getInputStream()
                    );
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return super.shouldInterceptRequest(view, request);
        }
    });
    
  • iOS端配置:针对WKWebView,在请求时添加禁用缓冲的头部:
    var request = URLRequest(url: URL(string: "your-sse-endpoint-url")!)
    request.setValue("no-cache", forHTTPHeaderField: "Cache-Control")
    request.setValue("keep-alive", forHTTPHeaderField: "Connection")
    webView.load(request)
    

2. 优化后端SSE输出配置

在Java后端的SseEmitter代码中,补充必要的响应头并强制刷新输出:

public SseEmitter callApiAsync(HttpRequest request) {
    SseEmitter emitter = new SseEmitter();
    // 添加SSE必备响应头
    emitter.setHeader("Content-Type", "text/event-stream");
    emitter.setHeader("Cache-Control", "no-cache");
    emitter.setHeader("Connection", "keep-alive");
    emitter.setHeader("X-Accel-Buffering", "no"); // 禁用Nginx等反向代理的缓冲

    executorService.execute(() -> {
        try {
            client.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())
            .thenAccept(response -> {
                try (InputStream inputStream = response.body()) {
                    BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
                    String line;
                    Matcher matcher;
                    while ((line = reader.readLine()) != null) {
                        matcher = GPT_TEXT_PATTERN.matcher(line);
                        line = matcher.find() ? matcher.group(1) : "";
                        line = line.replace(" ", EMPTRY_SPACE);
                        emitter.send(SseEmitter.event().data(line));
                        // 强制刷新输出,确保数据实时推送
                        HttpServletResponse resp = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
                        if (resp != null) resp.flushBuffer();
                    }
                    emitter.send(FINAL_MESSAGE);
                    emitter.complete();
                } catch (IOException e) {
                    emitter.completeWithError(e);
                }
            });
        } catch (Exception e) {
            emitter.completeWithError(e);
        }
    });
    return emitter;
}

3. 使用XMLHttpRequest替代EventSource/fetch

XMLHttpRequest在多数WebView中支持流式读取,通过监听progress事件获取实时数据:

function handleGptSuggestStreaming(params, target) {
    let xhr = new XMLHttpRequest();
    const url = getFullURL(GPT_SUGGEST_URL.format(params));
    xhr.open('GET', url, true);
    xhr.setRequestHeader('Accept', 'text/event-stream');
    xhr.setRequestHeader('Cache-Control', 'no-cache');
    
    let receivedData = '';
    xhr.onprogress = function() {
        const newData = xhr.responseText.substring(receivedData.length);
        receivedData = xhr.responseText;
        
        // 分割SSE事件并处理
        const events = newData.split(/\n\n/);
        events.forEach(event => {
            if (!event) return;
            const dataMatch = event.match(/^data: (.*)$/m);
            if (!dataMatch) return;
            
            const line = dataMatch[1];
            if (line.includes(FINAL_MESSAGE)) {
                xhr.abort();
                return;
            }
            
            const formattedText = line.replace(/\\n/g, '<br>')
                                    .replace(/\n/g, '<br>')
                                    .replace(/&nbsp;/g, ' ');
            updateTargetMessage(formattedText, target);
        });
    };
    
    xhr.onerror = () => xhr.abort();
    xhr.send();
}

原代码参考

前端EventSource方案JS代码

function handleGptSuggestCallEventSource(params, target) {
    let eventSource = mwSuggestEventSource(params)
     
     eventSource.onmessage = function(event) {
         if (event.data.includes(FINAL_MESSAGE)) {
            eventSource.close();  // Stop the EventSource
            return;
        } else {
            let formattedText = event.data.replace(/\\n/g, '<br>');
            formattedText = formattedText.replace(/\n/g, '<br>');
            formattedText = formattedText.replace(/&nbsp;/g, ' ');

            updateTargetMessage(formattedText, target);
        }
    };

    eventSource.onerror = function(err) {
        eventSource.close();
    };
    
}

function mwSuggestEventSource(params) {
    return callGPTEventSource(GPT_SUGGEST_URL.format(params));
}


function  callGPTEventSource(url) {
    try {
        return new EventSource(getFullURL(url));
    } catch (e) {
        showToastError(getStr(e));
    }
}

后端Java代码

public SseEmitter callApiAsync(HttpRequest request) {
        SseEmitter emitter = new SseEmitter();

        executorService.execute(() -> {
            try {
                
                client.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())
                .thenAccept(response -> {
                    try (InputStream inputStream = response.body()) {
                        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
                        String line;
                        Matcher matcher;
                        while ((line = reader.readLine()) != null) {
                            matcher = GPT_TEXT_PATTERN.matcher(line);
                            if (matcher.find()) {
                                line = matcher.group(1);
                            } else {
                                line = "";
                            }
                            line = line.replace(" ", EMPTRY_SPACE);
                            emitter.send(SseEmitter.event().data(line)); // Stream each line to client
                        }
                        emitter.send(FINAL_MESSAGE);
                        emitter.complete(); // Complete the emitter after finishing reading
                    } catch (IOException e) {
                        emitter.completeWithError(e);
                    }
                });
            } catch (Exception e) {
                emitter.completeWithError(e);
            }
        });

        return emitter;
    }

内容的提问来源于stack exchange,提问作者Yonoss

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 18:53:20