JuliusBrussee/caveman · error
kms: decode response
Error message
kms: decode %s response: %w
What it means
The KMS response body was read successfully but could not be decoded as the expected JSON shape into the output struct. The client unmarshals the limited body and wraps any json.Unmarshal failure with the operation name. HTTP 200 was returned but the payload is not valid JSON or has an unexpected schema.
Solutions
- Dump the raw response body (curl the endpoint) to see what is actually being returned — HTML means wrong endpoint/proxy interception.
- Pin and align client and server KMS API versions; check the server changelog for response schema changes.
- Verify the output struct tags match the server's JSON field names.
- Ensure Content-Type of the response is application/json before parsing.
- If the server is behind an auth gateway, check that requests aren't being redirected to a login page with a 200.
Example fix
// before
var out kms.DecryptOutput
if err := client.Decrypt(ctx, ct); err != nil { /* kms: decode Decrypt response: invalid character '<' */ }
// after
resp, _ := http.Get(cfg.KMSEndpoint + "/health") // sanity check endpoint returns JSON
ct := resp.Header.Get("Content-Type")
if !strings.HasPrefix(ct, "application/json") {
log.Fatalf("KMS endpoint not returning JSON (content-type %q); check URL/proxy", ct)
} Defensive patterns
Strategy: type-guard
Validate before calling
// probe the endpoint returns JSON before using the client
resp, _ := http.Get(cfg.KMSEndpoint + "/health")
if resp != nil && !strings.HasPrefix(resp.Header.Get("Content-Type"), "application/json") {
return errors.New("KMS endpoint is not serving JSON — check URL/proxy")
} Type guard
func looksLikeJSON(b []byte) bool {
s := bytes.TrimSpace(b)
return len(s) > 0 && (s[0] == '{' || s[0] == '[')
} Try / catch
if err != nil && strings.HasPrefix(err.Error(), "kms: decode ") {
// capture raw body for diagnosis; check server/client API version skew
return fmt.Errorf("unexpected KMS response schema: %w", err)
} Prevention
- Keep client structs and server API version in lockstep; test against the real server in CI.
- Check response Content-Type before parsing.
- Beware auth gateways returning 200 HTML login pages behind misconfigured proxies.
- Add a JSON schema test for KMS responses on every server upgrade.
When it happens
Trigger: Encrypt or Decrypt calls where the endpoint returns non-JSON (HTML error pages behind a 200, plaintext), or the KMS API response schema changed/does not match the client's output struct fields/types.
Common situations: Misrouted endpoint returning a login page or SPA HTML with status 200, KMS server version upgrade changing field names/types, a proxy intercepting and rewriting responses, or version skew between client structs and server API.
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 JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/ea08ff114cfb8225.
Report an issue: GitHub.
Appendix: source
Thrown at shared/platform/kms/kms.go:377
req.Header.Set("x-auth-token", c.token)
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("kms: %s request failed: %w", operation, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 32<<10))
return fmt.Errorf("kms: %s returned HTTP %d", operation, resp.StatusCode)
}
data, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBytes+1))
if err != nil {
return fmt.Errorf("kms: read %s response: %w", operation, err)
}
if len(data) > maxResponseBytes {
return fmt.Errorf("kms: %s response exceeds limit", operation)
}
if err := json.Unmarshal(data, output); err != nil {
return fmt.Errorf("kms: decode %s response: %w", operation, err)
}
return nil
}
View on GitHub (pinned to 3ee70a1026)