sipeed/picoclaw · error
read media: %w
Error message
read media: %w
What it means
storeRemoteMedia failed while streaming the body of an already-accepted HTTP 200 response from the WeCom CDN (media.go:297-299). The download runs on c.mediaClient, whose Timeout is wecomMediaTimeout = 30s (wecom.go:31,134) and covers the entire body read, so slow transfers of multi-MiB media surface here as deadline errors. The %w preserves the underlying *url.Error/*net.OpError for inspection.
Source
Thrown at pkg/channels/wecom/media.go:299
return "", fmt.Errorf("no media store available")
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, resourceURL, nil)
if err != nil {
return "", fmt.Errorf("create request: %w", err)
}
resp, err := c.mediaClient.Do(req)
if err != nil {
return "", fmt.Errorf("download media: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("download media returned HTTP %d", resp.StatusCode)
}
data, err := io.ReadAll(io.LimitReader(resp.Body, wecomOutboundMediaMaxBytes+1))
if err != nil {
return "", fmt.Errorf("read media: %w", err)
}
if len(data) > wecomOutboundMediaMaxBytes {
return "", fmt.Errorf("media too large")
}
if aesKey != "" {
key, keyErr := decodeMediaAESKey(aesKey)
if keyErr != nil {
return "", keyErr
}
data, err = decryptAESCBC(key, data)
if err != nil {
return "", fmt.Errorf("decrypt media: %w", err)
}
}
filename, contentType := detectWeComMediaMetadata(
data,View on GitHub (pinned to 49183d7e8d)
Solutions
- Unwrap the error: errors.As to *net.Error / errors.Is(err, context.DeadlineExceeded) to distinguish timeout from reset
- If timeout: raise wecomMediaTimeout (wecom.go:31) or reject oversized media earlier - 20 MiB in 30s needs ~0.7 MB/s sustained
- If reset: curl the exact CDN url from the same host to reproduce, then fix egress/proxy rules
- On retry, re-extract a fresh url from the inbound WeCom payload - CDN urls are signed and short-lived, replaying an expired one gives HTTP errors instead
Example fix
// before: fixed 30s shared client covers whole body
mediaClient: &http.Client{Timeout: wecomMediaTimeout}
// after: separate, larger budget for the media body read
mediaClient: &http.Client{
Timeout: wecomMediaTimeout, // dial + headers
Transport: &http.Transport{ResponseHeaderTimeout: 10 * time.Second},
}
// and read the body under an explicit per-file deadline:
ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel()
req = req.WithContext(ctx) Defensive patterns
Strategy: retry
Type guard
func isNetTimeout(err error) bool {
var ne net.Error
if errors.As(err, &ne) && ne.Timeout() {
return true
}
return errors.Is(err, context.DeadlineExceeded)
} Try / catch
var retryErrs = []error{context.DeadlineExceeded, io.ErrUnexpectedEOF, syscall.ECONNRESET}
func withRetry(ctx context.Context, n int, f func() error) error {
var err error
for i := 0; i < n; i++ {
if err = f(); err == nil || !isRetryable(err) {
return err
}
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(time.Duration(i+1) * time.Second):
}
}
return err
}
func isRetryable(err error) bool {
for _, target := range retryErrs {
if errors.Is(err, target) {
return true
}
}
var ne net.Error
return errors.As(err, &ne) && ne.Timeout()
} Prevention
- classify the wrapped error (net.Error.Timeout, ECONNRESET) before deciding retry vs abort
- size media for the pipe: 20 MiB inside a 30s client timeout needs ~0.7 MB/s sustained
- for WeCom CDN urls, retry only with a freshly extracted url from the inbound payload - signed urls expire
When it happens
Trigger: io.ReadAll(io.LimitReader(resp.Body, 20 MiB+1)) aborts mid-body: connection reset by CDN/proxy, TLS failure mid-stream, or the shared http.Client 30s Timeout expiring before up to 20 MiB finishes downloading after headers were already received.
Common situations: Mobile/weak links downloading multi-MiB voice or video messages; corporate proxies or firewalls RST-ing long-lived downloads; container networks with tight bandwidth limits; CDN node hiccups during large file transfer.
Related errors
- failed to get WeCom QR code: %w
- WeCom QR scan timed out after %s
- failed to query WeCom QR result: %w
- unexpected status %s
- decode JSON response: %w
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/1c71f7475c67f560.
Report an issue: GitHub.