// 请求侧:把 `PreparedRequest` 变成线上的字节。
//
// 头的装配顺序(RFC 语义上无所谓,可读性上有所谓):用户头在最前
//(原样保留顺序),自动补的头按 set_if_absent 跟在后面——用户显式设置
// 的同名头永远赢,与 axios 的 set_if_absent 口径一致。
// 例外是流式请求体的两个分帧头(见 build_request_headers):那两个由栈接管。
///|
/// 上传进度的分块大小:每写完这么多字节报告一次进度。
/// 64 KiB 是「回调次数」与「进度条平滑度」的常见折中,也是流式泵的读取块大小。
const UPLOAD_CHUNK_SIZE : Int32 = 65536
///|
/// 默认 User-Agent。版本号与 moon.mod 的 version 手工保持一致——
/// MoonBit 没有构建期注入,升版本时这里是唯一要跟着改的地方。
/// GitHub 等服务端会直接拒绝无 UA 的请求,axios 的 node 适配器与
/// Python requests 都是同一机制:用户没设就补一份库自己的。
const DEFAULT_USER_AGENT : String = "moonhttp/0.4.0"
///|
/// 装配请求头。自动头各有出处:
/// - `Host`:RFC 9112 §7.2,由栈按目标生成(重定向层已把用户显式 Host 丢掉);
/// - `Accept-Encoding: gzip`:**谁解压谁声明**(docs/15)——请求没声明时
/// 由我们声明并负责响应侧解压;用户声明过(含 `decompress: false` 时
/// 上层声明的 `identity`)就完全不动;
/// - `User-Agent`:`DEFAULT_USER_AGENT`,见上;
/// - `Connection`:连接池的开关决定要不要声明(docs/21)——池启用时不发
/// (HTTP/1.1 持久连接是默认,对齐 Go / Python / Node 的做法);池禁用
/// 时发 `Connection: close`(phase 1 的老口径,让服务端立刻回收)。
/// 用户显式设置永远赢(set_if_absent);用户声明的 close 还会被响应侧
/// 尊重——这条连接响应结束后不回池。
/// - 分帧头(`Content-Length` / `Transfer-Encoding`)按请求体形态落,见下。
///
/// 分帧头的归属分两档(docs/20):
/// - 缓冲 / 无 body:`Content-Length` 走 set_if_absent(用户显式设置仍然赢),
/// 无体发 0——phase 1 的老口径不变;
/// - 流式 body:两个头**由栈全权接管**——用户预设的一律不进头表(预设的
/// 长度可能与实际写出的字节数不符,拼出坏报文;两个头并存更是协议矛盾),
/// 再按 `content_length` 的有无落定长或 chunked。
fn build_request_headers(
request : @transport.PreparedRequest,
target : Target,
declare_close : Bool,
) -> @httpproto.HeaderMap {
let headers = @httpproto.HeaderMap::new()
let framing_managed = request.body is Some(@transport.RequestBody::Stream(_))
for entry in request.headers.entries() {
if framing_managed {
let name = entry.0.to_lower()
if name == "content-length" || name == "transfer-encoding" {
continue
}
}
headers.append(entry.0, entry.1)
}
headers.set_if_absent("Host", host_header(target))
headers.set_if_absent("Accept-Encoding", "gzip")
headers.set_if_absent("User-Agent", DEFAULT_USER_AGENT)
if declare_close {
headers.set_if_absent("Connection", "close")
}
match request.body {
Some(@transport.RequestBody::Stream(stream)) =>
match stream.content_length {
Some(length) => headers.set("Content-Length", length.to_string())
None => headers.set("Transfer-Encoding", "chunked")
}
// 缓冲形态保持老口径:用户显式设置的分帧头仍然赢。
Some(@transport.RequestBody::Buffered(bytes)) =>
headers.set_if_absent("Content-Length", bytes.length().to_string())
None => headers.set_if_absent("Content-Length", "0")
}
headers
}
///|
/// 渲染并写出请求头块。渲染失败是「请求构造不出来」(方法/头/目标的形状
/// 问题),归 `Unsupported`——不是对端的错,别记成 `Malformed`。
///
/// raise 类型不收紧到 `TransportError`:写连接失败要按 `StaleConnection`
/// 表达(坏连接能否重试由 send_head 裁决),它与 TransportError 是平级的
/// Error 子类型。
async fn write_request_head(
conn : Connection,
meth : String,
target : String,
headers : @httpproto.HeaderMap,
) -> Unit {
let head = @httpproto.render_request_head(meth, target, headers) catch {
@httpproto.ParseError::Malformed(message) =>
raise @transport.TransportError::Unsupported(message)
@httpproto.ParseError::Unsupported(message) =>
raise @transport.TransportError::Unsupported(message)
}
// 写失败按坏连接表达(StaleConnection):借来的连接可能早被对端关了,
// send_head 拿着这个信号换新连接重试;翻成 Network 的裁决也在那里。
conn.write(head) catch {
error => raise StaleConnection("写出请求头失败:\{error}")
}
}
///|
/// 写请求体:按形态分派——缓冲走整块(老路径),流式走泵循环(docs/20)。
async fn write_request_body(
conn : Connection,
body : @transport.RequestBody,
on_progress : @transport.ProgressCallback?,
) -> Unit {
match body {
Buffered(data) => write_buffered_body(conn, data, on_progress)
Stream(stream) => write_stream_body(conn, stream, on_progress)
}
}
///|
/// 写缓冲请求体:有上传进度回调时按 `UPLOAD_CHUNK_SIZE` 分块写、逐块报告。
/// 「报告了 = 已经交给内核」的口径(write 不缓冲,一次 syscall;
/// 见 docs/10-progress.md)。空 body 不触发任何回调——没有字节可报。
async fn write_buffered_body(
conn : Connection,
data : Bytes,
on_progress : @transport.ProgressCallback?,
) -> Unit {
match on_progress {
None =>
conn.write(data) catch {
error => raise StaleConnection("写出请求体失败:\{error}")
}
Some(report) => {
let total = data.length()
let mut sent = 0
while sent < total {
let mut end = sent + UPLOAD_CHUNK_SIZE
if end > total {
end = total
}
conn.write(data.exact_view(start=sent, end~)) catch {
error => raise StaleConnection("写出请求体失败:\{error}")
}
sent = end
report({ loaded: sent, total: Some(total), })
}
}
}
}
///|
/// 写流式请求体(docs/20):从读取流按 `UPLOAD_CHUNK_SIZE` 拉取、写向连接,
/// 直到 EOF(`read_some` 返回 `None`)。
///
/// 分帧由 `StreamBody::content_length` 决定,与 `build_request_headers` 落的头
/// 是同一来源:有声明长度就裸写,写出总数必须与声明相符——不符报 `Malformed`
///(本地数据与声明对不上,不是对端的错);没声明长度就 chunked,每块套
/// `render_chunk_frame`、收尾补终止帧。
///
/// 进度与缓冲路径同口径:每写完一块报告一次(chunked 模式 `total` 为 `None`,
/// 与下载侧 chunked 响应一致)。读源失败(如 MemoryReader 回调抛错)是
/// 本地问题,归 `Network`;写连接失败是坏连接,按 `StaleConnection` 表达
/// (能否重试由 send_head 裁决——流式体不可回卷,永远不重试)。
///
/// 空块跳过:chunked 下零长度帧就是终止帧(等于把报文提前截断),
/// 定长下也是噪音;EOF 只认 `None`,不认空段。
async fn write_stream_body(
conn : Connection,
stream : @transport.StreamBody,
on_progress : @transport.ProgressCallback?,
) -> Unit {
let chunked = stream.content_length is None
let mut sent = 0
while true {
let chunk = stream.reader.read_some(max_len=UPLOAD_CHUNK_SIZE) catch {
error => raise @transport.TransportError::Network(error.to_string())
}
match chunk {
None => break
Some(chunk) => {
if chunk.length() == 0 {
continue
}
let wire : Bytes = if chunked {
@httpproto.render_chunk_frame(chunk) catch {
@httpproto.ParseError::Malformed(message) =>
raise @transport.TransportError::Malformed(message)
@httpproto.ParseError::Unsupported(message) =>
raise @transport.TransportError::Unsupported(message)
}
} else {
chunk
}
conn.write(wire) catch {
error => raise StaleConnection("写出请求体失败:\{error}")
}
sent = sent + chunk.length()
match on_progress {
Some(report) =>
report({ loaded: sent, total: stream.content_length, })
None => ()
}
}
}
}
if chunked {
conn.write(@httpproto.render_last_chunk()) catch {
error =>
raise StaleConnection("写出请求体收尾帧失败:\{error}")
}
}
match stream.content_length {
Some(declared) =>
if declared != sent {
raise @transport.TransportError::Malformed(
"流式请求体与声明的长度不符:声明 \{declared} 字节,实际写出 \{sent} 字节",
)
}
None => ()
}
}