1
0
Fork 0

Disable Content-Type auto-detection by default

This commit is contained in:
Simon Delicata 2022-11-29 11:48:05 +01:00 committed by GitHub
parent 4d86668af3
commit db287c4d31
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 193 additions and 168 deletions

View file

@ -33,7 +33,7 @@ type Middleware struct {
Compress *Compress `json:"compress,omitempty" toml:"compress,omitempty" yaml:"compress,omitempty" label:"allowEmpty" file:"allowEmpty" kv:"allowEmpty" export:"true"`
PassTLSClientCert *PassTLSClientCert `json:"passTLSClientCert,omitempty" toml:"passTLSClientCert,omitempty" yaml:"passTLSClientCert,omitempty" export:"true"`
Retry *Retry `json:"retry,omitempty" toml:"retry,omitempty" yaml:"retry,omitempty" export:"true"`
ContentType *ContentType `json:"contentType,omitempty" toml:"contentType,omitempty" yaml:"contentType,omitempty" export:"true"`
ContentType *ContentType `json:"contentType,omitempty" toml:"contentType,omitempty" yaml:"contentType,omitempty" label:"allowEmpty" file:"allowEmpty" kv:"allowEmpty" export:"true"`
GrpcWeb *GrpcWeb `json:"grpcWeb,omitempty" toml:"grpcWeb,omitempty" yaml:"grpcWeb,omitempty" export:"true"`
Plugin map[string]PluginConf `json:"plugin,omitempty" toml:"plugin,omitempty" yaml:"plugin,omitempty" export:"true"`
@ -52,15 +52,9 @@ type GrpcWeb struct {
// +k8s:deepcopy-gen=true
// ContentType holds the content-type middleware configuration.
// This middleware exists to enable the correct behavior until at least the default one can be changed in a future version.
type ContentType struct {
// AutoDetect specifies whether to let the `Content-Type` header, if it has not been set by the backend,
// be automatically set to a value derived from the contents of the response.
// As a proxy, the default behavior should be to leave the header alone, regardless of what the backend did with it.
// However, the historic default was to always auto-detect and set the header if it was nil,
// and it is going to be kept that way in order to support users currently relying on it.
AutoDetect bool `json:"autoDetect,omitempty" toml:"autoDetect,omitempty" yaml:"autoDetect,omitempty" export:"true"`
}
// This middleware sets the `Content-Type` header value to the media type detected from the response content,
// when it is not set by the backend.
type ContentType struct{}
// +k8s:deepcopy-gen=true

View file

@ -0,0 +1,46 @@
package contenttype
import (
"context"
"net/http"
"github.com/traefik/traefik/v2/pkg/middlewares"
)
const (
typeName = "ContentType"
)
// ContentType is a middleware used to activate Content-Type auto-detection.
type contentType struct {
next http.Handler
name string
}
// New creates a new handler.
func New(ctx context.Context, next http.Handler, name string) (http.Handler, error) {
middlewares.GetLogger(ctx, name, typeName).Debug().Msg("Creating middleware")
return &contentType{next: next, name: name}, nil
}
func (c *contentType) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// Re-enable auto-detection.
if ct, ok := rw.Header()["Content-Type"]; ok && ct == nil {
middlewares.GetLogger(req.Context(), c.name, typeName).
Debug().Msg("Enable Content-Type auto-detection.")
delete(rw.Header(), "Content-Type")
}
c.next.ServeHTTP(rw, req)
}
func DisableAutoDetection(next http.Handler) http.HandlerFunc {
return func(rw http.ResponseWriter, req *http.Request) {
// Prevent Content-Type auto-detection.
if _, ok := rw.Header()["Content-Type"]; !ok {
rw.Header()["Content-Type"] = nil
}
next.ServeHTTP(rw, req)
}
}

View file

@ -0,0 +1,79 @@
package contenttype
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v2/pkg/testhelpers"
)
func TestAutoDetection(t *testing.T) {
testCases := []struct {
desc string
autoDetect bool
contentType string
wantContentType string
}{
{
desc: "Keep the Content-Type returned by the server",
autoDetect: false,
contentType: "application/json",
wantContentType: "application/json",
},
{
desc: "Don't auto-detect Content-Type header by default when not set by the server",
autoDetect: false,
contentType: "",
wantContentType: "",
},
{
desc: "Keep the Content-Type returned by the server with auto-detection middleware",
autoDetect: true,
contentType: "application/json",
wantContentType: "application/json",
},
{
desc: "Auto-detect when Content-Type header is not already set by the server with auto-detection middleware",
autoDetect: true,
contentType: "",
wantContentType: "text/plain; charset=utf-8",
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
var next http.Handler
next = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if test.contentType != "" {
w.Header().Set("Content-Type", test.contentType)
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("Test"))
})
if test.autoDetect {
var err error
next, err = New(context.Background(), next, "foo-content-type")
require.NoError(t, err)
}
server := httptest.NewServer(
DisableAutoDetection(next),
)
t.Cleanup(server.Close)
req := testhelpers.MustNewRequest(http.MethodGet, server.URL, nil)
res, err := server.Client().Do(req)
require.NoError(t, err)
assert.Equal(t, test.wantContentType, res.Header.Get("Content-Type"))
})
}
}

View file

@ -337,9 +337,7 @@ func init() {
Attempts: 42,
InitialInterval: 42,
},
ContentType: &dynamic.ContentType{
AutoDetect: true,
},
ContentType: &dynamic.ContentType{},
Plugin: map[string]dynamic.PluginConf{
"foo": {
"answer": struct{ Answer int }{

View file

@ -302,9 +302,7 @@
"attempts": 42,
"initialInterval": "42ns"
},
"contentType": {
"autoDetect": true
},
"contentType": {},
"plugin": {
"foo": {
"answer": {}

View file

@ -305,9 +305,7 @@
"attempts": 42,
"initialInterval": "42ns"
},
"contentType": {
"autoDetect": true
},
"contentType": {},
"plugin": {
"foo": {
"answer": {}

View file

@ -16,6 +16,7 @@ import (
"github.com/traefik/traefik/v2/pkg/middlewares/chain"
"github.com/traefik/traefik/v2/pkg/middlewares/circuitbreaker"
"github.com/traefik/traefik/v2/pkg/middlewares/compress"
"github.com/traefik/traefik/v2/pkg/middlewares/contenttype"
"github.com/traefik/traefik/v2/pkg/middlewares/customerrors"
"github.com/traefik/traefik/v2/pkg/middlewares/grpcweb"
"github.com/traefik/traefik/v2/pkg/middlewares/headers"
@ -181,12 +182,7 @@ func (b *Builder) buildConstructor(ctx context.Context, middlewareName string) (
return nil, badConf
}
middleware = func(next http.Handler) (http.Handler, error) {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if !config.ContentType.AutoDetect {
rw.Header()["Content-Type"] = nil
}
next.ServeHTTP(rw, req)
}), nil
return contenttype.New(ctx, next, middlewareName)
}
}

View file

@ -22,6 +22,7 @@ import (
"github.com/traefik/traefik/v2/pkg/ip"
"github.com/traefik/traefik/v2/pkg/logs"
"github.com/traefik/traefik/v2/pkg/middlewares"
"github.com/traefik/traefik/v2/pkg/middlewares/contenttype"
"github.com/traefik/traefik/v2/pkg/middlewares/forwardedheaders"
"github.com/traefik/traefik/v2/pkg/middlewares/requestdecorator"
"github.com/traefik/traefik/v2/pkg/safe"
@ -537,6 +538,8 @@ func createHTTPServer(ctx context.Context, ln net.Listener, configuration *stati
handler = http.AllowQuerySemicolons(handler)
handler = contenttype.DisableAutoDetection(handler)
if withH2c {
handler = h2c.NewHandler(handler, &http2.Server{
MaxConcurrentStreams: uint32(configuration.HTTP2.MaxConcurrentStreams),