Downloading zip file by chunk from Phoenix controller

I have a controller that will generate a zip file on-demand and send that to the user using Conn.chunk/2:

defmodule CoreWeb.Export.AppointmentImagesController do
  @moduledoc false

  alias Core.Marketplace.Ap.AppointmentEvent

  use CoreWeb, :controller

  def index(conn, %{"appointment_id" => appointment_id} = _params) do
    %{current_user: actor} = conn.assigns

    case Ash.get(AppointmentEvent, %{id: appointment_id}, actor: actor) do
      {:ok, appointment} ->
        IO.puts("generating stream")

        stream = image_as_zip_stream!(appointment)

        IO.puts("start download")

        conn =
          conn
          |> put_resp_content_type("application/zip")
          |> put_resp_header("content-disposition", ~s[attachment; filename="#{appointment_id}.zip"])
          |> send_chunked(:ok)

        IO.puts("start sending chunks")

        stream
        |> Stream.each(fn results ->
            case Plug.Conn.chunk(conn, results) do
              {:ok, _} -> IO.puts("got here!!!")
              {:error, error} -> dbg(error)
            end
          end)
        |> Stream.run()

        IO.puts("DONE!!!")

        conn

      {:error, _} ->
        conn
        |> put_status(500)
        |> json(%{error: "Failed to generete the zip file, try again later."})
    end
  end

  defp image_as_zip_stream!(appointment) do
    appointment
    |> Map.fetch!(:event_json)
    |> Map.fetch!("synced")
    |> Enum.with_index()
    |> Enum.map(fn {%{"s3Url" => url}, index} ->
      [bucket, path] = url |> URI.parse() |> Map.fetch!(:path) |> String.split("/", parts: 2, trim: true)

      bucket
      |> ExAws.S3.download_file(path, "#{index}.jpg")
      |> ExAws.stream!()
      |> then(& Zstream.entry("#{index}.jpg", &1))
    end)
    |> Zstream.zip()
  end
end

When I request this from chrome, the download will start and it will show the download as resuming:
image

When the download is around 200mb, the download will fail and I will get a Network Error in chrome:

I will not get any error message in the backend, it will just stop sending chunks:

1 Like

This is an example with Mint.
This is an example with Finch.

But my recommendation nowadays is to just use Req response streaming to a collectable, by @wojtekmach: Req — req v0.4.14

iex> resp = Req.get!("http://httpbin.org/stream/2", into: IO.stream())
# output: {"url": "http://httpbin.org/stream/2", ...}
# output: {"url": "http://httpbin.org/stream/2", ...}
iex> resp.status
200
iex> resp.body
%IO.Stream{}
1 Like