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

  1. Dump the raw response body (curl the endpoint) to see what is actually being returned — HTML means wrong endpoint/proxy interception.
  2. Pin and align client and server KMS API versions; check the server changelog for response schema changes.
  3. Verify the output struct tags match the server's JSON field names.
  4. Ensure Content-Type of the response is application/json before parsing.
  5. 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

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)