Search code examples
gohttperror-handling

How to check for http timout error in Go?


New to go. This is currently my code:

client := http.Client{
    Timeout: 10 * time.Millisecond,
}
resp, err := client.Get("http://google.com/")
if err != nil {
    if os.IsTimeout(err) {
        fmt.Println("There was a timeout")
    }
    panic(err)
}

This works, but os.IsTimeout writes:

// This function predates errors.Is, and the notion of whether an
// error indicates a timeout can be ambiguous. For example, the Unix
// error EWOULDBLOCK sometimes indicates a timeout and sometimes does not.
// New code should use errors.Is with a value appropriate to the call
// returning the error, such as os.ErrDeadlineExceeded.
if errors.Is(err, os.ErrDeadlineExceeded) {
    fmt.Println("There was a timeout 2")
}

Which doesn't work. I tried debugging, but there wasn't an easy way for me to see how to check for the specific error type. Coming from.NET I could directly see the exception type, and check for that, how do I go about this in the future?


Solution

  • You would not do it this way nowadays and setting a timeout on the client is left as per the api stability guarantee.

    Today, you'd set up a client and do a request with a context.WithTimeout.

    TL;DR: The context will be "Done" if the request times out before a response is received and not be "Done" if the response is returned before the timeout is reached.

    package main
    
    import (
        "context"
        "io"
        "log"
        "net/http"
        "net/url"
        "time"
    )
    
    func main() {
    
        // Start a http server to test the timeout
        srv := setupServer()
        defer srv.Shutdown(context.Background())
    
        // Examle 1: Using a context with a timeout for a request
        // =======================================================
    
        // create a default http client
        client := &http.Client{}
        ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    
        // Release resources when done
        defer cancel()
    
        // Create a new request with our context
        req, _ := http.NewRequestWithContext(ctx, "GET", "http://localhost:8080/hellotimeout", nil)
    
        // Start a timer
        start := time.Now()
        // Make the request
        _, err := client.Do(req)
    
        // Check if the request was anything other than a timeout
        // Note that we could check for the timeout error here.
        // However, we will use a select statement to demonstrate how to handle the timeout.
        if urlErr, isURLErr := err.(*url.Error); isURLErr && !urlErr.Timeout() {
            log.Printf("Something went wrong: %s", urlErr)
            return
        }
    
        select {
        // If the request times out, the context will be done.
        // If the request is completed here, the context will not be done and the default case will be executed.
        case <-ctx.Done():
            log.Printf("Request timed out after %s", time.Since(start))
        default:
            log.Printf("Request completed after %s", time.Since(start))
            log.Println("Processing response...")
        }
    
        // Examle 2: The same, but this time the request does not timeout
        // ==============================================================
    
        // Create a new request with our context
        ctx2, cancel2 := context.WithTimeout(context.Background(), 3*time.Second)
        defer cancel2()
        req, _ = http.NewRequestWithContext(ctx2, "GET", "http://localhost:8080/hello", nil)
    
        // Start a timer
        start = time.Now()
        // Make the request
        resp, err := client.Do(req)
    
        // Check if the request was anything other than a timeout
        if urlErr, isURLErr := err.(*url.Error); isURLErr && !urlErr.Timeout() {
            log.Printf("Something went wrong: %s", urlErr)
            return
        }
    
        select {
        case <-ctx2.Done():
            log.Printf("Request timed out after %s", time.Since(start))
            return
        default:
            log.Printf("Request completed after %s", time.Since(start))
        }
        log.Println("Processing response...")
        io.Copy(log.Writer(), resp.Body)
    }
    
    func setupServer() *http.Server {
        // Setup a server so we can test the timeout
        srv := &http.Server{Addr: ":8080"}
    
        http.Handle("/hellotimeout", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // Simulate a long running request
            time.Sleep(5 * time.Second)
            w.Write([]byte("Hello, World!"))
        }))
    
        http.Handle("/hello", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // Respond immediately
            w.Write([]byte("Hello, World!"))
        }))
    
        // Start the server in "background"
    
        go srv.ListenAndServe()
        return srv
    }
    

    Run on playground