I recently implemented an endpoint for uploading files via multipart HTTP POST with Phoenix, but I didn’t find a good documentation about how to test it, so I’m going to describe how I did it.
The test code
defmodule HelloWeb.MultipartControllerTest do
use HelloWeb.ConnCase
test "Multipart POST /api/files", %{conn: conn} do
boundary = "ef593866da4b40b59ef73e2feaab7fb1"
content = File.read!("test/hello_web/controllers/test.png")
name = "test-file"
filename = "test-file.png"
content_type = "image/png"
binary_body = """
--#{boundary}\r
Content-Disposition: form-data; name="#{name}"; filename="#{filename}"\r
Content-Type: #{content_type}\r
\r
#{content}\r
--#{boundary}--\r
"""
conn =
conn
|> Plug.Conn.put_req_header("content-type", "multipart/form-data; boundary=#{boundary}")
|> post(~p"/api/files", binary_body)
assert %{
^name => %{
"path" => path,
"content_type" => ^content_type,
"filename" => ^filename
}
} = json_response(conn, 200)
end
end
The above code test a simple multipart POST request with a handmade binary body. Please note the \r
at each end of line of the body binary and the content-type
header that tells it is a multipart request.
The tested response is just an echo of what the controller receives. Let see how the controller is implemented.
The implementation
Controller code is the following:
defmodule HelloWeb.MultipartController do
use HelloWeb, :controller
require Protocol
Protocol.derive(Jason.Encoder, Plug.Upload)
def create(conn, params) do
json(conn, params)
end
end
It is just an echo of “params” and, as test proves, it contains the parsed informations of the multipart request plus the (temporary) path of the files uploaded.
Indeed the multipart requests are automatically parsed by Plug.Parser(.MULTIPART)
and files are automatically stored in a temporary path by Plug.Upload
.
Note that, as documented, if name
key in Content-Disposition
doesn’t exist then the plug will discard that part.
The configuration that tells Plug.Parser
to parse multipart request is located in your Endpoint
module, like the following:
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Phoenix.json_library()
If you want to parse multipart requests by yourself you can remove :multipart
from parser configuration and use Plug.Conn.read_part_headers/2
and Plug.Conn.read_part_body/2
in your controllers.
Other implementation details
Router:
defmodule HelloWeb.Router do
use HelloWeb, :router
pipeline :api do
plug :accepts, ["json"]
end
scope "/api", HelloWeb do
pipe_through :api
post "/files", MultipartController, :create
end
end
Deps:
defp deps do
[
{:phoenix, "~> 1.7.2"},
{:jason, "~> 1.2"},
{:plug_cowboy, "~> 2.5"}
]
end