After many frustrating hours, I realized that serializing the presigned URL using Gson and then printing the resulting JSON was encoding certain characters. For example, this is part of the presigned URL prior to using Gson and printing it in the logs. I was able to use this presigned URL to upload a file successfully:
https://my-bucket.s3.amazonaws.com/f0329e43-c5ee-4151-87c5-c6736b5c7242?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20250912T023806Z...
And this is how it was encoded after using JSON and printing it to the logs:
{
"statusCode": 201,
"headers": {},
"body": "{\n \"id\": \"3da30011-8c20-4463-9f59-a31033276d0e\",\n \"version\": 0,\n \"presignedUrl\": \"https://my-bucket.s3.amazonaws.com/3da30011-8c20-4463-9f59-a31033276d0e?X-Amz-Algorithm\\u003dAWS4-HMAC-SHA256\\u0026X-Amz-Date\\u003d20250912T023806Z...\"\n}"
}
Beginners mistake since I'm new to presigned URLs and didn't know to look for this.