Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support any enumerable in mutlipart #444

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion lib/req/steps.ex
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,9 @@ defmodule Req.Steps do

* `File.Stream`

* `{value, options}` tuple. Supported options are `:filename` and `:content_type`
* `Enumerable`

* `{value, options}` tuple. Supported options are: `:filename`, `:content_type`, `:content_length`

* `:json` - if set, encodes the request body as JSON (using `Jason.encode_to_iodata!/1`), sets
the `accept` header to `application/json`, and the `content-type` header to `application/json`.
Expand All @@ -419,6 +421,14 @@ defmodule Req.Steps do
iex> resp.body["files"]
%{"b" => "2"}

Encoding streaming form (`multipart/form-data`):

iex> stream = Stream.cycle(["abc"]) |> Stream.take(3)
iex> fields = [file: {stream, filename: "b.txt", content_length: 9}]
iex> resp = Req.post!("https://httpbin.org/anything", form_multipart: fields)
iex> resp.body["files"]
%{"file" => "abcabcabc"}

Encoding JSON:

iex> Req.post!("https://httpbin.org/post", json: %{a: 2}).body["json"]
Expand Down
35 changes: 29 additions & 6 deletions lib/req/utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,11 @@ defmodule Req.Utils do
}
end

defp add_form_parts({_parts1, size1}, {_parts2, size2})
when not is_integer(size1) or not is_integer(size2) do
raise ArgumentError, "multipart part sizes must be integers"
end

defp add_form_parts({parts1, size1}, {parts2, size2})
when is_list(parts1) and is_list(parts2) do
{[parts1, parts2], size1 + size2}
Expand All @@ -474,7 +479,7 @@ defmodule Req.Utils do
end

defp encode_form_part({name, {value, options}}, boundary) do
options = Keyword.validate!(options, [:filename, :content_type])
options = Keyword.validate!(options, [:filename, :content_type, :content_length])

{parts, parts_size, options} =
case value do
Expand Down Expand Up @@ -504,6 +509,12 @@ defmodule Req.Utils do
end)

{stream, size, options}

enum ->
Enumerable.impl_for!(enum)
size = Keyword.get(options, :content_length, 0)

{enum, size, options}
end

params =
Expand All @@ -514,11 +525,11 @@ defmodule Req.Utils do
end

headers =
if content_type = options[:content_type] do
["content-type: ", content_type, @crlf]
else
[]
end
[
maybe_content_type(options),
maybe_content_length(options)
]
|> Enum.reject(&is_nil/1)

headers = ["content-disposition: form-data; name=\"#{name}\"", params, @crlf, headers]
header = [[@crlf, "--", boundary, @crlf, headers, @crlf]]
Expand All @@ -529,6 +540,18 @@ defmodule Req.Utils do
encode_form_part({name, {value, []}}, boundary)
end

defp maybe_content_type(options) do
if content_type = options[:content_type] do
["content-type: ", content_type, @crlf]
end
end

defp maybe_content_length(options) do
if content_length = options[:content_length] do
["content-length: ", Integer.to_string(content_length), @crlf]
end
end

@doc """
Loads .netrc file.

Expand Down
39 changes: 39 additions & 0 deletions test/req/utils_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,45 @@ defmodule Req.UtilsTest do
"""
end

test "raise when size is not integer" do
assert_raise ArgumentError, fn ->
enum = Stream.cycle([1]) |> Stream.take(1)
Req.Utils.encode_form_multipart([field1: {enum, content_length: "10"}], boundary: "foo")
end
end

test "content-length" do
%{content_type: content_type, body: body, size: size} =
Req.Utils.encode_form_multipart([field1: {"a", content_length: 11}], boundary: "foo")

body = IO.iodata_to_binary(body)

assert size == byte_size(body)
assert content_type == "multipart/form-data; boundary=foo"

assert body == """
\r\n\
--foo\r\n\
content-disposition: form-data; name=\"field1\"\r\n\
content-length: 11\r\n\
\r\n\
a\r\n\
--foo--\r\n\
"""
end

test "can accept any enumerable" do
enum = Stream.cycle(["a"]) |> Stream.take(1)

%{body: body, size: size} =
Req.Utils.encode_form_multipart([field1: {enum, content_length: 11}], boundary: "foo")

body = body |> Enum.to_list() |> IO.iodata_to_binary()

# content_length is +10 of actual size
assert size == byte_size(body) + 10
end

@tag :tmp_dir
test "can return stream", %{tmp_dir: tmp_dir} do
File.write!("#{tmp_dir}/2.txt", "22")
Expand Down
Loading