How to fix SignatureDoesNotMatch on an S3 presigned URL

What the error means

SignatureDoesNotMatch means S3 got your request, rebuilt the signature from what it received, and got a different value from the one in your URL. The signing key itself is usually fine. Most of the time, something in the request S3 received is different from the request you signed: a header, the path encoding, the method or the region.

You don't have to guess. S3 tells you what it signed.

Step 1: Read the error XML in full

A failed S3 request returns XML like this (shortened):

`xml

<Error>

<Code>SignatureDoesNotMatch</Code>

<Message>The request signature we calculated does not match the signature you provided...</Message>

<AWSAccessKeyId>AKIA...</AWSAccessKeyId>

<StringToSign>AWS4-HMAC-SHA256

20261016T120000Z

20261016/us-east-1/s3/aws4_request

...</StringToSign>

<CanonicalRequest>PUT

/my-bucket/report%202026.pdf

X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;...

host:s3.amazonaws.com

host

UNSIGNED-PAYLOAD</CanonicalRequest>

</Error>

`

The CanonicalRequest and StringToSign blocks are S3's version. Your SDK or code built its own version when it signed. The bug is wherever the two differ.

Tip: browsers often show only the first line. Open the network tab, or run the request with curl -i so you see the whole body.

Step 2: Get your own canonical request

Most SDKs can log what they signed:

The canonical request has six parts, one per line:

`

HTTP method

URI path (encoded)

query string (sorted, encoded)

canonical headers (lowercase name:value, one per line)

signed header names (semicolon-separated)

payload hash (UNSIGNED-PAYLOAD for most S3 presigned URLs)

`

Step 3: Compare line by line

Put the two versions side by side. Check these first:

1. Method. A URL signed for GET won't work for PUT. A URL signed for PUT won't work for POST.

2. Path encoding. Spaces must be %20, not +. Watch for +, #, ? and non-ASCII characters in object keys. S3 doesn't double-encode the path. If your key looks encoded twice (%2520), you encoded it before passing it to the SDK.

3. Signed headers. If content-type is in the signed header list, the upload must send the exact same Content-Type. This is the most common cause on browser PUT uploads: the URL was signed with image/png and the browser sent image/jpeg, or the reverse, or fetch added nothing at all.

4. Host. Virtual-hosted (bucket.s3.region.amazonaws.com) and path-style (s3.region.amazonaws.com/bucket) URLs give different canonical requests. A proxy or CDN that rewrites Host breaks the signature.

5. Region in the credential scope. The scope line (20261016/us-east-1/s3/aws4_request) must name the bucket's region.

6. Query string. Every parameter is part of the signature. Adding ?download=1 after signing breaks it.

Step 4: Rule out the key itself

If every line of the canonical request matches and the signature still differs, look at the credentials:

Common pitfalls

A faster way to do the diff

You can do all of this by hand with a text diff tool. We make Presign Pad, a free browser tool that does the comparison in one step: paste the SignatureDoesNotMatch XML and it shows a line-by-line diff against the canonical request and string to sign it computed for your request.

Try it