通用 HTTP 客户端(im.ttalk/clj-http):JDK java.net.http 之上的 Ring 式请求 / 响应 map、同步与
CompletableFuture 异步、按行真流式、大小写不敏感的头工具(designs/http-module.md)。零依赖:只用 JDK;
JSON、日志、错误模型都是调用方的事。与 agent 无关,可单独使用;clj-agent 的 provider / mcp / tools 都建在它上面。
(与社区的 dakrone/clj-http 无关:坐标是 im.ttalk/clj-http。)
(require '[im.ttalk.http :as http])
(http/request {:method :post :url "https://api.example.com/v1/x"
:headers {"Authorization" "Bearer …"} :body "{…}" :timeout-ms 30000})
;; => {:status 200 :headers {"content-type" ["application/json"]} :body "…" :url "…"}
(http/request-async req) ;; => CompletableFuture<同形状>
(http/stream-lines (assoc req :on-line f)) ;; 按行真流式 => {:future :cancel}
(http/header resp "Retry-After") ;; 取头:大小写不敏感,单值 / 多值都收
java.net.http.HttpClient 本身(client 造、default-client 取共享的那个);
各处的 :client 选项收的都是它。header / header-values,
不要直接 get——这正是 provider-reliability.md §1.3 那个 Retry-After 从未生效的 bug 的根因。IOException / HttpTimeoutException / IllegalArgumentException …),
异步版异常完成。把它们映射成什么(canonical error、{:status 0 :error …}、工具错误码)由调用方定。头工具移植自 Keelson jvm/modules/clj-net 的 im.ttalk.net.http(5c745869),改动:头名也收关键字;
值也收 java.util.List。
通用 HTTP 客户端(`im.ttalk/clj-http`):JDK `java.net.http` 之上的 Ring 式请求 / 响应 map、同步与
`CompletableFuture` 异步、按行真流式、大小写不敏感的头工具(designs/http-module.md)。零依赖:只用 JDK;
JSON、日志、错误模型都是调用方的事。与 agent 无关,可单独使用;clj-agent 的 provider / mcp / tools 都建在它上面。
(与社区的 dakrone/clj-http 无关:坐标是 im.ttalk/clj-http。)
```clojure
(require '[im.ttalk.http :as http])
(http/request {:method :post :url "https://api.example.com/v1/x"
:headers {"Authorization" "Bearer …"} :body "{…}" :timeout-ms 30000})
;; => {:status 200 :headers {"content-type" ["application/json"]} :body "…" :url "…"}
(http/request-async req) ;; => CompletableFuture<同形状>
(http/stream-lines (assoc req :on-line f)) ;; 按行真流式 => {:future :cancel}
(http/header resp "Retry-After") ;; 取头:大小写不敏感,单值 / 多值都收
```
- **client 就是 `java.net.http.HttpClient` 本身**(`client` 造、`default-client` 取共享的那个);
各处的 `:client` 选项收的都是它。
- **响应头**:小写头名 ⇒ 串向量(去掉 HTTP/2 伪头)。取值一律走 `header` / `header-values`,
不要直接 `get`——这正是 provider-reliability.md §1.3 那个 Retry-After 从未生效的 bug 的根因。
- **失败**:同步版原样抛(`IOException` / `HttpTimeoutException` / `IllegalArgumentException` …),
异步版异常完成。把它们映射成什么(canonical error、`{:status 0 :error …}`、工具错误码)由调用方定。
头工具移植自 Keelson `jvm/modules/clj-net` 的 `im.ttalk.net.http`(5c745869),改动:头名也收关键字;
值也收 `java.util.List`。HTTP 日期与 Retry-After 解析。移植自 Keelson jvm/modules/clj-net/src/im/ttalk/net/retry.clj(5c745869)的
parse-http-date / retry-after-ms,改动:不判上限(clj-net 是「≥ 上限且 ≥ 退避就忽略」;clj-agent 的口径是
「取 min(值, :max-delay)」,由调用方做,见 designs/http-module.md §1.3);时钟改为毫秒数参数。
HTTP 日期与 Retry-After 解析。移植自 Keelson `jvm/modules/clj-net/src/im/ttalk/net/retry.clj`(5c745869)的 `parse-http-date` / `retry-after-ms`,改动:**不判上限**(clj-net 是「≥ 上限且 ≥ 退避就忽略」;clj-agent 的口径是 「取 min(值, :max-delay)」,由调用方做,见 designs/http-module.md §1.3);时钟改为毫秒数参数。
网络错的分类(与客户端无关):按 java.net / javax.net.ssl 的异常类型与 JDK 固定的系统文案认。
任何客户端底下抛的都是这批异常,拿来分类都行。
移植自 Keelson jvm/modules/clj-net/src/im/ttalk/net/failure.clj(5c745869)的 classify / socket-kind /
timeout?,判定逻辑未改(timeout? 修了一处原版 bug,见函数体注释)。只给分类,不给可重试判定:clj-net 的 transient? 不把超时算可重试,而 clj-agent
的 canonical error 一直把 HttpTimeoutException 算 :transient——可重试与否仍由调用方的既有口径决定
(designs/http-module.md §1.3)。
网络错的分类(与客户端无关):按 `java.net` / `javax.net.ssl` 的异常类型与 JDK 固定的系统文案认。 任何客户端底下抛的都是这批异常,拿来分类都行。 移植自 Keelson `jvm/modules/clj-net/src/im/ttalk/net/failure.clj`(5c745869)的 `classify` / `socket-kind` / `timeout?`,判定逻辑未改(`timeout?` 修了一处原版 bug,见函数体注释)。**只给分类,不给可重试判定**:clj-net 的 `transient?` 不把超时算可重试,而 clj-agent 的 canonical error 一直把 `HttpTimeoutException` 算 `:transient`——可重试与否仍由调用方的既有口径决定 (designs/http-module.md §1.3)。
代理选择(与客户端无关)。移植自 Keelson jvm/modules/clj-net/src/im/ttalk/net/proxy.clj(5c745869),逻辑未改。
一个「代理」是下面三样之一,select 都收:
(from-environment) / (from-env m):环境变量那一档(同 C# HttpClient.DefaultProxy、curl):
http_proxy / HTTP_PROXY(CGI 环境下不读大写的,防 httpoxy)、https_proxy / HTTPS_PROXY、all_proxy / ALL_PROXY 兜底;
no_proxy / NO_PROXY:逗号分隔,* = 全部绕过,.a.com / a.com 都匹配 a.com 本身与子域。一个都没设 ⇒ nil。(web-proxy "http://h:p" {:bypass [正则…] :bypass-on-local? b}):固定 HTTP 代理 + 绕过表:绕过表的正则
对主机名、scheme://host、scheme://host:port 任一整串匹配(大小写不敏感)即绕过;bypass-on-local? ⇒ 不带点的主机名与回环也绕过。->proxy-selector 把前两种变成 ProxySelector。平常不直接用本 ns:(im.ttalk.http/client {:proxy :env}) 即可。代理选择(与客户端无关)。移植自 Keelson `jvm/modules/clj-net/src/im/ttalk/net/proxy.clj`(5c745869),逻辑未改。
一个「代理」是下面三样之一,`select` 都收:
- `(from-environment)` / `(from-env m)`:环境变量那一档(同 C# HttpClient.DefaultProxy、curl):
http_proxy / HTTP_PROXY(CGI 环境下不读大写的,防 httpoxy)、https_proxy / HTTPS_PROXY、all_proxy / ALL_PROXY 兜底;
no_proxy / NO_PROXY:逗号分隔,* = 全部绕过,.a.com / a.com 都匹配 a.com 本身与子域。一个都没设 ⇒ nil。
- `(web-proxy "http://h:p" {:bypass [正则…] :bypass-on-local? b})`:固定 HTTP 代理 + 绕过表:绕过表的正则
对主机名、scheme://host、scheme://host:port 任一整串匹配(大小写不敏感)即绕过;bypass-on-local? ⇒ 不带点的主机名与回环也绕过。
- 任何 java.net.ProxySelector。
`->proxy-selector` 把前两种变成 ProxySelector。平常不直接用本 ns:`(im.ttalk.http/client {:proxy :env})` 即可。SSE 分帧核(WHATWG HTML 9.2.6,与客户端无关):字节进、事件出;外加服务端用的拼帧(event-frame /
comment-frame / retry-frame,帧是 UTF-8 字节)。移植自 Keelson jvm/modules/clj-net/src/im/ttalk/net/sse.clj
(5c745869),逻辑未改;不搬阻塞读者与断线续读循环(designs/http-module.md §1.3:无调用方)。拼帧起初也没搬,
clj-agent-server 的 SSE 成了调用方后补上(designs/server-module.md §2)。
:event-type(缺省 "message"):data(多行 data 以 \n 连接):last-event-id(到这个事件为止最近一次
id: 的值,跨事件保留):reconnection-ms(这个事件自己带的 retry:,没有 nil)。(parse-all "event: chat\ndata: a\ndata: b\n\n") ;; => [{:event-type "chat" :data "a\nb" …}]
(let [p (parser)] (feed! p bytes) (take-event! p)) ;; 增量
SSE 分帧核(WHATWG HTML 9.2.6,与客户端无关):字节进、事件出;外加服务端用的拼帧(`event-frame` /
`comment-frame` / `retry-frame`,帧是 UTF-8 字节)。移植自 Keelson `jvm/modules/clj-net/src/im/ttalk/net/sse.clj`
(5c745869),逻辑未改;**不搬**阻塞读者与断线续读循环(designs/http-module.md §1.3:无调用方)。拼帧起初也没搬,
clj-agent-server 的 SSE 成了调用方后补上(designs/server-module.md §2)。
- 事件 = map:`:event-type`(缺省 "message")`:data`(多行 data 以 \n 连接)`:last-event-id`(到这个事件为止最近一次
id: 的值,跨事件保留)`:reconnection-ms`(这个事件自己带的 retry:,没有 nil)。
- 行结束收 \n、\r\n、\r(CRLF 被拆在两次 feed 之间也只算一次);按 UTF-8 解码、去 BOM,charset= 被规范忽略;
流在事件中途结束时那半个事件不派发。
```clojure
(parse-all "event: chat\ndata: a\ndata: b\n\n") ;; => [{:event-type "chat" :data "a\nb" …}]
(let [p (parser)] (feed! p bytes) (take-event! p)) ;; 增量
```cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |