When I do timeout 1000000 (httpLbs req httpman) and have bad dns settings, the request can take much longer than those 1000000 microseconds (1s), and in fact only fails when the local dns times out (which on my system is 20 seconds)
(I thought this was just an inherent issue in timeout, but nh2 at https://old.reddit.com/r/haskell/comments/1ierl0f/myth_and_truth_in_haskell_asynchronous_exceptions/ indicated it might actually be a bug in the network library, so reporting here in case it actually is unexpected.)
To reproduce
$ cat dnsbug.cabal
cabal-version: 2.4
name: dnsbug
version: 0.1.0.0
build-type: Simple
executable dnsbug
main-is: Bug.hs
build-depends: base >=4.12 && <5
, http-client
default-language: GHC2021
$ cat Bug.hs
{-# LANGUAGE OverloadedStrings #-}
module Main where
import Network.HTTP.Client
import System.Timeout (timeout)
main = do
httpman <- newManager defaultManagerSettings
req <- parseRequest "http://example.com"
res <- timeout 1000000 (httpLbs req httpman)
print res
$ grep ^PRETTY /etc/os-release
PRETTY_NAME="Ubuntu 24.04.1 LTS"
$ ghc --version
The Glorious Glasgow Haskell Compilation System, version 9.12.1
(also tested on 9.2.8)
With working DNS this does the expected thing:
$ cabal build
Up to date
$ cabal run
Just (Response {responseStatus = Status {statusCode = 200, statusMessage = "OK"}, responseVersion = HTTP/1.1, responseHeaders = [("Accept-Ranges","bytes"),("Content-Type","text/html"),("ETag","\"84238dfc8092e5d9c0dac8ef93371a07:1736799080.121134\""),("Last-Modified","Mon, 13 Jan 2025 20:11:20 GMT"),("Vary","Accept-Encoding"),("Content-Encoding","gzip"),("Content-Length","648"),("Cache-Control","max-age=1158"),("Date","Thu, 06 Feb 2025 10:17:28 GMT"),("Connection","keep-alive")], responseBody = "<!doctype html>\n<html>\n<head>\n <title>Example Domain</title>\n\n <meta charset=\"utf-8\" />\n <meta http-equiv=\"Content-type\" content=\"text/html; charset=utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <style type=\"text/css\">\n body {\n background-color: #f0f0f2;\n margin: 0;\n padding: 0;\n font-family: -apple-system, system-ui, BlinkMacSystemFont, \"Segoe UI\", \"Open Sans\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n \n }\n div {\n width: 600px;\n margin: 5em auto;\n padding: 2em;\n background-color: #fdfdff;\n border-radius: 0.5em;\n box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);\n }\n a:link, a:visited {\n color: #38488f;\n text-decoration: none;\n }\n @media (max-width: 700px) {\n div {\n margin: 0 auto;\n width: auto;\n }\n }\n </style> \n</head>\n\n<body>\n<div>\n <h1>Example Domain</h1>\n <p>This domain is for use in illustrative examples in documents. You may use this\n domain in literature without prior coordination or asking for permission.</p>\n <p><a href=\"https://www.iana.org/domains/example\">More information...</a></p>\n</div>\n</body>\n</html>\n", responseCookieJar = CJ {expose = []}, responseClose' = ResponseClose, responseOriginalRequest = Request {
host = "example.com"
port = 80
secure = False
requestHeaders = []
path = "/"
queryString = ""
method = "GET"
proxy = Nothing
rawBody = False
redirectCount = 10
responseTimeout = ResponseTimeoutDefault
requestVersion = HTTP/1.1
proxySecureMode = ProxySecureWithConnect
}
, responseEarlyHints = []})
Now change DNS to a fake IP, so that DNS requests will hang (until the local resolver times out):
$ sudo resolvectl dns eth0 192.0.2.1
$ time cabal run
dnsbug: Uncaught exception http-client-0.7.18-8b51d1b15e5c25165b3bb85934d446140d1bbf69417f7f85bf9c607f9642027b:Network.HTTP.Client.Types.HttpException:
HttpExceptionRequest Request {
host = "example.com"
port = 80
secure = False
requestHeaders = []
path = "/"
queryString = ""
method = "GET"
proxy = Nothing
rawBody = False
redirectCount = 10
responseTimeout = ResponseTimeoutDefault
requestVersion = HTTP/1.1
proxySecureMode = ProxySecureWithConnect
}
(ConnectionFailure Network.Socket.getAddrInfo (called with preferred socket type/protocol: AddrInfo {addrFlags = [], addrFamily = AF_UNSPEC, addrSocketType = Stream, addrProtocol = 0, addrAddress = 0.0.0.0:0, addrCanonName = Nothing}, host name: "example.com", service name: "80"): does not exist (Name or service not known))
While handling HttpExceptionContentWrapper {unHttpExceptionContentWrapper = ConnectionFailure Network.Socket.getAddrInfo (called with preferred socket type/protocol: AddrInfo {addrFlags = [], addrFamily = AF_UNSPEC, addrSocketType = Stream, addrProtocol = 0, addrAddress = 0.0.0.0:0, addrCanonName = Nothing}, host name: "example.com", service name: "80"): does not exist (Name or service not known)}
HasCallStack backtrace:
throwIO, called at ./Network/HTTP/Client/Core.hs:214:29 in http-client-0.7.18-8b51d1b15e5c25165b3bb85934d446140d1bbf69417f7f85bf9c607f9642027b:Network.HTTP.Client.Core
real 0m20,164s
user 0m0,093s
sys 0m0,063s
So that took 20s to time out where I asked for 1s.
strace of the above: https://termbin.com/n2y2
Now I re-enable working DNS and try again:
$ sudo systemctl restart systemd-resolved
$ time cabal run
Just (Response {responseStatus = Status {statusCode = 200, statusMessage = "OK"}, responseVersion = HTTP/1.1, responseHeaders = [("Accept-Ranges","bytes"),("Content-Type","text/html"),("ETag","\"84238dfc8092e5d9c0dac8ef93371a07:1736799080.121134\""),("Last-Modified","Mon, 13 Jan 2025 20:11:20 GMT"),("Vary","Accept-Encoding"),("Content-Encoding","gzip"),("Cache-Control","max-age=2835"),("Date","Thu, 06 Feb 2025 10:19:15 GMT"),("Content-Length","648"),("Connection","keep-alive")], responseBody = "<!doctype html>\n<html>\n<head>\n <title>Example Domain</title>\n\n <meta charset=\"utf-8\" />\n <meta http-equiv=\"Content-type\" content=\"text/html; charset=utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <style type=\"text/css\">\n body {\n background-color: #f0f0f2;\n margin: 0;\n padding: 0;\n font-family: -apple-system, system-ui, BlinkMacSystemFont, \"Segoe UI\", \"Open Sans\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n \n }\n div {\n width: 600px;\n margin: 5em auto;\n padding: 2em;\n background-color: #fdfdff;\n border-radius: 0.5em;\n box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);\n }\n a:link, a:visited {\n color: #38488f;\n text-decoration: none;\n }\n @media (max-width: 700px) {\n div {\n margin: 0 auto;\n width: auto;\n }\n }\n </style> \n</head>\n\n<body>\n<div>\n <h1>Example Domain</h1>\n <p>This domain is for use in illustrative examples in documents. You may use this\n domain in literature without prior coordination or asking for permission.</p>\n <p><a href=\"https://www.iana.org/domains/example\">More information...</a></p>\n</div>\n</body>\n</html>\n", responseCookieJar = CJ {expose = []}, responseClose' = ResponseClose, responseOriginalRequest = Request {
host = "example.com"
port = 80
secure = False
requestHeaders = []
path = "/"
queryString = ""
method = "GET"
proxy = Nothing
rawBody = False
redirectCount = 10
responseTimeout = ResponseTimeoutDefault
requestVersion = HTTP/1.1
proxySecureMode = ProxySecureWithConnect
}
, responseEarlyHints = []})
real 0m0,442s
user 0m0,090s
sys 0m0,037s
And if I shorten the timeout even more, still with working dns, it times out the expected way:
$ vim Bug.hs
$ cabal build &>/dev/null
$ time cabal run
Nothing
real 0m0,222s
user 0m0,078s
sys 0m0,046s
When I do
timeout 1000000 (httpLbs req httpman)and have bad dns settings, the request can take much longer than those 1000000 microseconds (1s), and in fact only fails when the local dns times out (which on my system is 20 seconds)(I thought this was just an inherent issue in timeout, but nh2 at https://old.reddit.com/r/haskell/comments/1ierl0f/myth_and_truth_in_haskell_asynchronous_exceptions/ indicated it might actually be a bug in the network library, so reporting here in case it actually is unexpected.)
To reproduce
(also tested on 9.2.8)
With working DNS this does the expected thing:
Now change DNS to a fake IP, so that DNS requests will hang (until the local resolver times out):
So that took 20s to time out where I asked for 1s.
strace of the above: https://termbin.com/n2y2
Now I re-enable working DNS and try again:
And if I shorten the timeout even more, still with working dns, it times out the expected way: