【问题标题】:Go http, send incoming http.request to an other server using client.Do转到 http,使用 client.Do 将传入的 http.request 发送到其他服务器
【发布时间】:2016-01-11 14:40:21
【问题描述】:

这是我的用例

我们有一个服务“foobar”,它有两个版本 legacyversion_2_of_doom(都在运行中)

为了实现从 legacyversion_2_of_doom 的转换,我们希望第一次将两个版本放在一起,并收到 POST 请求(因为这里只有一个 POST api 调用)两者都有。

我看到如何做到这一点的方式。会是

  1. 修改处理程序开头legacy的代码,以便将请求复制到version_2_of_doom

     func(w http.ResponseWriter, req *http.Request) {
         req.URL.Host = "v2ofdoom.local:8081"
         req.Host = "v2ofdoom.local:8081"
         client := &http.Client{}
         client.Do(req)
         // legacy code 
    

不过好像没有这么简单

http: Request.RequestURI can't be set in client requests. 失败

是否有众所周知的方法来执行这种操作(即不接触)http.Request 到另一个服务器?

【问题讨论】:

  • 你不需要为每个请求创建一个新的http.Client,它们可以被多次使用。

标签: go


【解决方案1】:

您需要将所需的值复制到新请求中。由于这与反向代理的作用非常相似,您可能想看看"net/http/httputil"ReverseProxy 的作用。

创建一个新请求,并仅复制要发送到下一个服务器的请求部分。如果您打算在两个地方都使用它,您还需要读取和缓冲请求正文:

func handler(w http.ResponseWriter, req *http.Request) {
    // we need to buffer the body if we want to read it here and send it
    // in the request. 
    body, err := ioutil.ReadAll(req.Body)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    // you can reassign the body if you need to parse it as multipart
    req.Body = ioutil.NopCloser(bytes.NewReader(body))

    // create a new url from the raw RequestURI sent by the client
    url := fmt.Sprintf("%s://%s%s", proxyScheme, proxyHost, req.RequestURI)

    proxyReq, err := http.NewRequest(req.Method, url, bytes.NewReader(body))

    // We may want to filter some headers, otherwise we could just use a shallow copy
    // proxyReq.Header = req.Header
    proxyReq.Header = make(http.Header)
    for h, val := range req.Header {
        proxyReq.Header[h] = val
    }

    resp, err := httpClient.Do(proxyReq)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadGateway)
        return
    }
    defer resp.Body.Close()

    // legacy code
}

【讨论】:

  • 我需要定义 httpClient := http.Client{} 、proxyscheme 和 proxyhost,但您的解决方案似乎有效,谢谢
  • 看来,对于 Go 1.13(我相信会在下个月发布),这应该相对简化。您应该能够调用req.Clone(<context>) 并取回克隆的*http.Request,并将其上下文设置为您选择的上下文。 go-review.googlesource.com/c/go/+/174324
【解决方案2】:

根据我的经验,实现这一点的最简单方法是简单地创建一个新请求并将您需要的所有请求属性复制到新请求对象中:

func(rw http.ResponseWriter, req *http.Request) {
    url := req.URL
    url.Host = "v2ofdoom.local:8081"

    proxyReq, err := http.NewRequest(req.Method, url.String(), req.Body)
    if err != nil {
        // handle error
    }

    proxyReq.Header.Set("Host", req.Host)
    proxyReq.Header.Set("X-Forwarded-For", req.RemoteAddr)

    for header, values := range req.Header {
        for _, value := range values {
            proxyReq.Header.Add(header, value)
        }
    }

    client := &http.Client{}
    proxyRes, err := client.Do(proxyReq)

    // and so on...

这种方法的好处是不修改原始请求对象(也许您的处理程序函数或存在于堆栈中的任何中间件函数仍然需要原始对象?)。

【讨论】:

  • 感谢您的回答,$http.Client{} $ 似乎有一些编译错误,而 http.NewRequest 似乎需要一个字符串而不是 URL 对象
  • 我还需要添加url.Scheme = "http"
  • 我不知道是否需要,但用proxyReq.Header = req.Header 替换双 for 循环似乎可行
  • 只要您不修改标题,就可以了。如果您想将不同的标头传递给其他服务(请注意,在我的示例中,我添加了 X-Forwarded-For 标头),通常最好实际复制标头。
  • 有一个 Header.Clone() 方法,所以不需要自己循环。如果转发请求,还可以通过 proxyRes 代理响应(如 w.Header().Add(header, value))上的示例循环将标头复制到 w 响应
【解决方案3】:

使用原始请求(仅在原始请求仍然需要时复制或复制):

func handler(w http.ResponseWriter, r *http.Request) {
    // Step 1: rewrite URL
    URL, _ := url.Parse("https://full_generic_url:123/x/y")
    r.URL.Scheme = URL.Scheme
    r.URL.Host = URL.Host
    r.URL.Path = singleJoiningSlash(URL.Path, r.URL.Path)
    r.RequestURI = ""

    // Step 2: adjust Header
    r.Header.Set("X-Forwarded-For", r.RemoteAddr)

    // note: client should be created outside the current handler()
    client := &http.Client{} 
    // Step 3: execute request
    resp, err := client.Do(r)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    // Step 4: copy payload to response writer
    copyHeader(w.Header(), resp.Header)
    w.WriteHeader(resp.StatusCode)
    io.Copy(w, resp.Body)
    resp.Body.Close()
}

// copyHeader and singleJoiningSlash are copy from "/net/http/httputil/reverseproxy.go"
func copyHeader(dst, src http.Header) {
    for k, vv := range src {
        for _, v := range vv {
            dst.Add(k, v)
        }
    }
}

func singleJoiningSlash(a, b string) string {
    aslash := strings.HasSuffix(a, "/")
    bslash := strings.HasPrefix(b, "/")
    switch {
    case aslash && bslash:
        return a + b[1:]
    case !aslash && !bslash:
        return a + "/" + b
    }
    return a + b
}

【讨论】:

    【解决方案4】:

    我已经看到了接受的答案,但我想说我不喜欢这个。我已经使用这个代码几个月了,但一段时间后你会遇到中断的请求(在我的例子中是POST 请求)。我的首选解决方案如下:

    r.URL.Host = "example.com"
    r.RequestURI = ""
    client := &http.Client{}
    
    delete(r.Header, "Accept-Encoding")
    delete(r.Headers, "Content-Length")
    resp, err := client.Do(r.WithContext(context.Background())
    if err != nil {
        return nil, err
    }
    return resp, nil
    

    【讨论】:

      猜你喜欢
      • 2021-11-12
      • 1970-01-01
      • 2017-11-30
      • 2020-05-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-01-22
      • 1970-01-01
      相关资源
      最近更新 更多