JuliusBrussee/caveman · error

bedrock: exact request payload hash is unavailable

Error message

bedrock: exact request payload hash is unavailable

What it means

SigV4 must sign the SHA-256 hash of the exact request body. If the request has a body but no GetBody function, the proxy cannot re-read the bytes to hash them, so it fails closed rather than signing a wrong payload hash (which AWS would reject with a signature mismatch).

Solutions

  1. Build the request body from []byte or bytes.Buffer/strings.Reader so http.NewRequestWithContext automatically populates GetBody
  2. If the body is already a stream, buffer it once (io.ReadAll) and reset req.Body with http.NoBody-free bytes.NewReader(raw), which also sets GetBody
  3. Ensure no middleware replaces req.Body after construction without also setting req.GetBody

Example fix

// before
req, _ := http.NewRequestWithContext(ctx, "POST", url, io.Reader(clientStream))
// after
raw, _ := io.ReadAll(clientStream)
req, _ := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(raw)) // GetBody set automatically
Defensive patterns

Strategy: validation

Validate before calling

if req.Body != nil && req.GetBody == nil {
	return errors.New("request body must be replayable (bytes.Reader/bytes.Buffer) for SigV4 signing")
}

Prevention

When it happens

Trigger: requestPayloadHash is called on a request where req.Body != nil and req.GetBody == nil — typically a request body built from a stream/reader rather than bytes.Buffer/bytes.Reader/string, so http.NewRequestWithContext could not snapshot it.

Common situations: Constructing the proxied request with an io.Reader body (e.g. piping from the client connection) instead of a concrete byte slice; wrapping the body in a custom ReadCloser; earlier middleware replacing the body with a streaming reader.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


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

Appendix: source

Thrown at proxy/providers/bedrock/signing.go:181

		scope[2] == signingRegion(req, upstream) && scope[3] == service && scope[4] == "aws4_request" &&
		req.Header.Get("X-Amz-Security-Token") == creds.SessionToken &&
		(!strings.HasPrefix(creds.AccessKeyID, "ASIA") || creds.SessionToken != "")
}

// requestPayloadHash returns the hash of the exact post-transform wire body.
// Gateways install it in context after transforming. Direct adapter callers and
// verification probes can fall back to net/http's replayable GetBody contract.
// A non-replayable body without a bound hash fails closed instead of emitting a
// signature over bytes that may differ from the request on the wire.
func requestPayloadHash(ctx context.Context, req *http.Request) (string, error) {
	if hash, ok := providers.RequestPayloadHash(ctx); ok {
		return hash, nil
	}
	if req.Body == nil {
		return awssig.HashPayload(nil), nil
	}
	if req.GetBody == nil {
		return "", fmt.Errorf("bedrock: exact request payload hash is unavailable")
	}
	body, err := req.GetBody()
	if err != nil {
		return "", fmt.Errorf("bedrock: exact request payload hash is unavailable")
	}
	defer body.Close()
	raw, err := io.ReadAll(body)
	if err != nil {
		return "", fmt.Errorf("bedrock: exact request payload hash is unavailable")
	}
	return awssig.HashPayload(raw), nil
}

func credentialAuthKind(credential providers.Credential) (string, error) {
	kind := strings.ToLower(strings.TrimSpace(credential.AuthKind))
	switch kind {
	case "bedrock_api_key", "aws_access_keys":
		return kind, nil

View on GitHub (pinned to 3ee70a1026)