golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
123.91k stars 17.65k forks source link

proposal: net/http/httputil: ReverseProxy add a API to get the length of proxy had sent data #57777

Open lwwgo opened 1 year ago

lwwgo commented 1 year ago

demo code:

revProxy := httputil.NewSingleHostReverseProxy(url)
  modifyFunc := func(res *http.Response) error {
        body, err := io.ReadAll(res.Body)
        res.Body = io.NopCloser(bytes.NewReader(body))
        if err != nil {
            return err
        }
        count := len(body)
        return nil
    }
    revProxy.ModifyResponse = modifyFunc

goal:get the length of proxy had sent data, it is successful, but it is block when response is live streaming, because io.ReadAll() can not read EOF or error from streaming。The stream has always existed and has no end。

Design:

type ReverseProxy struct {
    SendBodySize atomic.Int64
}
// copyBuffer returns any write errors or non-EOF read errors, and the amount
// of bytes written.
func (p *ReverseProxy) copyBuffer(dst io.Writer, src io.Reader, buf []byte) (int64, error) {
    if len(buf) == 0 {
        buf = make([]byte, 32*1024)
    }
    var written int64
    for {
        nr, rerr := src.Read(buf)
        if rerr != nil && rerr != io.EOF && rerr != context.Canceled {
            p.logf("httputil: ReverseProxy read error during body copy: %v", rerr)
        }
        if nr > 0 {
            nw, werr := dst.Write(buf[:nr])
            if nw > 0 {
                written += int64(nw)
                                 p.SendBodySize.Add(int64(nw))  // add code
            }
            if werr != nil {
                return written, werr
            }
            if nr != nw {
                return written, io.ErrShortWrite
            }
        }
        if rerr != nil {
            if rerr == io.EOF {
                rerr = nil
            }
            return written, rerr
        }
    }
}

func (p *ReverseProxy) ResetSendSize() {
    p.SendBodySize.Store(0)
}
seankhliao commented 1 year ago

This functionality looks orthogonal to the purpose of ReverseProxy (you could just as easily wrap it in middleware that will do the tracking for you, eg https://pkg.go.dev/github.com/felixge/httpsnoop). Additionally, a per-proxy count doesn't appear to be very useful, most people seem to want a per handler count.

ianlancetaylor commented 1 year ago

CC @neild @bradfitz

lwwgo commented 1 year ago

This functionality looks orthogonal to the purpose of ReverseProxy (you could just as easily wrap it in middleware that will do the tracking for you, eg https://pkg.go.dev/github.com/felixge/httpsnoop). Additionally, a per-proxy count doesn't appear to be very useful, most people seem to want a per handler count.

thanks for your response. felixge/httpsnoop package:

// myH is your app's http handler, perhaps a http.ServeMux or similar.
var myH http.Handler
// wrappedH wraps myH in order to log every request.
wrappedH := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    m := httpsnoop.CaptureMetrics(myH, w, r)   // This line is block, when header contains "Transfer-Encoding: chunked"
    log.Printf(
        "%s %s (code=%d dt=%s written=%d)",
        r.Method,
        r.URL,
        m.Code,
        m.Duration,
        m.Written,
    )
})
http.ListenAndServe(":8080", wrappedH)

It does not work, when ReverseProxy is used for proxying live streaming by http-flv format and backend http server response data by chunk mode, in other words, Transfer-Encoding: chunked. Unfortunately, CaptureMetrics() does not return any statistics until the live stream is closed. Obviously, felixge/httpsnoop package is ineffective for live stream proxy. I need a golang packet with real-time statistics of forwarding traffic. Do you have other solutions? @seankhliao Thanks.

neild commented 1 year ago

As @seankhliao says, this seems orthogonal to ReverseProxy. You could as easily want to track bytes sent by some other handler.

You should be able to build this with a middleware handler that wraps the http.ResponseWriter. Something like:

type CountingResponseWriter struct {
  http.ResponseWriter
  Count int
}

func (c *CountingResponseWriter) Write(p []byte) (n int, err error) {
  n, err = c.ResponseWriter.Write(p)
  c.Count += n // apply locking as needed
  return n, err
}

func (c *CountingResponseWriter) Unwrap() http.ResponseWriter { return c.ResponseWriter }