Merge branch v2.10 into v3.0

This commit is contained in:
Fernandez Ludovic 2023-06-20 19:06:51 +02:00
commit a7ef965412
36 changed files with 751 additions and 107 deletions

View file

@ -30,7 +30,7 @@ type serviceBuilder interface {
BuildHTTP(ctx context.Context, serviceName string) (http.Handler, error)
}
// customErrors is a middleware that provides the custom error pages..
// customErrors is a middleware that provides the custom error pages.
type customErrors struct {
name string
next http.Handler
@ -121,10 +121,10 @@ func newRequest(baseURL string) (*http.Request, error) {
return req, nil
}
// codeCatcher is a response writer that detects as soon as possible whether the
// response is a code within the ranges of codes it watches for. If it is, it
// simply drops the data from the response. Otherwise, it forwards it directly to
// the original client (its responseWriter) without any buffering.
// codeCatcher is a response writer that detects as soon as possible
// whether the response is a code within the ranges of codes it watches for.
// If it is, it simply drops the data from the response.
// Otherwise, it forwards it directly to the original client (its responseWriter) without any buffering.
type codeCatcher struct {
headerMap http.Header
code int
@ -144,6 +144,10 @@ func newCodeCatcher(rw http.ResponseWriter, httpCodeRanges types.HTTPCodeRanges)
}
func (cc *codeCatcher) Header() http.Header {
if cc.headersSent {
return cc.responseWriter.Header()
}
if cc.headerMap == nil {
cc.headerMap = make(http.Header)
}
@ -175,11 +179,25 @@ func (cc *codeCatcher) Write(buf []byte) (int, error) {
return cc.responseWriter.Write(buf)
}
// WriteHeader is, in the specific case of 1xx status codes, a direct call to the wrapped ResponseWriter, without marking headers as sent,
// allowing so further calls.
func (cc *codeCatcher) WriteHeader(code int) {
if cc.headersSent || cc.caughtFilteredCode {
return
}
// Handling informational headers.
if code >= 100 && code <= 199 {
// Multiple informational status codes can be used,
// so here the copy is not appending the values to not repeat them.
for k, v := range cc.Header() {
cc.responseWriter.Header()[k] = v
}
cc.responseWriter.WriteHeader(code)
return
}
cc.code = code
for _, block := range cc.httpCodeRanges {
if cc.code >= block[0] && cc.code <= block[1] {
@ -190,7 +208,11 @@ func (cc *codeCatcher) WriteHeader(code int) {
}
}
utils.CopyHeaders(cc.responseWriter.Header(), cc.Header())
// The copy is not appending the values,
// to not repeat them in case any informational status code has been written.
for k, v := range cc.Header() {
cc.responseWriter.Header()[k] = v
}
cc.responseWriter.WriteHeader(cc.code)
cc.headersSent = true
}
@ -247,6 +269,10 @@ func newCodeModifier(rw http.ResponseWriter, code int) *codeModifier {
// Header returns the response headers.
func (r *codeModifier) Header() http.Header {
if r.headerSent {
return r.responseWriter.Header()
}
if r.headerMap == nil {
r.headerMap = make(http.Header)
}
@ -261,14 +287,30 @@ func (r *codeModifier) Write(buf []byte) (int, error) {
return r.responseWriter.Write(buf)
}
// WriteHeader sends the headers, with the enforced code (the code in argument
// is always ignored), if it hasn't already been done.
func (r *codeModifier) WriteHeader(_ int) {
// WriteHeader sends the headers, with the enforced code (the code in argument is always ignored),
// if it hasn't already been done.
// WriteHeader is, in the specific case of 1xx status codes, a direct call to the wrapped ResponseWriter, without marking headers as sent,
// allowing so further calls.
func (r *codeModifier) WriteHeader(code int) {
if r.headerSent {
return
}
utils.CopyHeaders(r.responseWriter.Header(), r.Header())
// Handling informational headers.
if code >= 100 && code <= 199 {
// Multiple informational status codes can be used,
// so here the copy is not appending the values to not repeat them.
for k, v := range r.headerMap {
r.responseWriter.Header()[k] = v
}
r.responseWriter.WriteHeader(code)
return
}
for k, v := range r.headerMap {
r.responseWriter.Header()[k] = v
}
r.responseWriter.WriteHeader(r.code)
r.headerSent = true
}

View file

@ -3,8 +3,11 @@ package customerrors
import (
"context"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/http/httptrace"
"net/textproto"
"testing"
"github.com/stretchr/testify/assert"
@ -181,6 +184,88 @@ func TestHandler(t *testing.T) {
}
}
// This test is an adapted version of net/http/httputil.Test1xxResponses test.
func Test1xxResponses(t *testing.T) {
next := 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)
h.Add("User-Agent", "foobar")
_, _ = w.Write([]byte("Hello"))
w.WriteHeader(http.StatusBadGateway)
})
serviceBuilderMock := &mockServiceBuilder{handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = fmt.Fprintln(w, "My error page.")
})}
config := dynamic.ErrorPage{Service: "error", Query: "/", Status: []string{"200"}}
errorPageHandler, err := New(context.Background(), next, config, serviceBuilderMock, "test")
require.NoError(t, err)
server := httptest.NewServer(errorPageHandler)
t.Cleanup(server.Close)
frontendClient := server.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), http.MethodGet, server.URL, nil)
res, err := frontendClient.Do(req)
assert.Nil(t, 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"])
body, _ := io.ReadAll(res.Body)
assert.Equal(t, "My error page.\n", string(body))
}
type mockServiceBuilder struct {
handler http.Handler
}