{"record":{"id":"d6354a3fc9cab05b","repo":"Tencent/WeKnora","slug":"decode-response-w","errorCode":null,"errorMessage":"decode response: %w","messagePattern":"decode response: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/datasource/connector/yuque/client.go","lineNumber":145,"sourceCode":"\n\t\t// 401/403 → surface as ErrInvalidCredentials so DataSourceService can\n\t\t// distinguish bad-token from transient failures and auto-flag the source.\n\t\tif resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {\n\t\t\treturn fmt.Errorf(\"%w: status=%d body=%s\", datasource.ErrInvalidCredentials, resp.StatusCode, bodyPreview)\n\t\t}\n\n\t\tif resp.StatusCode < 200 || resp.StatusCode >= 300 {\n\t\t\tvar apiErr apiErrorBody\n\t\t\t_ = json.Unmarshal(body, &apiErr)\n\t\t\tif apiErr.Message != \"\" {\n\t\t\t\treturn fmt.Errorf(\"yuque api error: status=%d msg=%s\", resp.StatusCode, apiErr.Message)\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"yuque api error: status=%d body=%s\", resp.StatusCode, bodyPreview)\n\t\t}\n\n\t\tif result != nil {\n\t\t\tif err := json.Unmarshal(body, result); err != nil {\n\t\t\t\treturn fmt.Errorf(\"decode response: %w\", err)\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\treturn lastErr\n}\n\n// parseRetryAfter returns the Retry-After duration from the header, or fallback if unparseable.\n// Retry-After: \"0\" (or negative) is coerced to 100ms so we still yield and don't busy-retry.\n// Note: only integer-seconds form is supported (RFC 7231 also allows HTTP-date — not seen from Yuque).\nfunc parseRetryAfter(header string, fallback time.Duration) time.Duration {\n\tif header == \"\" {\n\t\treturn fallback\n\t}\n\tif secs, err := time.ParseDuration(header + \"s\"); err == nil {\n\t\tif secs <= 0 {\n\t\t\treturn 100 * time.Millisecond\n\t\t}","sourceCodeStart":127,"sourceCodeEnd":163,"githubUrl":"https://github.com/Tencent/WeKnora/blob/988cbb03305e055d8ebb7d46d9ac6cc0803cd074/internal/datasource/connector/yuque/client.go#L127-L163","documentation":"doRequest received a 2xx response but json.Unmarshal failed to decode the body into the expected response struct (v2UserResponse, v2RepoListResponse, v2DocListResponse, v2DocDetailResponse, etc.). This means HTTP succeeded but the payload doesn't match the connector's Go types — a contract mismatch between the connector models and what the server actually sent. It propagates un-retried to all client methods.","triggerScenarios":"Yuque returns 200 with JSON whose fields don't fit the target struct: a field typed as a number arrives as a string (or vice versa), data is null where an array is expected, the response wraps data differently (e.g. {\"data\":{...}} vs a bare object), or an endpoint returns a JSON literal (true/false) or truncated JSON from an intermediary.","commonSituations":"A self-hosted or older/newer Yuque version serializes fields differently than yuque.com; an API contract change (Yuque changing id to string, or renaming fields) after a platform update; a proxy mangling the body; pointing the connector at a non-Yuque service on the configured baseURL that returns 200 with unrelated JSON.","solutions":["Read the wrapped json.Unmarshal error — it names the exact JSON field and expected Go type; compare against the struct (v2UserResponse etc.) in the yuque package.","Dump the raw body (the log line 'body=' in doRequest logs it) and check whether it is actually Yuque's expected envelope {\"data\":...}.","If you're on a self-hosted Yuque, align its version with the connector's expectations or relax the struct (use json.RawMessage / interface{} for the offending field).","Confirm baseURL isn't pointing at a different service that happens to return 200 JSON.","If Yuque changed a field type (e.g. numeric id to string), update the model or add a custom UnmarshalJSON."],"exampleFix":"// before\ntype v2Repo struct {\n    ID int64 `json:\"id\"` // breaks if a Yuque version sends \"id\":\"12345\"\n}\n\n// after\ntype v2Repo struct {\n    ID int64 `json:\"-\"`\n}\nfunc (r *v2Repo) UnmarshalJSON(b []byte) error {\n    var raw struct {\n        ID json.Number `json:\"id\"`\n    }\n    if err := json.Unmarshal(b, &raw); err != nil {\n        return err\n    }\n    id, err := raw.ID.Int64()\n    if err != nil {\n        return fmt.Errorf(\"repo id %q: %w\", raw.ID, err)\n    }\n    r.ID = id\n    return nil\n}","handlingStrategy":"validation","validationCode":"// Go: sanity-check the decode contract against the live API before relying on it\nvar probe struct {\n    Data json.RawMessage `json:\"data\"`\n}\nif err := json.Unmarshal(body, &probe); err != nil || len(probe.Data) == 0 {\n    return fmt.Errorf(\"unexpected Yuque response envelope: %s\", truncate(string(body), 200))\n}","typeGuard":"// Go: lenient decode that reports which field failed instead of a blanket error\ntype v2UserResponse struct {\n    Data json.RawMessage `json:\"data\"`\n}\nfunc decodeUser(body []byte) (v2User, error) {\n    var env v2UserResponse\n    if err := json.Unmarshal(body, &env); err != nil {\n        return v2User{}, fmt.Errorf(\"decode response envelope: %w\", err)\n    }\n    var u v2User\n    if err := json.Unmarshal(env.Data, &u); err != nil {\n        return v2User{}, fmt.Errorf(\"decode user payload %s: %w\", truncate(string(env.Data), 200), err)\n    }\n    return u, nil\n}","tryCatchPattern":"me, err := cli.GetCurrentUser(ctx)\nif err != nil {\n    if strings.HasPrefix(err.Error(), \"decode response:\") {\n        // API contract drift — log payload sample, alert, and fall back to cached resource list\n        log.Errorf(\"yuque schema changed: %v\", err)\n        return cachedResources(), nil\n    }\n    return nil, err\n}","preventionTips":["Use json.Number or custom UnmarshalJSON for numeric fields (ids, counts) that some Yuque versions serialize as strings.","Make struct fields pointers or omitempty-tolerant so missing/null fields don't break decoding.","Add a contract test that decodes a recorded real response fixture for each endpoint.","When self-hosting, pin the Yuque version and re-run contract tests after upgrades.","Keep the raw body logged on decode failure (doRequest already logs it) for quick diagnosis."],"tags":["json-decode","type-mismatch","yuque","schema-drift"],"backgroundTag":"json-unmarshal-type-mismatch","analyzedSha":"988cbb03305e055d8ebb7d46d9ac6cc0803cd074","analyzedAt":"2026-09-02T14:41:08.344Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}