`Plug.Conn.read_body/2` not returning when client drops/crashes

I am working on an Phoenix API (PUT) route that allows the upload of rather large image files (more than 1gb). So I am trying to read the request body in chunks by parsing some requests parameters, opening an io_device (with File.open/2) and hand both my conn and the device to the following function:

  defp read_body_chunked(conn, io_device) do
    IO.inspect(conn, label: "read_body_chunked called")

    read_body(conn)
    |> IO.inspect()
    |> case do
      {:ok, data, conn} ->
        IO.binwrite(io_device, data)
        {:ok, conn}

      {:more, data, conn} ->
        IO.binwrite(io_device, data)
        read_body_chunked(conn, io_device)

      error ->
        error
    end
  end

This works in general if the connection is stable.

But if I kill my client (an Electron app) deliberately while a large file is being uploaded it looks like read_body/2 is still called but never returns. I get a final log with the label “read_body_chunked called”, but then nothing afterwards and I end up with a only partially uploaded file and no apparent way to recover.

What am I missing?

I am using :plug_cowboy 2.7 and Phoenix 1.7.20.

I believe that Cowboy will terminate the process that’s holding the conn when the remote end goes away; trapping exits inside (or around) read_body_chunked would give you more opportunity for logging.

How would I implement that?

The function above gets called directly in my controller module. So I would need to somehow inject that capture somewhere in the Phoenix endpoint/router?

Solved it with the following extension code:

  defp start_body_streaming(conn, io_device, target_path) do
    parent = self()

    spawn(fn ->
      Process.monitor(parent)

      receive do
        {:DOWN, _ref, :process, _pid, {:shutdown, :local_closed}} ->
          Logger.warning(
            "File upload got interrupted for `#{target_path}`, deleting data received so far."
          )

          File.rm(target_path)
      end
    end)

    stream_body(conn, io_device)
  end

  defp stream_body(conn, io_device) do
    read_body(conn)
    |> case do
      {:ok, data, conn} ->
        IO.binwrite(io_device, data)
        {:ok, conn}

      {:more, data, conn} ->
        IO.binwrite(io_device, data)
        stream_body(conn, io_device)

      error ->
        error
    end
  end

via `Plug.Conn.read_body/2` not returning when client drops/crashes · Issue #107 · elixir-plug/plug_cowboy · GitHub