{"record":{"id":"8a0bfddd6b30ff39","repo":"sipeed/picoclaw","slug":"download-media-returned-http-d-s","errorCode":null,"errorMessage":"download media returned HTTP %d: %s","messagePattern":"download media returned HTTP (.+?): (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/channels/wecom/media.go","lineNumber":434,"sourceCode":"\nfunc (c *WeComChannel) downloadRemoteMediaToTemp(\n\tctx context.Context,\n\tresourceURL, fallbackName string,\n) (string, string, string, error) {\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, resourceURL, nil)\n\tif err != nil {\n\t\treturn \"\", \"\", \"\", fmt.Errorf(\"create request: %w\", err)\n\t}\n\n\tresp, err := c.mediaClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", \"\", \"\", fmt.Errorf(\"download media: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tbody, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))\n\t\treturn \"\", \"\", \"\", fmt.Errorf(\"download media returned HTTP %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tdata, err := io.ReadAll(io.LimitReader(resp.Body, wecomOutboundMediaMaxBytes+1))\n\tif err != nil {\n\t\treturn \"\", \"\", \"\", fmt.Errorf(\"read media: %w\", err)\n\t}\n\tif len(data) > wecomOutboundMediaMaxBytes {\n\t\treturn \"\", \"\", \"\", fmt.Errorf(\"media too large\")\n\t}\n\n\tfilename, contentType := detectWeComMediaMetadata(\n\t\tdata,\n\t\tfallbackName,\n\t\tresp.Header.Get(\"Content-Type\"),\n\t\tresourceURL,\n\t\tresp.Header.Get(\"Content-Disposition\"),\n\t)\n\ttmpPath, err := writeWeComTempFile(\"wecom-outbound\", filename, data)","sourceCodeStart":416,"sourceCodeEnd":452,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/channels/wecom/media.go#L416-L452","documentation":"The remote server answered outbound media fetch with a non-200 status (media.go:432-434); the error embeds the status code and the first 1 KiB of the body for diagnosis. 403 typically means an expired signed URL or hotlink protection, 404 a deleted object, 401 required auth, 5xx an upstream failure. Unlike the inbound twin at media.go:294, this variant includes the body snippet.","triggerScenarios":"GET to part.Ref returns non-200: WeCom media_url used after expiry (signed, short-lived), object deleted before fetch, S3/OSS bucket requiring signed access, rate-limit 429 from the file host, or a 500 from a flaky origin.","commonSituations":"Forwarding WeCom-hosted media too long after receipt; passing ephemeral pre-signed S3 links through the bot; origin enforcing Referer/UA checks that reject the bot's fetch.","solutions":["Read the embedded status: 403 -> the signed URL expired; re-obtain a fresh url/media_id from the source and fetch promptly","404/410 -> the object is gone; drop the part and notify the sender, do not retry","401/403 on your own buckets -> fetch via signed URLs generated at send time","429/5xx -> retry with backoff; the message includes the body to confirm the origin's error text","For hotlink-protected hosts, mirror the file to storage you control and send that ref"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"var httpStatusRe = regexp.MustCompile(`download media returned HTTP (\\d+)`)\n\nfunc classifyDownloadStatus(err error) (int, bool) {\n\tm := httpStatusRe.FindStringSubmatch(err.Error())\n\tif m == nil {\n\t\treturn 0, false\n\t}\n\tn, _ := strconv.Atoi(m[1])\n\treturn n, true\n}\n\n// usage\nif code, ok := classifyDownloadStatus(err); ok {\n\tswitch {\n\tcase code == 403 || code == 401:\n\t\t// refresh the signed URL / credentials, then retry once\n\tcase code == 404 || code == 410:\n\t\t// permanent: drop the media part, keep sending the caption\n\tcase code == 429 || code >= 500:\n\t\t// backoff and retry\n\t}\n}","preventionTips":["fetch WeCom-hosted media immediately after receipt - signed urls are short-lived","prefer refs to storage you control for outbound media","log the embedded body snippet; it usually names the real reason"],"tags":["http","status-code","expired-url","authorization"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}