{"record":{"id":"96e6cb0ea0450a1d","repo":"Tencent/WeKnora","slug":"download-failed-status-d","errorCode":null,"errorMessage":"download failed: status=%d","messagePattern":"download failed: status=(.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/im/wecom/webhook_adapter.go","lineNumber":588,"sourceCode":"\tif !isAllowedIMAPIHost(rawURL, extraAllowedHost) {\n\t\tif err := secutils.ValidateURLForSSRF(rawURL); err != nil {\n\t\t\treturn nil, \"\", fmt.Errorf(\"URL rejected for security reasons: %v\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil)\n\tif err != nil {\n\t\treturn nil, \"\", fmt.Errorf(\"create request: %w\", err)\n\t}\n\n\tresp, err := httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, \"\", fmt.Errorf(\"download: %w\", err)\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tresp.Body.Close()\n\t\treturn nil, \"\", fmt.Errorf(\"download failed: status=%d\", resp.StatusCode)\n\t}\n\n\tlogger.Debugf(ctx, \"[WeCom] Download response: status=%d content-type=%s content-disposition=%s\",\n\t\tresp.StatusCode, resp.Header.Get(\"Content-Type\"), resp.Header.Get(\"Content-Disposition\"))\n\n\t// Try to extract filename from Content-Disposition header.\n\t// Supports both standard filename and RFC 5987 filename* parameters.\n\tif cd := resp.Header.Get(\"Content-Disposition\"); cd != \"\" {\n\t\tif _, params, err := mime.ParseMediaType(cd); err == nil {\n\t\t\t// Prefer filename* (RFC 5987, already decoded by mime.ParseMediaType)\n\t\t\tif fn := params[\"filename\"]; fn != \"\" {\n\t\t\t\tfileName = fn\n\t\t\t}\n\t\t} else {\n\t\t\t// Fallback: manual extraction for malformed headers\n\t\t\tif idx := strings.Index(cd, \"filename=\"); idx >= 0 {\n\t\t\t\textracted := strings.Trim(cd[idx+len(\"filename=\"):], \"\\\" \")\n\t\t\t\tif extracted != \"\" {","sourceCodeStart":570,"sourceCodeEnd":606,"githubUrl":"https://github.com/Tencent/WeKnora/blob/988cbb03305e055d8ebb7d46d9ac6cc0803cd074/internal/im/wecom/webhook_adapter.go#L570-L606","documentation":"downloadFromURL treats any non-200 HTTP status from the file host as a failure and returns 'download failed: status=%d' with the actual status code, closing the response body. WeCom media URLs can expire or the media can be deleted, producing 403/404. The response body is discarded, so retry logic must rely on the status code alone.","triggerScenarios":"Downloading a temporary media URL after its expiry (WeCom temporary media expires ~3 days); media deleted by the user; rate limiting (429) from the media CDN; transient 5xx from WeCom servers.","commonSituations":"Queued jobs retrying long after the message arrived and the media URL expired; fetching MediaId through GetMedia with an expired access_token (yields 4xx); bursts of downloads hitting CDN rate limits.","solutions":["Capture the status code from the error string and retry only on 5xx/429 with backoff — 403/404 mean the media is gone","For expired temporary media, ask the user to resend or fetch media promptly on message receipt","Ensure the WeCom access_token used for GetMedia is fresh (refresh on 40014/42001) before blaming the URL","Log the URL host and status to identify CDN-side rate limiting vs expiry"],"exampleFix":"// before: blind retry\nfor { rc, _, err := downloadFromURL(ctx, url, name, host); if err == nil { break } }\n// after: retry only transient statuses\nif strings.Contains(err.Error(), \"status=500\") || strings.Contains(err.Error(), \"status=429\") {\n    time.Sleep(backoff); continue\n}\nbreak // non-retryable","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"rc, name, err := adapter.DownloadFile(ctx, msg)\nif err != nil && strings.Contains(err.Error(), \"download failed: status=\") {\n    status := parseStatus(err)\n    if status >= 500 || status == 429 {\n        return retryWithBackoff(ctx) // transient\n    }\n    return err // 403/404: media expired or deleted, do not retry\n}","preventionTips":["Download media immediately on message receipt — temporary media expires","Refresh WeCom access_token on expiry before retrying GetMedia downloads","Use bounded exponential backoff and only retry 5xx/429","Monitor status-code distribution of media downloads to spot CDN issues"],"tags":["http","download","retry","wecom"],"backgroundTag":"http-download-failed","analyzedSha":"988cbb03305e055d8ebb7d46d9ac6cc0803cd074","analyzedAt":"2026-09-02T14:41:08.344Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}