Skip to content

Only send X-Amz-Target for the awsJson protocols - #2009

Merged
kubukoz merged 2 commits into
disneystreaming:series/0.19from
kubukoz:aws-amz-target-json-only
Sep 29, 2026
Merged

kubukoz merged 2 commits into
disneystreaming:series/0.19from
kubukoz:aws-amz-target-json-only

Conversation

@kubukoz

@kubukoz kubukoz commented Sep 29, 2026

Copy link
Copy Markdown
Member

Follow-up to #1596, same family of SES v2 breakage.

Problem

AwsSigning adds X-Amz-Target: <Service>.<Operation> to every request it signs, whatever the service's protocol. Only awsJson 1.0/1.1 uses that header, because every awsJson request goes to POST /. restJson1 and restXml route on the method and path, and the query protocols route on Action.

It isn't harmless on those protocols. A server that routes on the header first takes the request for awsJson and rejects it. fakecloud does exactly that, so with 0.19.13 every SES v2 (restJson1) SendEmail comes back as:

400 UnknownOperationException: The operation SimpleEmailService_v2.SendEmail is not recognized.

The header is also signed, so a user can't strip it in a middleware without invalidating the signature.

Fix

transformClient resolves the protocol from the service hints (AwsProtocol, the same resolution AwsClient uses to pick codecs). It passes signingFunction an amzTarget: Option[String], which is Some only for awsJson1_0/1_1. The header stays in the fixed, ordered list of signed headers when it's present, so awsJson canonical requests are byte-for-byte unchanged.

Tests

  • New RestJsonSes fixture in aws_example.smithy, mirroring SES v2 in full (restJson1, endpointPrefix: email, sigv4 ses), with regenerated bootstrapped code.
  • AwsEndpointAndSigningNameTest:
    • restJson1: X-Amz-Target is neither sent nor listed in SignedHeaders. Fails without the fix (checked by stashing the signer change).
    • awsJson (DynamoDB): the header is still sent and signed as DynamoDB_20120810.ListTables, so this can't regress the protocols that need it.
  • The AwsSignatureTest parity test against the AWS SDK signer passes Some(target), so it checks the same thing as before.
  • aws-http4s/test and aws-http4s3/test both pass: 698/698 each.

Found via kubukoz/invoicer#11, where it's the last blocker for moving SES onto the generated client.

🤖 Generated with Claude Code

kubukoz and others added 2 commits September 29, 2026 02:18
The signer added X-Amz-Target to every request. Only awsJson routes on it;
restJson1 and restXml route on the method and path, the query protocols on
the Action parameter. A server that routes on the header first (fakecloud)
therefore rejected every SES v2 call with UnknownOperationException.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kubukoz
kubukoz marked this pull request as ready for review September 29, 2026 00:57
@kubukoz

kubukoz commented Sep 29, 2026 •

Copy link
Copy Markdown
Member Author

End-to-end check against real AWS, with this branch's aws-http4s published locally and placed ahead of the 0.19.13 jar on a downstream app's classpath:

  • Real SES v2, this branch: one SendEmail call through the generated client, sent between two verified identities. The call used simple content with a UTF-8 subject and body and one attachment (the attachment field SES v2 added to simple content). SES accepted it, and the email arrived.
  • Real SES v2, unpatched 0.19.13: the same call also succeeds (200 OK), even though the request carries a signed X-Amz-Target: SimpleEmailService_v2.SendEmail. So real SES ignores the header. Since Resolve AWS signing name and endpoint host independently #1596 the credential scope is ses, which was what actually broke real AWS.
  • fakecloud (0.45.1): on 0.19.13 the same call is rejected with UnknownOperationException: The operation SimpleEmailService_v2.SendEmail is not recognized, because fakecloud routes on X-Amz-Target first. With this branch it goes through and fakecloud records the delivered email.
  • DynamoDB (awsJson1_0): the same app's DynamoDB integration suite passes against fakecloud with this branch, so the awsJson side, which still sends the header, didn't regress.

So on real AWS this is about correctness, not a failure: the header is meaningless for restJson1. The visible break is in emulators that route the way the awsJson spec says they should.

@mergify

mergify Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

This pull request does not currently match the merge queue conditions, so it cannot be queued from here. The box comes back if it matches again.

@kubukoz
kubukoz merged commit b362cd6 into disneystreaming:series/0.19 Sep 29, 2026
52 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants