A presigned URL is used with a different method, header or URL
CommonThe URL was generated for a PUT with one Content-Type and the upload sends another (browsers and libraries set their own), or the URL was altered: re-encoded, truncated, or split by the shell because it was not quoted.
Confirm
grep -o 'X-Amz-SignedHeaders=[^&]*' <<< "$URL"
content-type (or another header) listed as signed, while the request sends a different value or none
Fix
- Send exactly the Content-Type the URL was generated with, or generate it without a content type if clients cannot control the header.
- Use the URL byte for byte: no re-encoding, and in a shell quote it: curl -X PUT -T FILE -H "Content-Type: TYPE" "$URL".
- Generate the URL for the bucket's own Region, and a new one if it has expired.
curl -X PUT -T FILE -H "Content-Type: application/octet-stream" "$PRESIGNED_URL"
The Content-Type must equal the one used to generate the URL, and the URL must be quoted.