Reputation: 1234
I would like to know if there's any way of making a Go HTTP server aware of a timeout in the client, and immediately terminate the processing of the ongoing request. Currently, I've tried setting timeouts on the client side that actually work as expected on their side and the request finishes with context deadline exceeded (Client.Timeout exceeded while awaiting headers)
after the timeout is reached.
req, err := http.NewRequest(http.MethodGet, URL, nil)
if err != nil {
log.Fatal(err)
}
client := http.Client{Timeout: time.Second}
_, err = client.Do(req)
if err != nil {
log.Fatal(err)
}
I've also tried with different versions of the client code, like using a request with context, and got the same result, which is ok for the client side.
However, when it comes to detect the timeout on the server side, it turns out that the processing of the request continues until the server finishes its work, regardless of the timeout in the client, and what I would like to happen (I don't know if it's even possible) is to immediately terminate and abort the processing once the client has timed out.
The sever side code would be something like this (just for the sake of the example, in production code it would be something more sophisticated):
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Println("before sleep")
time.Sleep(3 * time.Second)
fmt.Println("after sleep")
fmt.Fprintf(w, "Done!")
}
func main() {
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
When the previous code is run, and a request hits the HTTP server, the following sequence of events occurs:
before sleep
context deadline exceeded (Client.Timeout exceeded while awaiting headers)
after sleep
But what I would like to happen is to terminate the process at step 3.
Thank being said, I'd like to know your thoughts about it, and whether you think what I want to do is feasible or not.
Upvotes: 1
Views: 1873
Reputation: 1568
There are a few different ideas at play here. First, to confirm what you are asking for, it looks like you want to make a client disconnection trigger the whole server to be shut down. To do this you can do the following:
context.WithCancel
or a channel
to use to propagate a shutdown eventHere is a complete sample program that produces the following output:
go run ./main.go
2021/03/04 17:56:44 client: starting request
2021/03/04 17:56:44 server: handler started
2021/03/04 17:56:45 client: deadline exceeded
2021/03/04 17:56:45 server: client request canceled
2021/03/04 17:56:45 server: performing server shutdown
2021/03/04 17:56:45 waiting for goroutines to finish
2021/03/04 17:56:45 All exited!
// main.go
package main
import (
"context"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"sync"
"time"
)
func main() {
wg := &sync.WaitGroup{}
srvContext, srvCancel := context.WithCancel(context.Background())
defer srvCancel()
srv := http.Server{
Addr: ":8000",
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("server: handler started")
select {
case <-time.After(2 * time.Second):
log.Printf("server: completed long request")
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
case <-r.Context().Done():
log.Printf("server: client request canceled")
srvCancel()
return
}
}),
}
// add a goroutine that watches for the server context to be canceled
// as a signal that it is time to stop the HTTP server.
wg.Add(1)
go func() {
defer wg.Done()
<-srvContext.Done()
log.Printf("server: performing server shutdown")
// optionally add a deadline context to avoid waiting too long
if err := srv.Shutdown(context.TODO()); err != nil {
log.Printf("server: shutdown failed with context")
}
}()
// just simulate making the request after a brief delay
wg.Add(1)
go makeClientRequest(wg)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
fmt.Fprintf(os.Stderr, "Server failed listening with error: %v\n", err)
return
}
log.Printf("waiting for goroutines to finish")
wg.Wait()
log.Printf("All exited!")
}
func makeClientRequest(wg *sync.WaitGroup) {
defer wg.Done()
// delay client request
time.Sleep(500 * time.Millisecond)
log.Printf("client: starting request")
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://127.0.0.1:8000", http.NoBody)
if err != nil {
log.Fatalf("failed making client request")
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
log.Printf("client: deadline exceeded")
} else {
log.Printf("client: request error: %v", err)
}
return
}
// got a non-error response
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
log.Printf("client: got response %d %s", resp.StatusCode, string(body))
}
Upvotes: 1