Merge branch v2.10 into v3.0
This commit is contained in:
commit
a7ef965412
36 changed files with 751 additions and 107 deletions
|
@ -58,9 +58,10 @@ func (ep *EntryPoint) SetDefaults() {
|
|||
|
||||
// HTTPConfig is the HTTP configuration of an entry point.
|
||||
type HTTPConfig struct {
|
||||
Redirections *Redirections `description:"Set of redirection" json:"redirections,omitempty" toml:"redirections,omitempty" yaml:"redirections,omitempty" export:"true"`
|
||||
Middlewares []string `description:"Default middlewares for the routers linked to the entry point." json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty" export:"true"`
|
||||
TLS *TLSConfig `description:"Default TLS configuration for the routers linked to the entry point." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
||||
Redirections *Redirections `description:"Set of redirection" json:"redirections,omitempty" toml:"redirections,omitempty" yaml:"redirections,omitempty" export:"true"`
|
||||
Middlewares []string `description:"Default middlewares for the routers linked to the entry point." json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty" export:"true"`
|
||||
TLS *TLSConfig `description:"Default TLS configuration for the routers linked to the entry point." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
||||
EncodeQuerySemicolons bool `description:"Defines whether request query semicolons should be URLEncoded." json:"encodeQuerySemicolons,omitempty" toml:"encodeQuerySemicolons,omitempty" yaml:"encodeQuerySemicolons,omitempty"`
|
||||
}
|
||||
|
||||
// HTTP2Config is the HTTP2 configuration of an entry point.
|
||||
|
|
|
@ -18,9 +18,6 @@ import (
|
|||
"go.opentelemetry.io/otel/metric"
|
||||
"go.opentelemetry.io/otel/metric/global"
|
||||
"go.opentelemetry.io/otel/metric/instrument"
|
||||
"go.opentelemetry.io/otel/metric/instrument/asyncfloat64"
|
||||
"go.opentelemetry.io/otel/metric/instrument/syncfloat64"
|
||||
"go.opentelemetry.io/otel/metric/unit"
|
||||
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
|
||||
"go.opentelemetry.io/otel/sdk/metric/aggregation"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
|
@ -57,9 +54,9 @@ func RegisterOpenTelemetry(ctx context.Context, config *types.OpenTelemetry) Reg
|
|||
routerEnabled: config.AddRoutersLabels,
|
||||
svcEnabled: config.AddServicesLabels,
|
||||
configReloadsCounter: newOTLPCounterFrom(meter, configReloadsTotalName, "Config reloads"),
|
||||
lastConfigReloadSuccessGauge: newOTLPGaugeFrom(meter, configLastReloadSuccessName, "Last config reload success", unit.Milliseconds),
|
||||
openConnectionsGauge: newOTLPGaugeFrom(meter, openConnectionsName, "How many open connections exist, by entryPoint and protocol", unit.Dimensionless),
|
||||
tlsCertsNotAfterTimestampGauge: newOTLPGaugeFrom(meter, tlsCertsNotAfterTimestampName, "Certificate expiration timestamp", unit.Milliseconds),
|
||||
lastConfigReloadSuccessGauge: newOTLPGaugeFrom(meter, configLastReloadSuccessName, "Last config reload success", "ms"),
|
||||
openConnectionsGauge: newOTLPGaugeFrom(meter, openConnectionsName, "How many open connections exist, by entryPoint and protocol", "1"),
|
||||
tlsCertsNotAfterTimestampGauge: newOTLPGaugeFrom(meter, tlsCertsNotAfterTimestampName, "Certificate expiration timestamp", "ms"),
|
||||
}
|
||||
|
||||
if config.AddEntryPointsLabels {
|
||||
|
@ -69,7 +66,7 @@ func RegisterOpenTelemetry(ctx context.Context, config *types.OpenTelemetry) Reg
|
|||
"How many HTTP requests with TLS processed on an entrypoint, partitioned by TLS Version and TLS cipher Used.")
|
||||
reg.entryPointReqDurationHistogram, _ = NewHistogramWithScale(newOTLPHistogramFrom(meter, entryPointReqDurationName,
|
||||
"How long it took to process the request on an entrypoint, partitioned by status code, protocol, and method.",
|
||||
unit.Milliseconds), time.Second)
|
||||
"ms"), time.Second)
|
||||
reg.entryPointReqsBytesCounter = newOTLPCounterFrom(meter, entryPointReqsBytesTotalName,
|
||||
"The total size of requests in bytes handled by an entrypoint, partitioned by status code, protocol, and method.")
|
||||
reg.entryPointRespsBytesCounter = newOTLPCounterFrom(meter, entryPointRespsBytesTotalName,
|
||||
|
@ -83,7 +80,7 @@ func RegisterOpenTelemetry(ctx context.Context, config *types.OpenTelemetry) Reg
|
|||
"How many HTTP requests with TLS are processed on a router, partitioned by service, TLS Version, and TLS cipher Used.")
|
||||
reg.routerReqDurationHistogram, _ = NewHistogramWithScale(newOTLPHistogramFrom(meter, routerReqDurationName,
|
||||
"How long it took to process the request on a router, partitioned by service, status code, protocol, and method.",
|
||||
unit.Milliseconds), time.Second)
|
||||
"ms"), time.Second)
|
||||
reg.routerReqsBytesCounter = newOTLPCounterFrom(meter, routerReqsBytesTotalName,
|
||||
"The total size of requests in bytes handled by a router, partitioned by status code, protocol, and method.")
|
||||
reg.routerRespsBytesCounter = newOTLPCounterFrom(meter, routerRespsBytesTotalName,
|
||||
|
@ -97,12 +94,12 @@ func RegisterOpenTelemetry(ctx context.Context, config *types.OpenTelemetry) Reg
|
|||
"How many HTTP requests with TLS processed on a service, partitioned by TLS version and TLS cipher.")
|
||||
reg.serviceReqDurationHistogram, _ = NewHistogramWithScale(newOTLPHistogramFrom(meter, serviceReqDurationName,
|
||||
"How long it took to process the request on a service, partitioned by status code, protocol, and method.",
|
||||
unit.Milliseconds), time.Second)
|
||||
"ms"), time.Second)
|
||||
reg.serviceRetriesCounter = newOTLPCounterFrom(meter, serviceRetriesTotalName,
|
||||
"How many request retries happened on a service.")
|
||||
reg.serviceServerUpGauge = newOTLPGaugeFrom(meter, serviceServerUpName,
|
||||
"service server is up, described by gauge value of 0 or 1.",
|
||||
unit.Dimensionless)
|
||||
"1")
|
||||
reg.serviceReqsBytesCounter = newOTLPCounterFrom(meter, serviceReqsBytesTotalName,
|
||||
"The total size of requests in bytes received by a service, partitioned by status code, protocol, and method.")
|
||||
reg.serviceRespsBytesCounter = newOTLPCounterFrom(meter, serviceRespsBytesTotalName,
|
||||
|
@ -235,9 +232,9 @@ func newGRPCExporter(ctx context.Context, config *types.OpenTelemetry) (sdkmetri
|
|||
}
|
||||
|
||||
func newOTLPCounterFrom(meter metric.Meter, name, desc string) *otelCounter {
|
||||
c, _ := meter.SyncFloat64().Counter(name,
|
||||
c, _ := meter.Float64Counter(name,
|
||||
instrument.WithDescription(desc),
|
||||
instrument.WithUnit(unit.Dimensionless),
|
||||
instrument.WithUnit("1"),
|
||||
)
|
||||
|
||||
return &otelCounter{
|
||||
|
@ -247,7 +244,7 @@ func newOTLPCounterFrom(meter metric.Meter, name, desc string) *otelCounter {
|
|||
|
||||
type otelCounter struct {
|
||||
labelNamesValues otelLabelNamesValues
|
||||
ip syncfloat64.Counter
|
||||
ip instrument.Float64Counter
|
||||
}
|
||||
|
||||
func (c *otelCounter) With(labelValues ...string) metrics.Counter {
|
||||
|
@ -322,27 +319,29 @@ func (c *gaugeCollector) set(name string, value float64, attributes otelLabelNam
|
|||
}
|
||||
}
|
||||
|
||||
func newOTLPGaugeFrom(meter metric.Meter, name, desc string, u unit.Unit) *otelGauge {
|
||||
func newOTLPGaugeFrom(meter metric.Meter, name, desc string, unit string) *otelGauge {
|
||||
openTelemetryGaugeCollector.values[name] = make(map[string]gaugeValue)
|
||||
|
||||
c, _ := meter.AsyncFloat64().Gauge(name,
|
||||
c, _ := meter.Float64ObservableGauge(name,
|
||||
instrument.WithDescription(desc),
|
||||
instrument.WithUnit(u),
|
||||
instrument.WithUnit(unit),
|
||||
)
|
||||
|
||||
err := meter.RegisterCallback([]instrument.Asynchronous{c}, func(ctx context.Context) {
|
||||
_, err := meter.RegisterCallback(func(ctx context.Context, observer metric.Observer) error {
|
||||
openTelemetryGaugeCollector.mu.Lock()
|
||||
defer openTelemetryGaugeCollector.mu.Unlock()
|
||||
|
||||
values, exists := openTelemetryGaugeCollector.values[name]
|
||||
if !exists {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, value := range values {
|
||||
c.Observe(ctx, value.value, value.attributes.ToLabels()...)
|
||||
observer.ObserveFloat64(c, value.value, value.attributes.ToLabels()...)
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}, c)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Unable to register OpenTelemetry meter callback")
|
||||
}
|
||||
|
@ -355,7 +354,7 @@ func newOTLPGaugeFrom(meter metric.Meter, name, desc string, u unit.Unit) *otelG
|
|||
|
||||
type otelGauge struct {
|
||||
labelNamesValues otelLabelNamesValues
|
||||
ip asyncfloat64.Gauge
|
||||
ip instrument.Float64ObservableGauge
|
||||
name string
|
||||
}
|
||||
|
||||
|
@ -375,10 +374,10 @@ func (g *otelGauge) Set(value float64) {
|
|||
openTelemetryGaugeCollector.set(g.name, value, g.labelNamesValues)
|
||||
}
|
||||
|
||||
func newOTLPHistogramFrom(meter metric.Meter, name, desc string, u unit.Unit) *otelHistogram {
|
||||
c, _ := meter.SyncFloat64().Histogram(name,
|
||||
func newOTLPHistogramFrom(meter metric.Meter, name, desc string, unit string) *otelHistogram {
|
||||
c, _ := meter.Float64Histogram(name,
|
||||
instrument.WithDescription(desc),
|
||||
instrument.WithUnit(u),
|
||||
instrument.WithUnit(unit),
|
||||
)
|
||||
|
||||
return &otelHistogram{
|
||||
|
@ -388,7 +387,7 @@ func newOTLPHistogramFrom(meter metric.Meter, name, desc string, u unit.Unit) *o
|
|||
|
||||
type otelHistogram struct {
|
||||
labelNamesValues otelLabelNamesValues
|
||||
ip syncfloat64.Histogram
|
||||
ip instrument.Float64Histogram
|
||||
}
|
||||
|
||||
func (h *otelHistogram) With(labelValues ...string) metrics.Histogram {
|
||||
|
|
|
@ -6,6 +6,8 @@ import (
|
|||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/http/httptrace"
|
||||
"net/textproto"
|
||||
"testing"
|
||||
|
||||
"github.com/andybalholm/brotli"
|
||||
|
@ -514,6 +516,86 @@ func TestMinResponseBodyBytes(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// This test is an adapted version of net/http/httputil.Test1xxResponses test.
|
||||
func Test1xxResponses(t *testing.T) {
|
||||
fakeBody := generateBytes(100000)
|
||||
|
||||
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)
|
||||
|
||||
if _, err := w.Write(fakeBody); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
|
||||
compress, err := New(context.Background(), next, dynamic.Compress{MinResponseBodyBytes: 1024}, "testing")
|
||||
require.NoError(t, err)
|
||||
|
||||
server := httptest.NewServer(compress)
|
||||
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)
|
||||
req.Header.Add(acceptEncodingHeader, gzipValue)
|
||||
|
||||
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"])
|
||||
|
||||
assert.Equal(t, gzipValue, res.Header.Get(contentEncodingHeader))
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
assert.NotEqualValues(t, body, fakeBody)
|
||||
}
|
||||
|
||||
func BenchmarkCompress(b *testing.B) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -4,8 +4,11 @@ package headers
|
|||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/http/httptrace"
|
||||
"net/textproto"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -111,3 +114,87 @@ func Test_headers_getTracingInformation(t *testing.T) {
|
|||
assert.Equal(t, "testing", name)
|
||||
assert.Equal(t, tracing.SpanKindNoneEnum, trace)
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
_, _ = w.Write([]byte("Hello"))
|
||||
})
|
||||
|
||||
cfg := dynamic.Headers{
|
||||
CustomResponseHeaders: map[string]string{
|
||||
"X-Custom-Response-Header": "test_response",
|
||||
},
|
||||
}
|
||||
|
||||
mid, err := New(context.Background(), next, cfg, "testing")
|
||||
require.NoError(t, err)
|
||||
|
||||
server := httptest.NewServer(mid)
|
||||
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)
|
||||
if string(body) != "Hello" {
|
||||
t.Errorf("Read body %q; want Hello", body)
|
||||
}
|
||||
|
||||
assert.Equal(t, "test_response", res.Header.Get("X-Custom-Response-Header"))
|
||||
}
|
||||
|
|
|
@ -31,10 +31,19 @@ func newResponseModifier(w http.ResponseWriter, r *http.Request, modifier func(*
|
|||
}
|
||||
}
|
||||
|
||||
// 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 *responseModifier) WriteHeader(code int) {
|
||||
if r.headersSent {
|
||||
return
|
||||
}
|
||||
|
||||
// Handling informational headers.
|
||||
if code >= 100 && code <= 199 {
|
||||
r.rw.WriteHeader(code)
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
r.code = code
|
||||
r.headersSent = true
|
||||
|
|
|
@ -193,7 +193,7 @@ func (r *responseWriter) WriteHeader(code int) {
|
|||
r.DisableRetries()
|
||||
}
|
||||
|
||||
if r.ShouldRetry() {
|
||||
if r.ShouldRetry() || r.written {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -207,6 +207,13 @@ func (r *responseWriter) WriteHeader(code int) {
|
|||
}
|
||||
|
||||
r.responseWriter.WriteHeader(code)
|
||||
|
||||
// Handling informational headers.
|
||||
// This allows to keep writing to r.headers map until a final status code is written.
|
||||
if code >= 100 && code <= 199 {
|
||||
return
|
||||
}
|
||||
|
||||
r.written = true
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/http/httptrace"
|
||||
"net/textproto"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
@ -309,3 +310,82 @@ func TestRetryWebsocket(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)
|
||||
|
||||
_, _ = w.Write([]byte("Hello"))
|
||||
})
|
||||
|
||||
retryListener := &countingRetryListener{}
|
||||
retry, err := New(context.Background(), next, dynamic.Retry{Attempts: 1}, retryListener, "traefikTest")
|
||||
require.NoError(t, err)
|
||||
|
||||
server := httptest.NewServer(retry)
|
||||
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)
|
||||
if string(body) != "Hello" {
|
||||
t.Errorf("Read body %q; want Hello", body)
|
||||
}
|
||||
|
||||
assert.Equal(t, 0, retryListener.timesCalled)
|
||||
}
|
||||
|
|
|
@ -553,7 +553,11 @@ func createHTTPServer(ctx context.Context, ln net.Listener, configuration *stati
|
|||
return nil, err
|
||||
}
|
||||
|
||||
handler = http.AllowQuerySemicolons(handler)
|
||||
if configuration.HTTP.EncodeQuerySemicolons {
|
||||
handler = encodeQuerySemicolons(handler)
|
||||
} else {
|
||||
handler = http.AllowQuerySemicolons(handler)
|
||||
}
|
||||
|
||||
handler = contenttype.DisableAutoDetection(handler)
|
||||
|
||||
|
@ -616,3 +620,23 @@ func (t *trackedConnection) Close() error {
|
|||
t.tracker.RemoveConnection(t.WriteCloser)
|
||||
return t.WriteCloser.Close()
|
||||
}
|
||||
|
||||
// This function is inspired by http.AllowQuerySemicolons.
|
||||
func encodeQuerySemicolons(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
if strings.Contains(req.URL.RawQuery, ";") {
|
||||
r2 := new(http.Request)
|
||||
*r2 = *req
|
||||
r2.URL = new(url.URL)
|
||||
*r2.URL = *req.URL
|
||||
|
||||
r2.URL.RawQuery = strings.ReplaceAll(req.URL.RawQuery, ";", "%3B")
|
||||
// Because the reverse proxy director is building query params from requestURI it needs to be updated as well.
|
||||
r2.RequestURI = r2.URL.RequestURI()
|
||||
|
||||
h.ServeHTTP(rw, r2)
|
||||
} else {
|
||||
h.ServeHTTP(rw, req)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -46,6 +46,7 @@ func directorBuilder(target *url.URL, passHostHeader bool) func(req *http.Reques
|
|||
|
||||
outReq.URL.Path = u.Path
|
||||
outReq.URL.RawPath = u.RawPath
|
||||
// If a plugin/middleware adds semicolons in query params, they should be urlEncoded.
|
||||
outReq.URL.RawQuery = strings.ReplaceAll(u.RawQuery, ";", "&")
|
||||
outReq.RequestURI = "" // Outgoing request should not have RequestURI
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue