{"record":{"id":"813ba868444f2860","repo":"sipeed/picoclaw","slug":"s-failed-d-s","errorCode":null,"errorMessage":"%s failed: %d %s","messagePattern":"(.+?) failed: (.+?) (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/channels/weixin/api.go","lineNumber":202,"sourceCode":"\treq, err := http.NewRequestWithContext(ctx, \"GET\", u.String(), nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\treq.Header[\"iLink-App-Id\"] = []string{weixinIlinkAppID}\n\treq.Header[\"iLink-App-ClientVersion\"] = []string{strconv.Itoa(weixinClientVersion)}\n\n\tresp, err := c.HttpClient.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\n\trespBody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn fmt.Errorf(\"%s failed: %d %s\", endpoint, resp.StatusCode, string(respBody))\n\t}\n\tif err := json.Unmarshal(respBody, respObj); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (c *ApiClient) GetQRCode(ctx context.Context, botType string) (*QRCodeResponse, error) {\n\t// get_bot_qrcode is GET, not POST\n\tvar qrcodeResp QRCodeResponse\n\tif err := c.getQR(ctx, \"ilink/bot/get_bot_qrcode\", map[string]string{\n\t\t\"bot_type\": botType,\n\t}, &qrcodeResp); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &qrcodeResp, nil\n}","sourceCodeStart":184,"sourceCodeEnd":220,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/channels/weixin/api.go#L184-L220","documentation":"Generic non-2xx failure from the Weixin iLink HTTP layer: the request reached the server, the response was read in full, but the status code was not 200. The error embeds the endpoint name, the numeric status, and the entire response body, which for iLink APIs usually carries a JSON error code/message explaining the refusal. Note that business-level errors inside a 200 body are handled elsewhere (json.Unmarshal), so this is purely an HTTP-status failure.","triggerScenarios":"Any ApiClient POST/GET (e.g. ilink/bot/get_bot_qrcode, send message, upload media) returning 4xx/5xx: 401/403 from an invalid or expired bot token, 404 from a wrong BaseURL or API path, 429 from rate limiting, 5xx during server maintenance, or an HTML error page from a captive portal/proxy.","commonSituations":"The bot_token expired after the Weixin session was invalidated (every API call then 401s); BaseURL misconfigured or redirected; hitting rate limits during message bursts; transient Weixin service instability; a proxy returning 407 because credentials were never supplied.","solutions":["Read the embedded response body in the error text — the iLink errcode/message pinpoints the cause (auth vs rate-limit vs server)","For 401/403, re-run the QR login flow to obtain a fresh bot_token","For 429, back off and retry with exponential delay; reduce send rate","For 5xx, retry after a pause; check Weixin service status","For 404, verify BaseURL (default https://ilinkai.weixin.qq.com/) and that no path-rewriting proxy is in front"],"exampleFix":"// before: treat every error the same\nif err := api.SendMessage(ctx, ...); err != nil { panic(err) }\n\n// after: surface status and body\nif err := api.SendMessage(ctx, ...); err != nil {\n    log.Printf(\"weixin api failed: %v\", err) // err already contains endpoint, status, body\n}","handlingStrategy":"retry","validationCode":"// health-check credentials and endpoint before a real send\nfunc weixinAPIHealthy(ctx context.Context, api *weixin.ApiClient) error {\n    // any cheap authenticated GET; non-200 surfaces here with status+body\n    return api.Ping(ctx)\n}","typeGuard":"type httpAPIError struct{ endpoint string; status int; body string }\n\nfunc isHTTPStatusError(err error) (httpAPIError, bool) {\n    // parse \"%s failed: %d %s\" produced by api.go:202\n    if err == nil {\n        return httpAPIError{}, false\n    }\n    var s string\n    if _, e := fmt.Sscanf(err.Error(), \"%s failed: %d\", &s, new(int)); e != nil {\n        return httpAPIError{}, false\n    }\n    return httpAPIError{endpoint: s}, true\n}","tryCatchPattern":"if err := api.Do(ctx, ...); err != nil {\n    if e, ok := isHTTPStatusError(err); ok {\n        switch {\n        case e.status >= 500 || e.status == 429:\n            retryWithBackoff() // transient\n        case e.status == 401 || e.status == 403:\n            relogin() // credential problem\n        default:\n            logAndPark(err) // 4xx: read body for iLink errcode\n        }\n    }\n}","preventionTips":["Always read the embedded response body — iLink errcode/message identifies the cause","Retry only 429/5xx; retrying 401 wastes the rate budget","Re-login proactively when token age approaches session lifetime","Back off message bursts to stay under rate limits"],"tags":["weixin","http","api","status-code","rate-limit"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}