Go HTTP Server Timeouts
A few days ago, I was looking into an idle timeout error reported by a client using one of our APIs. To better understand HTTP server timeouts, I wrote some server and client code for testing, which I'd like to share with you. This might help others gain a clearer understanding. I believe the code is mostly self-explanatory, so I'll keep the explanations brief.
Write Timeout
WriteTimeout is the maximum duration before timing out writes of the response.
To trigger a write timeout, we simulate a delay in the server's root handler. The client gets an io.EOF
error when it tries to receive the response from the server.
server.go
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
// create a new HTTP request multiplexer (router)
h := http.NewServeMux()
// register the root handler
h.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
// simulate a delay of 2 seconds to trigger a write timeout
time.Sleep(2 * time.Second)
// write the status
w.WriteHeader(http.StatusOK)
})
// create the server with a write timeout of 1 second
s := &http.Server{
Addr: ":8080",
Handler: h,
WriteTimeout: 1 * time.Second,
}
// start the server
fmt.Println("server: listening...")
err := s.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
fmt.Printf("server: %s\n", err.Error())
}
}
client.go
package main
import (
"fmt"
"net/http"
"os"
)
func main() {
// try to send
resp, err := http.Get("http://localhost:8080/")
if err != nil {
fmt.Printf("client: %s\n", err)
os.Exit(1)
}
defer resp.Body.Close()
fmt.Printf("client: %s\n", resp.Status)
}
Client Output:
client: Get "http://localhost:8080/": EOF
exit status 1
Idle Timeout
IdleTimeout is the maximum amount of time to wait for the next request when keep-alives are enabled.
To trigger an idle timeout, we simulate a delay in the client after sending the first request and reading the status of the first response from the server. The client gets an io.EOF
error when it tries to read the status of the second response.
server.go
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
// create a new HTTP request multiplexer (router)
h := http.NewServeMux()
// register the root handler
h.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
// write the status
w.WriteHeader(http.StatusOK)
})
// create the server with an idle timeout of 1 second
s := &http.Server{
Addr: ":8080",
Handler: h,
IdleTimeout: 1 * time.Second,
}
// start the server
fmt.Println("server: listening...")
err := s.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
fmt.Printf("server: %s\n", err.Error())
}
}
client.go
package main
import (
"bufio"
"fmt"
"net"
"os"
"time"
)
func main() {
// connect to the server
conn, err := net.Dial("tcp", "localhost:8080")
if err != nil {
fmt.Printf("client: %s\n", err.Error())
os.Exit(1)
}
// write the first request
fmt.Fprintf(conn, "GET / HTTP/1.1\r\n")
fmt.Fprintf(conn, "Host: localhost:8080\r\n")
fmt.Fprintf(conn, "Connection: keep-alive\r\n")
fmt.Fprintf(conn, "\r\n")
// read the status of the first response
reader := bufio.NewReader(conn)
status, err := reader.ReadString('\n')
if err != nil {
fmt.Printf("client: %s\n", err.Error())
os.Exit(1)
}
fmt.Printf("client: %s", status)
// discard the rest of the first response
for {
line, err := reader.ReadString('\n')
if err != nil {
fmt.Println("client: ", err.Error())
os.Exit(1)
}
if line == "\r\n" {
break
}
}
// simulate a delay of 2 seconds to trigger an idle timeout
time.Sleep(2 * time.Second)
// write the second request
fmt.Fprintf(conn, "GET / HTTP/1.1\r\n")
fmt.Fprintf(conn, "Host: localhost:8080\r\n")
fmt.Fprintf(conn, "Connection: keep-alive\r\n")
fmt.Fprintf(conn, "\r\n")
// try to read the status of the second response
status, err = reader.ReadString('\n')
if err != nil {
fmt.Printf("client: %s\n", err.Error())
os.Exit(1)
}
fmt.Printf("client: %s", status)
}
Client Output
client: HTTP/1.1 200 OK
client: EOF
exit status 1
Read Timeout
ReadTimeout is the maximum duration for reading the entire request, including the body.
To trigger a read timeout, we simulate a delay in the client after it connects to the server. The client gets an io.EOF
error when it tries to read the server's response status.
server.go
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
// create a new HTTP request multiplexer (router)
h := http.NewServeMux()
// register the root handler
h.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
// write the status
w.WriteHeader(http.StatusOK)
})
// create the server with a read timeout of 1 second
s := &http.Server{
Addr: ":8080",
Handler: h,
ReadTimeout: 1 * time.Second,
}
// start the server
fmt.Println("server: listening...")
err := s.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
fmt.Printf("server: %s\n", err.Error())
}
}
package main
import (
"bufio"
"fmt"
"net"
"os"
"time"
)
func main() {
// connect to the server
conn, err := net.Dial("tcp", "localhost:8080")
if err != nil {
fmt.Printf("client: %s\n", err.Error())
os.Exit(1)
}
// simulate a delay of 2 seconds to trigger a read timeout
time.Sleep(2 * time.Second)
// write the first request
fmt.Fprintf(conn, "GET / HTTP/1.1\r\n")
fmt.Fprintf(conn, "Host: localhost:8080\r\n")
fmt.Fprintf(conn, "Connection: keep-alive\r\n")
fmt.Fprintf(conn, "\r\n")
// read the status of the first response
reader := bufio.NewReader(conn)
status, err := reader.ReadString('\n')
if err != nil {
fmt.Printf("client: %s\n", err.Error())
os.Exit(1)
}
fmt.Printf("client: %s", status)
}
Client Output
client: EOF
exit status 1
Subscribe to my newsletter
Read articles from Erman İmer directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by