The parts of an S3 presigned URL
A presigned URL is a request signed with Signature Version 4 (SigV4) in advance: the signature and everything it needs travel in the query string, so whoever has the URL can make that one request without AWS credentials of their own - with the permissions of whoever signed it.
| Parameter | What it holds |
|---|---|
X-Amz-Algorithm | AWS4-HMAC-SHA256 for SigV4 |
X-Amz-Credential | The access key ID and the credential scope: AKID/20260115/eu-west-1/s3/aws4_request - date, Region, service |
X-Amz-Date | When it was signed, in UTC (20260115T090000Z) |
X-Amz-Expires | Seconds it stays valid after X-Amz-Date, up to 604800 (7 days) |
X-Amz-SignedHeaders | The headers the request must send as signed - at least host |
X-Amz-Security-Token | The session token, when temporary credentials signed it |
X-Amz-Signature | The signature itself: an HMAC-SHA256 only the secret key can produce or check |
The signature covers a canonical request: the HTTP method, the path, the sorted query string without the signature, the signed headers with their values, and the payload hash - UNSIGNED-PAYLOAD for a presigned URL. Change any of it, and the URL stops working.
Why AWS answers SignatureDoesNotMatch
AWS rebuilds the canonical request from what it received, signs it with the secret key of the access key ID in the request and compares. S3's error response shows its StringToSign and CanonicalRequest; other services put them in the message. The debugger lays them out, so you can compare them with what your client signed - the AWS SDKs log theirs at debug level.
| Symptom | Most common cause |
|---|---|
A browser or fetch upload to a presigned PUT URL fails, the same file works with curl | The URL signs content-type, and the browser sends another value or adds a charset |
| Uploads through presigned URLs started failing in 2025 after an SDK update | The SDK's default checksum is signed into the URL (x-amz-checksum-crc32): presign with requestChecksumCalculation: "WHEN_REQUIRED" |
| A PUT to a URL that works in the browser fails | It was presigned for GetObject; an upload needs PutObject |
Files with spaces, + or non-ASCII characters in the key fail | The URL was encoded again, or a + was read as a space, after signing |
| Every request fails, even from the CLI | The secret access key does not belong to the access key ID |
| "the region 'us-east-1' is wrong; expecting 'eu-west-1'" | The client signed for another Region than the bucket's |
How long a presigned URL lasts
X-Amz-Date plus X-Amz-Expires, at most 7 days - and never longer than the credentials that signed it. Signed with temporary credentials (an access key ID starting with ASIA, with an X-Amz-Security-Token), the URL dies with them: a Lambda function's or an EC2 instance's role credentials last hours, an assumed role's session 1 hour by default, and a URL made in the S3 console at most 12 hours. For a URL that lasts the full 7 days, sign with an IAM user's long-term access key.
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
// The bucket's Region, and no checksum of an empty body signed into upload URLs.
const s3 = new S3Client({ region: "eu-west-1", requestChecksumCalculation: "WHEN_REQUIRED" });
const url = await getSignedUrl(s3, new PutObjectCommand({
Bucket: "example-bucket",
Key: "uploads/report.pdf",
ContentType: "application/pdf", // the upload must send exactly this Content-Type
}), { expiresIn: 900 });Frequently asked questions
Is it safe to paste a presigned URL here?
The URL is read in your browser: nothing you paste is uploaded, processed on a server or stored. The page only counts that the debugger was used and which cause it named first, never the URL or the error. A presigned URL still grants access until it expires, so paste only into pages you trust - and prefer an expired one.
Can it check whether the signature is right?
No - that needs the secret access key, which neither the URL nor the error contains, and which should never be pasted into a web page. It reads everything around the signature, which is where the cause almost always is.
Why does my presigned URL work in the browser but not from my app, or the other way round?
The two send different requests: another method (a GET from the address bar, a PUT from the app), other headers (Content-Type, a charset, x-amz-* headers), or a URL encoded again by the app's HTTP library. Paste the URL with the error response the failing side got, and compare the canonical request AWS shows with what was signed.
What is SigV4a?
A variant of SigV4 with an asymmetric (ECDSA) signature valid in a set of Regions, used by S3 Multi-Region Access Points. Its algorithm is AWS4-ECDSA-P256-SHA256, its credential scope has no Region, and X-Amz-Region-Set lists the Regions instead.
References
AWS Signature Version 4 for API requests
Troubleshoot Signature Version 4 signed requests
Authenticating requests: using query parameters (S3)
Sharing objects with presigned URLs
Data integrity protections for Amazon S3 (AWS SDKs)