JuliusBrussee/caveman · error

awscreds: returned an unparseable response

Error message

awscreds: %s returned an unparseable response

What it means

credentialsFromJSON unmarshals the body returned by the ECS container metadata endpoint or EC2 IMDS into credentialJSON. If json.Unmarshal fails, the response was not the expected credential JSON object, and the error reports 'unparseable response' for the source (container or imds). The library throws this because continuing without validating the payload shape would produce zero-valued credentials.

Solutions

  1. Log/capture the raw body (temporarily) to see what the endpoint actually returned; an HTML page indicates interception by a proxy or wrong endpoint.
  2. Verify AWS_CONTAINER_CREDENTIALS_RELATIVE_URI points to the ECS agent's credentials path and that the ecs agent is running.
  3. If AWS_EC2_METADATA_SERVICE_ENDPOINT is overridden, reset it to http://169.254.169.254 and retry.
  4. Ensure no proxy env vars (HTTP_PROXY) route metadata traffic; use NO_PROXY=169.254.169.254.
Defensive patterns

Strategy: validation

Validate before calling

func looksLikeJSONBody(b []byte) bool {
    t := bytes.TrimSpace(b)
    return len(t) > 0 && (t[0] == '{' || t[0] == '"')
}

Try / catch

body, err := p.doJSON(client, req, "imds credentials")
if err != nil {
    return err
}
if !looksLikeJSONBody(body) {
    return fmt.Errorf("metadata endpoint returned non-JSON (%d bytes); check proxy/NO_PROXY", len(body))
}

Prevention

When it happens

Trigger: fromContainer or fromIMDS receives a 2xx body that is not valid JSON: an HTML error/login page from a proxy, a plain-text message, an empty body, or truncation at the maxBody limit.

Common situations: A corporate proxy or VPN captive portal intercepting requests to 169.254.169.254 and returning an HTML page with 200 OK; AWS_CONTAINER_CREDENTIALS_RELATIVE_URI pointing to a non-metadata HTTP service; custom AWS_EC2_METADATA_SERVICE_ENDPOINT pointing at the wrong server.

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/d444323297aafa41. Report an issue: GitHub.

Appendix: source

Thrown at shared/platform/awscreds/awscreds.go:621

	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("awscreds: %s request failed: %w", what, err)
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(io.LimitReader(resp.Body, maxBody))
	if err != nil {
		return nil, fmt.Errorf("awscreds: read %s response: %w", what, err)
	}
	if resp.StatusCode < 200 || resp.StatusCode > 299 {
		return nil, fmt.Errorf("awscreds: %s: http %d", what, resp.StatusCode)
	}
	return body, nil
}

func credentialsFromJSON(body []byte, source string) (*result, error) {
	var parsed credentialJSON
	if err := json.Unmarshal(body, &parsed); err != nil {
		return nil, fmt.Errorf("awscreds: %s returned an unparseable response", source)
	}
	if parsed.Code != "" && !strings.EqualFold(parsed.Code, "Success") {
		return nil, fmt.Errorf("awscreds: %s returned code %q", source, parsed.Code)
	}
	expires, err := parseExpiry(parsed.Expiration)
	if err != nil {
		return nil, fmt.Errorf("awscreds: %s credential expiry: %w", source, err)
	}
	return &result{
		creds: awssig.Credentials{
			AccessKeyID:     strings.TrimSpace(parsed.AccessKeyID),
			SecretAccessKey: strings.TrimSpace(parsed.SecretAccessKey),
			SessionToken:    strings.TrimSpace(parsed.Token),
		},
		expires: expires,
		source:  source,
	}, nil
}

View on GitHub (pinned to 3ee70a1026)