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

  1. 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.
  2. 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.
  3. 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.
  4. Confirm the baseURL points at the correct Yuque instance (yuque.com vs self-hosted) — wrong instances return 404 for valid paths.
  5. 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

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


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