如何为HTTP Server请求实现Context超时并终止超时后执行的Handler
如何为HTTP Server请求实现60秒Context超时并阻止超时后Handler继续执行?
需求是给HTTP Server的所有请求添加60秒的Context超时,其中部分请求Handler已经使用了带值的Context。当前遇到的问题是:超时后虽然能返回HTTP 408,但Handler会继续执行,无法及时终止。
实现前的原始代码
srv := &http.Server{ Addr: fmt.Sprintf("%s:%d", h.host, h.port), Handler: http.HandlerFunc(h.serve), } err := srv.ListenAndServe() ... func (h *HTTP) serve(w http.ResponseWriter, r *http.Request) { var allow []string for _, route := range h.routes { if route.regex != nil { // 正则路径匹配 matches := route.regex.FindStringSubmatch(r.URL.Path) lenMatches := len(matches) if lenMatches > 0 { if lenMatches == 1 { route.handler(w, r) // 需要添加60秒Context超时 } else { // 创建带值的Context ctx := context.WithValue(r.Context(), ctx_http, matches[1:]) route.handler(w, r.WithContext(ctx)) // 需要添加60秒Context超时 } return } } else { // 普通路径匹配 if strings.HasPrefix(r.URL.Path, route.path) { route.handler(w, r) // 需要添加60秒Context超时 return } } } http.Error(w, "Not found", http.StatusNotFound) }
实现后的异常情况
测试场景
- 请求Handler中设置10秒休眠
- Context超时设置为2秒
正常表现
- 请求超过2秒后返回
HTTP 408并关闭连接,符合预期
异常表现
- 超时后Handler仍继续执行,8秒后打印
req completed
输出日志
2023-08-28 20:14:47 context timeout exceeded 2023-08-28 20:14:47 cancel 2023-08-28 20:14:55 req completed
实现后的问题代码
func (h *HTTP) serve(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), time.Duration(2 * time.Second)) defer func(){ fmt.Println(time.Now(), "cancel") cancel() }() go func(){ for _, route := range h.routes { if route.regex != nil { // 正则路径匹配 matches := route.regex.FindStringSubmatch(r.URL.Path) lenMatches := len(matches) if lenMatches > 0 { // 捕获路径参数 if lenMatches > 1 { ctx = context.WithValue(ctx, ctx_http, matches[1:]) } route.handler(w, r.WithContext(ctx)) cancel() return } } else { // 普通路径匹配 if strings.HasPrefix(r.URL.Path, route.path) { route.handler(w, r.WithContext(ctx)) cancel() return } } } http.Error(w, "Not found", http.StatusNotFound) cancel() }() select { case <-ctx.Done(): switch ctx.Err() { case context.DeadlineExceeded: fmt.Println(time.Now(), "context timeout exceeded") http.Error(w, "Timeout", http.StatusRequestTimeout) case context.Canceled: fmt.Println(time.Now(), "context cancelled by force. whole process is complete") } } }
请求Handler代码
h.Route(serv.ALL, "/timeout", func(w http.ResponseWriter, r *http.Request){ defer serv.Recover(w) time.Sleep(10 * time.Second) fmt.Println(time.Now(), "req completed") io.WriteString(w, "timeout test") })
解决方案
问题根源
- 额外开启的goroutine导致Handler脱离了主流程控制,即使Context超时,goroutine内的代码仍会执行完毕。
- Handler内部没有监听Context的
Done()信号,无法感知超时并终止执行。 - 并发写入
ResponseWriter(超时后的http.Error和Handler后续的io.WriteString)可能引发未知错误。
修正后的代码
1. 改造serve函数,正确传递超时Context
func (h *HTTP) serve(w http.ResponseWriter, r *http.Request) { // 创建60秒超时的Context,继承原请求的Context ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second) defer cancel() // 确保请求结束时取消Context // 将超时Context替换到请求对象中 r = r.WithContext(ctx) var allow []string for _, route := range h.routes { if route.regex != nil { matches := route.regex.FindStringSubmatch(r.URL.Path) lenMatches := len(matches) if lenMatches > 0 { if lenMatches > 1 { // 基于超时Context创建带值的Context,确保值也继承超时特性 ctxWithValue := context.WithValue(ctx, ctx_http, matches[1:]) r = r.WithContext(ctxWithValue) } route.handler(w, r) return } } else { if strings.HasPrefix(r.URL.Path, route.path) { route.handler(w, r) return } } } http.Error(w, "Not found", http.StatusNotFound) }
2. 改造Handler,监听Context超时信号
h.Route(serv.ALL, "/timeout", func(w http.ResponseWriter, r *http.Request){ defer serv.Recover(w) // 用select同时监听休眠完成和Context超时 select { case <-time.After(10 * time.Second): fmt.Println(time.Now(), "req completed") io.WriteString(w, "timeout test") case <-r.Context().Done(): // 感知到Context超时/取消,立即终止执行 fmt.Println(time.Now(), "handler stopped due to context cancel") return } })
核心要点
- Context传递:所有带值的Context必须基于超时Context创建,确保超时特性被继承。
- 主动监听:Handler必须主动监听
r.Context().Done(),在耗时操作中插入检查,一旦触发就立即返回。 - 避免额外goroutine:http.Server会为每个请求自动分配goroutine,无需在serve函数中额外开启,防止并发写ResponseWriter和后台残留执行的问题。
内容的提问来源于stack exchange,提问作者clarkk
相关产品推荐
相关产品推荐

