{"record":{"id":"39e6c59ff8fc4a52","repo":"Tencent/WeKnora","slug":"yuque-api-error-status-d-msg-s","errorCode":null,"errorMessage":"yuque api error: status=%d msg=%s","messagePattern":"yuque api error: status=(.+?) msg=(.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/datasource/connector/yuque/client.go","lineNumber":138,"sourceCode":"\t\t\t\tif sErr := sleepCtx(ctx, retry5xxDelay); sErr != nil {\n\t\t\t\t\treturn sErr\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn lastErr\n\t\t}\n\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 {","sourceCodeStart":120,"sourceCodeEnd":156,"githubUrl":"https://github.com/Tencent/WeKnora/blob/988cbb03305e055d8ebb7d46d9ac6cc0803cd074/internal/datasource/connector/yuque/client.go#L120-L156","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nbookID, _ := strconv.ParseInt(staleID, 10, 64)\ndocs, err := cli.ListBookDocs(ctx, bookID) // yuque api error: status=404 msg=not_found\n\n// after\nrepos, err := cli.ListUserRepos(ctx, me.Login) // refresh book IDs first\nif !containsRepo(repos, staleID) {\n    log.Warnf(\"book %s no longer exists, skipping\", staleID)\n    return nil\n}\ndocs, err := cli.ListBookDocs(ctx, bookID)","handlingStrategy":"try-catch","validationCode":"// Go: verify the book exists before fetching its docs\nresp, err := http.NewRequestWithContext(ctx, http.MethodGet,\n    baseURL+\"/api/v2/repos/\"+bookID, nil)\nreq.Header.Set(\"X-Auth-Token\", token)\nresp, err := httpClient.Do(req)\nif err == nil && (resp.StatusCode == 404 || resp.StatusCode == 403) {\n    // skip this book — it is gone or inaccessible\n}","typeGuard":"// Go: distinguish Yuque API errors from other failures\nfunc isYuqueAPIError(err error) (status int, msg string, ok bool) {\n    if err == nil {\n        return 0, \"\", false\n    }\n    s := err.Error()\n    var statusInt int\n    if n, _ := fmt.Sscanf(s, \"yuque api error: status=%d\", &statusInt); n == 1 {\n        return statusInt, \"\", true\n    }\n    return 0, \"\", false\n}","tryCatchPattern":"docs, err := cli.ListBookDocs(ctx, bookID)\nif err != nil {\n    var status int\n    if _, _, ok := isYuqueAPIError(err); ok && strings.Contains(err.Error(), \"status=404\") {\n        log.Warnf(\"book %d no longer exists, removing from selection\", bookID)\n        return nil // skip, don't fail the whole sync\n    }\n    return fmt.Errorf(\"list docs for book %d: %w\", bookID, err)\n}","preventionTips":["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."],"tags":["api-error","http-4xx","yuque","resource-not-found"],"backgroundTag":"http-api-error-response","analyzedSha":"988cbb03305e055d8ebb7d46d9ac6cc0803cd074","analyzedAt":"2026-09-02T14:41:08.344Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}