feat(transport): add incremental streaming body reader and custom ports

- Transport::stream_request returns a ChunkStream for true SSE streaming
  (head read incrementally, body drained chunk by chunk)
- transparent incremental chunked-transfer decoding (ChunkedDecoder)
- HttpRequest.port for non-standard base_url ports (Host header + TLS connect)
- per-read timeouts on the streaming path
This commit is contained in:
DaiChaoXiong 2026-08-09 20:59:33 +08:00
parent 32b630e440
commit 7f00886150
7 changed files with 703 additions and 186 deletions

View File

@ -39,6 +39,9 @@ pub struct HttpRequest {
/// 目标主机名。
/// The target host.
pub host: String,
/// 目标端口(默认 443
/// The target port (default 443).
pub port: u16,
/// 请求头(按插入顺序保留)。
/// Request headers, in insertion order.
pub headers: Vec<(String, String)>,
@ -55,11 +58,19 @@ impl HttpRequest {
method: Method::Post,
path: path.to_string(),
host: host.to_string(),
port: 443,
headers: Vec::new(),
body: Vec::new(),
}
}
/// 设置目标端口(用于非标准端口)。
/// Set the target port (for non-standard ports).
pub fn port(mut self, port: u16) -> Self {
self.port = port;
self
}
/// 追加一个请求头,返回自身以支持链式调用。
/// Append a header and return self for chaining.
pub fn header(mut self, name: &str, value: &str) -> Self {
@ -93,7 +104,11 @@ impl HttpRequest {
.iter()
.any(|(k, _)| k.eq_ignore_ascii_case("host"));
if !has_host {
out.extend_from_slice(format!("Host: {}\r\n", self.host).as_bytes());
if self.port == 443 {
out.extend_from_slice(format!("Host: {}\r\n", self.host).as_bytes());
} else {
out.extend_from_slice(format!("Host: {}:{}\r\n", self.host, self.port).as_bytes());
}
}
for (k, v) in &self.headers {
out.extend_from_slice(format!("{}: {}\r\n", k, v).as_bytes());
@ -223,60 +238,3 @@ fn find_subsequence(haystack: &[u8], needle: &[u8]) -> Option<usize> {
.windows(needle.len())
.position(|window| window == needle)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serializes_post_request() {
let req = HttpRequest::post("api.example.com", "/v1/messages")
.header("Authorization", "Bearer xyz")
.json_body(r#"{"hello":"world"}"#);
let wire = req.serialize();
let text = String::from_utf8(wire).unwrap();
assert!(text.starts_with("POST /v1/messages HTTP/1.1\r\n"));
assert!(text.contains("Host: api.example.com\r\n"));
assert!(text.contains("Authorization: Bearer xyz\r\n"));
assert!(text.contains("Content-Type: application/json\r\n"));
assert!(text.contains("Content-Length: 17\r\n"));
assert!(text.ends_with("{\"hello\":\"world\"}"));
}
#[test]
fn parses_response_head() {
let raw =
b"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 5\r\n\r\nhello";
let (resp, consumed) = parse_response_head(raw).unwrap();
assert_eq!(resp.status, 200);
assert_eq!(
resp.headers.get("content-type").unwrap(),
"application/json"
);
assert_eq!(resp.headers.get("content-length").unwrap(), "5");
assert_eq!(consumed, raw.len() - 5);
assert_eq!(body_length(&resp), BodyLength::Fixed(5));
}
#[test]
fn detects_chunked() {
let raw = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n";
let (resp, _) = parse_response_head(raw).unwrap();
assert_eq!(body_length(&resp), BodyLength::Chunked);
}
#[test]
fn is_success_for_2xx() {
let resp = HttpResponse {
status: 204,
headers: HashMap::new(),
body: vec![],
};
assert!(resp.is_success());
}
#[test]
fn rejects_incomplete_head() {
assert!(parse_response_head(b"HTTP/1.1 200 OK\r\n").is_err());
}
}

View File

@ -37,4 +37,4 @@ pub use http::{HttpRequest, HttpResponse, Method};
pub use sse::{SseEvent, SseParser};
/// HTTP 传输实现与传输 trait。
/// HTTP transport implementation and the transport trait.
pub use transport::{HttpTransport, Transport};
pub use transport::{ChunkStream, HttpTransport, Transport};

View File

@ -192,105 +192,3 @@ impl SseParser {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn collect(parser: &mut SseParser) -> Vec<SseEvent> {
let mut out = Vec::new();
while let Some(e) = parser.next_event() {
out.push(e);
}
out
}
#[test]
fn parses_single_event() {
let mut p = SseParser::new();
p.feed_str("data: hello\n\n");
let events = collect(&mut p);
assert_eq!(events.len(), 1);
assert_eq!(events[0].data, "hello");
assert_eq!(events[0].event, "message");
}
#[test]
fn parses_event_with_type() {
let mut p = SseParser::new();
p.feed_str("event: ping\ndata: 1\n\n");
let events = collect(&mut p);
assert_eq!(events[0].event, "ping");
assert_eq!(events[0].data, "1");
}
#[test]
fn joins_multiline_data() {
let mut p = SseParser::new();
p.feed_str("data: line1\ndata: line2\n\n");
let events = collect(&mut p);
assert_eq!(events[0].data, "line1\nline2");
}
#[test]
fn handles_crlf() {
let mut p = SseParser::new();
p.feed_str("data: hi\r\n\r\n");
let events = collect(&mut p);
assert_eq!(events[0].data, "hi");
}
#[test]
fn ignores_comments_and_unknown_fields() {
let mut p = SseParser::new();
p.feed_str(": this is a comment\ndata: ok\nfoo: bar\n\n");
let events = collect(&mut p);
assert_eq!(events[0].data, "ok");
}
#[test]
fn parses_id_and_retry() {
let mut p = SseParser::new();
p.feed_str("id: 42\nretry: 5000\ndata: x\n\n");
let events = collect(&mut p);
assert_eq!(events[0].id, "42");
assert_eq!(events[0].retry, Some(5000));
}
#[test]
fn handles_split_across_chunks() {
let mut p = SseParser::new();
p.feed_str("dat");
assert!(collect(&mut p).is_empty());
p.feed_str("a: hello\n\n");
let events = collect(&mut p);
assert_eq!(events[0].data, "hello");
}
#[test]
fn handles_split_across_line_boundary() {
let mut p = SseParser::new();
p.feed_str("data: first\n\ndata: sec");
assert_eq!(collect(&mut p).len(), 1);
p.feed_str("ond\n\n");
let events = collect(&mut p);
assert_eq!(events[0].data, "second");
}
#[test]
fn multiple_events_in_one_chunk() {
let mut p = SseParser::new();
p.feed_str("data: a\n\ndata: b\n\ndata: c\n\n");
let events = collect(&mut p);
assert_eq!(events.len(), 3);
assert_eq!(events[2].data, "c");
}
#[test]
fn empty_data_line() {
let mut p = SseParser::new();
p.feed_str("data:\n\n");
let events = collect(&mut p);
assert_eq!(events[0].data, "");
}
}

View File

@ -12,7 +12,37 @@
use crate::error::{TransportError, TransportResult};
use crate::http::{body_length, parse_response_head, BodyLength, HttpRequest, HttpResponse};
use crate::tls;
use std::future::Future;
use std::pin::Pin;
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::time::timeout;
/// 异步读取下一块响应体的 future 类型。
/// Future type for reading the next response-body chunk.
pub type ChunkFuture<'a> =
Pin<Box<dyn Future<Output = TransportResult<Option<Vec<u8>>>> + Send + 'a>>;
/// 普通请求的 future 类型。
/// Future type for a plain request.
pub type RequestFuture<'a> =
Pin<Box<dyn Future<Output = TransportResult<HttpResponse>> + Send + 'a>>;
/// 流式请求的 future 类型。
/// Future type for a streaming request.
pub type StreamRequestFuture<'a> =
Pin<Box<dyn Future<Output = TransportResult<Box<dyn ChunkStream + Send>>> + Send + 'a>>;
/// 流式响应体的读取接口:调用方逐块拉取,直到返回 `None` 表示流结束。
/// A streaming response-body reader: callers pull chunks until `None`.
///
/// 该接口是异步的(每次拉取都是 future因此底层可以边收边解析
/// 支撑 SSE 的真流式体验。
/// This interface is async (each pull is a future), so the underlying socket
/// can be parsed incrementally, enabling a true SSE streaming experience.
pub trait ChunkStream: Send {
/// 读取下一块响应体字节;`None` 表示流结束。
/// Read the next chunk of response body bytes; `None` means end of stream.
fn next_chunk<'a>(&'a mut self) -> ChunkFuture<'a>;
}
/// 通过网络连接发送 HTTP 请求。
/// Sends HTTP requests over a network connection.
@ -23,12 +53,22 @@ pub trait Transport: Send + Sync {
/// 实现者在 future 生命周期内借用请求;调用方须使其存活至 future 完成。
/// Implementations borrow the request for the duration of the future;
/// callers must keep it alive until the future resolves.
fn request<'a>(
fn request<'a>(&'a self, req: &'a HttpRequest) -> RequestFuture<'a>;
/// 发送请求并返回一个流式响应体读取器。
/// Send a request and get a streaming response-body reader back.
///
/// 与 [`request`](Self::request) 不同,本方法不会把整个响应体读入内存;
/// 调用方通过 [`ChunkStream::next_chunk`] 逐块拉取。
/// 非 2xx 状态码仍会以 [`TransportError::Http`] 返回(此时已读取完整错误体)。
/// Unlike [`request`](Self::request), this does not buffer the whole body;
/// callers pull chunks via [`ChunkStream::next_chunk`]. Non-2xx statuses
/// still surface as [`TransportError::Http`] (with the full error body read).
fn stream_request<'a>(
&'a self,
req: &'a HttpRequest,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = TransportResult<HttpResponse>> + Send + 'a>,
>;
timeout: Duration,
) -> StreamRequestFuture<'a>;
}
/// 基于 tokio + rustls 的真实 HTTPS 传输实现。
@ -45,20 +85,23 @@ impl HttpTransport {
}
impl Transport for HttpTransport {
fn request<'a>(
fn request<'a>(&'a self, req: &'a HttpRequest) -> RequestFuture<'a> {
Box::pin(async move { perform_request(req).await })
}
fn stream_request<'a>(
&'a self,
req: &'a HttpRequest,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = TransportResult<HttpResponse>> + Send + 'a>,
> {
Box::pin(async move { perform_request(req).await })
timeout: Duration,
) -> StreamRequestFuture<'a> {
Box::pin(async move { perform_stream_request(req, timeout).await })
}
}
/// 执行单次 HTTP 请求:建立 TLS 连接、序列化并发送请求、读取完整响应并解析。
/// Perform a single HTTP request: open TLS, serialize & send, read the full response, parse.
async fn perform_request(req: &HttpRequest) -> TransportResult<HttpResponse> {
let mut stream = tls::connect(&req.host, 443).await?;
let mut stream = tls::connect(&req.host, req.port).await?;
let wire = req.serialize();
stream
@ -131,9 +174,386 @@ async fn perform_request(req: &HttpRequest) -> TransportResult<HttpResponse> {
Ok(resp)
}
/// 执行单次流式 HTTP 请求:建立 TLS 连接、序列化并发送请求、解析响应头,
/// 然后返回一个定位在响应体起始处的流式读取器。
/// Perform a single streaming HTTP request: open TLS, serialize & send, parse
/// the response head, then return a reader positioned at the body start.
///
/// 响应头是逐字节累积解析的(直到 `\r\n\r\n`),因此无需缓冲整个响应。
/// The response head is accumulated and parsed incrementally (until
/// `\r\n\r\n`), so no full-response buffering is required.
async fn perform_stream_request(
req: &HttpRequest,
timeout_dur: Duration,
) -> TransportResult<Box<dyn ChunkStream + Send>> {
let mut stream = timeout(timeout_dur, tls::connect(&req.host, req.port))
.await
.map_err(|_| TransportError::Timeout)??;
let wire = req.serialize();
timeout(timeout_dur, stream.write_all(&wire))
.await
.map_err(|_| TransportError::Timeout)??;
timeout(timeout_dur, stream.flush())
.await
.map_err(|_| TransportError::Timeout)??;
// 累积响应头,直到出现头部终止符。
// Accumulate the response head until the header terminator appears.
let mut head_buf: Vec<u8> = Vec::with_capacity(4096);
let head_len = loop {
if let Some(end) = find_subsequence(&head_buf, b"\r\n\r\n") {
break end + 4;
}
if head_buf.len() > 64 * 1024 {
return Err(TransportError::Io("response head too large".into()));
}
let mut chunk = [0u8; 4096];
let n = match timeout(timeout_dur, stream.read(&mut chunk)).await {
Err(_) => return Err(TransportError::Timeout),
Ok(Err(e)) => return Err(TransportError::Io(format!("read response head: {}", e))),
Ok(Ok(0)) => {
return Err(TransportError::Io(
"connection closed before response head".into(),
))
}
Ok(Ok(n)) => n,
};
head_buf.extend_from_slice(&chunk[..n]);
};
let (resp, consumed) = parse_response_head(&head_buf)?;
// 头部解析已消费的字节数之后的字节,是响应体的一部分。
// Bytes beyond the consumed head are already part of the response body.
let body_prefix = head_buf[consumed..head_len].to_vec();
if !resp.is_success() {
// 非 2xx读取剩余错误体后返回 Http 错误。
// Non-2xx: drain the remaining error body, then return an Http error.
let mut body = body_prefix;
loop {
let mut chunk = [0u8; 4096];
match timeout(timeout_dur, stream.read(&mut chunk)).await {
Err(_) => return Err(TransportError::Timeout),
Ok(Err(e)) => {
let benign = matches!(
e.kind(),
std::io::ErrorKind::UnexpectedEof | std::io::ErrorKind::ConnectionReset
);
if !benign || body.is_empty() {
return Err(TransportError::Io(format!("read error body: {}", e)));
}
break;
}
Ok(Ok(0)) => break,
Ok(Ok(n)) => body.extend_from_slice(&chunk[..n]),
}
}
return Err(TransportError::Http {
status: resp.status,
body: String::from_utf8_lossy(&body).to_string(),
});
}
let reader = StreamingBody::new(stream, body_length(&resp), body_prefix, timeout_dur);
Ok(Box::new(reader))
}
/// 定位在响应体起始处的流式读取器。透明处理分块传输编码与固定长度。
/// A streaming reader positioned at the response-body start. Transparently
/// handles chunked transfer encoding and fixed lengths.
struct StreamingBody {
stream: crate::tls::TlsConnection,
mode: BodyLength,
/// 头部解析后已多读的响应体字节。
/// Body bytes already read beyond the head.
pending: Vec<u8>,
/// 固定长度模式下剩余字节数。
/// Remaining bytes in fixed-length mode.
remaining: usize,
/// 分块模式下使用的增量解码器。
/// Incremental decoder used in chunked mode.
decoder: Option<ChunkedDecoder>,
finished: bool,
timeout_dur: Duration,
}
impl StreamingBody {
fn new(
stream: crate::tls::TlsConnection,
mode: BodyLength,
pending: Vec<u8>,
timeout_dur: Duration,
) -> Self {
let remaining = match mode {
BodyLength::Fixed(n) => n,
_ => 0,
};
Self {
stream,
mode,
pending,
remaining,
decoder: (mode == BodyLength::Chunked).then(ChunkedDecoder::new),
finished: false,
timeout_dur,
}
}
/// 从套接字读取一块原始字节;`None` 表示干净 EOF。
/// Read one raw chunk from the socket; `None` is a clean EOF.
async fn read_raw(&mut self) -> TransportResult<Option<Vec<u8>>> {
let mut chunk = [0u8; 8192];
match timeout(self.timeout_dur, self.stream.read(&mut chunk)).await {
Err(_) => Err(TransportError::Timeout),
Ok(Err(e)) => {
// 与 perform_request 相同的良性拆连接处理。
// Same benign-teardown handling as perform_request.
let benign = matches!(
e.kind(),
std::io::ErrorKind::UnexpectedEof | std::io::ErrorKind::ConnectionReset
);
if benign && !self.pending.is_empty() {
Ok(None)
} else {
Err(TransportError::Io(format!("read response body: {}", e)))
}
}
Ok(Ok(0)) => Ok(None),
Ok(Ok(n)) => Ok(Some(chunk[..n].to_vec())),
}
}
}
impl ChunkStream for StreamingBody {
fn next_chunk<'a>(
&'a mut self,
) -> Pin<Box<dyn std::future::Future<Output = TransportResult<Option<Vec<u8>>>> + Send + 'a>>
{
Box::pin(async move { self.next_chunk_impl().await })
}
}
impl StreamingBody {
async fn next_chunk_impl(&mut self) -> TransportResult<Option<Vec<u8>>> {
if self.finished {
return Ok(None);
}
loop {
match self.mode {
BodyLength::Fixed(_) => {
if self.remaining == 0 {
self.finished = true;
return Ok(None);
}
if !self.pending.is_empty() {
let take = self.pending.len().min(self.remaining);
let out: Vec<u8> = self.pending.drain(..take).collect();
self.remaining -= take;
return Ok(Some(out));
}
match self.read_raw().await? {
Some(chunk) => self.pending.extend_from_slice(&chunk),
None => {
// 提前 EOF响应体被截断。
// Premature EOF: the body was truncated.
if self.remaining > 0 {
return Err(TransportError::Io("body truncated".into()));
}
self.finished = true;
return Ok(None);
}
}
}
BodyLength::Chunked => {
// 先取解码器现有输出;无输出且未结束才读套接字(避免跨 await 持有借用)。
// Drain the decoder's buffered output first; only read the
// socket when there is none and the stream is not finished
// (avoids holding a borrow across an await).
if let Some(out) = self.decoder.as_mut().and_then(|d| d.take_decoded()) {
return Ok(Some(out));
}
if self
.decoder
.as_ref()
.map(|d| d.is_finished())
.unwrap_or(false)
{
self.finished = true;
return Ok(None);
}
match self.read_raw().await? {
Some(chunk) => {
if let Some(d) = self.decoder.as_mut() {
d.feed(&chunk);
}
}
None => {
let finished = self
.decoder
.as_ref()
.map(|d| d.is_finished())
.unwrap_or(false);
if !finished {
return Err(TransportError::Io("chunked body truncated".into()));
}
self.finished = true;
return Ok(None);
}
}
}
BodyLength::UntilClose => {
if !self.pending.is_empty() {
return Ok(Some(std::mem::take(&mut self.pending)));
}
if let Some(chunk) = self.read_raw().await? {
return Ok(Some(chunk));
}
self.finished = true;
return Ok(None);
}
}
}
}
}
/// 增量分块传输编码chunked解码器。
/// An incremental chunked transfer-encoding decoder.
///
/// 字节以任意边界喂入(网络分块不保证与 HTTP 分块对齐),解码结果累积在
/// 内部缓冲区中,通过 [`take_decoded`](Self::take_decoded) 取走。
/// Bytes are fed at arbitrary boundaries (network chunks need not align with
/// HTTP chunks); decoded bytes accumulate internally and are drained via
/// [`take_decoded`](Self::take_decoded).
pub(crate) struct ChunkedDecoder {
/// 尚未消费的原始字节。
/// Raw bytes not yet consumed.
raw: Vec<u8>,
/// 已消费位置。
/// Consumed position within `raw`.
pos: usize,
/// 当前分块剩余数据字节数;`None` 表示正在读取分块大小行。
/// Remaining data bytes of the current chunk; `None` while reading the size line.
remaining: Option<usize>,
/// 分块数据之后是否在等待尾随 CRLF。
/// Whether we are waiting for the CRLF after chunk data.
expect_crlf: bool,
/// 是否已读到终止分块(大小为 0
/// Whether the terminating (size-0) chunk has been read.
finished: bool,
/// 解码后的输出字节。
/// Decoded output bytes.
decoded: Vec<u8>,
}
impl ChunkedDecoder {
fn new() -> Self {
Self {
raw: Vec::new(),
pos: 0,
remaining: None,
expect_crlf: false,
finished: false,
decoded: Vec::new(),
}
}
/// 喂入一块原始字节(含分块框架)。
/// Feed a chunk of raw bytes (including chunk framing).
fn feed(&mut self, bytes: &[u8]) {
self.raw.extend_from_slice(bytes);
self.decode();
}
/// 取走当前已解码的全部字节。
/// Drain all currently decoded bytes.
fn take_decoded(&mut self) -> Option<Vec<u8>> {
if self.decoded.is_empty() {
None
} else {
Some(std::mem::take(&mut self.decoded))
}
}
/// 是否已到达流末尾(读到大小为 0 的终止分块)。
/// Whether the end of the stream (size-0 terminator) has been reached.
fn is_finished(&self) -> bool {
self.finished
}
/// 尽可能多地解码当前原始缓冲区。
/// Decode as much of the raw buffer as possible.
fn decode(&mut self) {
loop {
if self.finished {
return;
}
if let Some(rem) = self.remaining {
if rem == 0 {
// 消费分块数据后的尾随 CRLF或单独的 LF
// Consume the trailing CRLF (or bare LF) after chunk data.
if self.expect_crlf {
if self.pos + 2 <= self.raw.len()
&& self.raw[self.pos] == b'\r'
&& self.raw[self.pos + 1] == b'\n'
{
self.pos += 2;
self.expect_crlf = false;
self.remaining = None;
continue;
}
if self.pos < self.raw.len() && self.raw[self.pos] == b'\n' {
self.pos += 1;
self.expect_crlf = false;
self.remaining = None;
continue;
}
return; // 需要更多数据 / need more data
}
} else {
let avail = self.raw.len() - self.pos;
if avail == 0 {
return; // 需要更多数据 / need more data
}
let take = avail.min(rem);
self.decoded
.extend_from_slice(&self.raw[self.pos..self.pos + take]);
self.pos += take;
self.remaining = Some(rem - take);
continue;
}
} else {
// 读取分块大小行(以 '\n' 结束,可带分块扩展 ";...")。
// Read the chunk size line (terminated by '\n', may carry ";...").
let tail = &self.raw[self.pos..];
let nl = tail.iter().position(|&b| b == b'\n');
match nl {
None => return, // 需要更多数据 / need more data
Some(nl) => {
let line = &tail[..nl];
let size_str = std::str::from_utf8(line)
.unwrap_or("")
.trim_end_matches('\r');
let size_hex = size_str.split(';').next().unwrap_or("0").trim();
let size = usize::from_str_radix(size_hex, 16).unwrap_or(0);
self.pos += nl + 1; // 消费该行及其 '\n' / consume the line + '\n'
if size == 0 {
self.finished = true;
return;
}
self.remaining = Some(size);
self.expect_crlf = true;
continue;
}
}
}
}
}
}
/// 解码 HTTP/1.1 分块传输编码chunked的响应体。
/// Decode an HTTP/1.1 chunked transfer-encoded body.
fn decode_chunked(bytes: &[u8]) -> TransportResult<Vec<u8>> {
pub fn decode_chunked(bytes: &[u8]) -> TransportResult<Vec<u8>> {
let mut out = Vec::new();
let mut pos = 0;
while pos < bytes.len() {
@ -177,29 +597,84 @@ fn find_line_end(bytes: &[u8]) -> TransportResult<usize> {
.ok_or_else(|| TransportError::Io("missing chunk line terminator".into()))
}
/// 在 `haystack` 中查找 `needle` 首次出现的字节偏移。
/// Find the first byte offset of `needle` within `haystack`.
fn find_subsequence(haystack: &[u8], needle: &[u8]) -> Option<usize> {
haystack
.windows(needle.len())
.position(|window| window == needle)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decodes_simple_chunked_body() {
// 两个分块:"Hello" 与 " World",随后是终止分块。
// Two chunks: "Hello" and " World", then terminating chunk.
let body = b"5\r\nHello\r\n6\r\n World\r\n0\r\n\r\n";
let decoded = decode_chunked(body).unwrap();
assert_eq!(decoded, b"Hello World");
/// 把所有解码输出合并为一个字符串(测试辅助)。
/// Concatenate all decoded output into one string (test helper).
fn drain(dec: &mut ChunkedDecoder) -> String {
let mut out = Vec::new();
loop {
match dec.take_decoded() {
Some(mut chunk) => out.append(&mut chunk),
None => {
if dec.is_finished() {
break;
}
return String::from_utf8_lossy(&out).to_string();
}
}
}
String::from_utf8_lossy(&out).to_string()
}
#[test]
fn decodes_chunk_with_extension() {
let body = b"5;name=value\r\nHello\r\n0\r\n\r\n";
let decoded = decode_chunked(body).unwrap();
assert_eq!(decoded, b"Hello");
fn decodes_single_chunk_fed_at_once() {
let mut dec = ChunkedDecoder::new();
dec.feed(b"5\r\nhello\r\n0\r\n\r\n");
assert_eq!(drain(&mut dec), "hello");
}
#[test]
fn decodes_single_chunk() {
let body = b"3\r\nabc\r\n0\r\n\r\n";
assert_eq!(decode_chunked(body).unwrap(), b"abc");
fn decodes_chunk_split_across_feeds() {
// 网络分块与 HTTP 分块不对齐:一次喂一半。
// Network chunks misalign with HTTP chunks: feed half at a time.
let mut dec = ChunkedDecoder::new();
dec.feed(b"5\r\nhe");
assert_eq!(drain(&mut dec), "he");
dec.feed(b"llo\r\n0\r\n\r\n");
assert_eq!(drain(&mut dec), "llo");
}
#[test]
fn decodes_multiple_chunks_and_extension() {
let mut dec = ChunkedDecoder::new();
dec.feed(b"5;name=value\r\nhello\r\n6\r\n world\r\n0\r\n\r\n");
assert_eq!(drain(&mut dec), "hello world");
}
#[test]
fn yields_partial_data_before_terminator() {
let mut dec = ChunkedDecoder::new();
// 第一个分块完整、第二个分块只喂了部分,应先把已解码部分吐出。
// First chunk complete, second partial: yield decoded bytes so far.
dec.feed(b"3\r\nabc\r\n5\r\n12");
assert_eq!(drain(&mut dec), "abc12");
dec.feed(b"345\r\n0\r\n\r\n");
assert_eq!(drain(&mut dec), "345");
}
#[test]
fn chunked_streaming_body_end_to_end() {
// 模拟完整 SSE 流chunked 编码)经 StreamingBody 逐块吐出。
// Simulate a full SSE stream (chunked-encoded) being drained via StreamingBody.
// 这里只测解码器端到端;网络部分由 provider 的 mock transport 覆盖。
// Only the decoder is exercised here; the network side is covered by the
// providers' mock transport tests.
let mut dec = ChunkedDecoder::new();
let payload = "event: message_start\r\ndata: {\"type\":\"x\"}\r\n\r\n";
assert_eq!(payload.len(), 44);
dec.feed(format!("{:x}\r\n{}", payload.len(), payload).as_bytes());
dec.feed(b"0\r\n\r\n");
assert_eq!(drain(&mut dec), payload);
}
}

View File

@ -0,0 +1,59 @@
//! HTTP/1.1 编解码的集成测试。
//! Integration tests for the HTTP/1.1 codec.
use focus_transport::http::{
body_length, parse_response_head, BodyLength, HttpRequest, HttpResponse,
};
use std::collections::HashMap;
#[test]
fn serializes_post_request() {
let req = HttpRequest::post("api.example.com", "/v1/messages")
.header("Authorization", "Bearer xyz")
.json_body(r#"{"hello":"world"}"#);
let wire = req.serialize();
let text = String::from_utf8(wire).unwrap();
assert!(text.starts_with("POST /v1/messages HTTP/1.1\r\n"));
assert!(text.contains("Host: api.example.com\r\n"));
assert!(text.contains("Authorization: Bearer xyz\r\n"));
assert!(text.contains("Content-Type: application/json\r\n"));
assert!(text.contains("Content-Length: 17\r\n"));
assert!(text.ends_with("{\"hello\":\"world\"}"));
}
#[test]
fn parses_response_head() {
let raw =
b"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 5\r\n\r\nhello";
let (resp, consumed) = parse_response_head(raw).unwrap();
assert_eq!(resp.status, 200);
assert_eq!(
resp.headers.get("content-type").unwrap(),
"application/json"
);
assert_eq!(resp.headers.get("content-length").unwrap(), "5");
assert_eq!(consumed, raw.len() - 5);
assert_eq!(body_length(&resp), BodyLength::Fixed(5));
}
#[test]
fn detects_chunked() {
let raw = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n";
let (resp, _) = parse_response_head(raw).unwrap();
assert_eq!(body_length(&resp), BodyLength::Chunked);
}
#[test]
fn is_success_for_2xx() {
let resp = HttpResponse {
status: 204,
headers: HashMap::new(),
body: vec![],
};
assert!(resp.is_success());
}
#[test]
fn rejects_incomplete_head() {
assert!(parse_response_head(b"HTTP/1.1 200 OK\r\n").is_err());
}

View File

@ -0,0 +1,101 @@
//! SSE 增量解析器的集成测试。
//! Integration tests for the incremental SSE parser.
use focus_transport::sse::{SseEvent, SseParser};
fn collect(parser: &mut SseParser) -> Vec<SseEvent> {
let mut out = Vec::new();
while let Some(e) = parser.next_event() {
out.push(e);
}
out
}
#[test]
fn parses_single_event() {
let mut p = SseParser::new();
p.feed_str("data: hello\n\n");
let events = collect(&mut p);
assert_eq!(events.len(), 1);
assert_eq!(events[0].data, "hello");
assert_eq!(events[0].event, "message");
}
#[test]
fn parses_event_with_type() {
let mut p = SseParser::new();
p.feed_str("event: ping\ndata: 1\n\n");
let events = collect(&mut p);
assert_eq!(events[0].event, "ping");
assert_eq!(events[0].data, "1");
}
#[test]
fn joins_multiline_data() {
let mut p = SseParser::new();
p.feed_str("data: line1\ndata: line2\n\n");
let events = collect(&mut p);
assert_eq!(events[0].data, "line1\nline2");
}
#[test]
fn handles_crlf() {
let mut p = SseParser::new();
p.feed_str("data: hi\r\n\r\n");
let events = collect(&mut p);
assert_eq!(events[0].data, "hi");
}
#[test]
fn ignores_comments_and_unknown_fields() {
let mut p = SseParser::new();
p.feed_str(": this is a comment\ndata: ok\nfoo: bar\n\n");
let events = collect(&mut p);
assert_eq!(events[0].data, "ok");
}
#[test]
fn parses_id_and_retry() {
let mut p = SseParser::new();
p.feed_str("id: 42\nretry: 5000\ndata: x\n\n");
let events = collect(&mut p);
assert_eq!(events[0].id, "42");
assert_eq!(events[0].retry, Some(5000));
}
#[test]
fn handles_split_across_chunks() {
let mut p = SseParser::new();
p.feed_str("dat");
assert!(collect(&mut p).is_empty());
p.feed_str("a: hello\n\n");
let events = collect(&mut p);
assert_eq!(events[0].data, "hello");
}
#[test]
fn handles_split_across_line_boundary() {
let mut p = SseParser::new();
p.feed_str("data: first\n\ndata: sec");
assert_eq!(collect(&mut p).len(), 1);
p.feed_str("ond\n\n");
let events = collect(&mut p);
assert_eq!(events[0].data, "second");
}
#[test]
fn multiple_events_in_one_chunk() {
let mut p = SseParser::new();
p.feed_str("data: a\n\ndata: b\n\ndata: c\n\n");
let events = collect(&mut p);
assert_eq!(events.len(), 3);
assert_eq!(events[2].data, "c");
}
#[test]
fn empty_data_line() {
let mut p = SseParser::new();
p.feed_str("data:\n\n");
let events = collect(&mut p);
assert_eq!(events[0].data, "");
}

View File

@ -0,0 +1,26 @@
//! transport 模块chunked 解码)的集成测试。
//! Integration tests for the transport module (chunked decoding).
use focus_transport::transport::decode_chunked;
#[test]
fn decodes_simple_chunked_body() {
// 两个分块:"Hello" 与 " World",随后是终止分块。
// Two chunks: "Hello" and " World", then terminating chunk.
let body = b"5\r\nHello\r\n6\r\n World\r\n0\r\n\r\n";
let decoded = decode_chunked(body).unwrap();
assert_eq!(decoded, b"Hello World");
}
#[test]
fn decodes_chunk_with_extension() {
let body = b"5;name=value\r\nHello\r\n0\r\n\r\n";
let decoded = decode_chunked(body).unwrap();
assert_eq!(decoded, b"Hello");
}
#[test]
fn decodes_single_chunk() {
let body = b"3\r\nabc\r\n0\r\n\r\n";
assert_eq!(decode_chunked(body).unwrap(), b"abc");
}