Golang ReverseProxy返回空白页面问题排查

Golang ReverseProxy返回空白页面问题排查 你好!我正在尝试为我在 Ubuntu 20.04 服务器上运行的几个 docker-compose 应用创建一个统一的 Web 界面。我希望通过反向代理来访问这些服务,并在我的代码中实现该系统。Go Playground - The Go Programming Language

我可以看到存在一些连接性,对于服务 1,curl 显示 200,但在浏览器中却显示一个完全空白的页面。如果我直接 ping 服务 1,输出结果也是一样的。

$ curl -I -L sub1.example.com:8080                                                                                      
HTTP/1.1 200 OK                                                                                                       
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0                              
Content-Length: 522375                                                                                                
Content-Type: text/html; charset=utf-8                                                                                
Etag: "9b42b********************21be3733876e3b1"                                                                      
Expires: -1                                                                                                           
Last-Modified: Thu, 20 Jan 2022 11:26:56 GMT                                                                          
Pragma: no-cache                                                                                                      
X-Clacks-Overhead: GNU Terry Pratchett                                                                                
X-Content-Type-Options: nosniff                                                                                       
X-Frame-Options: sameorigin                                                                                           
X-Robots-Tag: noindex, nofollow, noimageindex                                                                         
Date: Sat, 05 Feb 2022 22:16:01 GMT

sub2 显示一个 404 消息 - 但它确实将 URL 重定向到了 “Location”(这就是为什么我认为存在某种连接)

$ curl -I -L sub2.example.com:8080                                                                                      
HTTP/1.1 302 Found                                                                                                    
Content-Language: en                                                                                                  
Content-Length: 0                                                                                                     
Content-Type: text/html; charset=utf-8                                                                                
Date: Sat, 05 Feb 2022 22:18:54 GMT                                                                                   
Location: /accounts/login/?next=/                                                                                     
Server: nginx/1.18.0 (Ubuntu)                                                                                         
Vary: Accept-Language, Cookie                                                                                         
                                                                                                                      
HTTP/1.1 404 Not Found                                                                                                
Content-Type: text/plain; charset=utf-8                                                                               
X-Content-Type-Options: nosniff                                                                                       
Date: Sat, 05 Feb 2022 22:18:54 GMT                                                                                   
Content-Length: 19

如果我直接 ping 服务 2,我得到的是 200

$ curl -I -L 172.23.0.4
HTTP/1.1 302 Found                                                                                                    
Server: nginx/1.18.0 (Ubuntu)                                                                                         
Date: Sat, 05 Feb 2022 22:21:53 GMT                                                                                   
Content-Type: text/html; charset=utf-8                                                                                
Content-Length: 0                                                                                                     
Connection: keep-alive                                                                                                
Location: /accounts/login/?next=/                                                                                     
Vary: Accept-Language, Cookie                                                                                         
Content-Language: en                                                                                                  
                                                                                                                      
HTTP/1.1 200 OK                                                                                                       
Server: nginx/1.18.0 (Ubuntu)                                                                                         
Date: Sat, 05 Feb 2022 22:21:53 GMT                                                                                   
Content-Type: text/html; charset=utf-8                                                                                
Content-Length: 12060                                                                                                 
Connection: keep-alive                                                                                                
Expires: Sat, 05 Feb 2022 22:21:53 GMT                                                                                
Cache-Control: max-age=0, no-cache, no-store, must-revalidate                                                         
Vary: Cookie, Accept-Language                                                                                         
Content-Language: en                                                                                                  
Set-Cookie: sfcsrftoken=DAgNIckiQcyvy4********************8KHrYcSpvHf11HOfNu6x01bgiybpNH; expires=Sat, 04 Feb 2023 22:21:53 GMT; Max-Age=31449600; Path=/; SameSite=Lax                                                                     
Set-Cookie: sessionid=yzj9cjvh3thhx8lqudqj0tf90dqxcu1i; expires=Sun, 06 Feb 2022 22:21:53 GMT; HttpOnly; Max-Age=86400; Path=/; SameSite=Lax

任何帮助都将不胜感激!!谢谢!


更多关于Golang ReverseProxy返回空白页面问题排查的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于Golang ReverseProxy返回空白页面问题排查的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


从你的描述和代码来看,ReverseProxy返回空白页面通常是由于响应处理或代理配置问题导致的。以下是几个常见原因及解决方案:

1. 响应体未正确复制

ReverseProxy默认会复制响应体,但如果目标服务使用了分块传输编码或特殊的内容编码,可能需要自定义DirectorModifyResponse

package main

import (
    "fmt"
    "net/http"
    "net/http/httputil"
    "net/url"
)

func main() {
    // 创建目标URL
    target, _ := url.Parse("http://localhost:8080")
    
    // 创建反向代理
    proxy := httputil.NewSingleHostReverseProxy(target)
    
    // 自定义Director处理请求头
    originalDirector := proxy.Director
    proxy.Director = func(req *http.Request) {
        originalDirector(req)
        // 设置必要的请求头
        req.Header.Set("X-Forwarded-Host", req.Host)
        req.Header.Set("X-Forwarded-Proto", "http")
        // 移除可能引起问题的头
        req.Header.Del("Accept-Encoding")
    }
    
    // 修改响应处理
    proxy.ModifyResponse = func(resp *http.Response) error {
        // 确保响应头正确
        resp.Header.Set("X-Proxy", "Go-Reverse-Proxy")
        return nil
    }
    
    // 错误处理
    proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
        fmt.Printf("代理错误: %v\n", err)
        http.Error(w, "代理错误", http.StatusBadGateway)
    }
    
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        proxy.ServeHTTP(w, r)
    })
    
    http.ListenAndServe(":8081", nil)
}

2. 处理重定向问题

从你的curl输出看,服务2返回了302重定向。浏览器和代理对重定向的处理可能不同。

package main

import (
    "net/http"
    "net/http/httputil"
    "net/url"
)

func main() {
    target, _ := url.Parse("http://localhost:8080")
    proxy := httputil.NewSingleHostReverseProxy(target)
    
    // 禁用自动重定向,手动处理
    proxy.Transport = &http.Transport{
        DisableKeepAlives: false,
    }
    
    // 修改响应,处理重定向
    proxy.ModifyResponse = func(resp *http.Response) error {
        // 如果是重定向,修改Location头
        if resp.StatusCode >= 300 && resp.StatusCode < 400 {
            if location := resp.Header.Get("Location"); location != "" {
                // 这里可以修改重定向URL
                resp.Header.Set("Location", location)
            }
        }
        return nil
    }
    
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        proxy.ServeHTTP(w, r)
    })
    
    http.ListenAndServe(":8081", nil)
}

3. 完整的代理示例,包含调试信息

package main

import (
    "io"
    "log"
    "net/http"
    "net/http/httputil"
    "net/url"
    "os"
)

func main() {
    // 设置日志
    log.SetOutput(os.Stdout)
    
    target, err := url.Parse("http://localhost:8080")
    if err != nil {
        log.Fatal(err)
    }
    
    proxy := httputil.NewSingleHostReverseProxy(target)
    
    // 自定义传输层以添加日志
    proxy.Transport = &loggingTransport{}
    
    // 自定义错误处理
    proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
        log.Printf("代理请求错误: %v", err)
        w.WriteHeader(http.StatusBadGateway)
        w.Write([]byte("代理服务错误"))
    }
    
    // 修改响应
    proxy.ModifyResponse = func(resp *http.Response) error {
        log.Printf("响应状态: %d", resp.StatusCode)
        log.Printf("响应头: %v", resp.Header)
        return nil
    }
    
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        log.Printf("收到请求: %s %s", r.Method, r.URL.Path)
        proxy.ServeHTTP(w, r)
    })
    
    log.Println("代理服务器启动在 :8081")
    http.ListenAndServe(":8081", nil)
}

type loggingTransport struct{}

func (t *loggingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    // 记录请求信息
    log.Printf("代理请求: %s %s", req.Method, req.URL.String())
    log.Printf("请求头: %v", req.Header)
    
    // 使用默认传输
    resp, err := http.DefaultTransport.RoundTrip(req)
    if err != nil {
        log.Printf("传输错误: %v", err)
        return nil, err
    }
    
    // 记录响应信息
    log.Printf("原始响应状态: %d", resp.StatusCode)
    
    // 如果需要,可以读取响应体进行调试
    if os.Getenv("DEBUG") == "true" {
        body, _ := io.ReadAll(resp.Body)
        resp.Body.Close()
        log.Printf("响应体: %s", string(body))
        resp.Body = io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(io.NopCloser(
回到顶部