0

When making a http Get request in Go, it waits the full timeout time before returning an error, even when there is no network connection.

I assume in the internals it knows pretty quickly it has failed; I'd like that error to propagate up as soon as possible instead of waiting for the timeout time. I do want it to try for 20s when the network is there and just slow. How can I setup a client with this behaviour?

Code to see issue:

var client = &http.Client{
    Timeout: time.Second * 20,
}

response, err := client.Get(url)

If it matters I'm using gomobile and it's running on the iOS simulator.

Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189
scosman
  • 2,343
  • 18
  • 34
  • 5
    _” I assume in the internals it knows pretty quickly it has failed”_, no it doesn’t, which is why it has to time out. – JimB May 20 '23 at 18:11
  • 1
    Does this answer your question? [Check for Internet connection from application](https://stackoverflow.com/questions/50056144/check-for-internet-connection-from-application) – Oshawott May 20 '23 at 18:32
  • @JimB can you add some details explaining that? The initial TCP handshake will fail without a connected network interface, and at that point there's no chance of success. What's it doing for the next 19.99 seconds? – scosman May 21 '23 at 02:30
  • @Oshawott that answer suggests making the same Get call we're doing here. It would have the same issue (long timeout). – scosman May 21 '23 at 02:31
  • If you send something out over the network, the only way to know if it succeeded is by waiting for a response. That response could arrive at any time, or never, how long you wait is up to you, but waiting some amount of time is the only thing you can do. – JimB May 21 '23 at 13:48
  • This isn't the case of sending something out and not knowing how long something external will take. A networking stack first picks an active network interface (eg: eth0, wlan1) to send it out over. This question is about the case where there isn't an active network interface, nothing is sent out, and it fails rather quickly (entirely local to this device). In that case it doesn't need to wait for any response (nothing was sent), and should be able to error quickly. That might be abstracted away and there's no easy way in go, but it is possible and network stacks in other languages do this. – scosman May 21 '23 at 14:54
  • If there isn’t an active interface, then the call will fail immediately, because you can’t bind or send anything. I just double checked, and disconnecting the interface does just that with the default http transport. If the local network is down and returns a suitable icmp error from dns, it fails in a couple milliseconds. If you are waiting for the full timeout, it’s because the os has presented you with a working network interface and you are sending packets into the void. – JimB May 21 '23 at 15:00
  • I'm still seeing 20s on the platform I mentioned (iOS simulator, go mobile/gobind). Sounds like might be platform specific issue. Still, I think question is valid and above comments are targeted at a different case than described in question. I'll try real device later -- might be a simulator network binding issue and the answer is: the iOS simulator isn't perfect. – scosman May 21 '23 at 15:10
  • You are starting with the presumption that the runtime knows the call has failed and simply waits the full 20sec to notify you, which would be pretty ridiculous. You can either probe the network yourself with a short timeout (like what some modern browsers do), or reduce part of the overall timeout. In most cases you could safely set the connect timeout to something much shorter, while keeping the overall timeout the same. – JimB May 21 '23 at 15:14
  • As for the simulator, it’s probably working just fine. This is the most common network failure condition, you are going to encounter broken networks far more often than the device’s network turned off entirely. You also need to consider that you may be reusing an existing connection, in which case the runtime has no choice but to assume it’s working and wait for the receive or request timeout – JimB May 21 '23 at 15:27
  • I made no such presumption. I observed a non ideal outcome that is technically avoidable, that is very real, and consistently reproduces. I then asked stack overflow if anyone has a solution. That’s all. I think you might have misread my question but the details are all there. – scosman May 21 '23 at 18:47
  • Just to make sure we don’t keep looping: this question is specially for when there is no active network interface as mentioned in title. – scosman May 21 '23 at 18:53
  • “I assume in the internals it knows pretty quickly it has failed” is an incorrect assumption which led to a question which was not possible to answer. There is no error to “propagate up as soon as possible”, but you can easily change timeouts for various phases of the request. If there were no active interface, then the error would be returned immediately. If that’s not the case it is a bug somewhere and would require a [mre] for someone to help diagnose. – JimB May 21 '23 at 18:55
  • You’re answering a different question than asked. Your own test showed it can detect this. The question has the min example including platform. The answer here is “platform issue” not “it can’t detect this case and error quickly” – scosman May 21 '23 at 19:14

2 Answers2

1

The initial TCP handshake will fail without a connected network interface, and at that point there's no chance of success. What's it doing for the next 19.99 seconds?

The Go's HTTP client waits for the full timeout duration before returning an error when there is no network connection, because the client does not know the state of the network when it initiates the request. It only knows that it has not received a response within the specified timeout period.
The client is waiting for the operating system's TCP stack to return an error when it fails to establish a connection, and that can take some time because of various factors like the network configuration, the operating system's TCP/IP implementation, etc.

I do not know of a direct way to set a faster timeout for the case where there's no network connection in Go's HTTP client.

Another approach would be to combining the use of a context with a timeout and a separate goroutine that checks the network connectivity status. The goroutine would cancel the context if it detects that there's no network connection.
The request would use NewRequestWithContext

For instance:

ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()

// Check network connection in a separate goroutine.
go func() {
    if !isNetworkAvailable() {
        cancel()
    }
}()

req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
    log.Fatal(err)
}

response, err := client.Do(req)
if err != nil {
    log.Fatal(err)
}

You would need to implement isNetworkAvailable(): a function to check if there is a network connection available. This can be platform-specific, and on iOS you would likely need to use a system API to check the network status.

Note that this approach might not work perfectly in all scenarios. For example, the network could become available just after isNetworkAvailable() checks it, but before the HTTP request is made. In this case, the request would be canceled even though the network is available. But it might help in cases where the network is known to be unavailable for a longer period.


For instance:

import (
    "net"
    "time"
)

func isNetworkAvailable() bool {
    timeout := 2 * time.Second
    conn, err := net.DialTimeout("tcp", "8.8.8.8:53", timeout)
    if err != nil {
        return false
    }
    if conn != nil {
        defer conn.Close()
    }
    return true
}

This function tries to establish a TCP connection to 8.8.8.8 on port 53 (the port used by DNS servers) with a timeout of 2 seconds.
(as JimB adds in the comments: "All you can do is try to make some network calls with shorter timeouts")

If the function can't establish a connection within that time, it assumes there's no network connection and returns false.
If it can establish a connection, it closes the connection and returns true.

VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
  • If there’s no network at all, the dns or connect call will fail immediately just like with any other language. They’re nothing specific to go in this situation. All you can do is try to make some network calls with shorter timeouts. – JimB May 21 '23 at 13:45
  • @JimB Good point. That gave me a (crude) idea for `isNetworkAvailable()` – VonC May 21 '23 at 14:49
  • Usually we just set a shorter timeouts for the initial parts of the request like dial, handshake, response headers etc, which ever we can assume will be reliable indicators based on the situation, but probing like your suggestion is valid too. – JimB May 21 '23 at 15:34
-1

The answer here ends up being quite simple: golang internals can and does know when there is no network, and propagates up the failure in a timely manner without waiting for the 20s timeout. Nothing is sent over the network, and there's nothing to wait on. Go seems to be doing everything properly, and there's no changes needed to the sample code.

The issue still reproduces consistently, but only on the iOS simulator. It seems to be an issue specific to how the iOS simulator mapped connections to the host OS. Not sure if this is a longstanding issue, or a one-off on my MacOS/simulator pairing. On the host MacOS and real iOS devices it works properly, timing out immediately, when there's no network interface.

There's no need for an extra request, as that's just another path to same conclusion, which adds the possibility of other failures. Might be helpful to differentiate network issues from issues with specific service, or get an indicator of real network status (past the existence of a connected network interface) sooner.

scosman
  • 2,343
  • 18
  • 34