sipeed/picoclaw · error
%s failed: %d %s
Error message
%s failed: %d %s
What it means
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.
Source
Thrown at pkg/channels/weixin/api.go:202
req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil)
if err != nil {
return err
}
req.Header["iLink-App-Id"] = []string{weixinIlinkAppID}
req.Header["iLink-App-ClientVersion"] = []string{strconv.Itoa(weixinClientVersion)}
resp, err := c.HttpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("%s failed: %d %s", endpoint, resp.StatusCode, string(respBody))
}
if err := json.Unmarshal(respBody, respObj); err != nil {
return err
}
return nil
}
func (c *ApiClient) GetQRCode(ctx context.Context, botType string) (*QRCodeResponse, error) {
// get_bot_qrcode is GET, not POST
var qrcodeResp QRCodeResponse
if err := c.getQR(ctx, "ilink/bot/get_bot_qrcode", map[string]string{
"bot_type": botType,
}, &qrcodeResp); err != nil {
return nil, err
}
return &qrcodeResp, nil
}View on GitHub (pinned to 49183d7e8d)
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
Example fix
// before: treat every error the same
if err := api.SendMessage(ctx, ...); err != nil { panic(err) }
// after: surface status and body
if err := api.SendMessage(ctx, ...); err != nil {
log.Printf("weixin api failed: %v", err) // err already contains endpoint, status, body
} Defensive patterns
Strategy: retry
Validate before calling
// health-check credentials and endpoint before a real send
func weixinAPIHealthy(ctx context.Context, api *weixin.ApiClient) error {
// any cheap authenticated GET; non-200 surfaces here with status+body
return api.Ping(ctx)
} Type guard
type httpAPIError struct{ endpoint string; status int; body string }
func isHTTPStatusError(err error) (httpAPIError, bool) {
// parse "%s failed: %d %s" produced by api.go:202
if err == nil {
return httpAPIError{}, false
}
var s string
if _, e := fmt.Sscanf(err.Error(), "%s failed: %d", &s, new(int)); e != nil {
return httpAPIError{}, false
}
return httpAPIError{endpoint: s}, true
} Try / catch
if err := api.Do(ctx, ...); err != nil {
if e, ok := isHTTPStatusError(err); ok {
switch {
case e.status >= 500 || e.status == 429:
retryWithBackoff() // transient
case e.status == 401 || e.status == 403:
relogin() // credential problem
default:
logAndPark(err) // 4xx: read body for iLink errcode
}
}
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- HTTP %d: %s
- Failed to load config
- Request failed with status ${res.status}
- Failed to fetch config
- Failed to save config
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/813ba868444f2860.
Report an issue: GitHub.