sipeed/picoclaw · error

decode JSON response: %w

Error message

decode JSON response: %w

What it means

The WeCom endpoint returned HTTP 200 but the body was not valid JSON for the target struct. Typical causes: the server (or an intercepting proxy/captive portal) returned an HTML page with 200, an empty body, a BOM prefix, or a payload whose types mismatch (string where a number is expected). This error propagates wrapped inside 124/131 depending on which call site hit it.

Source

Thrown at cmd/picoclaw/internal/auth/wecom.go:411

		return err
	}

	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, readErr := io.ReadAll(io.LimitReader(resp.Body, 8192))
		if readErr != nil {
			return fmt.Errorf("unexpected status %s", resp.Status)
		}
		return fmt.Errorf("unexpected status %s: %s", resp.Status, strings.TrimSpace(string(body)))
	}

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

	return nil
}

func wecomPlatformCode() int {
	switch runtime.GOOS {
	case "darwin":
		return 1
	case "windows":
		return 2
	case "linux":
		return 3
	default:
		return 0
	}
}

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. curl the endpoint from the same machine/network and inspect what the body actually is
  2. Connect from an unrestricted network or disable the interfering proxy/VPN
  3. Complete any captive-portal login, then retry the WeCom login
  4. Update picoclaw if the relay's JSON schema changed (type mismatches decode as JSON errors)
Defensive patterns

Strategy: try-catch

Validate before calling

// Sniff the body before decoding when a proxy is suspected
probe, _ := io.ReadAll(io.LimitReader(resp.Body, 1))
if len(probe) == 1 && probe[0] == '<' {
	return errors.New("received HTML instead of JSON — captive portal or proxy interception")
}

Type guard

func isJSONDecodeErr(err error) bool {
	var syn *json.SyntaxError
	var typeErr *json.UnmarshalTypeError
	return errors.As(err, &syn) || errors.As(err, &typeErr)
}

Try / catch

if err := json.NewDecoder(resp.Body).Decode(out); err != nil {
	if isJSONDecodeErr(err) {
		// likely proxy/captive portal: advise network change instead of retrying blindly
	}
	return fmt.Errorf("decode JSON response: %w", err)
}

Prevention

When it happens

Trigger: Captive portal/Wi-Fi sign-in page served instead of the API response; transparent proxy rewriting responses; relay returning an empty 200 during degradation; upstream response schema type change (string errcode vs int) breaking Unmarshal.

Common situations: Hotel/airport Wi-Fi intercepting HTTPS via DNS tricks; corporate SSL-inspection proxy injecting its own 200 page; relay partial outage; picoclaw's response structs outdated versus relay.

Related errors


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