Upload Step 2 fails with a 200 status

Iā€™m following the documentation and examples to upload a file. (I am not able to use the official JS or ruby libraries, so I am doing it with HTTPS).

Step 1 works, giving me a URL, request_headers, and an id.
When using the URL and request_headers from Step 1 to perform Step 2, instead of getting an error or the expected 202 status, I get this:

{status: 200, headers: %{"content-length" => ["0"], "date" => ["Fri, 31 Jan 2025 07:10:24 GMT"], "etag" => ["removed"], "server" => ["AmazonS3"], "x-amz-id-2" => ["removed"], "x-amz-request-id" => ["removed"], "x-amz-server-side-encryption" => ["AES256"], "x-amz-version-id" => ["removed"]}, body: "", trailers: %{}, private: %{}}

The body is empty, the content-length is zero, and the status is 200.

The request_headers from Step 1 are empty, so the only headers Iā€™m sending to step 2 is {ā€œContent-Typeā€, ā€œimage/jpegā€} with the image binary

What am I doing wrong?

Here is the request iā€™m making for step 2:

PUT step1.url HTTP/1.1
Content-Type: image/png
<FILE_BINARY_CONTENT>

curl gives me the same response:

curl -v -X PUT "url from step1" \
     -H "Content-Type: image/jpeg" \
     --data-binary "@/path/to/image.jpeg"

Hello @Chris_Richards and welcome to the community!

Make sure youā€™re sending the exact URL (including all query parameters) and all headers returned by step 1. Even if request_headers is empty, you still need to include any query string (e.g. ?x-amz-acl=...) in the PUT request. S3 often responds with 200 and an empty body if the upload succeeds. As long as you use the full URL and headers from step 1, you can move on to step 3 once you see that 200.

The responseā€™s content-length and empty body refer to the response from S3, not your upload payload. A 200 with an empty body from S3 is normal for a successful PUT. If you suspect your file isnā€™t actually being sent, confirm that the query string from step 1 is fully included, youā€™re setting all the returned headers, and your code is streaming the binary file contents without any transformation (not multipart, no extra JSON wrapping, etc.). Also verify with debug logs or by performing a GET/HEAD on the final S3 URL to confirm the file was uploaded.

1 Like

I verified Iā€™m sending the exact URL and curl says it sent the file.

But if I do curl -X GET URL I get an Access Denied message. My token is for Content Management API and I am able to upload new records without issue. Its just the File Upload thatā€™s doing this.

Step 3, https://site-api.datocms.com/uploads returns 200 with a job and id. But when I try to use that id with https://site-api.datocms.com/job-results/#{job_id} I get a ā€œ404 NOT_FOUNDā€,

Iā€™m not sure what Iā€™m missing, is there an extra permission I need outside of the Content Management API Token that I use for records?

Hey @Chris_Richards,

Can you please log the exact request & response chain from Curl and send it to us at support@datocms.com (or DM us here, or post it publicly after stripping the credentials)? Itā€™s hard to troubleshoot this without seeing the exact payload going over the wire.

I suspect that whatā€™s going on is that the AWS presigned URL from step 1 isnā€™t getting sent to S3 correctly. That signed URL has nothing to do with your Dato tokens, but is rather a single-use secret generated by S3 (in step 1) to allow you to PUT a file directly to them ā€“ entirely outside of Dato. Itā€™s a secret token that you send as part of the URL query string.

Once that file is successfully uploaded to S3, step 3 just ā€œassociatesā€ it with a upload record inside Dato. The 404 not found for a job request means itā€™s still processing (as documented in step 4), so you could keep polling until it 200s or just ignore it and wait a few seconds.

In a bit more detailā€¦

Please make sure the attributes.url from Step 1ā€™s response is being used, in its entirety, for PUTing the image to S3. It should look something like this:

https://datosrl-images.s3.eu-west-1.amazonaws.com/150894/1738359901-filename.png?x-amz-acl=private&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=SECRET%2F20250131%2Feu-west-1%2Fs3%2Faws4_request&X-Amz-Date=20250131T214501Z&X-Amz-Expires=900&X-Amz-SignedHeaders=host&X-Amz-Signature=SIGNATURESTRING

If you then PUT the image to S3 and get back a 200 with an empty body, thatā€™s totally normal (as Marcelo said).

Then in step 3, you need to send a body like:

{"data":{"type":"upload","attributes":{"path":"/150894/1738359901-screenshot-000977.png"},"relationships":{}}}

Where the path is the data.id from Step 1ā€™s Dato response (not step 2 from AWS)

If that went through right, you should get a 202 Accepted, not a 200.

Then if you poll for the job result, it will 404 until it completes, but you donā€™t really need to worry about that. The image should just show up in the media area after a few seconds. If it doesnā€™t, something is failing along the way, but itā€™s hard to say what exactly without seeing your exact requests & responses.


Also, if you go to the media area in your project and open the browserā€™s network inspector and then try to upload an image, you can see the exact requests & responses being sent. The media area does it the same way our client does, or the way you would if you were uploading via raw HTTP. If you right-click an entry and copy it as curl, you can re-use that request elsewhere and compare it to what you have now.

In the inspector:

  • Step 1 would be a POST to https://site-api.datocms.com/upload-requests
  • Step 2 would be a PUT to https://datosrl-images.s3.eu-west-1.amazonaws.com/ (or stepā€™s URL)
  • Step 3 would be a POST to https://site-api.datocms.com/uploads
  • (I think we skip step 4 in the media area and instead just refresh the list of uploads)