{"record":{"id":"7a8df28795a3f0c6","repo":"Tencent/WeKnora","slug":"yuque-api-error-status-d-body-s","errorCode":null,"errorMessage":"yuque api error: status=%d body=%s","messagePattern":"yuque api error: status=(.+?) body=(.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/datasource/connector/yuque/client.go","lineNumber":140,"sourceCode":"\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 {\n\tif header == \"\" {\n\t\treturn fallback","sourceCodeStart":122,"sourceCodeEnd":158,"githubUrl":"https://github.com/Tencent/WeKnora/blob/988cbb03305e055d8ebb7d46d9ac6cc0803cd074/internal/datasource/connector/yuque/client.go#L122-L158","documentation":"Same branch as the msg variant: doRequest gets a non-2xx status (outside 401/403/429/5xx) but the body either isn't JSON or has no parseable \"message\" field, so the client falls back to embedding a truncated (500-char) raw body in the error. This is the diagnostic fallback that tells you Yuque returned an unrecognized error payload. It is returned un-retried to every caller of doRequest.","triggerScenarios":"A non-2xx response whose body lacks the {\"message\":\"...\"} shape: an HTML error page from a proxy/reverse-proxy/gateway, an empty body, or a Yuque error format the apiErrorBody struct doesn't match (e.g. message nested under a different key), for any endpoint from Ping through GetDocDetail.","commonSituations":"A corporate proxy or WAF intercepts the request and returns an HTML 403/400 block page; a self-hosted Yuque (or its nginx front) emits non-JSON error pages; a typo'd baseURL (e.g. missing path prefix) yields a non-JSON 404; an API gateway rate-limits or errors with a custom payload.","solutions":["Inspect the body= content in the error — it identifies who actually produced the response (HTML → proxy/nginx; JSON with a different shape → API version mismatch).","If the body is an HTML block page, fix the network path: check proxy env vars (HTTP_PROXY/HTTPS_PROXY), corporate egress rules, or the Yuque instance's fronting nginx config.","Verify baseURL is exactly the API root (e.g. https://www.yuque.com/api/v2 or the self-hosted equivalent); wrong roots return non-JSON 404s.","Capture the full response with curl -i to see headers and confirm which server answered.","If the body shows a message under a non-standard key, extend apiErrorBody to match your Yuque version's error format."],"exampleFix":"// before\nbaseURL: \"https://www.yuque.com\" // page host, API paths then 404 with HTML\n// after\nbaseURL: \"https://www.yuque.com/api/v2\" // or set api_base_url in the datasource config","handlingStrategy":"type-guard","validationCode":"// Go: probe the base URL for a JSON-capable API before configuring\nresp, err := http.Get(baseURL + \"/api/v2/user\")\nif err == nil {\n    ct := resp.Header.Get(\"Content-Type\")\n    if !strings.Contains(ct, \"application/json\") {\n        // a proxy/html responder sits in front — fix network path or base URL\n    }\n}","typeGuard":"// Go: detect the raw-body fallback variant\nfunc isYuqueRawBodyError(err error) (bodySnippet string, ok bool) {\n    if err == nil {\n        return \"\", false\n    }\n    if strings.HasPrefix(err.Error(), \"yuque api error: status=\") && strings.Contains(err.Error(), \"body=\") {\n        i := strings.Index(err.Error(), \"body=\")\n        return err.Error()[i+5:], true\n    }\n    return \"\", false\n}","tryCatchPattern":"err := cli.Ping(ctx)\nif err != nil {\n    if body, ok := isYuqueRawBodyError(err); ok {\n        if strings.Contains(strings.ToLower(body), \"<html\") {\n            return fmt.Errorf(\"a proxy/firewall intercepted the Yuque request, check network egress: %s\", body)\n        }\n    }\n    return err\n}","preventionTips":["Pin the baseURL to the API root (https://www.yuque.com/api/v2 or your instance's equivalent) and never a page URL.","Check Content-Type of responses during setup to detect intercepting proxies early.","Set NO_PROXY/proxy env correctly in containerized deployments so corporate proxies don't answer for Yuque.","Keep apiErrorBody in sync with your Yuque version's error JSON shape so the msg branch is preferred over the body fallback."],"tags":["api-error","http-4xx","yuque","non-json-response"],"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"}