From aa0a623a73799caee6700f1b620093123ae70d7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=92=8B=E5=A4=A9=E7=BF=94?= Date: Tue, 2 Jun 2026 16:02:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20Raw=20API=20HTML?= =?UTF-8?q?=20=E5=93=8D=E5=BA=94=E8=87=AA=E5=8A=A8=E6=A3=80=E6=B5=8B?= =?UTF-8?q?=E4=B8=8E=E8=AF=8A=E6=96=AD=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 当 GitLink API 返回 HTML 页面(如登录页)而非 JSON 数据时, 自动识别并返回结构化错误信息,包含可能原因和修复建议。 Co-Authored-By: Claude Opus 4.7 --- internal/client/client.go | 36 ++++++++++++++++++- internal/client/client_test.go | 65 ++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/internal/client/client.go b/internal/client/client.go index 1d20aed..94f0cb1 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -107,10 +107,21 @@ func (c *Client) Do(method, path string, body interface{}, query url.Values) (*o } } + // Detect HTML responses (GitLink returns login pages when auth is missing) + if detectHTMLResponse(respData) { + msg := "服务器返回了 HTML 页面而非 JSON 数据" + suggestion := suggestHTMLFix() + return output.ErrorEnvelope(resp.StatusCode, msg, suggestion), + &APIError{ + StatusCode: resp.StatusCode, + Code: "HTML_RESPONSE", + Message: msg + "\n" + suggestion, + } + } + // Parse JSON var raw map[string]interface{} if err := json.Unmarshal(respData, &raw); err != nil { - // Not JSON, return as-is return output.SuccessEnvelope(string(respData), nil), nil } @@ -215,3 +226,26 @@ func suggestFix(code int) string { return "" } } + +func detectHTMLResponse(data []byte) bool { + trimmed := bytes.TrimSpace(data) + if len(trimmed) == 0 { + return false + } + prefixes := []string{"...`, true}, + {"html 小写开头", `...`, true}, + {"HTML 大写开头", `...`, true}, + {"doctype 小写开头", ``, true}, + {"空响应体", "", false}, + {"纯文本", `just some text`, false}, + {"空白后 HTML", ` `, true}, + {"JSON 数组", `[1,2,3]`, false}, + {"HTML 片段(无前缀)", `content`, false}, + {"XML 声明后跟 HTML", ``, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := detectHTMLResponse([]byte(tt.body)); got != tt.wantHTML { + t.Errorf("detectHTMLResponse(%q) = %v, want %v", tt.body, got, tt.wantHTML) + } + }) + } +} + +func TestClientDoHTMLResponse(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html") + w.Write([]byte(`Sign inPlease log in`)) + })) + defer server.Close() + + c := &Client{HTTP: server.Client(), BaseURL: server.URL} + env, err := c.Do("GET", "/api/test", nil, nil) + if err == nil { + t.Fatal("expected error for HTML response") + } + if env == nil { + t.Fatal("expected envelope for HTML response") + } + if env.OK { + t.Fatal("expected OK=false for HTML response") + } + apiErr, ok := err.(*APIError) + if !ok { + t.Fatalf("expected *APIError, got %T", err) + } + if apiErr.Code != "HTML_RESPONSE" { + t.Fatalf("Code = %v, want HTML_RESPONSE", apiErr.Code) + } +} + +func TestSuggestHTMLFix(t *testing.T) { + msg := suggestHTMLFix() + if msg == "" { + t.Fatal("suggestHTMLFix should return a non-empty message") + } + if !strings.Contains(msg, "gitlink-cli auth login") { + t.Fatal("suggestHTMLFix should mention auth login") + } +}