Disable Content-Type auto-detection by default
This commit is contained in:
parent
4d86668af3
commit
db287c4d31
20 changed files with 193 additions and 168 deletions
|
@ -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
|
||||
|
||||
|
|
46
pkg/middlewares/contenttype/content_type.go
Normal file
46
pkg/middlewares/contenttype/content_type.go
Normal 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)
|
||||
}
|
||||
}
|
79
pkg/middlewares/contenttype/content_type_test.go
Normal file
79
pkg/middlewares/contenttype/content_type_test.go
Normal 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"))
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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 }{
|
||||
|
|
|
@ -302,9 +302,7 @@
|
|||
"attempts": 42,
|
||||
"initialInterval": "42ns"
|
||||
},
|
||||
"contentType": {
|
||||
"autoDetect": true
|
||||
},
|
||||
"contentType": {},
|
||||
"plugin": {
|
||||
"foo": {
|
||||
"answer": {}
|
||||
|
|
|
@ -305,9 +305,7 @@
|
|||
"attempts": 42,
|
||||
"initialInterval": "42ns"
|
||||
},
|
||||
"contentType": {
|
||||
"autoDetect": true
|
||||
},
|
||||
"contentType": {},
|
||||
"plugin": {
|
||||
"foo": {
|
||||
"answer": {}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue