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

  1. Unwrap the error: errors.As to *net.Error / errors.Is(err, context.DeadlineExceeded) to distinguish timeout from reset
  2. If timeout: raise wecomMediaTimeout (wecom.go:31) or reject oversized media earlier - 20 MiB in 30s needs ~0.7 MB/s sustained
  3. If reset: curl the exact CDN url from the same host to reproduce, then fix egress/proxy rules
  4. 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

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


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/1c71f7475c67f560. Report an issue: GitHub.