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

  1. 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.
  2. Dump the raw body (the log line 'body=' in doRequest logs it) and check whether it is actually Yuque's expected envelope {"data":...}.
  3. 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).
  4. Confirm baseURL isn't pointing at a different service that happens to return 200 JSON.
  5. 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

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


AI-assisted analysis of Tencent/WeKnora@988cbb0330 (2026-09-02). Data as JSON: /api/errors/d6354a3fc9cab05b. Report an issue: GitHub.