axios 风格的 MoonBit HTTP 客户端:实例与配置合并、四种请求体形态、流式与 SSE、自动重定向
Dependencies
moon add q2316367743/moonhttpimport {
"q2316367743/moonhttp@0.1.0",
}///|
async fn main {
// 实例上一次性配好 base_url 与公共头,之后的请求只写路径
let api = @moonhttp.create(
@moonhttp.Config::default()
.with_base_url("https://api.github.com")
.with_timeout(5_000)
.with_common_header("Accept", "application/vnd.github+json"),
)
let res = api.request(
@moonhttp.Config::new("/repos/moonbitlang/core").with_params({
"per_page": 3,
}),
)
println(res.status) // 200
println(res.text()) // 响应体原文,按 response_encoding 解码(默认 UTF-8)
println((try! res.json()).stringify()) // 要对象就 json()
}| 字段 | 合并方式 |
|---|---|
| url / 请求方法 / 请求体 | 只取请求级,实例默认值里的同名字段丢弃 |
| base_url / timeout / max_redirects / response_encoding / 进度回调 / cancel_token | 请求级优先,缺省回退实例默认值 |
| params / headers / auth / proxy | 逐层合并(头名大小写不敏感) |
| validate_status / params_serializer | 请求级提供即整体接管 |
// 从既有实例派生:继承默认值与拦截器,再叠加本次的配置
let search = api.create(@moonhttp.Config::default().with_timeout(15_000))| 入口 | 拿到什么 | 什么时候用 |
|---|---|---|
| client.request(config) | Response,响应体已读全 | 常规请求 |
| client.stream(config) | StreamResponse,原始字节流 | 下载、自己按块处理 |
| client.sse(config) | SseStream,解析好的事件流 | 消费 SSE |
| 构建器 | 发出去的内容 | 自动补的 Content-Type |
|---|---|---|
| with_data_from_str(s) | s 的 UTF-8 字节,一个字节不改 | 不补 |
| with_data_from_json(j) | j.stringify() 之后的 JSON 文本 | application/json |
| with_data_from_form(form) | multipart/form-data 正文 | multipart/form-data; boundary=... |
| with_data_from_urlencoded(j) | a=1&b=2 形式 | application/x-www-form-urlencoded |
// JSON 请求体
api.request(@moonhttp.Config::new("/users")
.with_method(@moonhttp.Method::Post)
.with_data_from_json({ "name": "moon" }))
// 带文件的表单:文件按「字节 + 文件名」传入,库不读盘
let form = @moonhttp.FormData::new()
.append_text("title", "假期照片")
.append_file("avatar", "a.png", bytes, content_type="image/png")
api.request(@moonhttp.Config::new("/upload")
.with_method(@moonhttp.Method::Post)
.with_data_from_form(form))res.text() // 按 response_encoding(默认 UTF-8)解码成文本
res.bytes() // 原样取出字节,不经过任何解码
try! res.json() // 先按同一编码解码,再 @json.parse(失败抛 @json.ParseError)
res.content_length() // 响应体字节数
res.is_success() // 状态码是不是 2xxlet plain = @moonhttp.Client::new() // 给重试用的裸实例:不带这层拦截器,天然不会无限递归
let client = @moonhttp.Client::new(
interceptors~ = @moonhttp.Interceptors::new()
// 请求侧:发送前改配置(加认证头、改地址、给所有请求注入公共 body 字段)
.use_request(config => config.with_header("X-Token", "secret"))
// 响应侧:原样返回就只是观察;要改就用 with_status / with_headers / with_body / with_text / with_json
.use_response(response => response)
// 响应侧的错误路径:非 2xx、超时、断连都会走到这里——统一错误处理与重试写在这
.use_response(response => response, on_rejected=error => plain.request(error.config())),
)let stop = @moonhttp.CancelToken::new()
@async.with_task_group(group => {
let running = group.spawn(() => {
api.request(@moonhttp.Config::new("/reports/big.csv").with_cancel_token(stop)) catch {
error if error.is_cancelled() => println("已取消:" + error.message())
}
})
@async.sleep(2_000)
stop.cancel(message="Operation canceled by the user.")
running.wait()
})let client = @moonhttp.create(
@moonhttp.Config::default()
.with_base_url("https://api.example.com")
.with_proxy("127.0.0.1", port=9000, username="mikeymike", password="rapunz3l"),
)client.request(
@moonhttp.Config::new("/upload")
.with_method(@moonhttp.Method::Post)
.with_data_from_json({ "name": "moon" })
.with_on_upload_progress(fn(event) { println("已上传 \{event.loaded} 字节") })
.with_on_download_progress(fn(event) { println("已下载 \{event.loaded} 字节") }),
)///|
async fn download(api : @moonhttp.Client) -> Unit raise @moonhttp.HttpError {
let res = api.stream(@moonhttp.Config::new("/big-file"))
println(res.status) // 响应头已到手,body 还没读
while res.read_some() is Some(chunk) {
println(chunk.length())
}
}///|
async fn watch(api : @moonhttp.Client) -> Unit raise @moonhttp.HttpError {
let events = api.sse(@moonhttp.Config::new("/events"))
while events.next_event() is Some(event) {
println(event.event + ": " + event.data) // message: {...}
}
}try {
ignore(api.request(config))
} catch {
@moonhttp.HttpError(info) => {
println(info.code) // BadRequest
println(info.message) // 请求失败,状态码 404
println(info.response.unwrap().status) // 404
println(info.config.url) // 出错时的配置,便于定位
}
}| ErrorCode | 触发时机 |
|---|---|
| BadRequest | 状态码 4xx 且未通过校验 |
| BadResponse | 状态码 5xx(或其它非 2xx) |
| Network | 连接 / DNS / TLS 失败、读响应体中途断连、代理拒绝建隧道 |
| Timeout | 超过 timeout |
| Cancelled | 被 CancelToken 取消(error.is_cancelled()) |
| InvalidUrl | 既没有 url 也没有可用的 base_url |
| NotSupported | 传输层无法完成该请求(例如重定向到非 http(s) 协议) |
| TooManyRedirects | 重定向次数超过 max_redirects |
let mock = @transport.MockTransport::new(response)
let transport : &@transport.Transport = mock
ignore(@moonhttp.Client::new(transport=transport).request(@moonhttp.Config::new("/users")))
println(mock.last_request().unwrap().url) // 已经拼好 base_url 与 query 的完整地址moon check # 类型检查
moon test # 全部测试(不需要外网)
moon run src/main # 快速上手示例(真实网络,本地手动测试用,不随包发布)
moon run src/main/proxy # 其余七个方向同理:把 <方向> 换成 basics/methods/proxy/
# progress/redirect/interceptors/sse 之一
moon info && moon fmt # 更新 .mbti 接口并格式化,提交前跑一次chmod +x .githooks/pre-commit && git config core.hooksPath .githooksclient.request(config) catch {
error if error.is_cancelled() => println("已取消:" + error.message())
error => println(error.to_string())
}.use_request(config => {
if token == "" {
raise @moonhttp.HttpError::new("缺少 token", @moonhttp.ErrorCode::BadRequest, config)
}
config.with_header("Authorization", token)
})pub struct Client {
// private fields
}pub struct Interceptors {
// private fields
}let client = @moonhttp.Client::new(
interceptors=@moonhttp.Interceptors::new()
.use_request(config => config.with_header("X-Token", token))
.use_response(response => response.with_text(unwrap(response.text()))),
)fn Interceptors::use_request(self : Interceptors, on_request : async (Config) -> Config raise HttpError) -> Interceptorsfn Interceptors::use_response(self : Interceptors, on_fulfilled : async (Response) -> Response raise HttpError, on_rejected? : async (HttpError) -> Response raise HttpError) -> InterceptorsInstall
Download zipaxios 风格的 MoonBit HTTP 客户端:实例与配置合并、四种请求体形态、流式与 SSE、自动重定向
Dependencies