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

HTTP响应流异常:JS端接收整段数据而非分片

HTTP流式传输分片接收问题解决

我用Rust的actix-web实现HTTP响应流式传输,想在JavaScript端逐个读取数据块,但实际JS一次性接收了所有数据,没有按预期分片。

后端Rust代码

use std::convert::Infallible;
use async_stream::stream;
use actix_web::{get, HttpResponse};
use actix_web::web::Bytes;

#[get("/stream")]
pub async fn stream() -> HttpResponse {
    HttpResponse::Ok()
        .streaming(stream! {
            let mut count = 0;
            while count < 10 {
                yield Ok::<_, Infallible>(Bytes::from(count.to_string()));
                count += 1;
            }
        })
}

前端JavaScript代码

async function get_stream() {
    const response = await fetch('/stream');
    const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();

    while (true) {
        let {value, done} = await reader.read();
        if (done) break;
        console.log('Received', value);
    }

    console.log('Response fully received');
}

实际输出

Received 0123456789
stream.js:69 Response fully received

期望输出

Received 0
Received 1
Received 2
Received 3
Received 4
Received 5
Received 6
Received 7
Received 8
Received 9
stream.js:69 Response fully received

问题原因

TCP是面向流的协议,本身不保证数据包的边界。加上Nagle算法会合并小数据包以提升传输效率,再加上actix-web内部的缓冲机制,导致多个小的Bytes块被合并成一个大的HTTP帧发送,最终JS端一次性接收所有数据。

解决方案

方案1:添加延迟让服务器逐个发送块

通过在每个数据块发送后添加短暂延迟,让服务器有足够时间将当前块发送出去,避免被合并:

use std::convert::Infallible;
use async_stream::stream;
use actix_web::{get, HttpResponse};
use actix_web::web::Bytes;
use tokio::time::{sleep, Duration}; // 新增导入

#[get("/stream")]
pub async fn stream() -> HttpResponse {
    HttpResponse::Ok()
        .streaming(stream! {
            let mut count = 0;
            while count < 10 {
                yield Ok::<_, Infallible>(Bytes::from(count.to_string()));
                sleep(Duration::from_millis(100)).await; // 添加100ms延迟
                count += 1;
            }
        })
}

方案2:给数据块添加分隔符,客户端按分隔符解析

在每个数据块末尾添加明确的分隔符(比如换行符),客户端接收后按分隔符分割数据,确保能识别每个独立块:

后端修改:

yield Ok::<_, Infallible>(Bytes::from(format!("{}\n", count))); // 每个块后加换行

前端修改:

async function get_stream() {
    const response = await fetch('/stream');
    const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
    let buffer = ''; // 用于暂存未分割的剩余数据

    while (true) {
        let {value, done} = await reader.read();
        if (done) {
            // 处理最后剩余的未分割数据
            if (buffer.trim()) console.log('Received', buffer.trim());
            break;
        }
        
        buffer += value;
        // 按换行符分割数据
        const lines = buffer.split('\n');
        buffer = lines.pop() || ''; // 把最后不完整的行放回缓冲
        
        // 输出每个完整的块
        for (const line of lines) {
            if (line) console.log('Received', line);
        }
    }

    console.log('Response fully received');
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 17:20:45