Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
u0-uu-0u opened this issue Feb 10, 2025 · 2 comments
Open

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

u0-uu-0u opened this issue Feb 10, 2025 · 2 comments

Comments

@u0-uu-0u
Copy link

u0-uu-0u commented Feb 10, 2025

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
@nh2
Copy link

nh2 commented Feb 10, 2025

(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 clarify:

timeout just sends the async exception. That should always work. It looks like the code that receives the exception blocks it. We should find out which code that is, in http-client or some underlying function that it uses.

@u0-uu-0u
Copy link
Author

u0-uu-0u commented Feb 11, 2025

throwIO, called at ./Network/HTTP/Client/Core.hs:214:29 in http-client-0.7.18-8b51d1b15e5c25165b3bb85934d446140d1bbf69417f7f85bf9c607f9642027b:Network.HTTP.Client.Core

looks like

wrapExc req0 = handle $ throwIO . toHttpException req0
the throwIO , I guess on calling httpRaw', a function which I do not at all comprehend.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants