Http client with time-limited brRead

Http client with timeouts applied in between body read events.
Note that the response timeout in
http-client is applied only when
receiving the response headers which is not always satisfactory given that a
slow server may send the rest of the response very slowly.
How do I test this?
A slow server can be emulated in Nginx with the following configuration.
nginx.conf
user nobody;
worker_processes 2;
events {
worker_connections 1024;
}
http {
default_type application/octet-stream;
sendfile on;
server {
listen 8010;
server_name main;
location /slow {
echo 1; echo_flush;
# send extra chunks of the response body once in 20 sec
echo_sleep 20; echo 2; echo_flush;
echo_sleep 20; echo 3; echo_flush;
echo_sleep 20; echo 4;
}
location /very/slow {
echo 1; echo_flush;
echo_sleep 20; echo 2; echo_flush;
# chunk 3 is extremely slow (40 sec)
echo_sleep 40; echo 3; echo_flush;
echo_sleep 20; echo 4;
}
}
}
GHCi REPL session (cabal repl).
*N.HTTP.C.BrReadWithTimeout> man <- newManager defaultManagerSettings
*N.HTTP.C.BrReadWithTimeout> reqVerySlow <- parseRequest "GET http://127.0.0.1:8010/very/slow"
*N.HTTP.C.BrReadWithTimeout> reqSlow <- parseRequest "GET http://127.0.0.1:8010/slow"
*N.HTTP.C.BrReadWithTimeout> :set +s
*N.HTTP.C.BrReadWithTimeout> httpLbs reqVerySlow man
Response {responseStatus = Status {statusCode = 200, statusMessage = "OK"}, responseVersion = HTTP/1.1, responseHeaders = [("Server","nginx/1.28.0"),("Date","Thu, 26 Feb 2026 13:33:38 GMT"),("Content-Type","application/octet-stream"),("Transfer-Encoding","chunked"),("Connection","keep-alive")], responseBody = "1\n2\n3\n4\n", responseCookieJar = CJ {expose = []}, responseClose' = ResponseClose, responseOriginalRequest = Request {
host = "127.0.0.1"
port = 8010
secure = False
requestHeaders = []
path = "/very/slow"
queryString = ""
method = "GET"
proxy = Nothing
rawBody = False
redirectCount = 10
responseTimeout = ResponseTimeoutDefault
requestVersion = HTTP/1.1
proxySecureMode = ProxySecureWithConnect
}
, responseEarlyHints = []}
(80.08 secs, 1,266,728 bytes)
*N.HTTP.C.BrReadWithTimeout> httpLbsBrReadWithTimeout reqVerySlow man
*** Exception: HttpExceptionRequest Request {
host = "127.0.0.1"
port = 8010
secure = False
requestHeaders = []
path = "/very/slow"
queryString = ""
method = "GET"
proxy = Nothing
rawBody = False
redirectCount = 10
responseTimeout = ResponseTimeoutMicro 30000000
requestVersion = HTTP/1.1
proxySecureMode = ProxySecureWithConnect
}
ResponseTimeout
While handling HttpExceptionContentWrapper {unHttpExceptionContentWrapper = ResponseTimeout}
HasCallStack backtrace:
throwIO, called at ./Network/HTTP/Client/BrReadWithTimeout.hs:123:26 in http-client-brread-timeout-0.3.0.0-inplace:Network.HTTP.Client.BrReadWithTimeout
*N.HTTP.C.BrReadWithTimeout> httpLbsBrReadWithTimeout reqSlow man
Response {responseStatus = Status {statusCode = 200, statusMessage = "OK"}, responseVersion = HTTP/1.1, responseHeaders = [("Server","nginx/1.28.0"),("Date","Thu, 26 Feb 2026 13:37:49 GMT"),("Content-Type","application/octet-stream"),("Transfer-Encoding","chunked"),("Connection","keep-alive")], responseBody = "1\n2\n3\n4\n", responseCookieJar = CJ {expose = []}, responseClose' = ResponseClose, responseOriginalRequest = Request {
host = "127.0.0.1"
port = 8010
secure = False
requestHeaders = []
path = "/slow"
queryString = ""
method = "GET"
proxy = Nothing
rawBody = False
redirectCount = 10
responseTimeout = ResponseTimeoutDefault
requestVersion = HTTP/1.1
proxySecureMode = ProxySecureWithConnect
}
, responseEarlyHints = []}
(60.05 secs, 1,261,728 bytes)
Here, the first request comes from the standard httpLbs which, after timely
receiving of the first chunk of the response (including headers and the first
chunk of the body), no longer applies any timeouts and may last as long as the
response endures: in this case, it lasts 80 seconds and successfully returns.
In the second request, httpLbsBrReadWithTimeout timely receives the first
chunk of the response too, the second chunk comes in 20 seconds, and finally,
as the third chunk is going to come in 40 seconds which exceeds the default
response timeout value (30 seconds), the function throws ResponseTimeout
exception after 50 seconds from the start of the request. In the third request,
httpLbsBrReadWithTimeout returns successfully after 60 seconds because every
extra chunk of the response was coming every 20 seconds without triggering the
timeout.