I can see two main issues in the example code below, but I don't know how to solve them correctly.
If the timeout handler does not get the signal through the errCh that the next handler has completed or an error occured, it will reply "408 Request timeout" to the request.
The problem here is that the ResponseWriter is not safe to be used by multiple goroutines. And the timeout handler starts a new goroutine when executing the next handler.
Issues:
How to prevent the next handler from writing into the ResponseWriter when the ctx's Done channel times out in the timeout handler.
How to prevent the timeout handler from replying 408 status code when the next handler is writing into the ResponseWriter but it has not finished yet and the ctx's Done channel times out in the timeout handler.
package main
import (
"context"
"fmt"
"net/http"
"time"
)
func main() {
http.Handle("/race", handlerFunc(timeoutHandler))
http.ListenAndServe(":8080", nil)
}
func timeoutHandler(w http.ResponseWriter, r *http.Request) error {
const seconds = 1
ctx, cancel := context.WithTimeout(r.Context(), time.Duration(seconds)*time.Second)
defer cancel()
r = r.WithContext(ctx)
errCh := make(chan error, 1)
go func() {
// w is not safe for concurrent use by multiple goroutines
errCh <- nextHandler(w, r)
}()
select {
case err := <-errCh:
return err
case <-ctx.Done():
// w is not safe for concurrent use by multiple goroutines
http.Error(w, "Request timeout", 408)
return nil
}
}
func nextHandler(w http.ResponseWriter, r *http.Request) error {
// just for fun to simulate a better race condition
const seconds = 1
time.Sleep(time.Duration(seconds) * time.Second)
fmt.Fprint(w, "nextHandler")
return nil
}
type handlerFunc func(w http.ResponseWriter, r *http.Request) error
func (fn handlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := fn(w, r); err != nil {
http.Error(w, "Server error", 500)
}
}