Roadmap 定位
這是 System Design Roadmap 階段 0 的 HTTP client 深入教材。
開啟互動 HTML 版
本教材沿用「生活直覺 → 工程術語橋接 → High-level 定義 → 底層機制 → 實作決策」的閱讀順序。目標不是背四個 timeout 名稱,而是能在 FastAPI 服務呼叫下游時,精確回答:時間花在哪裡、哪個資源已飽和、取消發生在哪一端、是否可以 retry,以及如何用測試證明設定真的有效。
0. 先用圖建立全貌:兩條時間軸、四個局部關卡
在讀任何設定值之前,先把一次 request 看成「穿過多個關卡」,並把局部 timeout 與整體 deadline 分開。後面的所有 exception、pool sizing 與 retry 判斷,都能放回這兩張圖定位。
0.1 一次 HTTP request 經過哪些關卡?
- Pool timeout 發生在 B。
- Connect timeout 發生在 C。
- Write timeout 發生在 D。
- Read timeout 發生在 F。
- E 的 server execution 並不完全受 client timeout 控制。
0.2 局部 inactivity timeout 與整體 deadline
局部 timeout 問「這一段多久沒有進展」;overall deadline 問「整個 operation 還剩多少生命」。不要用四個局部 timeout 的數值相加,假裝那就是可靠的 end-to-end budget。
1. 先建立完整心智模型:一次 HTTP 呼叫不是一個等待
1.1 生活直覺:餐廳外送不是只有「等餐」
一次外送訂單可能依序經過:等待外送員名額、前往餐廳、交付訂單、餐廳分批出餐、把餐送回。若只說「最多等五秒」,你無法知道五秒限制的是哪一段,也無法判斷超時後餐廳是否仍在做餐。
HTTP client 的一次 request 也有多個階段:
- 從 connection pool 取得可用連線,或取得建立新連線的容量。
- DNS、TCP connect、TLS handshake,必要時還有 proxy tunnel。
- 傳送 request headers 與 body。
- 等待 response headers 與 body chunks。
- 完整消費或關閉 response,決定連線能否回到 pool 重用。
1.2 工程術語橋接
| 外送比喻 | HTTPX/網路術語 | 主要限制 |
|---|---|---|
| 等外送員名額 | acquire connection from pool | pool timeout、max connections |
| 前往餐廳 | DNS/TCP/TLS | connect timeout |
| 交付訂單 | write request bytes | write timeout |
| 等待每一批餐 | receive response chunks | read timeout |
| 整趟訂單時限 | end-to-end deadline | asyncio.timeout()/上游 deadline |
1.3 High-level 定義
HTTPX 的 connect、read、write、pool timeout 是不同 I/O 階段的等待上限。它們主要防止某個網路操作或資源等待長時間沒有進展;它們不是「整個業務操作必須在 N 秒內完成」的完整 deadline。
1.4 一條 request 時間線
caller starts
│
├─ pool wait ─────────────── PoolTimeout
├─ DNS/TCP/TLS ───────────── ConnectTimeout
├─ request body chunks ───── WriteTimeout
├─ response chunks ───────── ReadTimeout
└─ parse / application work
caller returns同一次 request 可能只經過其中部分階段。若 keep-alive connection 可重用,通常不會重新做 TCP/TLS;若 response body 很小,write/read 階段也可能極短。
2. Timeout 到底保證什麼?先拆掉三個危險誤解
2.1 Timeout 不是成功時間的承諾
read=5.0 不代表 request 一定在五秒內完成。HTTPX 官方定義是等待 response data chunk 的上限;若 server 每四秒送一小段資料,總下載可以遠超過五秒而不觸發 read timeout。
2.2 Timeout 不是遠端取消協議
client 停止等待,不代表 server 的 handler、database transaction 或第三方 side effect 被回滾。client 可能關閉 socket,但遠端程式可能已經:
- 寫入 database 並 commit。
- 發送 email、扣款或發布 message。
- 還在 CPU 計算或等待另一個 downstream。
因此 timeout 後直接 retry 非冪等 POST,可能把一次不確定結果放大成重複 side effect。
2.3 Timeout 不是 failure detector
Read timeout 只能告訴你「在設定時間內沒有收到下一段資料」,不能證明:
- server crash。
- 網路完全中斷。
- server 尚未開始處理。
- server 沒有 commit。
精確術語是 local observation:caller 在自己的等待邊界內沒有觀察到預期進展。
2.4 Timeout、deadline、cancellation 的差異
| 概念 | 問題 | 範圍 |
|---|---|---|
| Timeout | 某個操作最多等多久? | pool/connect/read/write 等局部階段 |
| Deadline | 整個工作最晚何時完成? | 多次 request、retry、parse、DB 等完整鏈路 |
| Cancellation | caller 是否要求停止本地 task? | 本地 coroutine/task;不自動回滾遠端 |
| Server timeout | server handler/statement 最多跑多久? | 遠端自己的執行邊界 |
3. 四種 HTTPX timeout 的正式邊界
3.1 Connect timeout
Connect timeout 限制建立到目標 host 的連線所能等待的時間。實際路徑可能包含 DNS lookup、TCP handshake、TLS handshake、proxy connect;具體階段由 transport 與網路環境決定。
import httpx
timeout = httpx.Timeout(
connect=2.0,
read=5.0,
write=5.0,
pool=0.5,
)
async with httpx.AsyncClient(timeout=timeout) as client:
response = await client.get("https://api.example.com/health")Connect timeout 常見原因:錯誤 IP、firewall 丟包、DNS 過慢、TLS endpoint 不回應、proxy 無法建立 tunnel。它和 ConnectError 不同:timeout 是超過等待上限;error 可能是立即 connection refused、DNS failure 或 TLS error。
3.2 Read timeout
Read timeout 限制等待下一個 response data chunk 的時間。它不是整份 response 的總下載時間,也不只是「等待第一個 byte」。
timeout = httpx.Timeout(5.0, read=2.0)
async with httpx.AsyncClient(timeout=timeout) as client:
async with client.stream("GET", "https://api.example.com/export") as response:
response.raise_for_status()
async for chunk in response.aiter_bytes():
consume(chunk)若 upstream 每 1.5 秒持續送資料,read=2.0 可讓長串流繼續;若某次兩個 chunk 間隔超過兩秒,會觸發 ReadTimeout。
3.3 Write timeout
Write timeout 限制等待 request body chunk 寫入網路的時間。小型 JSON request 通常很快完成;大型 upload、慢速對端、壅塞 socket buffer 或 streaming request 更容易遇到。
async def body_stream():
for part in generate_parts():
yield part
async with httpx.AsyncClient(timeout=timeout) as client:
response = await client.post(
"https://api.example.com/import",
content=body_stream(),
)Write timeout 不代表 server 沒收到任何資料;server 可能已收到一部分 body,甚至已根據 headers 建立工作。
3.4 Pool timeout
Pool timeout 限制等待 connection pool 可用容量的時間。這是 client 端排隊,不是 upstream network latency。
limits = httpx.Limits(
max_connections=20,
max_keepalive_connections=10,
keepalive_expiry=15.0,
)
timeout = httpx.Timeout(5.0, pool=0.25)
client = httpx.AsyncClient(timeout=timeout, limits=limits)當 20 個 connection 都在使用中,第 21 個 request 會等待;超過 0.25 秒仍拿不到 capacity 才拋出 PoolTimeout。它通常是 backpressure 訊號:你的呼叫並發度、upstream latency 或 pool size 彼此不匹配。
3.5 Exception hierarchy
HTTPError
└── RequestError
└── TransportError
└── TimeoutException
├── ConnectTimeout
├── ReadTimeout
├── WriteTimeout
└── PoolTimeout不要用同一個 catch 把 timeout、HTTP 5xx 與程式 bug 全部轉成「503」。response.raise_for_status() 產生的是 HTTPStatusError;它代表有收到 HTTP response,和 transport timeout 的觀測語意不同。
4. HTTPX 的預設值與配置方式
HTTPX 官方文件目前說明:預設會在五秒 network inactivity 後拋出 timeout exception。這不是五秒整體 deadline。
4.1 單一 float
client = httpx.AsyncClient(timeout=10.0)這適合簡單工具,但正式服務通常應明確寫出各階段,讓 code review 能看出設計意圖。
4.2 細分四種 timeout
timeout = httpx.Timeout(
timeout=5.0,
connect=1.0,
pool=0.2,
)第一個 timeout=5.0 是未個別指定欄位的預設;connect 與 pool 再覆寫。也可以四個欄位全部明確指定。
4.3 Per-request override
async with httpx.AsyncClient(timeout=default_timeout) as client:
normal = await client.get(normal_url)
report = await client.get(report_url, timeout=30.0)override 應是經過分類的 endpoint policy,不應讓每個 call site 任意猜數字。
4.4 timeout=None
client = httpx.AsyncClient(timeout=None)停用 timeout 會讓 stalled I/O 長期占用 connection、task 與上游配額。除非外層有更嚴格且可證明的 deadline,正式服務通常不應全域停用。
5. Overall deadline:如何限制整條工作時間
5.1 為什麼四種 timeout 仍不夠
假設一次業務操作會:
- pool wait 0.2 秒。
- connect 0.8 秒。
- read 4 秒後收到一個 chunk。
- 再 read 4 秒收到下一個 chunk。
- retry 一次。
每一段都可能未超過局部 timeout,但總時間遠超過 caller 的 latency budget。
5.2 用 asyncio.timeout() 包住完整操作
import asyncio
import httpx
async def fetch_with_deadline(client: httpx.AsyncClient, url: str) -> dict:
try:
async with asyncio.timeout(3.0):
response = await client.get(url)
response.raise_for_status()
return response.json()
except TimeoutError as exc:
raise RuntimeError("overall request deadline exceeded") from exc外層 deadline 包含 pool、connect、write、read、response parsing,以及你放在 scope 內的 retry delay。內層 HTTPX timeout 仍有價值,因為它能提供更精確的 failure classification。
5.3 傳遞剩餘 budget
在多層 service chain 中,A 不應把自己的 2 秒 deadline 原封不動地給 B。A 還需要 response mapping、logging、DB commit 與回傳時間;應保留安全 margin,將「剩餘 budget」傳給下游。
from time import monotonic
def remaining(deadline: float, reserve: float = 0.05) -> float:
value = deadline - monotonic() - reserve
if value <= 0:
raise TimeoutError("no downstream budget remains")
return value跨 service 傳遞 deadline 時要明確約定:使用 duration 還是 timestamp、哪個 clock domain、proxy 是否會扣除耗時,以及不可信 client header 是否允許直接控制內部資源。
5.4 Deadline 與 retry budget
如果整體只剩 120ms,就不該開始一個 connect timeout 2 秒的 retry。每次 attempt 前重新計算剩餘時間,並限制最大 attempts。
6. Connection pool:重用、容量與排隊
這是一個 queueing system。Pool 不是「越大越好」的 cache;它同時是 upstream concurrency 的閘門。
6.1 為什麼要重用 Client
HTTPX 官方建議正式程式使用 Client/AsyncClient。同一 client 可以重用 TCP/TLS connection,降低 handshake latency、CPU 與 network round trips。
# 反例:hot path 每次建立新 pool
async def bad_fetch(url: str):
async with httpx.AsyncClient() as client:
return await client.get(url)這段語法會正確 close,但在高頻呼叫中無法跨 request 重用 pool。Web service 應在 application lifespan 建立一次,shutdown 時關閉。
6.2 三個 Limits 欄位
| 欄位 | 官方預設 | 意義 |
|---|---|---|
max_connections | 100 | pool 允許同時存在/使用的 connection 上限 |
max_keepalive_connections | 20 | 可保留供重用的 idle keep-alive connections 上限 |
keepalive_expiry | 5 秒 | idle keep-alive connection 保留多久 |
max_keepalive_connections 不應高於 max_connections。過低會頻繁重建連線;過高會保留更多 socket、TLS state 與 upstream capacity。
6.3 Pool 是 bulkhead,不只是效能設定
Pool limit 可以限制單一 process 對 upstream 的同時壓力。沒有上限的 concurrency 可能在 upstream 變慢時形成正回饋:
upstream latency ↑
→ in-flight requests ↑
→ sockets / memory / tasks ↑
→ queueing ↑
→ timeout / retry ↑
→ upstream load 再 ↑有界 pool 加上短 pool timeout 會較早拒絕,保護本服務和 upstream,但它不是完整 bulkhead:你仍可能需要 per-host semaphore、queue、rate limit 或 circuit breaker。
6.4 粗略容量推理
Little's Law 提供直覺:in_flight ≈ throughput × latency。若單一 worker 對 upstream 峰值 50 requests/s,p95 latency 0.2 秒,平均 in-flight 約 10;還要考慮 burst、p99、streaming 與多 host。
不要直接用平均值配置。實務步驟:
- 量測每個 upstream 的 throughput 與 latency distribution。
- 設定你願意允許的最大 in-flight。
- 乘上 process/worker/pod 數量,確認全域連線數。
- 對照 upstream、NAT、load balancer、file descriptor 與 database 的限制。
- 壓測 pool wait、拒絕率與 recovery,而不只是成功 latency。
6.5 HTTP/2 的額外維度
HTTP/2 可在一條 connection 上 multiplex 多個 streams,因此「connection 數」不等於「同時 request 數」。但 stream concurrency 仍受 server 設定、flow control 與 transport 實作限制。不要假設開啟 HTTP/2 就不需要 backpressure。
7. FastAPI 中正確管理 AsyncClient 生命週期
7.1 Lifespan 建立與關閉
from contextlib import asynccontextmanager
import httpx
from fastapi import FastAPI, Request
@asynccontextmanager
async def lifespan(app: FastAPI):
timeout = httpx.Timeout(connect=1.0, read=2.0, write=2.0, pool=0.2)
limits = httpx.Limits(
max_connections=50,
max_keepalive_connections=20,
keepalive_expiry=15.0,
)
app.state.http = httpx.AsyncClient(timeout=timeout, limits=limits)
try:
yield
finally:
await app.state.http.aclose()
app = FastAPI(lifespan=lifespan)
@app.get("/aggregate")
async def aggregate(request: Request):
response = await request.app.state.http.get("https://api.example.com/data")
response.raise_for_status()
return response.json()7.2 每個 worker 都有自己的 pool
四個 Uvicorn worker 代表四個 process,也就是四個 AsyncClient 與四個 pool。若每個 max_connections=100,理論上的 process-level 合計可能達 400,再乘 pod 數量。
7.3 依 upstream 隔離 client
付款、搜尋、推薦服務的 latency 和重要性不同。使用不同 client/semaphore 可以避免推薦服務塞滿共用 pool,連付款也拿不到 connection。
7.4 不要洩漏 streaming response
使用 manual streaming mode 時,必須確保 response 被關閉,否則 connection 無法回到 pool。
async def stream_upstream(client: httpx.AsyncClient, upstream_url: str):
request = client.build_request("GET", upstream_url)
response = await client.send(request, stream=True)
try:
async for chunk in response.aiter_bytes():
yield chunk
finally:
await response.aclose()8. Cancellation、disconnect 與遠端 side effect
8.1 本地 task 被取消時
當 asyncio.timeout() 到期或上游 client disconnect 導致 handler cancellation,本地 await 通常會收到 cancellation。HTTP transport 可能關閉 connection,但遠端是否停止取決於 server、protocol 與它正在做的工作。
8.2 Commit 前後的四種情境
| 情境 | client 觀察 | server 狀態 | retry 風險 |
|---|---|---|---|
| connect 前失敗 | 未建立連線 | 多半未執行 | 較低,但仍要依 operation |
| request 傳一半 | WriteTimeout | 可能收到部分內容 | protocol dependent |
| server commit 前慢 | ReadTimeout | 可能之後 rollback 或 commit | 不確定 |
| server commit 後 response 丟失 | ReadTimeout | side effect 已完成 | 直接 retry 可重複執行 |
8.3 解法不是把 timeout 調大
對重要寫入,應使用:
- idempotency key 與 request fingerprint。
- database unique constraint。
- 可查詢 operation status。
- outbox/inbox 或 workflow state。
- 明確區分「接受工作」與「完成工作」的 asynchronous API。
9. Retry:只有在語意允許時才是韌性
9.1 可重試性來自 operation semantics
通常較安全的候選:GET、HEAD、具 idempotency key 的寫入、明確文件化為 idempotent 的 API。危險候選:沒有 deduplication 的付款、寄信、扣庫存、建立訂單。
9.2 依 exception 分類
PoolTimeout:本地已飽和;立即 retry 常讓 queue 更糟。ConnectTimeout:可能是短暫網路問題,也可能是錯誤設定。ReadTimeout:遠端結果未知,寫入尤其危險。- HTTP 429/503:可依
Retry-After與 API contract 處理。 - HTTP 4xx:多數是永久性 client error,不應盲目 retry。
9.3 Exponential backoff with jitter
import asyncio
import random
async def backoff(attempt: int, cap: float = 1.0) -> None:
upper = min(cap, 0.05 * (2**attempt))
await asyncio.sleep(random.uniform(0.0, upper))Jitter 避免大量 clients 同時 timeout 後同步重試。仍須配合最大 attempts 與 overall deadline。
9.4 一個 deadline-aware retry skeleton
import asyncio
import httpx
from time import monotonic
async def get_json(client: httpx.AsyncClient, url: str, budget: float) -> dict:
deadline = monotonic() + budget
last_error: Exception | None = None
for attempt in range(3):
time_left = deadline - monotonic()
if time_left <= 0:
break
try:
async with asyncio.timeout(time_left):
response = await client.get(url)
response.raise_for_status()
return response.json()
except (httpx.ConnectTimeout, httpx.ReadTimeout) as exc:
last_error = exc
if attempt == 2:
break
await backoff(attempt, cap=max(0.0, deadline - monotonic()))
raise TimeoutError("retry budget exhausted") from last_error這只是 GET 範例。正式實作還需處理 cancellation、metrics、429/503 policy、response closing 與可測試的 random/clock adapter。
10. Observability:不要只記「request failed」
10.1 最低限度欄位
- upstream service/host,不要把高 cardinality 完整 URL 當 metric label。
- method、route template、attempt。
- exception class:Connect/Read/Write/Pool timeout。
- overall deadline exceeded 與 local cancellation 分開。
- elapsed time、remaining budget。
- pool configuration、worker/pod identity。
- response status class。
10.2 建議 metrics
http_client_requests_total{upstream,method,outcome}
http_client_duration_seconds{upstream,method}
http_client_timeouts_total{upstream,phase}
http_client_pool_wait_seconds{upstream}
http_client_in_flight{upstream}
http_client_retries_total{upstream,reason}HTTPX 高階 API 不一定直接暴露完整 pool wait timing;可使用 event hooks、transport trace extension、外層 semaphore timing 或 observability integration,但要驗證量測點真正代表什麼。
10.3 Trace 的問題
一條 trace 應回答:caller 的 2 秒去了哪裡?是在自己的 queue、pool wait、connect、server processing、body streaming、retry sleep,還是 response parsing?如果 span 只包整個 client.get(),只能看到症狀,無法定位資源瓶頸。
11. 常見反模式與診斷
11.1 每個 request 建一個 AsyncClient
症狀: handshake 多、latency 抖動、socket churn。 根因: pool 生命週期太短。 修正: application/worker scope client。
11.2 所有 timeout 都設成同一個大數字
症狀: 本地 pool 排隊 30 秒後才失敗。 根因: 沒區分資源等待與 upstream I/O。 修正: pool timeout 通常應短;connect/read/write 依 endpoint SLO 設計,再加 overall deadline。
11.3 timeout=None 再依賴 load balancer
症狀: task、connection 長期懸掛。 根因: 把另一層設備的 timeout 當 application resource policy。 修正: 每層都要有明確且由外向內遞減的 budget。
11.4 PoolTimeout 就加大 pool
症狀: PoolTimeout 暫時下降,但 upstream 更慢或 file descriptors 暴增。 根因: 沒先找 latency、concurrency、泄漏或 retry storm。 修正: 量測 in-flight、pool wait、response closing、upstream capacity 後再調整。
11.5 ReadTimeout 後無條件 retry POST
症狀: duplicate order/charge。 根因: 把 response 未收到誤認為 operation 未執行。 修正: idempotency key、status query、deduplication constraint。
12. Production 設定決策表
| 問題 | 先問什麼 | 典型手段 |
|---|---|---|
| Connect 慢 | DNS、TCP、TLS 還是 proxy? | 短 connect timeout、network telemetry |
| Read 慢 | 首 byte 還是 chunk gap?server 是否 commit? | read timeout、server timeout、idempotency |
| Pool 飽和 | concurrency、latency、leak、retry 哪個上升? | bounded pool、semaphore、load shedding |
| 總時間超標 | 是否跨多個 attempts/operations? | outer deadline、remaining budget |
| 大量 POST retry | 是否具 dedup contract? | idempotency key、operation status |
| Worker 數增加 | 每 worker pool 是否乘上去? | 全域 capacity calculation |
| Streaming | response 是否一定 close? | context manager/finally aclose() |
設定值不是教材可以替你的 production 決定的常數。它必須來自 latency SLO、upstream contract、failure cost、worker topology 與壓測。
13. 最小實驗:親手製造四種 timeout
13.1 實驗服務
建立 FastAPI upstream,提供:
/delay-headers?seconds=:延遲 response。/stream?gap=:每隔固定時間送 chunk。/consume-slowly:慢速讀 request body。/commit-then-delay:先寫 PostgreSQL 並 commit,再延遲 response。
13.2 PoolTimeout 實驗
import asyncio
import httpx
async def main():
limits = httpx.Limits(max_connections=1, max_keepalive_connections=1)
timeout = httpx.Timeout(connect=1.0, read=2.0, write=2.0, pool=0.05)
async with httpx.AsyncClient(limits=limits, timeout=timeout) as client:
async def call(number: int):
try:
await client.get("http://127.0.0.1:8001/delay-headers?seconds=0.5")
return number, "ok"
except httpx.PoolTimeout:
return number, "pool-timeout"
print(await asyncio.gather(*(call(i) for i in range(5))))
asyncio.run(main())13.3 必須提交的證據
- 表格:scenario、HTTPX exception、總耗時、server 是否收到 request、server 是否 commit。
- Pool size 1/5/20 下的 pool wait distribution。
- read timeout 和 overall deadline 對慢速 stream 的不同結果。
- commit 後 response timeout,證明 client timeout 不會回滾 server transaction。
- 有/無 idempotency key retry 的資料庫結果。
13.4 pytest 驗收方向
import asyncio
import httpx
import pytest
@pytest.mark.anyio
async def test_pool_timeout_is_classified(upstream_url: str):
limits = httpx.Limits(max_connections=1)
timeout = httpx.Timeout(1.0, pool=0.01)
async with httpx.AsyncClient(limits=limits, timeout=timeout) as client:
first = client.get(f"{upstream_url}/delay-headers?seconds=0.3")
second = client.get(f"{upstream_url}/delay-headers?seconds=0.3")
results = await asyncio.gather(first, second, return_exceptions=True)
assert any(isinstance(item, httpx.PoolTimeout) for item in results)測試必須在可控的本機 upstream 執行,避免依賴公共網站 latency。
14. 自我檢查
Q1. `read=5` 是否代表整個 response 五秒內完成?
不是。它限制等待 response data chunk 的時間;持續有 chunk 的長串流可超過五秒。
Q2. PoolTimeout 是否表示 upstream TCP connect 太慢?
不是。它發生在 client 等待 pool capacity;request 可能尚未開始 connect。
Q3. ReadTimeout 後可以斷定 server 沒有 commit 嗎?
不可以。server 可能已完成 side effect,只是 response 遲到或遺失。
Q4. 為什麼不能在 hot loop 中反覆建立 AsyncClient?
每個 client 有自己的 connection pool;生命週期過短會失去 TCP/TLS 重用並製造 socket churn。
Q5. `max_connections=100` 在四 workers、五 pods 時代表什麼?
這通常是每個 client/process 的限制,理論合計可能達 100 × 4 × 5;仍要考慮 per-origin 與 HTTP/2 行為。
Q6. 為什麼 PoolTimeout 後立即 retry 可能更糟?
它通常表示本地 queue 已飽和;retry 增加競爭與排隊,可能形成 retry storm。
Q7. Timeout 與 deadline 最核心差異?
timeout 通常限制一個局部等待;deadline 限制完整操作在某個最晚時間前完成。
Q8. 哪些 timeout 可以安全 retry?
不能只看 exception class。必須同時看 method/operation semantics、idempotency、剩餘 budget 與 upstream contract。
Q9. Streaming response 最容易造成哪種 pool 問題?
未完整消費又未 close response,connection 無法歸還 pool,最終造成 pool starvation。
Q10. HTTP/2 是否讓 connection limit 無關緊要?
否。它能 multiplex streams,但仍有 server stream limit、flow control、socket 與應用層 backpressure。
15. 官方來源
- HTTPX Timeouts
- HTTPX Resource Limits
- HTTPX Clients
- HTTPX Async Support
- HTTPX Exceptions
- HTTPX Extensions
- Python asyncio timeouts
閱讀官方文件時,持續問三個問題:這個限制量測哪一段?它是 inactivity timeout 還是 overall deadline?超時後遠端 side effect 的狀態是否可知?