I am trying to handle a context timeout for every request. We have the following server structures:
Flow overview:
Go Server: Basically, acts as a [Reverse-proxy].2
Auth Server: Check for requests Authentication.
Application Server: Core request processing logic.
Now if Authorization server isn't able to process a request in stipulated time, then I want to close the goroutine from memory.
Here is what I tried:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
req, _ := http.NewRequest("GET", authorizationServer, nil)
req.Header = r.Header
req.WithContext(ctx)
res, error := client.Do(req)
select {
case <-time.After(10 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err()) // prints "context deadline exceeded"
}
Over here, context returns as "deadline exceeded", if the request is not processed in stipulated time. But it continues to process that request and return response in more than the specified time. So, how can I stop the request flow (goroutine), when time is exceeded.
I've also the complete request needs to be processed in 60 seconds with this code:
var netTransport = &http.Transport{
Dial: (&net.Dialer{
Timeout: 60 * time.Second,
}).Dial,
TLSHandshakeTimeout: 60 * time.Second,
}
client := &http.Client{
Timeout: time.Second * 60,
Transport: netTransport,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
So do I need any separate context implementations as well?
Note1: It would be awesome, if we can manage the timeout on every requests (goroutine) created by HTTP server, using context.
What happens in your code is very correct and behaves as expected.
You create a context with 5 seconds
timeout. You pass it to the request
and make that request. Let's say that request returns in 2 seconds. You then do a select
and either wait 10 seconds or wait for the context to finish. Context will always finish in the initial 5 seconds from when it was created and will also give that error every time it reaches the end.
The context
is independent of the request and it will reach its deadline unless cancelled previously. You cancel the request when the function finishes using defer
.
In your code the request takes your timeout in consideration. But the ctx.Err()
will return deadline exceeded
every time it reaches the timeout. Since that's what happens inside the context
. calling ctx.Err() multiple times will return the same error.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
go func () {
select {
case <-time.After(10 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err()) // prints "context deadline exceeded"
}
}()
req, _ := http.NewRequest("GET", authorizationServer, nil)
req.Header = r.Header
req = req.WithContext(ctx)
res, error := client.Do(req)
From the context documentation:
// Err returns a non-nil error value after Done is closed. Err returns
// Canceled if the context was canceled or DeadlineExceeded if the
// context's deadline passed. No other values for Err are defined.
// After Done is closed, successive calls to Err return the same value.
In your code, the timeout will always be reached and not cancelled, that is why you receive DeadlineExceeeded
. Your code is correct except the select part which will block until either 10 seconds pass or context timeout is reached. In your case always the context timeout is reached.
You should check the error
returned by the client.Do
call and not worry about the context
error in here. You are the one controlling the context. If the request times out, a case you should test of course, then a proper error would be returned for you to verify.