JuliusBrussee/caveman · error

awscreds: build request

Error message

awscreds: build %s request: %w

What it means

imdsGet wraps an error from http.NewRequestWithContext when building one of the two IMDS metadata GETs (role name, then credentials), labeled by the what parameter (e.g. "imds role", "imds credentials"). The library throws this when the endpoint URL is unparseable or the context is already dead.

Solutions

  1. Inspect the wrapped %w cause: fix net/url parse errors by correcting the endpoint; fix context errors by increasing the timeout.
  2. Set AWS_EC2_METADATA_SERVICE_ENDPOINT to a plain absolute http(s) URL with no extra path junk.
  3. Reuse a single healthy context with sufficient deadline for the whole IMDS sequence.
  4. Fall back to the default endpoint by unsetting the env var.

Example fix

// before
ctx, cancel := context.WithTimeout(ctx, time.Microsecond) // dead before GETs
// after
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
Defensive patterns

Strategy: try-catch

Validate before calling

deadline, ok := ctx.Deadline()
if !ok || time.Until(deadline) < 2*time.Second {
    return errors.New("context deadline too tight for IMDS token+get sequence")
}

Try / catch

if err != nil {
    var urlErr *url.Error
    if errors.As(err, &urlErr) && errors.Is(urlErr.Err, context.DeadlineExceeded) {
        // retry with a fresh, longer-lived context
    }
}

Prevention

When it happens

Trigger: The role-name or credentials URL derived from the IMDS base (base + /latest/meta-data/iam/security-credentials/...) is malformed, or ctx was canceled between the token fetch and the GET.

Common situations: Endpoint env var with trailing slash handled but other junk characters present; context timeout fired mid-sequence; custom endpoint missing scheme.

Understand the failure class

Background: "Invalid URL" errors: why new URL(), URI.parse, and reqwest::Url reject your string — missing scheme, whitespace, and bad path format — this error's family across 39 libraries.

Related errors


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

Appendix: source

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

			role = line
			break
		}
	}
	if role == "" {
		return nil, errors.New("awscreds: no instance profile role attached")
	}

	credBody, err := p.imdsGet(ctx, base+"/latest/meta-data/iam/security-credentials/"+url.PathEscape(role), token, "imds credentials")
	if err != nil {
		return nil, err
	}
	return credentialsFromJSON(credBody, "imds")
}

func (p *Provider) imdsGet(ctx context.Context, endpoint, token, what string) ([]byte, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
	if err != nil {
		return nil, fmt.Errorf("awscreds: build %s request: %w", what, err)
	}
	req.Header.Set("X-aws-ec2-metadata-token", token)
	return p.doJSON(p.link, req, what)
}

// doJSON performs one attempt and returns the bounded body. A non-2xx response
// is reported by status only: a metadata body holds credential material.
func (p *Provider) doJSON(client *http.Client, req *http.Request, what string) ([]byte, error) {
	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 {

View on GitHub (pinned to 3ee70a1026)