Skip to content

HTTP 客户端

Spire 的 HTTP 客户端能力分成两层:

  • soulsoft_net_http 提供 HttpClientHttpRequestMessageHttpContent 和处理器管道
  • soulsoft_extensions_http 提供 IHttpClientFactory、命名客户端和容器集成

建议按这个顺序理解:

  1. 先学会直接用 HttpClient 发请求
  2. 再理解请求管道里 HttpClientDelegatingHandlerHttpClientHandler 各自做什么
  3. 最后再接入 IHttpClientFactory

最小示例

cangjie
import soulsoft_net_http.*

main(): Int64 {
    // 直接创建 HttpClient。
    let client = HttpClient()

    // 发送 GET 请求,并在离开作用域时自动关闭响应对象。
    try (response = client.get("https://example.com")) {
        response.ensureSuccessStatusCode()
        println(response.content.readAsString())
    }

    // 使用完成后关闭客户端。
    client.close()
    return 0
}

这里先记住三件事:

  • HttpClient() 可以直接使用
  • get() 返回 HttpResponseMessage,处理完要关闭
  • ensureSuccessStatusCode() 会在非 2xx 时抛出异常

常见用法

基础地址和默认请求头

cangjie
import soulsoft_net_http.*
import stdx.encoding.url.*

let client = HttpClient()
// 设置基础地址,后续可以发送相对路径请求。
client.baseAddress = URL.parse("https://api.example.com")
// 设置默认请求头。
client.defaultRequestHeaders.add("Accept", "application/json")

let body = client.getString("/users/1")
println(body)

// 关闭客户端。
client.close()

这部分有两个关键规则:

  • 相对地址依赖 baseAddress
  • 一旦发出过请求,就不能再修改 baseAddress

defaultRequestHeaders 还有一个行为要注意:

  • 如果请求对象自己已经设置了同名头部,默认头不会覆盖这组头部

发送请求体

最常用的几个内容类型是:

  • StringContent
  • JsonContent
  • FormUrlContent
  • MultipartFormDataContent

例如发送 JSON:

cangjie
import soulsoft_net_http.*

let client = HttpClient()
// 构造 JSON 请求体。
let content = JsonContent.create("{\"username\":\"admin\"}")

try (response = client.post("https://api.example.com/login", content)) {
    response.ensureSuccessStatusCode()
}

// 关闭客户端。
client.close()

例如上传文件:

cangjie
import soulsoft_net_http.*

let client = HttpClient()

// 构造一个文件内容对象。
let fileContent = ByteArrayContent("hello file".toArray())
fileContent.headers.add("Content-Type", "text/plain")

// 用 multipart/form-data 包装上传内容。
let form = MultipartFormDataContent()
form.add(fileContent, "file", "hello.txt")

try (response = client.post("https://api.example.com/upload", form)) {
    response.ensureSuccessStatusCode()
}

// 关闭客户端。
client.close()

有一个容易写错的地方:

  • Content-Type 这类内容头应该写到 content.headers
  • 不应该写到 request.headers

如果你的对象实现了 ISerialization<T>,也可以直接让 JsonContent 负责序列化:

cangjie
// 如果对象支持序列化,可以直接交给 JsonContent 处理。
let content = JsonContent.create(loginRequest)

自定义请求

当快捷方法不够用时,用 HttpRequestMessage

cangjie
import soulsoft_net_http.*

let client = HttpClient()
// 构造完整请求对象,手动指定方法和地址。
let request = HttpRequestMessage(HttpMethod.Post, "https://api.example.com/users")
request.headers.add("X-Trace-Id", "trace-001")
request.content = JsonContent.create("{\"name\":\"alice\"}")

try (response = client.send(request)) {
    response.ensureSuccessStatusCode()
    println(response.content.readAsString())
}

// 关闭客户端。
client.close()

读取响应

最常见的读取方式有三种:

  • readAsString():一次性读成字符串
  • readAsByteArray():一次性读成字节数组
  • readAsStream():边读边处理,适合 SSE 或大响应
cangjie
import std.io.*
import soulsoft_net_http.*

let client = HttpClient()

try (response = client.get("https://api.example.com/profile")) {
    response.ensureSuccessStatusCode()

    // 一次性把响应体读成字符串。
    let text = response.content.readAsString()
    println(text)
}

try (response = client.get("https://api.example.com/events")) {
    response.ensureSuccessStatusCode()

    // 以流的方式读取响应体。
    let stream = response.content.readAsStream()
    let chunk = Array<Byte>(256, repeat: 0)
    let n = stream.read(chunk)
    println(n)
}

// 关闭客户端。
client.close()

如果响应是 JSON,并且目标类型实现了 ISerialization<T>,还可以直接反序列化:

cangjie
// 如果响应 JSON 对应的类型可序列化,可以直接读成对象。
let profile = response.content.readFromJson<UserProfile>()

请求管道

一次请求通常会经过三层:

HttpClient 做什么

HttpClient 是调用入口,负责准备请求和发起调用,不直接做底层网络通信。它主要做四件事:

  • 提供 getpostputdeletesend 这些请求 API
  • 提供 getStringgetByteArraygetStream 这些常用读取方法
  • 管理 baseAddressdefaultRequestHeaders
  • 在发送前解析相对地址,并补齐默认请求头

再记两个实际行为即可:

  • getString()getByteArray()getStream() 会先检查响应是否为 2xx
  • delete() 也支持携带 HttpContent

DelegatingHandler 做什么

DelegatingHandler 是中间处理器,用来在主处理器前后插入横切逻辑:

  • 请求发出前修改请求
  • 调用 super.send(request) 继续向下传递
  • 响应返回后追加后置处理

典型场景就是日志、鉴权、重试和审计。

HttpClientHandler 做什么

HttpClientHandler 是尾节点,负责真正的网络发送:

  • HttpRequestMessage 转成底层请求
  • 合并 request.headerscontent.headers
  • 调用底层 Client.send(...)
  • 把响应包装成 HttpResponseMessage

所以它通常放在整条请求管道的最后。

HttpClientHandlerHttpClient 的关系

两者的分工可以直接记成一句话:

  • HttpClient 面向业务代码,负责“方便地发请求”
  • HttpClientHandler 面向底层传输,负责“真正把请求送出去”

如果没有自定义处理器,默认链路基本就是:

text
HttpClient -> HttpClientHandler -> network

一个处理器示例

cangjie
import soulsoft_net_http.*

class AuthHandler <: DelegatingHandler {
    public init(innerHandler: HttpMessageHandler) {
        // 把下一个处理器交给基类维护。
        super(innerHandler)
    }

    protected override func send(request: HttpRequestMessage): HttpResponseMessage {
        // 在请求发出前统一补鉴权头。
        request.headers.set("Authorization", "Bearer token")
        return super.send(request)
    }
}

// 用自定义处理器包住默认的底层处理器。
let client = HttpClient(AuthHandler(HttpClientHandler()))

try (response = client.get("https://api.example.com/me")) {
    response.ensureSuccessStatusCode()
}

// 关闭客户端。
client.close()

这里再记一条约束:

  • DelegatingHandler 实例不要复用到多条管道里

如果一个处理器实例已经挂过 innerHandler,再把它放进新的管道,构建时会失败。

与依赖注入集成

如果项目已经使用 soulsoft_extensions_injection,通常不再直接到处 new HttpClient(),而是交给 IHttpClientFactory 管理。

命名客户端

cangjie
import soulsoft_extensions_http.*
import soulsoft_extensions_injection.*
import soulsoft_net_http.*
import stdx.encoding.url.*

let services = ServiceCollection()

// 注册一个名为 github 的命名客户端。
let _ = services.addHttpClient("github", { client =>
    client.baseAddress = URL.parse("https://api.github.com")
    client.defaultRequestHeaders.add("Accept", "application/json")
})

// 解析工厂并创建命名客户端。
let root = services.build()
let factory = root.getOrThrow<IHttpClientFactory>()
let client = factory.createClient("github")

try (response = client.get("/users/octocat")) {
    response.ensureSuccessStatusCode()
}

// 释放客户端和根容器。
client.close()
root.close()

这一层的核心价值只有一句话:

  • HttpClient 可以按需创建,但同名客户端会复用底层处理器

给命名客户端加处理器

cangjie
import soulsoft_extensions_http.*
import soulsoft_extensions_injection.*
import soulsoft_net_http.*

class AuthHandler <: DelegatingHandler {
    public init() {
        // 由工厂稍后注入 inner handler。
        super()
    }

    protected override func send(request: HttpRequestMessage): HttpResponseMessage {
        // 在发送前附加鉴权头。
        request.headers.set("Authorization", "Bearer token")
        return super.send(request)
    }
}

let services = ServiceCollection()
// 把处理器注册为瞬时服务。
services.addTransient<AuthHandler, AuthHandler>()

// 给命名客户端挂上自定义处理器并设置底层处理器复用时间。
let _ = services.addHttpClient("secured")
    .addHttpMessageHandler<AuthHandler>()
    .setHandlerLifetime(Duration.minute * 2)

let root = services.build()
let client = root.getOrThrow<IHttpClientFactory>().createClient("secured")
client.close()
root.close()

这里有两个实践点:

  • 处理器尽量注册成 Transient
  • setHandlerLifetime() 控制的是底层处理器复用时间,不是 HttpClient 对象寿命

核心功能小结

如果只看日常开发里最常用的能力,可以把这套 HTTP 客户端总结成下面几项:

  • HttpClient 快速发送 GET、POST、PUT、DELETE 请求
  • HttpRequestMessage 自定义方法、头部和请求体
  • StringContentJsonContentFormUrlContentMultipartFormDataContent 组织请求体
  • readAsString()readAsByteArray()readAsStream() 读取响应
  • DelegatingHandler 扩展日志、鉴权、重试等横切逻辑
  • IHttpClientFactory 在 DI 场景里统一管理客户端和底层处理器

使用建议

  • 简单场景直接用 soulsoft_net_http.HttpClient
  • 已经接入 DI 时,优先使用 IHttpClientFactory
  • 响应对象优先用 try (response = ...) 自动关闭
  • 需要边收边处理响应时,用 readAsStream()

当前实现的一个注意点

按当前源码实现,默认的 HttpClientHandler() 会创建底层 ClientBuilder,并使用:

  • TrustAll TLS 验证模式
  • 75 秒读超时

这更接近“开箱即可连通”的默认行为。
如果你的生产环境需要严格证书校验、代理或其他网络策略,应当显式自定义主处理器。