JuliusBrussee/caveman · error

awscreds: returned code

Error message

awscreds: %s returned code %q

What it means

After successfully parsing the metadata JSON, credentialsFromJSON checks the optional 'Code' field. If it is present and not (case-insensitively) "Success", the metadata service reported an application-level error inside a 2xx response (e.g. "BadRequest", "ExpiredToken"), so the library refuses to return credentials. This catches IMDS/ECS responses where the error is carried in the body rather than the HTTP status.

Solutions

  1. Read the quoted code in the error message; look it up in AWS metadata-service error documentation (e.g. BadRequest means the request path/role is wrong).
  2. For a just-attached instance profile, wait a few seconds and retry until credentials propagate.
  3. Verify the role name used in the IMDS path matches a role listed at /latest/meta-data/iam/security-credentials/.
  4. If persistent, re-check the task/instance IAM configuration in the AWS console.
Defensive patterns

Strategy: retry

Validate before calling

var probe struct{ Code string `json:"Code"` }
if json.Unmarshal(body, &probe) == nil && probe.Code != "" && !strings.EqualFold(probe.Code, "Success") {
    // treat as transient for BadRequest right after role attach
}

Try / catch

creds, err := provider.Credentials(ctx)
if err != nil && strings.Contains(err.Error(), "returned code") {
    // log code, backoff and retry a limited number of times
    return retryWithBackoff(ctx, func() error { _, err = provider.Credentials(ctx); return err })
}

Prevention

When it happens

Trigger: fromContainer or fromIMDS gets JSON whose Code field is set to something other than Success, e.g. the IMDS credentials document returns Code="BadRequest" or an ECS agent error code embedded in a 200 response.

Common situations: Instance profile credentials not yet propagated right after attaching a role; ECS task role temporarily unavailable; requesting a role name that no longer exists while the service still returns 200 with an error code.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/8b5c6964e64f4322. Report an issue: GitHub.

Appendix: source

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

	}
	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
}

// parseExpiry maps an absent expiry to the zero time, which means "never
// refresh" to the cache.

View on GitHub (pinned to 3ee70a1026)