Skip to content

Commit 5d132dc

Browse files
try all addresses returned by getAddrInfo
Currently, openTCPConnection only considered the first address returned by getAddrInfo. This can cause connection failures. For example, if the first address returned is an IPv6 address, but the HTTP server only listens on IPv4, then the connection will be refused, and the library gives up. Update openTCPConnection to try each address returned by getAddrInfo. This is a naïve algorithm; better approaches are possible (e.g. see https://en.wikipedia.org/wiki/Happy_Eyeballs). This change may regress performance of this library if getAddrInfo returns two or more addresses and several connection attempts time out (as opposed to "connection refused"). But most users would probably prefer to succeed slowly than to fail unnecessarily.
1 parent 8ca3be5 commit 5d132dc

File tree

1 file changed

+31
-8
lines changed

1 file changed

+31
-8
lines changed

Network/TCP.hs

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
{-# LANGUAGE ScopedTypeVariables #-}
12
{-# LANGUAGE TypeSynonymInstances #-}
23
-----------------------------------------------------------------------------
34
-- |
@@ -61,7 +62,7 @@ import Network.Socket ( socketToHandle )
6162
import Data.Char ( toLower )
6263
import Data.Word ( Word8 )
6364
import Control.Concurrent
64-
import Control.Exception ( onException )
65+
import Control.Exception ( IOException, bracketOnError, try )
6566
import Control.Monad ( liftM, when )
6667
import System.IO ( Handle, hFlush, IOMode(..), hClose )
6768
import System.IO.Error ( isEOFError )
@@ -236,15 +237,37 @@ openTCPConnection_ uri port stashInput = do
236237
-- like this as it just does a once-only installation of a shutdown handler to run at program exit,
237238
-- rather than actually shutting down after the action
238239
addrinfos <- withSocketsDo $ getAddrInfo (Just $ defaultHints { addrFamily = AF_UNSPEC, addrSocketType = Stream }) (Just fixedUri) (Just . show $ port)
240+
241+
let
242+
connectAddrInfo a = bracketOnError
243+
(socket (addrFamily a) Stream defaultProtocol) -- acquire
244+
Network.Socket.close -- release
245+
( \s -> do
246+
setSocketOption s KeepAlive 1
247+
connect s (addrAddress a)
248+
socketConnection_ fixedUri port s stashInput )
249+
250+
-- try multiple addresses; return Just connected socket or Nothing
251+
tryAddrInfos [] = return Nothing
252+
tryAddrInfos (h:t) =
253+
let next = \(_ :: IOException) -> tryAddrInfos t
254+
in try (connectAddrInfo h) >>= either next (return . Just)
255+
239256
case addrinfos of
240257
[] -> fail "openTCPConnection: getAddrInfo returned no address information"
241-
(a:_) -> do
242-
s <- socket (addrFamily a) Stream defaultProtocol
243-
onException (do
244-
setSocketOption s KeepAlive 1
245-
connect s (addrAddress a)
246-
socketConnection_ fixedUri port s stashInput
247-
) (Network.Socket.close s)
258+
259+
-- single AddrInfo; call connectAddrInfo directly so that specific
260+
-- exception is thrown in event of failure
261+
[ai] -> connectAddrInfo ai `catchIO` (\e -> fail $
262+
"openTCPConnection: failed to connect to "
263+
++ show (addrAddress ai) ++ ": " ++ show e)
264+
265+
-- multiple AddrInfos; try each until we get a connection, or run out
266+
ais ->
267+
let
268+
err = fail $ "openTCPConnection: failed to connect; tried addresses: "
269+
++ show (fmap addrAddress ais)
270+
in tryAddrInfos ais >>= maybe err return
248271

249272
-- | @socketConnection@, like @openConnection@ but using a pre-existing 'Socket'.
250273
socketConnection :: BufferType ty

0 commit comments

Comments
 (0)