Finch HTTP 客户端:timeout / 业务错误 / SSE
Finch 是 Elixir 生态里基于 Mint / :httpc 的 HTTP 客户端。它走 BEAM 进程模型,能复用连接、能做 SSE、能精确控制超时和错误语义。
一、为什么不用 HTTPoison
HTTPoison 长期占据 Elixir HTTP 客户端生态,但它基于 :hackney,是个 NIF 库,并发模型与 BEAM 不对齐。Finch 是云原生 Elixir 团队推荐的替代品:
- 基于 Mint(纯 Elixir)或
:httpc(Erlang)适配器。 - 连接池 由 Supervisor 管理,自动复用 TCP/TLS。
- 可取消:每次请求是独立 Task,可以异步取消。
- SSE 支持:
Finch.stream/4流式响应。
二、启动 Finch 实例
# config/config.exs
config :my_app, :finch, [
MyApp.Finch,
pools: %{
"https://api.example.com" => [size: 10, max_connections: 50],
"https://internal.example.com" => [size: 5, max_connections: 10]
},
defaults: [connect_timeout: 1_000, request_timeout: 5_000]
]
# application.ex
children = [
{Finch, name: MyApp.Finch}
]
每个 Finch 实例是一个 supervisor,包含若干 connection pool。pool 按 host 隔离,让热点域名不抢冷门域名的连接。
三、同步请求:request/3
def get_user(id) do
Finch.build(:get, "https://api.example.com/users/\#{id}")
|> Finch.request(MyApp.Finch)
|> case do
{:ok, %Finch.Response{status: 200, body: body}} ->
{:ok, Jason.decode!(body)}
{:ok, %Finch.Response{status: 404}} ->
{:error, :not_found}
{:ok, %Finch.Response{status: s}} when s >= 500 ->
{:error, {:upstream, s}}
{:error, %Mint.TransportError{reason: reason}} ->
{:error, {:network, reason}}
end
end
注意几个返回分支:
{:ok, %Finch.Response{}}:HTTP 层成功了,但业务层可能失败(4xx/5xx)。{:error, %Mint.TransportError{}}:TCP/TLS 失败,连接层错误。{:error, %Mint.HTTPError{}}:HTTP 协议错误(解析失败、连接超时)。
四、超时三件套
Finch 把超时分成三段,每段都有明确含义:
| 超时 | 含义 | 默认值 |
|---|---|---|
| connect_timeout | 建立 TCP/TLS 连接 | 5s |
| request_timeout | 从请求发起到完整响应 | 无(按需设置) |
| pool_timeout | 从 pool 里拿连接 | 5s |
单独请求里覆盖:
Finch.build(:get, url)
|> Finch.request(MyApp.Finch, receive_timeout: 2_000, pool_timeout: 500)
五、业务错误模型
调用外部 API 时,HTTP 状态码只是开始。要把上游错误映射到业务错误:
defmodule MyApp.External do
@type error ::
{:network, term()}
| {:upstream, 500..599, body :: term()}
| {:client, 400..499, body :: term()}
| {:decode, term()}
@spec call(Finch.Request.t()) :: {:ok, term()} | {:error, error()}
def call(req) do
case Finch.request(req, MyApp.Finch, receive_timeout: 3_000) do
{:ok, %Finch.Response{status: 200, body: body}} ->
case Jason.decode(body) do
{:ok, decoded} -> {:ok, decoded}
{:error, reason} -> {:error, {:decode, reason}}
end
{:ok, %Finch.Response{status: s, body: body}} when s in 400..499 ->
{:error, {:client, s, safe_decode(body)}}
{:ok, %Finch.Response{status: s, body: body}} when s in 500..599 ->
{:error, {:upstream, s, safe_decode(body)}}
{:error, %Mint.TransportError{reason: reason}} ->
{:error, {:network, reason}}
end
end
defp safe_decode(b), do: with {:ok, v} <- Jason.decode(b), do: v
end
关键思想:
- 4xx 通常是请求错误,重试不会变好,记日志返回。
- 5xx 是上游问题,可能重试,但要做退避(exponential backoff)。
- 网络错误:TLS、TCP、DNS 都可能瞬时失败,有限重试。
六、并发:async + Task.await_many
def fetch_many(ids) do
Task.async_stream(ids, fn id ->
MyApp.External.get_user(id)
end, max_concurrency: 10, timeout: 10_000, on_timeout: :kill_task)
|> Enum.to_list()
end
size。否则新请求会卡在 pool_timeout。七、SSE:流式响应
LLM、事件推送、监控面板常用 Server-Sent Events。Finch 把响应体当作可枚举流:
def stream_events(url, on_event) do
Finch.build(:get, url, headers: [{"accept", "text/event-stream"}])
|> Finch.stream(MyApp.Finch, receive_timeout: :infinity, body_chunks: :infinite)
|> Stream.flat_map(fn
{:status, _status} -> []
{:headers, _} -> []
{:data, data} -> [data]
{:done, _} -> []
end)
|> Stream.each(&on_event.(&1))
|> Stream.run()
end
关键点:
receive_timeout: :infinity:长连接禁用默认超时。body_chunks: :infinite:不限制 chunk 累计。Finch.stream/4返回一个 enumerable,每项是{:status, _}/{:headers, _}/{:data, _}/{:done, _}。- 取消时调用
Task.shutdown(task, :brutal_kill)即可中止整个流。
八、可观测性
Finch 默认带 telemetry 事件:
[:finch, :request, :start]/[:finch, :request, :stop]/[:finch, :request, :error]- 元数据包含
result(:ok | :error)、status、duration、pool、host等
把这些事件转发到 Prometheus / OpenTelemetry,就能看到 P50 / P95 / 错误率。
九、生产清单
- 每个上游域名一个 pool;按 SLA 决定 pool size。
- 业务超时 < 用户感知超时 < 进程超时。
- 4xx 不要重试;5xx 与网络错误用指数退避重试,最多 3 次。
- SSE 一定要
receive_timeout: :infinity。 - 异步任务用
on_timeout: :kill_task,防止泄漏。 - 记录
pool_timeout错误:如果频繁出现,说明连接池不够。
十、测验
Finch 推荐的底层适配器是?
pool 与 host 的推荐关系?
request_timeout 控制的是?
pool_timeout 应该?
4xx 响应通常意味着?
5xx 响应的推荐策略?
Finch.stream/4 接收超时应该?
Task.async_stream 的 on_timeout: :kill_task 作用?
max_concurrency 与 pool size 的关系?
Finch 默认发出的 telemetry 事件前缀是?
**下一步:**Finch + Plug + Ecto 三件套齐了,下一阶段可以进入真实项目:写一个最小可用的 API 服务,跑 mix release,部署到一台机器。