Tencent/WeKnora · error
decode response: %w
Error message
decode response: %w
What it means
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.
Source
Thrown at internal/datasource/connector/yuque/client.go:145
// 401/403 → surface as ErrInvalidCredentials so DataSourceService can
// distinguish bad-token from transient failures and auto-flag the source.
if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
return fmt.Errorf("%w: status=%d body=%s", datasource.ErrInvalidCredentials, resp.StatusCode, bodyPreview)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
var apiErr apiErrorBody
_ = json.Unmarshal(body, &apiErr)
if apiErr.Message != "" {
return fmt.Errorf("yuque api error: status=%d msg=%s", resp.StatusCode, apiErr.Message)
}
return fmt.Errorf("yuque api error: status=%d body=%s", resp.StatusCode, bodyPreview)
}
if result != nil {
if err := json.Unmarshal(body, result); err != nil {
return fmt.Errorf("decode response: %w", err)
}
}
return nil
}
return lastErr
}
// parseRetryAfter returns the Retry-After duration from the header, or fallback if unparseable.
// Retry-After: "0" (or negative) is coerced to 100ms so we still yield and don't busy-retry.
// Note: only integer-seconds form is supported (RFC 7231 also allows HTTP-date — not seen from Yuque).
func parseRetryAfter(header string, fallback time.Duration) time.Duration {
if header == "" {
return fallback
}
if secs, err := time.ParseDuration(header + "s"); err == nil {
if secs <= 0 {
return 100 * time.Millisecond
}View on GitHub (pinned to 988cbb0330)
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.
Example fix
// before
type v2Repo struct {
ID int64 `json:"id"` // breaks if a Yuque version sends "id":"12345"
}
// after
type v2Repo struct {
ID int64 `json:"-"`
}
func (r *v2Repo) UnmarshalJSON(b []byte) error {
var raw struct {
ID json.Number `json:"id"`
}
if err := json.Unmarshal(b, &raw); err != nil {
return err
}
id, err := raw.ID.Int64()
if err != nil {
return fmt.Errorf("repo id %q: %w", raw.ID, err)
}
r.ID = id
return nil
} Defensive patterns
Strategy: validation
Validate before calling
// Go: sanity-check the decode contract against the live API before relying on it
var probe struct {
Data json.RawMessage `json:"data"`
}
if err := json.Unmarshal(body, &probe); err != nil || len(probe.Data) == 0 {
return fmt.Errorf("unexpected Yuque response envelope: %s", truncate(string(body), 200))
} Type guard
// Go: lenient decode that reports which field failed instead of a blanket error
type v2UserResponse struct {
Data json.RawMessage `json:"data"`
}
func decodeUser(body []byte) (v2User, error) {
var env v2UserResponse
if err := json.Unmarshal(body, &env); err != nil {
return v2User{}, fmt.Errorf("decode response envelope: %w", err)
}
var u v2User
if err := json.Unmarshal(env.Data, &u); err != nil {
return v2User{}, fmt.Errorf("decode user payload %s: %w", truncate(string(env.Data), 200), err)
}
return u, nil
} Try / catch
me, err := cli.GetCurrentUser(ctx)
if err != nil {
if strings.HasPrefix(err.Error(), "decode response:") {
// API contract drift — log payload sample, alert, and fall back to cached resource list
log.Errorf("yuque schema changed: %v", err)
return cachedResources(), nil
}
return nil, err
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- create request: %w
- execute request: %w
- read response body: %w
- yuque api error: status=%d msg=%s
- yuque api error: status=%d body=%s
AI-assisted analysis of Tencent/WeKnora@988cbb0330 (2026-09-02).
Data as JSON: /api/errors/d6354a3fc9cab05b.
Report an issue: GitHub.