chenhg5/cc-connect · error

decode response: %w

Error message

decode response: %w

What it means

After getDownloadURL receives a 200 response from the DingTalk media download API, it decodes the JSON body into a downloadResponse struct. This error wraps any JSON decoding failure — malformed body, wrong content type (e.g. HTML error page behind a proxy), or an unexpected field layout.

Source

Thrown at platform/dingtalk/dingtalk.go:713

		return "", fmt.Errorf("create request: %w", err)
	}

	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("x-acs-dingtalk-access-token", token)

	resp, err := p.httpClient.Do(req)
	if err != nil {
		return "", fmt.Errorf("do request: %w", err)
	}
	defer func() { _ = resp.Body.Close() }()

	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("api returned status %d", resp.StatusCode)
	}

	var result downloadResponse
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return "", fmt.Errorf("decode response: %w", err)
	}

	if result.DownloadUrl == "" {
		return "", fmt.Errorf("empty downloadUrl in response")
	}

	return result.DownloadUrl, nil
}

func (p *Platform) getAccessToken() (string, error) {
	p.tokenMu.Lock()
	defer p.tokenMu.Unlock()

	// Return cached token if still valid
	if p.accessToken != "" && time.Now().Before(p.tokenExpiry) {
		return p.accessToken, nil
	}

View on GitHub (pinned to 4000b2338a)

Solutions

  1. Log the raw response body (before decoding) to see what DingTalk actually returned.
  2. If the body is HTML, a proxy/gateway is interfering — bypass or configure the proxy for api.dingtalk.com.
  3. Verify the DingTalk API schema: the field must be downloadUrl (camelCase) in the response.
  4. Retry once on transient decode failures, then surface the error to the user chat.
  5. Ensure resp.Body isn't already consumed by an earlier io.ReadAll in the same request path.

Example fix

// before: silent about body shape
var result downloadResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
    return "", fmt.Errorf("decode response: %w", err)
}

// after: capture raw body for diagnosis
raw, _ := io.ReadAll(resp.Body)
var result downloadResponse
if err := json.Unmarshal(raw, &result); err != nil {
    return "", fmt.Errorf("decode response %q: %w", string(raw), err)
}
Defensive patterns

Strategy: try-catch

Try / catch

url, err := p.getDownloadURL(ctx, code)
if err != nil {
    if strings.Contains(err.Error(), "decode response") {
        logRawBodyForDiagnosis() // or retry once
    }
    return err
}

Prevention

When it happens

Trigger: handleImageMessage / handleFileMessage / downloadAudio hit this when the download API returns HTTP 200 but a body that is not valid JSON or does not match the expected {"downloadUrl": ...} shape — commonly an HTML error page from a gateway, truncated response, or a changed API schema.

Common situations: Corporate proxy or CDN intercepting and returning an HTML login/captcha page, DingTalk returning an empty 200 body, Go version mismatch irrelevant but schema drift in DingTalk API responses, or TLS interception appliances re-writing responses.

Understand the failure class

Background: "failed to unmarshal" / json.Unmarshal errors: why parsing a response into a Go struct fails and how to fix it — this error's family across 23 libraries.

Related errors


AI-assisted analysis of chenhg5/cc-connect@4000b2338a (2026-09-06). Data as JSON: /api/errors/79e12e0e348efe37. Report an issue: GitHub.