From 515612ddc53ce14ea657e60b2f7f8c4fe3cf6ef8 Mon Sep 17 00:00:00 2001 From: Taoyouce Date: Tue, 7 Jul 2026 14:36:56 +0000 Subject: [PATCH] =?UTF-8?q?fix(client):=20=E9=A1=B6=E5=B1=82=20JSON=20?= =?UTF-8?q?=E6=95=B0=E7=BB=84/=E6=A0=87=E9=87=8F=E5=93=8D=E5=BA=94?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E9=99=8D=E7=BA=A7=E4=B8=BA=E8=BD=AC=E4=B9=89?= =?UTF-8?q?=E5=AD=97=E7=AC=A6=E4=B8=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../get-retry-and-error-consistency.md | 8 ++++++ internal/client/client.go | 7 +++++ internal/client/client_test.go | 28 +++++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/doc/changes/get-retry-and-error-consistency.md b/doc/changes/get-retry-and-error-consistency.md index a72ffd4..5b45005 100644 --- a/doc/changes/get-retry-and-error-consistency.md +++ b/doc/changes/get-retry-and-error-consistency.md @@ -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` 全绿。 diff --git a/internal/client/client.go b/internal/client/client.go index 8512243..66776e5 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -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) { diff --git a/internal/client/client_test.go b/internal/client/client_test.go index c46226b..b5434b5 100644 --- a/internal/client/client_test.go +++ b/internal/client/client_test.go @@ -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")