feat: 添加 Raw API HTML 响应自动检测与诊断提示

当 GitLink API 返回 HTML 页面(如登录页)而非 JSON 数据时,
自动识别并返回结构化错误信息,包含可能原因和修复建议。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
蒋天翔 2026-06-02 16:02:59 +08:00
parent 6e7ee9cd12
commit aa0a623a73
2 changed files with 100 additions and 1 deletions

View File

@ -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{"<!DOCTYPE", "<html", "<HTML", "<!doctype"}
for _, p := range prefixes {
if bytes.HasPrefix(trimmed, []byte(p)) {
return true
}
}
return false
}
func suggestHTMLFix() string {
return "API 返回了 HTML 页面而非 JSON 数据。" +
"可能原因:\n" +
" 1. 未登录或 Token 已过期 → 运行 gitlink-cli auth login\n" +
" 2. Token 权限不足 → 在 GitLink 平台重新生成 Token\n" +
" 3. API 端点不存在 → 检查路径是否正确\n" +
" 4. 使用 Shortcut 命令替代 Raw API → 运行 gitlink-cli --help 查看可用命令"
}

View File

@ -7,6 +7,7 @@ import (
"net/url"
"os"
"path/filepath"
"strings"
"testing"
)
@ -525,3 +526,67 @@ func TestShouldAppendJSONSuffixSkipsExistingJSONPath(t *testing.T) {
t.Fatal("existing .json path should not get another suffix")
}
}
func TestDetectHTMLResponse(t *testing.T) {
tests := []struct {
name string
body string
wantHTML bool
}{
{"正常 JSON", `{"key":"value"}`, false},
{"DOCTYPE 开头", `<!DOCTYPE html><html>...</html>`, true},
{"html 小写开头", `<html><head>...</head></html>`, true},
{"HTML 大写开头", `<HTML><HEAD>...</HEAD></HTML>`, true},
{"doctype 小写开头", `<!doctype html><html lang="en">`, true},
{"空响应体", "", false},
{"纯文本", `just some text`, false},
{"空白后 HTML", ` <!DOCTYPE html>`, true},
{"JSON 数组", `[1,2,3]`, false},
{"HTML 片段(无前缀)", `<body>content</body>`, false},
{"XML 声明后跟 HTML", `<?xml version="1.0"?><!DOCTYPE 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(`<!DOCTYPE html><html><head><title>Sign in</title></head><body>Please log in</body></html>`))
}))
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")
}
}