Skip to content

httpLbs is not interruptable by timeout when dns is down #553

@u0-uu-0u

Description

@u0-uu-0u

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions