fix(client): 顶层 JSON 数组/标量响应不再降级为转义字符串

This commit is contained in:
Taoyouce 2026-07-07 14:36:56 +00:00
parent 7128960529
commit 515612ddc5
3 changed files with 43 additions and 0 deletions

View File

@ -10,6 +10,9 @@
两种呈现Agent 无法统一消费。
3. 生产实测发现:请求不存在的 API 路径时,网关回落到 Web 前端返回
200 + HTML 首页CLI 误判为成功并把整页 HTML 当数据输出 `ok:true`
4. 生产实测发现:返回顶层 JSON 数组的遗留端点(如
`/:owner/:repo/branches`)被降级为转义字符串输出,`--jq`/表格渲染
等下游能力全部失效。
## 变更内容
@ -23,15 +26,20 @@
- 非 JSON 响应若为 HTML 页Content-Type 或 doctype 探测),返回
`non_api_response` 错误信封(`ok:false` + 建议检查路径),
不再把 HTML 首页当成功数据。
- 顶层 JSON 数组/标量响应按解码后的结构原样进入数据信封,不再降级为
转义字符串。
## 生产验证
- `api GET /nonexistent-endpoint-xyz``ok:false, code:non_api_response`
(修复前:`ok:true` + 整页 HTML
- `api GET /gitlink/gitlink-cli/branches``data` 为结构化数组
(修复前:整个数组被输出为一条转义字符串)。
- `repo +info` 正常端点行为不变。
## 测试
`internal/client/retry_test.go` 6 个用例503 重试后成功、超上限停止、
POST 不重试、404 不重试、连接拒绝重试后报错、HTML 回落判错。
`client_test.go` 新增顶层数组解码用例。
`go test ./...` / `go vet` / `gofmt` 全绿。

View File

@ -183,6 +183,13 @@ func (c *Client) Do(method, path string, body interface{}, query url.Values) (*o
// Parse JSON
var raw map[string]interface{}
if err := json.Unmarshal(respData, &raw); err != nil {
// Some legacy endpoints (e.g. /:owner/:repo/branches) answer with a
// top-level JSON array or scalar; keep the decoded value instead of
// degrading it to an escaped string.
var nonObject interface{}
if jsonErr := json.Unmarshal(respData, &nonObject); jsonErr == nil {
return output.SuccessEnvelope(nonObject, nil), nil
}
// Unknown API paths fall through to the web frontend, which answers
// 200 with an HTML page; surface that as an error instead of data.
if isHTMLResponse(resp, respData) {

View File

@ -149,6 +149,34 @@ func TestClientDoNonJSON(t *testing.T) {
}
}
func TestClientDoTopLevelArray(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`[{"name":"master"},{"name":"develop"}]`))
}))
defer server.Close()
c := &Client{HTTP: server.Client(), BaseURL: server.URL}
env, err := c.Do("GET", "/api/test/branches", nil, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !env.OK {
t.Fatal("expected OK=true for array response")
}
items, ok := env.Data.([]interface{})
if !ok {
t.Fatalf("expected decoded array, got %T", env.Data)
}
if len(items) != 2 {
t.Fatalf("expected 2 items, got %d", len(items))
}
first, ok := items[0].(map[string]interface{})
if !ok || first["name"] != "master" {
t.Fatalf("unexpected first item: %#v", items[0])
}
}
func TestClientDoStatusError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")