You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为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")
})

解决方案

问题根源

  1. 额外开启的goroutine导致Handler脱离了主流程控制,即使Context超时,goroutine内的代码仍会执行完毕。
  2. Handler内部没有监听Context的Done()信号,无法感知超时并终止执行。
  3. 并发写入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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 01:45:54