Tencent/WeKnora · error
yuque api error: status=%d msg=%s
Error message
yuque api error: status=%d msg=%s
What it means
doRequest returns this when the Yuque Open API replies with a non-2xx status (excluding 401/403, 429, and 5xx, which have dedicated branches) AND the response body parsed into an apiErrorBody carrying a non-empty "message" field. It surfaces the HTTP status plus Yuque's own error message so callers can see the API-level reason. It is not retried — it propagates immediately to all client methods (Ping, GetCurrentUser, ListUserGroups, listReposPaginated, ListBookDocs, GetDocDetail).
Source
Thrown at internal/datasource/connector/yuque/client.go:138
if sErr := sleepCtx(ctx, retry5xxDelay); sErr != nil {
return sErr
}
continue
}
return lastErr
}
// 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 {View on GitHub (pinned to 988cbb0330)
Solutions
- Check the status and msg in the error: for 404, verify the book/doc/resource ID still exists and re-pick resources via ListResources so ResourceIDs are refreshed.
- For 404 on ListUserGroups, ignore it — the connector already treats it as 'no groups'; if you call the client directly, treat 404 there as an empty list.
- Verify the token has read access to the target book/group (Yuque scopes per repo and group); re-authorize with a broader token if needed.
- Confirm the baseURL points at the correct Yuque instance (yuque.com vs self-hosted) — wrong instances return 404 for valid paths.
- If the message is unhelpful, reproduce the request with curl using the same X-Auth-Token header to see the full response body.
Example fix
// before
bookID, _ := strconv.ParseInt(staleID, 10, 64)
docs, err := cli.ListBookDocs(ctx, bookID) // yuque api error: status=404 msg=not_found
// after
repos, err := cli.ListUserRepos(ctx, me.Login) // refresh book IDs first
if !containsRepo(repos, staleID) {
log.Warnf("book %s no longer exists, skipping", staleID)
return nil
}
docs, err := cli.ListBookDocs(ctx, bookID) Defensive patterns
Strategy: try-catch
Validate before calling
// Go: verify the book exists before fetching its docs
resp, err := http.NewRequestWithContext(ctx, http.MethodGet,
baseURL+"/api/v2/repos/"+bookID, nil)
req.Header.Set("X-Auth-Token", token)
resp, err := httpClient.Do(req)
if err == nil && (resp.StatusCode == 404 || resp.StatusCode == 403) {
// skip this book — it is gone or inaccessible
} Type guard
// Go: distinguish Yuque API errors from other failures
func isYuqueAPIError(err error) (status int, msg string, ok bool) {
if err == nil {
return 0, "", false
}
s := err.Error()
var statusInt int
if n, _ := fmt.Sscanf(s, "yuque api error: status=%d", &statusInt); n == 1 {
return statusInt, "", true
}
return 0, "", false
} Try / catch
docs, err := cli.ListBookDocs(ctx, bookID)
if err != nil {
var status int
if _, _, ok := isYuqueAPIError(err); ok && strings.Contains(err.Error(), "status=404") {
log.Warnf("book %d no longer exists, removing from selection", bookID)
return nil // skip, don't fail the whole sync
}
return fmt.Errorf("list docs for book %d: %w", bookID, err)
} Prevention
- Refresh book IDs via ListResources before each full sync instead of trusting stored ResourceIDs indefinitely.
- Treat 404 on ListUserGroups as 'no groups' (as the connector does) rather than a fatal error.
- Log the status+msg pair to alert on which specific books/docs lose access.
- Validate the datasource (connector.Validate) periodically to catch access revocation early.
When it happens
Trigger: Any Yuque v2 endpoint returns a 4xx (other than 401/403/429) with a JSON body containing {"message":"..."} — e.g. 404 from /api/v2/repos/{bookID}/docs for a deleted/inaccessible book, 404 from /api/v2/users/{id}/groups when the user has joined no groups, 404 from /api/v2/repos/docs/{docID} for a doc the token cannot see, or 400 from a malformed path/parameter.
Common situations: A configured book was deleted or its ID is stale in ResourceIDs; a doc was removed between listing and detail fetch; a team token lacks access to a specific book; the user has zero group memberships (the ListUserGroups 404 case, which connector.go intentionally swallows); API path changes after a Yuque API version update.
Related errors
- yuque api error: status=%d body=%s
- list team repos: %w
- list personal repos: %w
- list docs for book %d: %w
- failed to generate questions: %w
AI-assisted analysis of Tencent/WeKnora@988cbb0330 (2026-09-02).
Data as JSON: /api/errors/39e6c59ff8fc4a52.
Report an issue: GitHub.