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&...
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:
- AWS CLI: add
--debug. The output includes the canonical request and string to sign. - boto3: turn on debug logging for
botocore.auth. - Your own code: print the canonical request before you hash it.
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:
- The secret key doesn't belong to the access key ID shown in
AWSAccessKeyId. - There's stray whitespace or a newline in the secret (common when it's read from an env file).
- You used temporary credentials but didn't include the session token (
X-Amz-Security-Token).
Common pitfalls
- Frontend adds headers. Some HTTP libraries add
Content-Typeor other headers on their own. Set them yourself and make them match what you signed. - Copying the URL through a tool that re-encodes it. Chat apps, email clients and some loggers turn
%2Finto/or escape&. Test with the raw URL. - Signing on one host, sending to another. A URL signed for the S3 endpoint fails if you send it through a CloudFront domain without a matching setup.
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.