// 真实传输层:基于 moonbitlang/async/http 的 FetchFn 实现。
//
// 契约:**任何失败都必须折叠进 FetchOutcome**,不向引擎抛异常。
// 单页请求失败只应让这一页记 status=0 + error,不能让整次采集终止——
// 否则一个超时、一次 TLS 握手失败就会毁掉跑了半小时的任务。
//
// - 30 秒整体超时(@async.with_timeout_opt)
// - 协议错误 / 解码错误 / 底层抛出的异常:捕获后折叠为 status=0 + error
// - 重定向跟随行为取决于底层客户端(js 后端经 Node fetch 自动跟随)
// - 显式发送 User-Agent:robots.txt 的组匹配依赖它
///|
const HTTP_TIMEOUT_MS : Int32 = 30_000
///|
/// FetchFn 的默认实现。永不抛异常。
pub async fn http_fetch(url : String, user_agent : String) -> FetchOutcome {
let headers : Map[@http.CaseInsensitiveString, String] = Map([], capacity=4)
let _ = headers.set(@http.CaseInsensitiveString("user-agent"), user_agent)
let task : async () -> FetchOutcome = async fn() -> FetchOutcome {
try {
let (resp, data) = @http.get(url, headers~)
let body = data.text()
{ status: resp.code, body, error: None, }
} catch {
e =>
{
status: 0,
body: "",
error: Some("transport error: " + e.to_string()),
}
}
}
match @async.with_timeout_opt(HTTP_TIMEOUT_MS, task) {
Some(outcome) => outcome
None => { status: 0, body: "", error: Some("timeout after 30s"), }
}
}