自研Go反向代理可访问站点但静态资源加载异常问题咨询
问题背景
自行实现的Go反向代理完成鉴权后转发请求到Prometheus源站,出现以下现象:
- 站点主页面可正常加载
- 页面引用的所有静态资源(样式、脚本、图片)无法正常渲染
- 直连源站与走代理的响应
Content-Type头部存在差异 - 直接通过代理访问单个静态资源的完整URL时,资源可以正常显示
代理实现核心代码如下:
// ReverseProxy is a blind forwarding reverse proxy that is intended to run // after all registered middleware func ReverseProxy(w http.ResponseWriter, req *http.Request) { // This represents the origin server, which would be loaded dynamically from the deployment config or // external environment variables, config map, secrets vault, etc for key, ele := range mw { fmt.Printf("Invoking middleware: %s\n", key) err := ele(w, req) if err != nil { unauthorizedPage(w, req) return } } fmt.Printf("\n\n") // Maps to internal container host (multi-container pod) host := "localhost" port, _ := strconv.Atoi(os.Getenv("POST_AUTH_TARGET_PORT")) originSvr, err := url.Parse(fmt.Sprintf("http://%s:%d", host, port)) if err != nil { w.WriteHeader(http.StatusInternalServerError) fmt.Fprint(w, req) return } req.Header.Add("X-Forwarded-Host", req.Host) req.Header.Add("X-Origin-Host", originSvr.Host) req.Host = originSvr.Host req.URL.Host = originSvr.Host req.URL.Scheme = originSvr.Scheme req.RequestURI = "" originResp, err := http.DefaultClient.Do(req) if err != nil { w.WriteHeader(http.StatusInternalServerError) fmt.Fprint(w, req) return } w.WriteHeader(originResp.StatusCode) io.Copy(w, originResp.Body) }

根因定位
核心逻辑遗漏:代理收到源站响应后,没有将源站返回的响应头部透传给客户端。
所有现象都可以对应上这个问题:
- 主页面能加载:Go的
http.ResponseWriter在首次调用Write()方法时,如果没有提前设置Content-Type,会自动对写入内容做嗅探识别,HTML格式特征明显,会被自动标记为text/html,所以浏览器可以正常解析主页。 - 静态资源无法渲染:浏览器对页面内嵌加载的静态资源(CSS、JS、字体、图片)有严格的MIME类型校验,必须和响应头的
Content-Type匹配才会执行/渲染。没有透传源站的Content-Type时,自动嗅探对CSS、JS这类资源的识别准确率极低,经常会被标记为text/plain,浏览器会直接拦截不符合类型要求的资源,导致页面样式、脚本、图片全部加载失败。 - 直接访问静态资源正常:浏览器对地址栏直接访问的资源MIME校验非常宽松,哪怕
Content-Type错误也会尝试直接渲染内容,所以单独打开资源链接看起来是正常的。
另外代码里还有两个会引发线上故障的隐患:
- 没有关闭源站响应的
Body,会持续泄漏TCP连接,运行一段时间后会耗尽文件描述符导致代理完全不可用 - 没有过滤逐跳(Hop-by-hop)响应头,这类头部仅在代理和源站的单段连接上生效,透传给客户端会引发各种奇怪的连接、缓存异常
修复方法
- 最小修改方案:在写入状态码、拷贝响应体之前,先透传所有非逐跳的响应头,同时补上响应体关闭逻辑:
originResp, err := http.DefaultClient.Do(req) if err != nil { w.WriteHeader(http.StatusInternalServerError) fmt.Fprint(w, req) return } // 关闭源站响应体,避免连接泄漏 defer originResp.Body.Close() // 透传源站响应头 for headerKey, headerValues := range originResp.Header { // 跳过逐跳头,不要透传给客户端 if strings.EqualFold(headerKey, "Connection") || strings.EqualFold(headerKey, "Keep-Alive") || strings.EqualFold(headerKey, "Proxy-Authenticate") || strings.EqualFold(headerKey, "Proxy-Authorization") || strings.EqualFold(headerKey, "TE") || strings.EqualFold(headerKey, "Trailers") || strings.EqualFold(headerKey, "Transfer-Encoding") || strings.EqualFold(headerKey, "Upgrade") { continue } for _, v := range headerValues { w.Header().Add(headerKey, v) } } w.WriteHeader(originResp.StatusCode) io.Copy(w, originResp.Body)
- 生产环境推荐方案:不要手动实现反向代理逻辑,直接使用Go标准库自带的
net/http/httputil.ReverseProxy,上述的头部透传、逐跳头过滤、连接复用、错误处理、资源清理逻辑都已经做了完备实现,只需要自定义鉴权逻辑和请求修改逻辑即可,避免重复造轮子踩坑。
内容的提问来源于stack exchange,提问作者Ryan
相关产品推荐
相关产品推荐

