spurgus
Aborting Req request if max size exceeded while streaming
Hi!
I have a Downloader module that GETs files given a URL, using Req. It has a timeout option but I would like to abort the download when it exceeds a given maximum size, so I don’t have to wait until the file has been completely downloaded to check the final size.
I think that can be done using Req’s streaming capabilities but I’m not being able to get it working. Can anyone help with this? This is my module so far:
defmodule Utils.Downloader do
require Logger
@default_receive_timeout_ms 20_000
def download(url, opts \\ []) do
Logger.debug("[#{__MODULE__}] downloading...")
with {:ok, req_client} <- prepare_req_client(opts),
{:ok, %Req.Response{status: 200, body: body, headers: headers}} <-
Req.get(req_client, url: url),
{:ok, content_type} <- get_content_type(headers) do
Logger.debug("[#{__MODULE__}] finished downloading...")
{:ok, %{content_type: content_type, data: body}}
else
{:error, :cant_get_content_type} ->
{:error, :cant_get_content_type}
%Req.Response{status: status} when status != 200 ->
{:erorr, :cant_download_image}
end
end
defp get_content_type(headers) do
headers
|> Enum.find(fn {key, _} -> String.downcase(key) == "content-type" end)
|> case do
{_, value} when is_list(value) -> {:ok, hd(value)}
{_, value} when is_binary(value) -> {:ok, value}
nil -> {:error, :cant_get_content_type}
end
end
defp prepare_req_client(opts \\ []) do
receive_timeout_ms = opts[:receive_timeout_ms] || @default_receive_timeout_ms
client = Req.new(receive_timeout: receive_timeout_ms)
{:ok, client}
end
end
Marked As Solved
jswanner
Does this not work?
into: fn {:data, data}, {req, resp} ->
resp = Req.Response.update_private(resp, :length, byte_size(data), &(&1 + byte_size(data)))
if Req.Response.get_private(resp, :length) > max_size_bytes do
{:halt, {req, RuntimeError.exception(message: "streamed content length too large")}}
else
{:cont, {req, update_in(resp.body, &(&1 <> data))}}
end
end
Also Liked
jswanner
Welcome to the forum @spurgus!
If the response includes the content-length header then you can check that from a response step:
req =
Req.new()
|> Req.Request.prepend_response_steps(validate_content_length: fn {req, resp} ->
with [header] <- Req.Response.get_header(resp, "content-length"),
{content_length, ""} <- Integer.parse(header) do
if content_length > @max_content_length do
Req.cancel_async_response(resp)
{req, RuntimeError.exception(message: "content-length too large")}
else
{req, resp}
end
else
_ ->
Req.cancel_async_response(resp)
{req, RuntimeError.exception(message: "Invalid content-length")}
end
end)
Otherwise, you’ll have to keep track of received bytes and halt the request. You mentioned streaming but didn’t say which form of streaming (into: :self, into: &fun/2, into: collectable). Here’s an example for the function form of streaming:
Req.get(req, into: fn {:data, data}, {req, resp} ->
resp = Req.Response.update_private(resp, :length, 0, & &1 + byte_size(data))
if Req.Response.get_private(resp, :length) > @max_content_length do
{:halt, {req, RuntimeError.exception(message: "content length too large")}}
else
{:cont, {req, resp}}
end
end)
jswanner
Try:
{:cont, {req, update_in(resp.body, &(&1 <> data))}}
I believe this into: &fun/2 option is envisioned for scenarios where you’ll be doing something with the data as it’s coming in (such as sending it to another process), rather than accumulating it and processing it at the end.
jswanner
I was not meaning to imply this use of into: &fun/2 is wrong, just pointing out Req doesn’t accumulate the body for you with this option (maybe it should?).
spurgus
Yes, that’s my concern, someone trying to take your server down, making you download huge files, so my idea was to use streaming to abort the download as soon as the max size has been detected.
Let’s see if I can get this to work…
BartOtten
Never trust headers unless you’ve validated them.
Once I had an Elixir bot running in a hostile environment (Kodi addon ecosystem) . I am quite sure it would not have survived the first week if I trusted the headers. Content-lengths spoofing (read: simply returning an everlasting stream of random bits) was one of the first concerns.







