// 自研 HTTP/1.x 传输实现:`Transport` trait 的真实实现,
// 2026-09-30 起是 `Client::new` 的缺省 transport(旧栈 AsyncHttpTransport 已删除,
// 对照见 docs/18)。它带来的能力:
// - **自定义方法**:请求行由 httpproto 渲染,方法 token 原样落线
// (`PROPFIND` 等 WebDAV 方法开箱即用,docs/16);
// - **gzip 自持**:声明与解压都在本实现内完成(docs/17 口径 4);
// - **连接池**:空闲连接按「服务谁」分桶复用,省掉重复握手
// (2026-10 落地,契约见 docs/17 口径 4,设计与实现见 docs/21)。
///|
/// 自研 HTTP/1.x 传输实现。状态只有池:同一个实例派生的所有
/// `Client`(`Client::create` 共享 transport 引用)共享同一池。
pub struct HttpConnTransport {
priv pool : ConnectionPool
}
///|
/// 创建自研传输实现。
///
/// - `max_idle_per_host`:每 host 的空闲连接上限,缺省 4;
/// 传 0 禁用池(行为退回 phase 1:每请求一条连接 + `Connection: close`)。
/// - `idle_timeout_ms`:空闲连接的保活时长,缺省 30 秒;
/// 传非正数表示不过期(对端关闭由取用时的重试兜底)。
pub fn HttpConnTransport::new(
max_idle_per_host? : Int32,
idle_timeout_ms? : Int32,
) -> HttpConnTransport {
let max_idle = match max_idle_per_host {
Some(value) => value
None => DEFAULT_MAX_IDLE_PER_HOST
}
let idle_timeout = match idle_timeout_ms {
Some(value) => value
None => DEFAULT_IDLE_TIMEOUT_MS
}
{ pool: ConnectionPool::new(max_idle, idle_timeout), }
}
///|
/// 发送一次请求:先借池(可能免去握手)→ 写头写体 → 等响应头,
/// 然后把「还开着的连接」按分帧交给响应体流;响应体消费完由
/// dispose 回调决定还池还是关闭(docs/21)。
///
/// 取消作用域包住**整跳**(Transport 契约的一半):建连、TLS 握手、
/// CONNECT 握手、写头、写体(含上传进度回调)、等响应头。从上传进度
/// 回调里取消也落在同一个子任务上,下一块写出去之前就会断。
/// 响应体阶段的超时与取消由 `ResponseBody` 按**单次读取**管。
pub impl @transport.Transport for HttpConnTransport with fn send(self, request) {
@transport.with_abort_scope(request.signal, () => self.send_head(request))
}
///|
/// 建连到响应头的整段(含等响应头)受 `timeout` 约束,错误统一收敛为
/// `@transport.TransportError`:超时、协议解析(Malformed / Unsupported 原样对应)、
/// 其余(DNS、拒连、TLS、代理拒绝……)归网络层并保留原始文本。
///
/// 重试口径(docs/17 分期表「取用时坏连接重试一次」):借来的连接可能
/// 早已被对端悄悄关掉,**响应开始之前**的失败(写失败 / 状态行干净 EOF,
/// 即 `StaleConnection`)在请求体可回卷(无体或缓冲体)时换新连接重试
/// 一次。重试与首跳共享同一个 timeout 预算。
async fn HttpConnTransport::send_head(
self : HttpConnTransport,
request : @transport.PreparedRequest,
) -> @transport.RawResponse raise @transport.TransportError {
// 方法 token 校验在**建连前**(docs/16 落地清单的要求):
// 拼不进请求行的方法不值得一次 TCP/TLS 握手
let meth = request.http_method.to_string()
if !@httpproto.is_token(meth) {
raise @transport.TransportError::Unsupported(
"请求方法不是合法 token:\{meth}",
)
}
let target = parse_target(request.url)
if target.scheme == "socks5" {
// socks5 只为「代理地址的解析」而认识;作为请求目标没有 HTTP 语义
raise @transport.TransportError::Unsupported(
"请求目标只支持 http/https 协议:\{target.scheme}",
)
}
let key = pool_key(target, request.proxy)
let reused = self.pool.acquire(key)
// 池禁用时保留 phase 1 的线上行为:声明 close 让服务端立刻回收
let declare_close = self.pool.disabled()
// 重试资格的一半:请求体可回卷(无体或缓冲体)。流式体读过的字节
// 拿不回来,重试等于换一份内容,直接报错。
let replayable = request.body is (None | Some(Buffered(_)))
let run = (conn : Connection) => {
// errdefer 只覆盖到 build_raw_response 为止的失败路径:
// 失败(含取消)一律毒化关闭;成功则连接所有权随响应体移交,
// 结局由 dispose 回调定(读完还池 / 半途关闭)。
errdefer conn.close()
let headers = build_request_headers(request, target, declare_close)
write_request_head(conn, meth, target.target, headers)
match request.body {
Some(body) => write_request_body(conn, body, request.on_upload_progress)
None => ()
}
let head = read_response_head(conn)
self.build_raw_response(key, conn, request, meth, head)
}
let send_once = () => {
match reused {
Some(conn) =>
if replayable {
run(conn) catch {
StaleConnection(_) => {
// 死连接已被 errdefer 关掉。此时请求还没被服务端开始响应
//(最多是「发出去了但没等到头」),换新连接再试一次。
// 缓冲体与上传进度回调会因此重放一遍,见 docs/21。
let fresh = dial(target, request.proxy)
run(fresh)
}
// 其余失败不是「借来的连接死了」,原样外抛
error => raise error
}
} else {
run(conn)
}
None => {
let fresh = dial(target, request.proxy)
run(fresh)
}
}
}
try {
// timeout 为 None 或非正数时不加限制
match request.timeout {
Some(milliseconds) if milliseconds > 0 =>
@async.with_timeout(milliseconds, send_once)
_ => send_once()
}
} catch {
@async.TimeoutError => raise @transport.TransportError::Timeout
// 解析错误的两类原样对应:语法坏 → Malformed(ERR_BAD_RESPONSE),
// 能力边界(HTTP/2 前言等)→ Unsupported(ERR_NOT_SUPPORTED)
@httpproto.ParseError::Malformed(message) =>
raise @transport.TransportError::Malformed(message)
@httpproto.ParseError::Unsupported(message) =>
raise @transport.TransportError::Unsupported(message)
// 自己人(dial / 读写辅助函数)抛的 TransportError 原样透传:
// 里面是翻好的文案(隧道 407 的「代理拒绝建立隧道:HTTP 407 …」等),
// 落到下面的 catch-all 会被包成「Network(TransportError.Network)」,
// 把人话冲成一句类型名。
@transport.TransportError::Network(message) =>
raise @transport.TransportError::Network(message)
@transport.TransportError::Malformed(message) =>
raise @transport.TransportError::Malformed(message)
@transport.TransportError::Unsupported(message) =>
raise @transport.TransportError::Unsupported(message)
@transport.TransportError::Cancelled(reason) =>
raise @transport.TransportError::Cancelled(reason)
@transport.TransportError::Timeout =>
raise @transport.TransportError::Timeout
// 坏连接信号到了这里 = 没资格重试(新连接死了,或流式体不可回卷):
// 对端在响应开始前断开是网络失败(不再是 phase 1 的 Malformed)
StaleConnection(message) =>
raise @transport.TransportError::Network(message)
// 其余(建连、TLS……)归网络层,保留原始文本
error => raise @transport.TransportError::Network(error.to_string())
}
}
///|
/// 响应头到手后的收尾:multimap 压平(取最后一个值)、按需解 gzip、
/// 组装带连接所有权的 `RawResponse`。
///
/// 压平规则(docs/17 待决问题的落地):同名头取最后一个值;
/// `Set-Cookie` 的多值全量收集进 `set_cookies`(docs/05 / 17 挂起的
/// 「响应 cookie」落在这里,cookie jar 的自动维护吃的是这份列表)。
///
/// gzip(谁解压谁声明,docs/15):只有「请求没声明 Accept-Encoding、
/// 由我们声明了 gzip」这条路径才解压——用户自己声明的交给上层
/// (缓冲路径 `decode_response_body` / 流式按自描述原则交原字节),
/// `identity` 声明(decompress: false)更是一概不动。解压后
/// `Content-Encoding` 与 `Content-Length` 都成了假话,一并摘掉
/// (`total` 随之归 `None`)。
///
/// 复用判定(docs/21)在响应头层面先定「服务端允许不允许」:
/// 分帧不是 UntilClose(体以关闭收尾,天然不可复用)、请求方没自己
/// 声明 close、`connection_reusable`(版本 × Connection 头规则)通过。
/// 「体读没读满」由 BodyReader 的读取状态补全,两者在 dispose 时合取。
fn HttpConnTransport::build_raw_response(
self : HttpConnTransport,
key : String,
conn : Connection,
request : @transport.PreparedRequest,
meth : String,
head : ResponseHead,
) -> @transport.RawResponse raise @transport.TransportError {
let mut headers = @headers.Headers::new()
let set_cookies : Array[String] = []
for entry in head.headers.entries {
let (name, value) = entry
if name.to_lower() == "set-cookie" {
set_cookies.push(value)
}
headers = headers.set(name, value)
}
let framing = @httpproto.body_framing(meth == "HEAD", head.code, head.headers) catch {
@httpproto.ParseError::Malformed(message) =>
raise @transport.TransportError::Malformed(message)
@httpproto.ParseError::Unsupported(message) =>
raise @transport.TransportError::Unsupported(message)
}
let total = match framing {
@httpproto.BodyFraming::Length(value) => Some(value)
_ => None
}
let request_declared_close = match request.headers.get("Connection") {
Some(value) => @httpproto.declares_close(value)
None => false
}
let server_allows = !(framing is @httpproto.BodyFraming::UntilClose) &&
!request_declared_close &&
@httpproto.connection_reusable(head.version, head.headers)
let body_reader = BodyReader::open(conn, framing, server_allows~)
let mut reader : &@io.Reader = body_reader
let declared_by_us = !request.headers.has("Accept-Encoding")
let response_is_gzip = match headers.get("Content-Encoding") {
Some(value) => value.trim(chars=" \t").to_lower() == "gzip"
None => false
}
if declared_by_us && response_is_gzip {
reader = @gzip.Decoder(reader)
headers = headers.remove("Content-Encoding").remove("Content-Length")
}
let pool = self.pool
@transport.RawResponse::{
status: head.code,
status_text: head.reason,
headers,
set_cookies,
// 连接的所有权在这里移交:EOF / close() 之外的失败由 errdefer 兜底,
// EOF 与显式释放走 dispose——读完且可复用则还池,否则关闭(docs/21)。
body: @transport.ResponseBody::open_wire_dispose(
reader,
fn(end) {
match end {
Finished =>
if body_reader.is_reusable() {
pool.release(key, conn)
} else {
conn.close()
}
// 半途放弃 / 读失败 / 取消:字节流停在哪不可知,一律毒化
Abandoned => conn.close()
}
},
timeout=request.timeout,
total~,
signal=request.signal,
),
}
}
///|
pub extend HttpConnTransport with @transport.Transport::{send}