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

Feature: Partial request body & Error when limit exceeded #356

Open
wants to merge 2 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
35 changes: 32 additions & 3 deletions lib/httpoison/base.ex
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,8 @@ defmodule HTTPoison.Base do
* `:follow_redirect` - a boolean that causes redirects to be followed
* `:max_redirect` - an integer denoting the maximum number of redirects to follow
* `:params` - an enumerable consisting of two-item tuples that will be appended to the url as query string parameters
* `:max_body_length` - a non-negative integer denoting the max response body length. Errors when body length exceeds. See :hackney.body/2
* `:max_body_length` - a non-negative integer denoting the max response body length. default: :infinity
* `:partial_response` - a boolean denoting whether exceeding `:max_body_length` returns an error, or a partial response. default: false

Timeouts can be an integer or `:infinity`

Expand Down Expand Up @@ -643,9 +644,12 @@ defmodule HTTPoison.Base do
)

{:ok, status_code, headers, client} ->
max_length = Keyword.get(options, :max_body_length, :infinity)
opts = %{
max_length: Keyword.get(options, :max_body_length, :infinity),
partial_response: Keyword.get(options, :partial_response, false)
}

case :hackney.body(client, max_length) do
case parse_request_body(client, opts, "") do
{:ok, body} ->
response(
process_status_code,
Expand All @@ -669,6 +673,31 @@ defmodule HTTPoison.Base do
end
end

defp parse_request_body(client, %{max_length: max} = opts, acc)
when max >= byte_size(acc) do
case :hackney.stream_body(client) do
{:ok, data} ->
parse_request_body(client, opts, acc <> data)

:done ->
{:ok, acc}

{:error, reason} = error ->
case reason do
{:closed, bin} when is_binary(bin) ->
{:error, {:closed, acc <> bin}}

_ ->
error
end
end
end

defp parse_request_body(_client, %{partial_response: true}, acc), do: {:ok, acc}

defp parse_request_body(_client, %{partial_response: false}, acc),
do: {:error, {:body_too_large, acc}}

defp do_request(method, request_url, request_headers, {:stream, enumerable}, hn_options) do
with {:ok, ref} <- :hackney.request(method, request_url, request_headers, :stream, hn_options) do
failures =
Expand Down
65 changes: 48 additions & 17 deletions test/httpoison_base_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ defmodule HTTPoisonBaseTest do
{:ok, 200, "headers", :client}
end)

expect(:hackney, :body, fn _, _ -> {:ok, "response"} end)
expect(:hackney, :stream_body, fn _ -> {:ok, "response"} end)
expect(:hackney, :stream_body, fn _ -> :done end)

assert Example.post!("localhost", "body") ==
%HTTPoison.Response{
Expand All @@ -57,7 +58,8 @@ defmodule HTTPoisonBaseTest do
{:ok, 200, "headers", :client}
end)

expect(:hackney, :body, fn _, _ -> {:ok, "response"} end)
expect(:hackney, :stream_body, fn _ -> {:ok, "response"} end)
expect(:hackney, :stream_body, fn _ -> :done end)

assert ExampleParamsOptions.get!("localhost", [], params: %{foo: "bar"}) ==
%HTTPoison.Response{
Expand Down Expand Up @@ -87,7 +89,8 @@ defmodule HTTPoisonBaseTest do
{:ok, 200, "headers", :client}
end)

expect(:hackney, :body, fn _, _ -> {:ok, "response"} end)
expect(:hackney, :stream_body, fn _ -> {:ok, "response"} end)
expect(:hackney, :stream_body, fn _ -> :done end)

assert HTTPoison.post!("localhost", "body", [], timeout: 12345) ==
%HTTPoison.Response{
Expand All @@ -104,7 +107,8 @@ defmodule HTTPoisonBaseTest do
{:ok, 200, "headers", :client}
end)

expect(:hackney, :body, fn _, _ -> {:ok, "response"} end)
expect(:hackney, :stream_body, fn _ -> {:ok, "response"} end)
expect(:hackney, :stream_body, fn _ -> :done end)

assert HTTPoison.post!("localhost", "body", [], recv_timeout: 12345) ==
%HTTPoison.Response{
Expand All @@ -120,7 +124,8 @@ defmodule HTTPoisonBaseTest do
:post, "http://localhost", [], "body", [proxy: "proxy"] -> {:ok, 200, "headers", :client}
end)

expect(:hackney, :body, fn _, _ -> {:ok, "response"} end)
expect(:hackney, :stream_body, fn _ -> {:ok, "response"} end)
expect(:hackney, :stream_body, fn _ -> :done end)

assert HTTPoison.post!("localhost", "body", [], proxy: "proxy") ==
%HTTPoison.Response{
Expand All @@ -145,7 +150,8 @@ defmodule HTTPoisonBaseTest do
{:ok, 200, "headers", :client}
end)

expect(:hackney, :body, fn _, _ -> {:ok, "response"} end)
expect(:hackney, :stream_body, fn _ -> {:ok, "response"} end)
expect(:hackney, :stream_body, fn _ -> :done end)

assert HTTPoison.post!(
"localhost",
Expand Down Expand Up @@ -173,7 +179,8 @@ defmodule HTTPoisonBaseTest do
{:ok, 200, "headers", :client}
end)

expect(:hackney, :body, fn _, _ -> {:ok, "response"} end)
expect(:hackney, :stream_body, fn _ -> {:ok, "response"} end)
expect(:hackney, :stream_body, fn _ -> :done end)

assert HTTPoison.post!(
"localhost",
Expand All @@ -197,7 +204,8 @@ defmodule HTTPoisonBaseTest do
:post, "http://localhost", [], "body", [proxy: "proxy"] -> {:ok, 200, "headers", :client}
end)

expect(:hackney, :body, fn _, _ -> {:ok, "response"} end)
expect(:hackney, :stream_body, fn _ -> {:ok, "response"} end)
expect(:hackney, :stream_body, fn _ -> :done end)

assert HTTPoison.post!("localhost", "body") ==
%HTTPoison.Response{
Expand All @@ -215,7 +223,8 @@ defmodule HTTPoisonBaseTest do
{:ok, 200, "headers", :client}
end)

expect(:hackney, :body, fn _, _ -> {:ok, "response"} end)
expect(:hackney, :stream_body, fn _ -> {:ok, "response"} end)
expect(:hackney, :stream_body, fn _ -> :done end)

assert HTTPoison.post!("localhost", "body") ==
%HTTPoison.Response{
Expand All @@ -233,7 +242,8 @@ defmodule HTTPoisonBaseTest do
{:ok, 200, "headers", :client}
end)

expect(:hackney, :body, fn _, _ -> {:ok, "response"} end)
expect(:hackney, :stream_body, fn _ -> {:ok, "response"} end)
expect(:hackney, :stream_body, fn _ -> :done end)

assert HTTPoison.post!("https://localhost", "body") ==
%HTTPoison.Response{
Expand All @@ -251,7 +261,8 @@ defmodule HTTPoisonBaseTest do
:post, "http://localhost", [], "body", [] -> {:ok, 200, "headers", :client}
end)

expect(:hackney, :body, fn _, _ -> {:ok, "response"} end)
expect(:hackney, :stream_body, fn _ -> {:ok, "response"} end)
expect(:hackney, :stream_body, fn _ -> :done end)

assert HTTPoison.post!("localhost", "body") ==
%HTTPoison.Response{
Expand All @@ -271,7 +282,8 @@ defmodule HTTPoisonBaseTest do
{:ok, 200, "headers", :client}
end)

expect(:hackney, :body, fn _, _ -> {:ok, "response"} end)
expect(:hackney, :stream_body, fn _ -> {:ok, "response"} end)
expect(:hackney, :stream_body, fn _ -> :done end)

assert HTTPoison.post!("localhost", "body", [], ssl: [certfile: "certs/client.crt"]) ==
%HTTPoison.Response{
Expand All @@ -291,7 +303,8 @@ defmodule HTTPoisonBaseTest do
{:ok, 200, "headers", :client}
end)

expect(:hackney, :body, fn _, _ -> {:ok, "response"} end)
expect(:hackney, :stream_body, fn _ -> {:ok, "response"} end)
expect(:hackney, :stream_body, fn _ -> :done end)

assert HTTPoison.post!("localhost", "body", [], follow_redirect: true) ==
%HTTPoison.Response{
Expand All @@ -307,7 +320,8 @@ defmodule HTTPoisonBaseTest do
{:ok, 200, "headers", :client}
end)

expect(:hackney, :body, fn _, _ -> {:ok, "response"} end)
expect(:hackney, :stream_body, fn _ -> {:ok, "response"} end)
expect(:hackney, :stream_body, fn _ -> :done end)

assert HTTPoison.post!("localhost", "body", [], max_redirect: 2) ==
%HTTPoison.Response{
Expand All @@ -323,7 +337,8 @@ defmodule HTTPoisonBaseTest do
{:ok, 200, "headers", :client}
end)

expect(:hackney, :body, fn _, :infinity -> {:ok, "response"} end)
expect(:hackney, :stream_body, fn _ -> {:ok, "response"} end)
expect(:hackney, :stream_body, fn _ -> :done end)

assert HTTPoison.get("localhost") ==
{:ok,
Expand All @@ -338,9 +353,25 @@ defmodule HTTPoisonBaseTest do
{:ok, 200, "headers", :client}
end)

expect(:hackney, :body, fn _, _ -> {:error, "some error"} end)
expect(:hackney, :stream_body, fn _ -> {:ok, "response"} end)

assert HTTPoison.get("localhost", [], max_body_length: 3) ==
{:error, %HTTPoison.Error{id: nil, reason: "some error"}}
{:error, %HTTPoison.Error{id: nil, reason: {:body_too_large, "response"}}}

expect(:hackney, :request, fn :get, "http://localhost", [], "", [] ->
{:ok, 200, "headers", :client}
end)

expect(:hackney, :stream_body, fn _ -> {:ok, "response"} end)
expect(:hackney, :stream_body, fn _ -> {:ok, "additionalcontent"} end)

assert HTTPoison.get("localhost", [], max_body_length: 12, partial_response: true) ==
{:ok,
%HTTPoison.Response{
status_code: 200,
headers: "headers",
body: "responseadditionalcontent",
request_url: "http://localhost"
}}
end
end