Golang H2C Server 不写全文。 Chrome 说:"Server reset stream"。仅发生在 http2 连接上

Golang H2C Server doesn't write full body. Chrome says: "Server reset stream". Only occurs over http2 connection

Go 的最新版本 (1.153)

下面是再现性的代码。请尝试访问 https://easy-dp.ngrok.io 以查看问题。

这是我所做的:

  1. 创建一个访问 Gzipped/Br 编码内容的反向代理
  2. 请求公开可用 URL,我刚抓取 Google 分析
  3. 尝试使用 proxy.modifyresponse 函数通过 http2 连接对响应进行编码和解码
  4. 观看内容被丢弃。

但是,这只会在以下情况下发生:

package main

import (
    "bytes"
    "compress/gzip"
    "fmt"
    "golang.org/x/net/http2"
    "golang.org/x/net/http2/h2c"
    "io/ioutil"
    "net/http"
    "net/http/httputil"
    "strconv"
    "time"
)

func ForwardAnalytics(req *http.Request) {
    req.URL.Scheme = "https"
    req.URL.Host = "www.google-analytics.com"
    req.Host = "www.google-analytics.com"
    req.URL.Path = "/analytics.js"
    req.Header.Set("Accept-Encoding", "gzip")
}

func ModifyAnalytics(r *http.Response) error {
    bytesFromBody, err := ioutil.ReadAll(r.Body)
    defer r.Body.Close()
    if err != nil {
        return nil
    }
    if r.Header.Get("Content-Encoding") == "gzip" {
        gzipReader, err := gzip.NewReader(bytes.NewBuffer(bytesFromBody))
        if err != nil {
            return nil
        }
        defer gzipReader.Close()
        readableBytes, err := ioutil.ReadAll(gzipReader)
        var b bytes.Buffer
        gzipWriter, err := gzip.NewWriterLevel(&b, gzip.DefaultCompression)
        if err != nil {
            return nil
        }
        defer gzipWriter.Close()
        writtenLen, err := gzipWriter.Write(readableBytes)
        fmt.Println("Wrote ", writtenLen)
        if err != nil {
            return nil
        }
        r.ContentLength = int64(len(readableBytes))
        r.Header.Set("Content-Length", strconv.FormatInt(int64(len(readableBytes)), 10))
        r.Body = ioutil.NopCloser(&b)

        return nil
    } else {
        return nil
    }
}


func handleProxy(w http.ResponseWriter, req *http.Request) {
    proxy := httputil.ReverseProxy{
        Director: ForwardAnalytics
    }
    proxy.ModifyResponse = ModifyAnalytics
    proxy.ServeHTTP(w, req)

}

func main() {
    h2s := &http2.Server{
        IdleTimeout: 20 * time.Second,
    }
    mux := http.NewServeMux()
    mux.HandleFunc( "/", handleProxy)
    s := &http.Server{
        ReadHeaderTimeout: 20 * time.Second,
        ReadTimeout:       10 * time.Second,
        WriteTimeout:      30 * time.Second,
        Addr:              "localhost:8456",
        Handler:           h2c.NewHandler(mux, h2s),
    }
    s.ListenAndServe()
}

您希望看到什么?

我希望看到在 H2C 连接上打开字节、修改字节以及更新响应主体的功能

你看到了什么?

有两件事值得注意:

  1. Chrome 给出了一个很好的小错误,扩展了正在发生的事情
{"params":{"description":"Server reset stream.","net_error":"ERR_HTTP2_PROTOCOL_ERROR","stream_id":5},"phase":0,"source":{"id":1493828,"start_time":"732370299","type":1},"time":"732375561","type":224},
  1. 在正常的http连接下,没有问题,但在https连接下,脚本可能会或可能不会打印到一定长度。有时根本不打印,有时打印大约 30%。

这是一个跨浏览器问题。

The Content-Length header indicates the size of the entity body in the message, in bytes. The size includes any content encodings (the Content-Length of a gzip-compressed text file will be the compressed size, not the original size).

src

我以为我已经尝试过了,但由于我关闭 gzip 编写器的方式,我将 运行 保留在 ERR_CONTENT_LENGTH_MISMATCH 中。

最终处理程序如下所示:

    if r.Header.Get("Content-Encoding") == "gzip" {
        gzipReader, err := gzip.NewReader(bytes.NewBuffer(bytesFromBody))
        if err != nil {
            return nil
        }
        defer gzipReader.Close()
        readableBytes, err := ioutil.ReadAll(gzipReader)
        var b bytes.Buffer
        gzipWriter, err := gzip.NewWriterLevel(&b, gzip.DefaultCompression)
        if err != nil {
            return nil
        }
        writtenLen, err := gzipWriter.Write(readableBytes)
        gzipWriter.Close() // This was the culprit. It needed to be closed here
        fmt.Println("Wrote ", writtenLen)
        if err != nil {
            return nil
        }
        r.ContentLength = int64(b.Len())
        r.Header.Set("Content-Length", strconv.FormatInt(int64(b.Len()), 10))
        r.Body = ioutil.NopCloser(&b)
        return nil
    }