add req transport to fetch remote feeds

This commit is contained in:
mmacia committed 2026-05-20 21:51:20 +00:00
1 parent 2424a989bc
commit 1640417cb5
2 files changed
+129

No files matched your search

+103
View File
@@ -0,0 +1,103 @@
defmodule FeedReader.ReqTransport do
@moduledoc """
Req implementation of `FeedReader.Transport`.
GET responses are returned as raw, lazy chunks. Automatic Req decompression
is disabled so the later download pipeline can enforce decoded-byte limits.
Status 400 and 500 responses and transport failures are retried; redirect
failures and TLS alerts are terminal. The default is five retries after the
initial request and five redirects.
"""
@behaviour FeedReader.Transport
@default_headers [
{"user-agent", "feedex/1"},
{"accept-encoding", "gzip, deflate, identity"}
]
@impl true
@spec request(String.t(), keyword()) ::
{:ok, FeedReader.Transport.response()} | {:error, term()}
def request(url, opts \\ [])
def request(url, opts) when is_binary(url) do
http_options = Keyword.get(opts, :http_options, [])
request_options =
http_options
|> Keyword.put_new(:headers, @default_headers)
|> Keyword.put(:into, :self)
|> Keyword.put(:raw, true)
|> Keyword.put(:decode_body, false)
|> Keyword.put(:compressed, false)
|> Keyword.put(:max_redirects, 5)
|> Keyword.put(:max_retries, retry_attempts(opts))
|> Keyword.put(:retry, &retry?/2)
|> Keyword.put(:retry_delay, retry_delay(opts))
case Req.run([url: url], request_options) do
{request, %Req.Response{} = response} ->
handle_response(request, response)
{_request, error} ->
{:error, error_reason(error)}
end
end
def request(_url, _opts), do: {:error, :invalid_url}
defp handle_response(request, response) do
case response.status do
status when status in 200..299 ->
{:ok,
%{
status: status,
headers: Req.get_headers_list(response),
final_url: URI.to_string(request.url),
body: response.body
}}
403 ->
{:error, :forbidden}
404 ->
{:error, :not_found}
429 ->
{:error, :too_many_requests}
500 ->
{:error, :internal_server_error}
status ->
{:error, {:http_status, status}}
end
end
defp retry_attempts(opts) do
case Keyword.get(opts, :http_retry_attempts, 5) do
attempts when is_integer(attempts) and attempts >= 0 -> attempts
_ -> 5
end
end
defp retry_delay(opts) do
case Keyword.get(opts, :retry_delay, 0) do
delay when is_integer(delay) and delay >= 0 -> fn _ -> delay end
delay when is_function(delay, 1) -> delay
_ -> fn _ -> 0 end
end
end
defp retry?(_request, %Req.Response{status: status}), do: status in [400, 500]
defp retry?(_request, %Req.TransportError{reason: {:tls_alert, _}}), do: false
defp retry?(_request, %Req.TransportError{}), do: true
defp retry?(_request, %Req.TooManyRedirectsError{}), do: false
defp retry?(_request, _error), do: false
defp error_reason(%Req.TransportError{reason: {:tls_alert, {reason, _message}}}), do: reason
defp error_reason(%Req.TransportError{reason: reason}), do: reason
defp error_reason(%Req.TooManyRedirectsError{} = error), do: error
defp error_reason(error), do: error
end