Skip to content

feat(request-plugin): support multipart/form-data bodies - #412

Open
Taure wants to merge 1 commit into
masterfrom
feat/multipart-form-data
Open

feat(request-plugin): support multipart/form-data bodies#412
Taure wants to merge 1 commit into
masterfrom
feat/multipart-form-data

Conversation

@Taure

@Taure Taure commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

Closes #411

Nova had no way to accept an uploaded file. The request plugin only knew about JSON and urlencoded bodies, and since should_read_body/1 ignored the content-type it drained a multipart body into body whenever decode_json_body was set - which every app generated by rebar3 nova new sets globally. That meant reading the parts from a controller with cowboy_req:read_part/1 did not work either, because the body was already consumed by the time the controller ran.

What this adds

A read_multipart_body option on nova_request_plugin:

{pre_request, nova_request_plugin, #{read_multipart_body => true}}
upload(#{params := #{<<"title">> := Title}, files := Files}) ->
    [ok = file:write_file(<<"/tmp/", Filename/binary>>, Body)
     || #{filename := Filename, body := Body} <- Files],
    {json, 200, #{}, #{title => Title, uploaded => length(Files)}}.
  • Regular form fields go under params, uploaded files under files as maps of name, filename, content_type and body.
  • Parts are buffered in memory, so a single part is capped at max_file_size (8 MB by default, matching cowboy's own per-read default). Pass #{read_multipart_body => #{max_file_size => Bytes}} to change it. A part above the cap gets a 413 and the controller is never called.
  • A part with a malformed content-disposition gets a 400 rather than crashing the handler.

Behaviour change

A multipart/form-data body is no longer drained into body by decode_json_body / read_urlencoded_body. Without the new option the body is left untouched, so a controller can stream the parts itself and hand the updated request back:

upload(Req) ->
    {Files, Req1} = read_parts(Req, []),
    {json, 200, #{}, Req1, #{uploaded => length(Files)}}.

Nothing was reading a multipart body out of body before this, since the plugin never parsed it.

pre_request/4's spec now also admits the {stop, ...} return it could already produce.

Tests

Five new eunit tests over a scripted cowboy_req: fields plus files, chunked {more, ...} parts joined into one body, 413 on oversize, 400 on a malformed part, and one asserting read_body is never called for a multipart body.

Docs

guides/plugins.md gets the option-table row and a "File uploads" section covering both the plugin and the raw read_part route. guides/controllers.md gets a cross-link next to the outbound sendfile section, which is where people currently land when looking for uploads.

Checks

xref clean, eunit 362 tests / 0 failures, dialyzer clean, ex_doc no warnings.

Nova had no way to accept an uploaded file. The request plugin only knew
about JSON and urlencoded bodies, and since should_read_body/1 ignored the
content-type it drained a multipart body into `body` whenever
decode_json_body was set - which every generated app sets globally - so
reading the parts from a controller with cowboy_req:read_part/1 did not
work either.

Add a `read_multipart_body` option that reads the parts up front, putting
regular fields under `params` and uploaded files under `files` as maps of
name, filename, content_type and body. Parts are buffered in memory so a
single part is capped at `max_file_size` (8 MB by default) and a part above
the cap is answered with 413. A part with a malformed content-disposition
is answered with 400.

Without the option a multipart body is now left untouched, so a controller
can stream the parts itself and hand the updated request back.

Closes #411
@sendtopms

Copy link
Copy Markdown

One small suggestion, while reading a request (read_multipart_body), it can be pushed to callback function (of user configured) to avoid accumulating in memory.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

multipart/form-data

2 participants