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

Ignore :into collectable for non-200 responses #435

Merged
merged 4 commits into from
Nov 24, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 3 additions & 0 deletions lib/req.ex
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,9 @@ defmodule Req do

into: File.stream!("path")

Note that the collectable is only used, if the response status is 200. In other cases,
the body is accumulated and processed as usual.

* `:self` - stream response body into the current process mailbox.

Received messages should be parsed with `Req.parse_message/2`.
Expand Down
18 changes: 11 additions & 7 deletions lib/req/finch.ex
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,16 @@ defmodule Req.Finch do
end

defp finch_stream_into_collectable(req, finch_req, finch_name, finch_options, collectable) do
{acc, collector} = Collectable.into(collectable)
resp = Req.Response.new()

fun = fn
{:status, status}, {acc, req, resp} ->
{acc, req, %{resp | status: status}}
{:status, 200}, {nil, req, resp} ->
{acc, collector} = Collectable.into(collectable)
Copy link
Contributor Author

@jonatanklosko jonatanklosko Nov 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically we should call collector.(:halt) in case of an error. It wasn't the case in the existing code, but I assume that's unexpected.

The issue is that Finch.stream doesn't return the accumulator in case of errors, so we have no access to {acc, collector}. In the previous code this could've been fixed, because we call Collectable.into(collectable) at the top, but with the new code it's not possible. However, I do think we should only call Collectable.into(collectable) if we know the status is 200, since this call may have side effects (such as creating a file).

@wojtekmach @josevalim any ideas?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should change Finch to return the accumulator? Or do you mean the Collector never returns the accumulator?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should change Finch to return the accumulator?

Yeah, I think that's exactly what we would need, I'm just not sure if that breaking change is acceptable :D

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not v1.0 yet. Plus, who is halting a collectable anyway?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FTR I will send a PR to finch. I guess it's not blocking this PR, since we hasn't been halting so far either.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{{acc, collector}, req, %{resp | status: 200}}

{:status, status}, {nil, req, resp} ->
{acc, collector} = Collectable.into("")
{{acc, collector}, req, %{resp | status: status}}

{:headers, fields}, {acc, req, resp} ->
resp =
Expand All @@ -146,18 +150,18 @@ defmodule Req.Finch do

{acc, req, resp}

{:data, data}, {acc, req, resp} ->
{:data, data}, {{acc, collector}, req, resp} ->
acc = collector.(acc, {:cont, data})
{acc, req, resp}
{{acc, collector}, req, resp}

{:trailers, fields}, {acc, req, resp} ->
fields = finch_fields_to_map(fields)
resp = update_in(resp.trailers, &Map.merge(&1, fields))
{acc, req, resp}
end

case Finch.stream(finch_req, finch_name, {acc, req, resp}, fun, finch_options) do
{:ok, {acc, req, resp}} ->
case Finch.stream(finch_req, finch_name, {nil, req, resp}, fun, finch_options) do
{:ok, {{acc, collector}, req, resp}} ->
acc = collector.(acc, :done)
{req, %{resp | body: acc}}

Expand Down
3 changes: 3 additions & 0 deletions lib/req/request.ex
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ defmodule Req.Request do

into: File.stream!("path")

Note that the collectable is only used, if the response status is 200. In other cases,
the body is accumulated and processed as usual.

* `:options` - the options to be used by steps. The exact representation of options is private.
Calling `request.options[key]`, `put_in(request.options[key], value)`, and
`update_in(request.options[key], fun)` is allowed. `get_option/3` and `delete_option/2`
Expand Down
13 changes: 8 additions & 5 deletions lib/req/steps.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1032,17 +1032,20 @@ defmodule Req.Steps do
end

collectable ->
{acc, collector} = Collectable.into(collectable)

response =
Req.Response.new(
status: conn.status,
headers: conn.resp_headers
)

acc = collector.(acc, {:cont, conn.resp_body})
acc = collector.(acc, :done)
{request, %{response | body: acc}}
if conn.status == 200 do
{acc, collector} = Collectable.into(collectable)
acc = collector.(acc, {:cont, conn.resp_body})
acc = collector.(acc, :done)
{request, %{response | body: acc}}
else
{request, %{response | body: conn.resp_body}}
end
end
end

Expand Down
36 changes: 36 additions & 0 deletions test/req/finch_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,42 @@ defmodule Req.FinchTest do
assert resp.body == ["chunk1", "chunk2"]
end

@tag :tmp_dir
test "into: collectable non-200", %{tmp_dir: tmp_dir} do
# Ignores the collectable and returns body as usual

File.mkdir_p!(tmp_dir)
file = Path.join(tmp_dir, "result.bin")

%{url: url} =
start_tcp_server(fn socket ->
{:ok, "GET / HTTP/1.1\r\n" <> _} = :gen_tcp.recv(socket, 0)

body = ~s|{"error": "not found"}|

data = """
HTTP/1.1 404 OK
content-length: #{byte_size(body)}
content-type: application/json

#{body}
"""

:ok = :gen_tcp.send(socket, data)
end)
jonatanklosko marked this conversation as resolved.
Show resolved Hide resolved

resp =
Req.get!(
url: url,
into: File.stream!(file)
jonatanklosko marked this conversation as resolved.
Show resolved Hide resolved
)

assert resp.status == 404
assert resp.body == %{"error" => "not found"}

refute File.exists?(file)
end

test "into: collectable handle error" do
assert {:error, %Req.TransportError{reason: :econnrefused}} =
Req.get(
Expand Down
26 changes: 26 additions & 0 deletions test/req/steps_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -1927,6 +1927,32 @@ defmodule Req.StepsTest do
refute_receive _
end

@tag :tmp_dir
test "into: collectable non-200", %{tmp_dir: tmp_dir} do
# Ignores the collectable and returns body as usual

File.mkdir_p!(tmp_dir)
file = Path.join(tmp_dir, "result.bin")

req =
Req.new(
plug: fn conn ->
conn = Plug.Conn.send_chunked(conn, 404)
{:ok, conn} = Plug.Conn.chunk(conn, "foo")
{:ok, conn} = Plug.Conn.chunk(conn, "bar")
conn
end,
into: []
)

resp = Req.request!(req)
assert resp.status == 404
assert resp.body == "foobar"
refute_receive _

refute File.exists?(file)
end

test "errors" do
req =
Req.new(
Expand Down
Loading