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

  1. Log env.Body verbatim (it is short) - the content immediately identifies HTML vs wrong-schema vs BOM
  2. errors.As(*json.SyntaxError) means non-JSON; *json.UnmarshalTypeError means schema drift - compare fields against wecomUploadMediaInitResponse/FinishResponse
  3. Align picoclaw and gateway versions
  4. 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

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


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