Skip to content

Commit

Permalink
gzhttp: Handle informational headers (#815)
Browse files Browse the repository at this point in the history
* Handle informational headers
* review: gate 1xx responses forward to go1.20+
  • Loading branch information
rtribotte committed May 3, 2023
1 parent 003aa4f commit ea3eeea
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 0 deletions.
8 changes: 8 additions & 0 deletions gzhttp/compress.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,15 @@ func (w *GzipResponseWriter) startPlain() error {
}

// WriteHeader just saves the response code until close or GZIP effective writes.
// In the specific case of 1xx status codes, WriteHeader is directly calling the wrapped ResponseWriter.
func (w *GzipResponseWriter) WriteHeader(code int) {
// Handle informational headers
// This is gated to not forward 1xx responses on builds prior to go1.20.
if shouldWrite1xxResponses() && code >= 100 && code <= 199 {
w.ResponseWriter.WriteHeader(code)
return
}

if w.code == 0 {
w.code = code
}
Expand Down
9 changes: 9 additions & 0 deletions gzhttp/compress_go119.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//go:build !go1.20
// +build !go1.20

package gzhttp

// shouldWrite1xxResponses indicates whether the current build supports writes of 1xx status codes.
func shouldWrite1xxResponses() bool {
return false
}
9 changes: 9 additions & 0 deletions gzhttp/compress_go120.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//go:build go1.20
// +build go1.20

package gzhttp

// shouldWrite1xxResponses indicates whether the current build supports writes of 1xx status codes.
func shouldWrite1xxResponses() bool {
return true
}
87 changes: 87 additions & 0 deletions gzhttp/compress_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ package gzhttp

import (
"bytes"
"context"
"fmt"
"io"
"math/rand"
"net"
"net/http"
"net/http/httptest"
"net/http/httptrace"
"net/textproto"
"net/url"
"os"
"strconv"
Expand Down Expand Up @@ -1796,3 +1799,87 @@ func TestGzipHandlerNilContentType(t *testing.T) {

assertEqual(t, "", res.Header().Get("Content-Type"))
}

// This test is an adapted version of net/http/httputil.Test1xxResponses test.
func Test1xxResponses(t *testing.T) {
// do not test 1xx responses on builds prior to go1.20.
if !shouldWrite1xxResponses() {
return
}

wrapper, _ := NewWrapper()
handler := wrapper(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
h := w.Header()
h.Add("Link", "</style.css>; rel=preload; as=style")
h.Add("Link", "</script.js>; rel=preload; as=script")
w.WriteHeader(http.StatusEarlyHints)

h.Add("Link", "</foo.js>; rel=preload; as=script")
w.WriteHeader(http.StatusProcessing)

w.Write(testBody)
},
))

frontend := httptest.NewServer(handler)
defer frontend.Close()
frontendClient := frontend.Client()

checkLinkHeaders := func(t *testing.T, expected, got []string) {
t.Helper()

if len(expected) != len(got) {
t.Errorf("Expected %d link headers; got %d", len(expected), len(got))
}

for i := range expected {
if i >= len(got) {
t.Errorf("Expected %q link header; got nothing", expected[i])

continue
}

if expected[i] != got[i] {
t.Errorf("Expected %q link header; got %q", expected[i], got[i])
}
}
}

var respCounter uint8
trace := &httptrace.ClientTrace{
Got1xxResponse: func(code int, header textproto.MIMEHeader) error {
switch code {
case http.StatusEarlyHints:
checkLinkHeaders(t, []string{"</style.css>; rel=preload; as=style", "</script.js>; rel=preload; as=script"}, header["Link"])
case http.StatusProcessing:
checkLinkHeaders(t, []string{"</style.css>; rel=preload; as=style", "</script.js>; rel=preload; as=script", "</foo.js>; rel=preload; as=script"}, header["Link"])
default:
t.Error("Unexpected 1xx response")
}

respCounter++

return nil
},
}
req, _ := http.NewRequestWithContext(httptrace.WithClientTrace(context.Background(), trace), "GET", frontend.URL, nil)
req.Header.Set("Accept-Encoding", "gzip")

res, err := frontendClient.Do(req)
if err != nil {
t.Fatalf("Get: %v", err)
}

defer res.Body.Close()

if respCounter != 2 {
t.Errorf("Expected 2 1xx responses; got %d", respCounter)
}
checkLinkHeaders(t, []string{"</style.css>; rel=preload; as=style", "</script.js>; rel=preload; as=script", "</foo.js>; rel=preload; as=script"}, res.Header["Link"])

assertEqual(t, "gzip", res.Header.Get("Content-Encoding"))

body, _ := io.ReadAll(res.Body)
assertEqual(t, gzipStrLevel(testBody, gzip.DefaultCompression), body)
}

0 comments on commit ea3eeea

Please sign in to comment.