Using file upload in Phoenix Liveview with Cloudflare R2

Hello there,
I want to upload the files to the Cloudflare R2 with the liveview standard form. Has anybody done it before and could help me with the setup? Currently if I try to upload new file, the url seems to be working correctly, but I get 400 response from cloudflare, saying Bad Request. It seems like some authorization issues. To setup the standard image upload I mostly followed Chris Mccord deep dive to uploads, and liveview documentation. Cloudflare say R2 is compatible with S3 API, although I can’t find any examples online of setuping this, so this is how I’m doing it:

def params_with_image(socket, params) do
    path =
      socket
      |> consume_uploaded_entries(:image, fn _meta, entry ->
        {:ok, Path.join(@r2_url, filename(entry))}
      end)
    path = List.first(path)

    IO.inspect(path)
    Map.put(params, "image_upload", path)
  end

efp presign_upload(entry, socket) do
    uploads = socket.assigns.upload
    config = %{
      region: "auto",
      access_key_id: System.fetch_env!("AWS_ACCESS_KEY_ID"),
      secret_access_key: System.fetch_env!("AWS_SECRET_ACCESS_KEY")
    }

    {:ok, fields} =
      SimpleS3Upload.sign_form_upload(config, @r2_bucket,
        key: filename(entry),
        content_type: entry.client_type,
        max_file_size: uploads[entry.upload_config].max_file_size,
        expires_in: :timer.hours(1)
      )

    meta = %{
    uploader: "S3",
    key: filename(entry),
    url: @r2_url,
    fields: fields}
    {:ok, meta,

It’s a bit late for the reply, but the reason it doesn’t work is because Cloudflare R2 does not support POST via presigned url yet. Here’s the doc:

POST, which performs uploads via native HTML forms, is not currently supported.

So, instead of a POST, a PUT request can be used. You can use ExAws.S3 to generate the presigned url with PUT:

def presigned_put_upload(key) do
  ExAws.Config.new(:s3)
  |> ExAws.S3.presigned_url(:put, @bucket, key)
end

Then in your presign_upload(..):

defp presign_upload(entry, socket) do
    # ...

    {:ok, url} = presigned_put_upload(filename(entry))

    meta = %{
      uploader: "S3",
      key: filename(entry),
      url: url,
    }
    {:ok, meta, socket}
end

All this is to allow us to pass the presigned URL with PUT method to the JS side. In the JavaScript side, you’ll need to make some changes, similar to below:

Uploaders.S3 = function(entries, onViewError){
   entries.forEach(entry => {
     let formData = new FormData()
     let {url, fields} = entry.meta
-    Object.entries(fields).forEach(([key, val]) => formData.append(key, val))
-    formData.append("file", entry.file)

     let xhr = new XMLHttpRequest()
     onViewError(() => xhr.abort())
     xhr.onload = () => xhr.status === 204 ? entry.progress(100) : entry.error()
     xhr.onerror = () => entry.error()
     xhr.upload.addEventListener("progress", (event) => {
-      if(event.lengthComputable){
+      if(event.lengthComputable) {
         let percent = Math.round((event.loaded / event.total) * 100)
-        if(percent < 100){ entry.progress(percent) }
+.       # For some reason, when using PUT, we need to change `<` to `<=`, 
+        # else, the progress will stuck, never reach 100% and the live view side
+        # won't be triggered.
+        if (percent <= 100) { entry.progress(percent) } 
       }
     })

-    xhr.open("POST", url, true)
-    xhr.send(formData)
+    xhr.open("PUT", url, true)
+    xhr.send(entry.file)
   })
}

Hope it helps!

2 Likes