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

@ -1,6 +1,6 @@
---
title: "Traefik ContentType Documentation"
description: "Traefik Proxy's HTTP middleware can automatically specify the content-type header if it has not been defined by the backend. Read the technical documentation."
description: "Traefik Proxy's HTTP middleware automatically sets the `Content-Type` header value when it is not set by the backend. Read the technical documentation."
---
# ContentType
@ -8,84 +8,59 @@ description: "Traefik Proxy's HTTP middleware can automatically specify the cont
Handling Content-Type auto-detection
{: .subtitle }
The Content-Type middleware - or rather its `autoDetect` option -
specifies whether to let the `Content-Type` header,
if it has not been defined 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 not already defined,
and altering this behavior would be a breaking change which would impact many users.
This middleware exists to enable the correct behavior until at least the default one can be changed in a future version.
The Content-Type middleware sets the `Content-Type` header value to the media type detected from the response content,
when it is not set by the backend.
!!! info
As explained above, for compatibility reasons the default behavior on a router (without this middleware),
is still to automatically set the `Content-Type` header.
Therefore, given the default value of the `autoDetect` option (false),
simply enabling this middleware for a router switches the router's behavior.
The scope of the Content-Type middleware is the MIME type detection done by the core of Traefik (the server part).
Therefore, it has no effect against any other `Content-Type` header modifications (e.g.: in another middleware such as compress).
## Configuration Examples
```yaml tab="Docker"
# Disable auto-detection
# Enable auto-detection
labels:
- "traefik.http.middlewares.autodetect.contenttype.autodetect=false"
- "traefik.http.middlewares.autodetect.contenttype=true"
```
```yaml tab="Kubernetes"
# Disable auto-detection
# Enable auto-detection
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: autodetect
spec:
contentType:
autoDetect: false
contentType: {}
```
```yaml tab="Consul Catalog"
# Disable auto-detection
- "traefik.http.middlewares.autodetect.contenttype.autodetect=false"
# Enable auto-detection
- "traefik.http.middlewares.autodetect.contenttype=true"
```
```json tab="Marathon"
"labels": {
"traefik.http.middlewares.autodetect.contenttype.autodetect": "false"
"traefik.http.middlewares.autodetect.contenttype": "true"
}
```
```yaml tab="Rancher"
# Disable auto-detection
# Enable auto-detection
labels:
- "traefik.http.middlewares.autodetect.contenttype.autodetect=false"
- "traefik.http.middlewares.autodetect.contenttype=true"
```
```yaml tab="File (YAML)"
# Disable auto-detection
# Enable auto-detection
http:
middlewares:
autodetect:
contentType:
autoDetect: false
contentType: {}
```
```toml tab="File (TOML)"
# Disable auto-detection
# Enable auto-detection
[http.middlewares]
[http.middlewares.autodetect.contentType]
autoDetect=false
```
## Configuration Options
### `autoDetect`
`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.
```

View file

@ -45,3 +45,8 @@ and should be explicitly combined using logical operators to mimic previous beha
`Query` can take a single value to match is the query value that has no value (e.g. `/search?mobile`).
`HostHeader` has been removed, use `Host` instead.
## Content-Type Auto-Detection
In v3, the `Content-Type` header is not auto-detected anymore when it is not set by the backend.
One should use the `ContentType` middleware to enable the `Content-Type` header value auto-detection.

View file

@ -17,7 +17,7 @@
- "traefik.http.middlewares.middleware05.compress=true"
- "traefik.http.middlewares.middleware05.compress.excludedcontenttypes=foobar, foobar"
- "traefik.http.middlewares.middleware05.compress.minresponsebodybytes=42"
- "traefik.http.middlewares.middleware06.contenttype.autodetect=true"
- "traefik.http.middlewares.middleware06.contenttype=true"
- "traefik.http.middlewares.middleware07.digestauth.headerfield=foobar"
- "traefik.http.middlewares.middleware07.digestauth.realm=foobar"
- "traefik.http.middlewares.middleware07.digestauth.removeheader=true"

View file

@ -137,7 +137,6 @@
minResponseBodyBytes = 42
[http.middlewares.Middleware06]
[http.middlewares.Middleware06.contentType]
autoDetect = true
[http.middlewares.Middleware07]
[http.middlewares.Middleware07.digestAuth]
users = ["foobar", "foobar"]

View file

@ -141,8 +141,7 @@ http:
- foobar
minResponseBodyBytes: 42
Middleware06:
contentType:
autoDetect: true
contentType: {}
Middleware07:
digestAuth:
users:

View file

@ -762,19 +762,9 @@ spec:
type: object
contentType:
description: 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.
properties:
autoDetect:
description: 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.
type: boolean
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: object
digestAuth:
description: 'DigestAuth holds the digest auth middleware configuration.

View file

@ -19,7 +19,7 @@
| `traefik/http/middlewares/Middleware05/compress/excludedContentTypes/0` | `foobar` |
| `traefik/http/middlewares/Middleware05/compress/excludedContentTypes/1` | `foobar` |
| `traefik/http/middlewares/Middleware05/compress/minResponseBodyBytes` | `42` |
| `traefik/http/middlewares/Middleware06/contentType/autoDetect` | `true` |
| `traefik/http/middlewares/Middleware06/contentType` | `` |
| `traefik/http/middlewares/Middleware07/digestAuth/headerField` | `foobar` |
| `traefik/http/middlewares/Middleware07/digestAuth/realm` | `foobar` |
| `traefik/http/middlewares/Middleware07/digestAuth/removeHeader` | `true` |

View file

@ -17,7 +17,7 @@
"traefik.http.middlewares.middleware05.compress": "true",
"traefik.http.middlewares.middleware05.compress.excludedcontenttypes": "foobar, foobar",
"traefik.http.middlewares.middleware05.compress.minresponsebodybytes": "42",
"traefik.http.middlewares.middleware06.contenttype.autodetect": "true",
"traefik.http.middlewares.middleware06.contenttype": "true",
"traefik.http.middlewares.middleware07.digestauth.headerfield": "foobar",
"traefik.http.middlewares.middleware07.digestauth.realm": "foobar",
"traefik.http.middlewares.middleware07.digestauth.removeheader": "true",

View file

@ -185,19 +185,9 @@ spec:
type: object
contentType:
description: 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.
properties:
autoDetect:
description: 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.
type: boolean
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: object
digestAuth:
description: 'DigestAuth holds the digest auth middleware configuration.

View file

@ -762,19 +762,9 @@ spec:
type: object
contentType:
description: 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.
properties:
autoDetect:
description: 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.
type: boolean
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: object
digestAuth:
description: 'DigestAuth holds the digest auth middleware configuration.

View file

@ -21,32 +21,12 @@
[http.routers]
[http.routers.router1]
service = "service1"
rule = "PathPrefix(`/css/ct/nomiddleware`) || PathPrefix(`/pdf/ct/nomiddleware`)"
rule = "PathPrefix(`/`)"
[http.routers.router2]
service = "service1"
middlewares = ["autodetect"]
rule = "PathPrefix(`/css/ct/middlewareauto`) || PathPrefix(`/pdf/ct/middlewareauto`)"
[http.routers.router3]
service = "service1"
middlewares = ["noautodetect"]
rule = "PathPrefix(`/css/ct/middlewarenoauto`) || PathPrefix(`/pdf/ct/middlewarenoauto`)"
[http.routers.router4]
service = "service1"
rule = "PathPrefix(`/css/noct/nomiddleware`) || PathPrefix(`/pdf/noct/nomiddleware`)"
[http.routers.router5]
service = "service1"
middlewares = ["autodetect"]
rule = "PathPrefix(`/css/noct/middlewareauto`) || PathPrefix(`/pdf/noct/middlewareauto`)"
[http.routers.router6]
service = "service1"
middlewares = ["noautodetect"]
rule = "PathPrefix(`/css/noct/middlewarenoauto`) || PathPrefix(`/pdf/noct/middlewarenoauto`)"
rule = "PathPrefix(`/autodetect`)"
[http.services]
[http.services.service1]
@ -56,7 +36,3 @@
url = "{{ .Server }}"
[http.middlewares.autodetect.contentType]
autoDetect=true
[http.middlewares.noautodetect.contentType]
autoDetect=false

View file

@ -1166,9 +1166,10 @@ func (s *SimpleSuite) TestSecureAPI(c *check.C) {
func (s *SimpleSuite) TestContentTypeDisableAutoDetect(c *check.C) {
srv1 := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.Header()["Content-Type"] = nil
switch req.URL.Path[:4] {
path := strings.TrimPrefix(req.URL.Path, "/autodetect")
switch path[:4] {
case "/css":
if !strings.Contains(req.URL.Path, "noct") {
if strings.Contains(req.URL.Path, "/ct") {
rw.Header().Set("Content-Type", "text/css")
}
@ -1177,7 +1178,7 @@ func (s *SimpleSuite) TestContentTypeDisableAutoDetect(c *check.C) {
_, err := rw.Write([]byte(".testcss { }"))
c.Assert(err, checker.IsNil)
case "/pdf":
if !strings.Contains(req.URL.Path, "noct") {
if strings.Contains(req.URL.Path, "/ct") {
rw.Header().Set("Content-Type", "application/pdf")
}
@ -1211,37 +1212,13 @@ func (s *SimpleSuite) TestContentTypeDisableAutoDetect(c *check.C) {
err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 10*time.Second, try.BodyContains("127.0.0.1"))
c.Assert(err, checker.IsNil)
err = try.GetRequest("http://127.0.0.1:8000/css/ct/nomiddleware", time.Second, try.HasHeaderValue("Content-Type", "text/css", false))
err = try.GetRequest("http://127.0.0.1:8000/css/ct", time.Second, try.HasHeaderValue("Content-Type", "text/css", false))
c.Assert(err, checker.IsNil)
err = try.GetRequest("http://127.0.0.1:8000/pdf/ct/nomiddleware", time.Second, try.HasHeaderValue("Content-Type", "application/pdf", false))
err = try.GetRequest("http://127.0.0.1:8000/pdf/ct", time.Second, try.HasHeaderValue("Content-Type", "application/pdf", false))
c.Assert(err, checker.IsNil)
err = try.GetRequest("http://127.0.0.1:8000/css/ct/middlewareauto", time.Second, try.HasHeaderValue("Content-Type", "text/css", false))
c.Assert(err, checker.IsNil)
err = try.GetRequest("http://127.0.0.1:8000/pdf/ct/nomiddlewareauto", time.Second, try.HasHeaderValue("Content-Type", "application/pdf", false))
c.Assert(err, checker.IsNil)
err = try.GetRequest("http://127.0.0.1:8000/css/ct/middlewarenoauto", time.Second, try.HasHeaderValue("Content-Type", "text/css", false))
c.Assert(err, checker.IsNil)
err = try.GetRequest("http://127.0.0.1:8000/pdf/ct/nomiddlewarenoauto", time.Second, try.HasHeaderValue("Content-Type", "application/pdf", false))
c.Assert(err, checker.IsNil)
err = try.GetRequest("http://127.0.0.1:8000/css/noct/nomiddleware", time.Second, try.HasHeaderValue("Content-Type", "text/plain; charset=utf-8", false))
c.Assert(err, checker.IsNil)
err = try.GetRequest("http://127.0.0.1:8000/pdf/noct/nomiddleware", time.Second, try.HasHeaderValue("Content-Type", "application/pdf", false))
c.Assert(err, checker.IsNil)
err = try.GetRequest("http://127.0.0.1:8000/css/noct/middlewareauto", time.Second, try.HasHeaderValue("Content-Type", "text/plain; charset=utf-8", false))
c.Assert(err, checker.IsNil)
err = try.GetRequest("http://127.0.0.1:8000/pdf/noct/nomiddlewareauto", time.Second, try.HasHeaderValue("Content-Type", "application/pdf", false))
c.Assert(err, checker.IsNil)
err = try.GetRequest("http://127.0.0.1:8000/css/noct/middlewarenoauto", time.Second, func(res *http.Response) error {
err = try.GetRequest("http://127.0.0.1:8000/css/noct", time.Second, func(res *http.Response) error {
if ct, ok := res.Header["Content-Type"]; ok {
return fmt.Errorf("should have no content type and %s is present", ct)
}
@ -1249,13 +1226,25 @@ func (s *SimpleSuite) TestContentTypeDisableAutoDetect(c *check.C) {
})
c.Assert(err, checker.IsNil)
err = try.GetRequest("http://127.0.0.1:8000/pdf/noct/middlewarenoauto", time.Second, func(res *http.Response) error {
err = try.GetRequest("http://127.0.0.1:8000/pdf/noct", time.Second, func(res *http.Response) error {
if ct, ok := res.Header["Content-Type"]; ok {
return fmt.Errorf("should have no content type and %s is present", ct)
}
return nil
})
c.Assert(err, checker.IsNil)
err = try.GetRequest("http://127.0.0.1:8000/autodetect/css/ct", time.Second, try.HasHeaderValue("Content-Type", "text/css", false))
c.Assert(err, checker.IsNil)
err = try.GetRequest("http://127.0.0.1:8000/autodetect/pdf/ct", time.Second, try.HasHeaderValue("Content-Type", "application/pdf", false))
c.Assert(err, checker.IsNil)
err = try.GetRequest("http://127.0.0.1:8000/autodetect/css/noct", time.Second, try.HasHeaderValue("Content-Type", "text/plain; charset=utf-8", false))
c.Assert(err, checker.IsNil)
err = try.GetRequest("http://127.0.0.1:8000/autodetect/pdf/noct", time.Second, try.HasHeaderValue("Content-Type", "application/pdf", false))
c.Assert(err, checker.IsNil)
}
func (s *SimpleSuite) TestMuxer(c *check.C) {

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),