sipeed/picoclaw · error
decode wecom response body: %w
Error message
decode wecom response body: %w
What it means
json.Unmarshal of the upload ack body failed (media.go:664-665). The ack envelope had bytes, but they are not the expected JSON shape: an HTML/text error page, a schema that changed (renamed or retyped fields), or a UTF-8 BOM in front of the JSON. The %w preserves *json.SyntaxError / *json.UnmarshalTypeError for precise diagnosis.
Source
Thrown at pkg/channels/wecom/media.go:665
title = trimWeComBytes(title, 64)
if title == "" {
title = "video"
}
description = trimWeComBytes(description, 512)
return &wecomVideoContent{
MediaID: mediaID,
Title: title,
Description: description,
}
}
func decodeWeComEnvelopeBody[T any](env wecomEnvelope) (T, error) {
var out T
if len(env.Body) == 0 {
return out, fmt.Errorf("wecom response body is empty")
}
if err := json.Unmarshal(env.Body, &out); err != nil {
return out, fmt.Errorf("decode wecom response body: %w", err)
}
return out, nil
}
func (c *WeComChannel) uploadOutboundMedia(
ctx context.Context,
localPath, filename, contentType string,
part bus.MediaPart,
) (*wecomOutboundMedia, error) {
_ = ctx
contentType = detectLocalWeComContentType(localPath, contentType)
filename = ensureWeComOutboundFilename(filename, localPath, contentType)
data, err := os.ReadFile(localPath)
if err != nil {
return nil, fmt.Errorf("read media file: %w", err)
}View on GitHub (pinned to 49183d7e8d)
Solutions
- Log env.Body verbatim (it is short) - the content immediately identifies HTML vs wrong-schema vs BOM
- errors.As(*json.SyntaxError) means non-JSON; *json.UnmarshalTypeError means schema drift - compare fields against wecomUploadMediaInitResponse/FinishResponse
- Align picoclaw and gateway versions
- If the body is an errcode/errmsg error envelope, surface that error instead of the generic decode failure
Example fix
// before: decode any body shape
if err := json.Unmarshal(env.Body, &out); err != nil {
return out, fmt.Errorf("decode wecom response body: %w", err)
}
// after: recognize gateway error envelopes first
var probe struct {
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
}
if json.Unmarshal(env.Body, &probe) == nil && probe.ErrCode != 0 {
return out, fmt.Errorf("wecom api error %d: %s", probe.ErrCode, probe.ErrMsg)
}
env.Body = bytes.TrimPrefix(env.Body, []byte("\xef\xbb\xbf")) // strip BOM
if err := json.Unmarshal(env.Body, &out); err != nil {
return out, fmt.Errorf("decode wecom response body: %w (body=%q)", err, env.Body)
} Defensive patterns
Strategy: try-catch
Type guard
func isJSONErr(err error) bool {
var se *json.SyntaxError
var te *json.UnmarshalTypeError
return errors.As(err, &se) || errors.As(err, &te)
} Try / catch
var synErr *json.SyntaxError
var typErr *json.UnmarshalTypeError
switch {
case errors.As(err, &synErr):
// body was not JSON at all - dump it and check gateway health
log.Printf("non-JSON ack body: %q", rawBody)
case errors.As(err, &typErr):
// schema drift: field %s got %s, expected %s
log.Printf("schema mismatch: %s", typErr.Error())
default:
log.Printf("decode failed: %v", err)
} Prevention
- pin compatible channel/gateway versions; upgrade together
- strip UTF-8 BOMs before unmarshalling gateway responses
- log ack bodies on decode failure - it turns a mystery into a one-line diagnosis
When it happens
Trigger: upload init/finish ack body is HTML or plain text; server sends {"errcode":...,"errmsg":...} instead of the expected upload_id/media_id shape; number-vs-string type drift after a gateway version change; BOM-prefixed JSON.
Common situations: Custom WeCom gateways fronting the real API; version skew between picoclaw and the bridge; gateways that wrap errors in the ack body instead of the envelope status.
Related errors
- wecom response body is empty
- wecom upload init returned empty upload_id
- wecom upload finish returned empty media_id
- ${label} must be a JSON object.
- ${label}.${key} must be a string.
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/9889c8d407f02463.
Report an issue: GitHub.