From 32d963c4984d7adee5651e1ff2586d193857d2df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Meadows-J=C3=B6nsson?= Date: Sat, 7 Feb 2026 14:40:00 +0100 Subject: [PATCH] Fix Req receive timeouts across HTTP module The HTTP module was not passing receive_timeout to Req, so all calls used Req's default 15s. Typesense passed recv_timeout options but they were ignored (unused _opts parameter). Set a 30s default receive_timeout on all HTTP calls and thread through the option for callers like Typesense that need a longer 60s timeout. --- lib/hexdocs/http.ex | 52 +++++++++++++++++++++++++++------ lib/hexdocs/search/typesense.ex | 4 +-- 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/lib/hexdocs/http.ex b/lib/hexdocs/http.ex index 16be574..fbe7df6 100644 --- a/lib/hexdocs/http.ex +++ b/lib/hexdocs/http.ex @@ -1,11 +1,17 @@ defmodule Hexdocs.HTTP do @max_retry_times 5 @base_sleep_time 200 + @receive_timeout 30_000 require Logger def head(url, headers) do - case Req.head(url, headers: headers, retry: false, decode_body: false) do + case Req.head(url, + headers: headers, + retry: false, + decode_body: false, + receive_timeout: @receive_timeout + ) do {:ok, response} -> {:ok, response.status, normalize_headers(response.headers)} @@ -14,8 +20,15 @@ defmodule Hexdocs.HTTP do end end - def get(url, headers, _opts \\ []) do - case Req.get(url, headers: headers, retry: false, decode_body: false) do + def get(url, headers, opts \\ []) do + timeout = Keyword.get(opts, :receive_timeout, @receive_timeout) + + case Req.get(url, + headers: headers, + retry: false, + decode_body: false, + receive_timeout: timeout + ) do {:ok, response} -> {:ok, response.status, normalize_headers(response.headers), response.body} @@ -25,7 +38,13 @@ defmodule Hexdocs.HTTP do end def get_stream(url, headers) do - case Req.get(url, headers: headers, retry: false, decode_body: false, into: :self) do + case Req.get(url, + headers: headers, + retry: false, + decode_body: false, + receive_timeout: @receive_timeout, + into: :self + ) do {:ok, response} -> stream = stream_body(response.body.ref) {:ok, response.status, normalize_headers(response.headers), stream} @@ -41,7 +60,7 @@ defmodule Hexdocs.HTTP do body: body, retry: false, decode_body: false, - receive_timeout: 10_000 + receive_timeout: @receive_timeout ) do {:ok, response} -> {:ok, response.status, normalize_headers(response.headers), response.body} @@ -51,8 +70,16 @@ defmodule Hexdocs.HTTP do end end - def post(url, headers, body, _opts \\ []) do - case Req.post(url, headers: headers, body: body, retry: false, decode_body: false) do + def post(url, headers, body, opts \\ []) do + timeout = Keyword.get(opts, :receive_timeout, @receive_timeout) + + case Req.post(url, + headers: headers, + body: body, + retry: false, + decode_body: false, + receive_timeout: timeout + ) do {:ok, response} -> {:ok, response.status, normalize_headers(response.headers), response.body} @@ -61,8 +88,15 @@ defmodule Hexdocs.HTTP do end end - def delete(url, headers, _opts \\ []) do - case Req.delete(url, headers: headers, retry: false, decode_body: false) do + def delete(url, headers, opts \\ []) do + timeout = Keyword.get(opts, :receive_timeout, @receive_timeout) + + case Req.delete(url, + headers: headers, + retry: false, + decode_body: false, + receive_timeout: timeout + ) do {:ok, response} -> {:ok, response.status, normalize_headers(response.headers), response.body} diff --git a/lib/hexdocs/search/typesense.ex b/lib/hexdocs/search/typesense.ex index 4e189ba..2251a94 100644 --- a/lib/hexdocs/search/typesense.ex +++ b/lib/hexdocs/search/typesense.ex @@ -29,7 +29,7 @@ defmodule Hexdocs.Search.Typesense do url = url("collections/#{collection()}/documents/import?action=create") headers = [{"x-typesense-api-key", api_key()}] - case HTTP.post(url, headers, ndjson, recv_timeout: @timeout) do + case HTTP.post(url, headers, ndjson, receive_timeout: @timeout) do {:ok, 200, _resp_headers, ndjson} -> ndjson |> String.split("\n") @@ -63,7 +63,7 @@ defmodule Hexdocs.Search.Typesense do url = url("collections/#{collection()}/documents?" <> query) headers = [{"x-typesense-api-key", api_key()}] - case HTTP.delete(url, headers, recv_timeout: @timeout) do + case HTTP.delete(url, headers, receive_timeout: @timeout) do {:ok, 200, _resp_headers, _body} -> :ok