{"record":{"id":"2aed9f685af3afab","repo":"chenhg5/cc-connect","slug":"wecom-ws-download-http-s","errorCode":null,"errorMessage":"wecom-ws: download HTTP %s","messagePattern":"wecom-ws: download HTTP (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"platform/wecom/websocket_media.go","lineNumber":350,"sourceCode":"\t\t}\n\t\treturn filepath.Base(val)\n\t}\n\treturn \"\"\n}\n\nfunc downloadWeComWSMedia(ctx context.Context, urlStr, aesKey string) (data []byte, fileName string, err error) {\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, urlStr, nil)\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\tclient := &http.Client{Timeout: 90 * time.Second}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode < 200 || resp.StatusCode >= 300 {\n\t\treturn nil, \"\", fmt.Errorf(\"wecom-ws: download HTTP %s\", resp.Status)\n\t}\n\tfileName = parseContentDispositionFilename(resp.Header.Get(\"Content-Disposition\"))\n\tlim := io.LimitReader(resp.Body, wecomWSMediaMaxBytes+1)\n\traw, err := io.ReadAll(lim)\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\tif len(raw) > wecomWSMediaMaxBytes {\n\t\treturn nil, \"\", fmt.Errorf(\"wecom-ws: media larger than %d bytes\", wecomWSMediaMaxBytes)\n\t}\n\tif aesKey != \"\" {\n\t\traw, err = wecomDecryptFile(raw, aesKey)\n\t\tif err != nil {\n\t\t\treturn nil, \"\", err\n\t\t}\n\t}\n\treturn raw, fileName, nil\n}","sourceCodeStart":332,"sourceCodeEnd":368,"githubUrl":"https://github.com/chenhg5/cc-connect/blob/4000b2338aa6e850c99df54f8b0ed6ed7460b401/platform/wecom/websocket_media.go#L332-L368","documentation":"downloadWeComWSMedia performs an HTTP GET of the WeCom media URL and requires a 2xx status. Any non-2xx response (404 expired URL, 403 denied, 5xx server error) is wrapped with the response status string via this error. The body is not decrypted, so callers get a clear network-layer failure.","triggerScenarios":"GET on the media URL returns 404/403/410/5xx — typically because the WeCom websocket media URL has expired (they are short-lived) or the bot lacks media access.","commonSituations":"Retrying a media URL minutes after receipt (expired); WeCom service incident; media deleted before download; misconfigured proxy returning an auth page.","solutions":["Re-fetch a fresh media URL from the WeCom API immediately before downloading; do not cache URLs.","Check resp.Status in logs to distinguish 403 (permissions) from 404/410 (expired/deleted).","Verify bot credentials/scopes if 401/403 persists, and check WeCom service status for 5xx."],"exampleFix":"// before\nurl := cachedMediaURL[msgID] // minutes old\nraw, err := downloadWeComWSMedia(url, key)\n// after\nurl, err := fetchFreshMediaURL(msgID) // always fresh\nif err != nil { return err }\nraw, err := downloadWeComWSMedia(url, key)","handlingStrategy":"retry","validationCode":"if u, err := url.Parse(mediaURL); err != nil || u.Host == \"\" { return fmt.Errorf(\"bad media url\") }","typeGuard":null,"tryCatchPattern":"raw, err := downloadWeComWSMedia(url, key)\nif err != nil {\n    var herr *HTTPStatusError // or inspect status string\n    if strings.Contains(err.Error(), \"404\") || strings.Contains(err.Error(), \"410\") {\n        url, _ = fetchFreshMediaURL(msgID)\n        raw, err = downloadWeComWSMedia(url, key)\n    }\n}","preventionTips":["Never cache WeCom media URLs; fetch fresh at download time.","Distinguish 4xx (expired/forbidden) from 5xx (server) when retrying.","Set a short HTTP timeout and verify bot credentials on repeated 401/403."],"tags":["wecom","http","media-download","network"],"backgroundTag":"http-error-response","analyzedSha":"4000b2338aa6e850c99df54f8b0ed6ed7460b401","analyzedAt":"2026-09-06T11:45:09.575Z","contentChangedAt":"2026-09-06T11:45:09.575Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}