Stuck on "Saving..." with no terminal feedback?

I don’t normally like asking for help with things because I prefer figuring them out myself, but I’m kind of stumped at the moment because I’m getting no feedback for my error from the terminal so I have no leads to go on.

For context I’ve been trying to figure out uploads to S3 for the last few days and have worked my way through Chris McCord’s upload video from a couple of years ago alongside External Uploads — Phoenix LiveView v0.18.11

Uploading locally to my “uploads” folder works fine, but when I add &presign_upload/2 to allow_uploads, every time I click the “save” button I just get the phx-disable error where it displays “Saving…” and does nothing. There’s no terminal output so I have no idea what I’m doing wrong.
I can still submit posts without images attached as well regardless of the presign. I should also add for additional context on my set up, I was able to upload files easily using ExAWS in the controller, so I know my bucket and config are set up. I’ve also updated the sha256 function on Chris’ simples3uplaod form.

So my question is:
What is the best way to deal with a form that just shows “Saving…” and nothing on the terminal? Logger and Inspect only seem to work when things are actually in motion, but I’m currently stuck it seems. The only way I can get a terminal output seems to be from intentionally breaking things to fault find them.
For example if I delete the region from my config in the presign_upload function it will throw an error saying its missing, so I know the function is at least being called. It’s kind of hard to fault find like this though

If there are any common causes for “saving…” or no terminal outputs, or anyways to easily fault find them advice would be aprreciated.

Also on a different note, is it impossible to use EXAWS.put_object in liveview? I find it really weird how easy it is to use in a controller, but how impossible I’ve been finding it in liveview

Thanks

Presigned upload are a mechanic to skip the server by directly uploading to your cloud provider from the browser. So understandably the server can’t help you figure out what’s wrong, as it’s not involved at all. You’d need to look at the JS uploader code you’re using, the browser console and generally development tools in the browser.

That’s also why this doesn’t use ex_aws. When uploading files to your server you can then use ex_aws to upload to cloud from there, but that’s additional traffic and complexity people usually try to avoid.

Oh, thank you for that info. Was hoping the guide would be up-to-date or complete enough I wouldn’t have to worry about the uploader code section potentially being the issue but I guess that’s where I need to look next.

Thanks again. Haven’t ever had to deal with S3, or using JS live this in Phoenix before so its a lot of firsts and a lot of confusions. :slight_smile:

So I’ve been trying to get liveview S3 uploads working for a few days now and I’m about ready to just keel over and die…but before I do I figured I’d ask to see if I’m missing something obvious.

My current progress:

  • Can upload files using a controller to S3 with no issues
  • I went through Chris McCords video and can upload files to my uploads folder with no issues
    video https://www.youtube.com/watch?v=PffpT2eslH8&t=1636s&ab_channel=ChrisMcCord
  • When I got the direct to S3 section I hit a wall as something with the presign is failing, but I have no idea what as its alien to me
  • I’ve tried using the uploads guide but its not really helping, especially seeing as its similar but different to the youtube video so I have no idea which bits are right and which bits are wrong.
    External Uploads — Phoenix LiveView v0.20.2

In terms of what I have so far, this is within my form_component.ex

defp s3_host, do: "//#{@bucket}.s3.amazonaws.com"
defp s3_key(entry), do: "#{entry.uuid}.#{ext(entry)}"

defp presign_entry(entry, socket) do
uploads = socket.assigns.uploads
key = s3_key(entry)

config = %{
  scheme: "http://",
  host: "s3.amazonaws.com",
  region: "eu-west-2",
  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, @bucket,
    key: key,
    content_type: entry.client_type,
    max_file_size: uploads[entry.upload_config].max_file_size,
    expires_in: :timer.hours(1)
  )

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

My javascript is a direct copy and paste of External Uploads — Phoenix LiveView v0.20.2
And the SimpleS3Upload is taken from Simple, dependency-free S3 Form Upload using HTTP POST sigv4 · GitHub

The first time I tried to click save I got an error with

defp sha256(secret, msg), do: :crypto.hmac(:sha256, secret, msg)

which I have now changed to

defp sha256(secret, msg), do: :crypto.mac(:hmac, :sha256, secret, msg)

This has removed the error, but has also left me in a state where when I click “Save” I get stuck on “saving…”

So basically, I’m looking for any wisdom on how to fix this. Is there anything obvious in my aws permissions I could be missing? Has anything changed significantly since the hexdocs/video were made that is causing me to make a major mistake?

Any info on this is greatly appreciated.

Hi @GazeIntoTheAbyss what does your browser show in its console, do you see any errors there?

Mod note: I merged your other post into this thread since it’s really the same problem you’re stuck on, and removed the “solved” marker from this thread so that people understand you’re still having trouble

Firstly sorry for making a second post.
Secondly, I don’t often use the console as I’m a total rookie and now feel like an idiot :slight_smile:

When I click save I get:
|> no uploader configured for S3

logError @ utils.js:7
uploader @ upload_entry.js:102
(anonymous) @ live_uploader.js:119
initAdapterUpload @ live_uploader.js:118
(anonymous) @ view.js:988
finish @ view.js:667
(anonymous) @ view.js:674
after @ live_socket.js:838
requestDOMUpdate @ live_socket.js:259
(anonymous) @ view.js:670
(anonymous) @ push.js:76
matchReceive @ push.js:76
(anonymous) @ push.js:107
trigger @ channel.js:278
(anonymous) @ channel.js:70
trigger @ channel.js:278
(anonymous) @ socket.js:550
decode @ serializer.js:25
onConnMessage @ socket.js:537
conn.onmessage @ socket.js:235

|> Uncaught TypeError: callback is not a function
at LiveUploader.initAdapterUpload (live_uploader.js:127:7)
at view.js:988:20
at finish (view.js:667:13)
at view.js:674:15
at TransitionSet.after (live_socket.js:838:7)
at LiveSocket.requestDOMUpdate (live_socket.js:259:22)
at Object.callback (view.js:670:29)
at push.js:76:23
at Array.forEach ()
at Push.matchReceive (push.js:76:8)

initAdapterUpload @ live_uploader.js:127
(anonymous) @ view.js:988
finish @ view.js:667
(anonymous) @ view.js:674
after @ live_socket.js:838
requestDOMUpdate @ live_socket.js:259
(anonymous) @ view.js:670
(anonymous) @ push.js:76
matchReceive @ push.js:76
(anonymous) @ push.js:107
trigger @ channel.js:278
(anonymous) @ channel.js:70
trigger @ channel.js:278
(anonymous) @ socket.js:550
decode @ serializer.js:25
onConnMessage @ socket.js:537
conn.onmessage @ socket.js:23

Can you show the javascript code you have set up?

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {uploaders: Uploaders, params: {_csrf_token: csrfToken}})

// Show progress bar on live navigation and form submits
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
window.addEventListener("phx:page-loading-start", info => topbar.show())
window.addEventListener("phx:page-loading-stop", info => topbar.hide())

// connect if there are any LiveViews on the page
liveSocket.connect()

// expose liveSocket on window for web console debug logs and latency simulation:
// >> liveSocket.enableDebug()
// >> liveSocket.enableLatencySim(1000)  // enabled for duration of browser session
// >> liveSocket.disableLatencySim()
window.liveSocket = liveSocket

let Uploaders = {}

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){
        let percent = Math.round((event.loaded / event.total) * 100)
        if(percent < 100){ entry.progress(percent) }
      }
    })

    xhr.open("POST", url, true)
    xhr.send(formData)
  })
}

Just did a quick test here and got this:

image

And what I did was remove the uploaders: from the liveSocket config.

@GazeIntoTheAbyss, check your liveSocket config, here’s mine:

let liveSocket = new LiveSocket("/live", Socket, {
    uploaders: Uploaders,
    hooks: Hooks,
    params: { _csrf_token: csrfToken }
})

I’m not 100% sure about this, but I think the issue is that you are defining stuff after you’re using them. So in your first two lines you do {uploaders: Uploaders} but at this point in time Uploaders is undefined. I would consider moving the whole let Uploaders = {} and following Uploaders.S3 up above where you initialize the livesocket.

1 Like
let Uploaders = {}

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){
        let percent = Math.round((event.loaded / event.total) * 100)
        if(percent < 100){ entry.progress(percent) }
      }
    })

    xhr.open("POST", url, true)
    xhr.send(formData)
  })
}
let liveSocket = new LiveSocket("/live", Socket, {uploaders: Uploaders, params: {_csrf_token: csrfToken}})

When laid out like this I get a no route found for POST error instead. Which may or may not be an improvement. It’s something new so I like it :slight_smile:

@GazeIntoTheAbyss that’s a step forward! As a small note, if you do three back ticks (```) above and below your code block in the forum you’ll get a nicely formatted code block. I have edited your post to show this, it makes reading it a lot easier.

Can you log the full error?

I’m sorry but I’m slightly confused as to what you removed as your code snippet shows uploaders: Uploaders. I currently have the same as your snippet minus the hooks.

I was writing my answer before I saw yours where you posted the entire javascript code, as @benwilson512 pointed out, you were using Uploaders before it was defined.

Edited because it has nothing to do with your problem right now.

Do you mean the route error?
Its just the standard routing error listing all my routes.
My router

live "/user/:username/posts/new", UserLive.Index, :new_post
# no route found for POST /user/username/posts/new
user_index_path  GET     /user/:username/posts/new

Hi @GazeIntoTheAbyss I’m saying that if you say that there is an error, it’s very important that you copy and paste the full error here so that we understand what you’re seeing. EDIT: I take it that the form actually submitted then and you’re redirecting?

I rolled with the example shown in the guide so my cors is looking like this currently

[
    {
        "AllowedHeaders": [
            "*"
        ],
        "AllowedMethods": [
            "PUT",
            "POST"
        ],
        "AllowedOrigins": [
            "*"
        ],
        "ExposeHeaders": []
    }
]

https://hexdocs.pm/phoenix_live_view/uploads-external.html
Does it need to be more custom than this? I figured the guide one would match the guide code

Sorry, I misread the error and edited, the thing that I’m seeing is that your defp s3_host, do: "//#{@bucket}.s3.amazonaws.com" which you pass as the url to the presign doesn’t have the same format as the one in the docs, which have the format "http://#{bucket}.s3-#{config.region}.amazonaws.com", yours is calling something internal to your app, which would explain the POST error since it is not defined in your router (and neither should be).

Oh sorry I misunderstood. I figured it was kind of pointless to send all my routes as I was using a live route and getting a POST error.

On another note though, when this line is above my uploader code, I can submit a post without an image. So if I type something and click save it will save it with no issues.

let liveSocket = new LiveSocket("/live", Socket, {
uploaders: Uploaders, 
params: {_csrf_token: csrfToken}})

If I move this line below my uploader I get the routing error wether I upload a file or just text.
Still kind of happy to have broken it in a way I can see though, so thanks for telling me to use the console. KobraKai told me to do it as well earlier but it didn’t click with me as I’m so used to using the terminal to look for errors.