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" 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 # ContentType
@ -8,84 +8,59 @@ description: "Traefik Proxy's HTTP middleware can automatically specify the cont
Handling Content-Type auto-detection Handling Content-Type auto-detection
{: .subtitle } {: .subtitle }
The Content-Type middleware - or rather its `autoDetect` option - The Content-Type middleware sets the `Content-Type` header value to the media type detected from the response content,
specifies whether to let the `Content-Type` header, when it is not set by the backend.
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.
!!! info !!! 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). 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). Therefore, it has no effect against any other `Content-Type` header modifications (e.g.: in another middleware such as compress).
## Configuration Examples ## Configuration Examples
```yaml tab="Docker" ```yaml tab="Docker"
# Disable auto-detection # Enable auto-detection
labels: labels:
- "traefik.http.middlewares.autodetect.contenttype.autodetect=false" - "traefik.http.middlewares.autodetect.contenttype=true"
``` ```
```yaml tab="Kubernetes" ```yaml tab="Kubernetes"
# Disable auto-detection # Enable auto-detection
apiVersion: traefik.containo.us/v1alpha1 apiVersion: traefik.containo.us/v1alpha1
kind: Middleware kind: Middleware
metadata: metadata:
name: autodetect name: autodetect
spec: spec:
contentType: contentType: {}
autoDetect: false
``` ```
```yaml tab="Consul Catalog" ```yaml tab="Consul Catalog"
# Disable auto-detection # Enable auto-detection
- "traefik.http.middlewares.autodetect.contenttype.autodetect=false" - "traefik.http.middlewares.autodetect.contenttype=true"
``` ```
```json tab="Marathon" ```json tab="Marathon"
"labels": { "labels": {
"traefik.http.middlewares.autodetect.contenttype.autodetect": "false" "traefik.http.middlewares.autodetect.contenttype": "true"
} }
``` ```
```yaml tab="Rancher" ```yaml tab="Rancher"
# Disable auto-detection # Enable auto-detection
labels: labels:
- "traefik.http.middlewares.autodetect.contenttype.autodetect=false" - "traefik.http.middlewares.autodetect.contenttype=true"
``` ```
```yaml tab="File (YAML)" ```yaml tab="File (YAML)"
# Disable auto-detection # Enable auto-detection
http: http:
middlewares: middlewares:
autodetect: autodetect:
contentType: contentType: {}
autoDetect: false
``` ```
```toml tab="File (TOML)" ```toml tab="File (TOML)"
# Disable auto-detection # Enable auto-detection
[http.middlewares] [http.middlewares]
[http.middlewares.autodetect.contentType] [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`). `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. `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=true"
- "traefik.http.middlewares.middleware05.compress.excludedcontenttypes=foobar, foobar" - "traefik.http.middlewares.middleware05.compress.excludedcontenttypes=foobar, foobar"
- "traefik.http.middlewares.middleware05.compress.minresponsebodybytes=42" - "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.headerfield=foobar"
- "traefik.http.middlewares.middleware07.digestauth.realm=foobar" - "traefik.http.middlewares.middleware07.digestauth.realm=foobar"
- "traefik.http.middlewares.middleware07.digestauth.removeheader=true" - "traefik.http.middlewares.middleware07.digestauth.removeheader=true"

View file

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

View file

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

View file

@ -762,19 +762,9 @@ spec:
type: object type: object
contentType: contentType:
description: ContentType holds the content-type middleware configuration. description: ContentType holds the content-type middleware configuration.
This middleware exists to enable the correct behavior until at least This middleware sets the `Content-Type` header value to the media
the default one can be changed in a future version. type detected from the response content, when it is not set by the
properties: backend.
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
type: object type: object
digestAuth: digestAuth:
description: 'DigestAuth holds the digest auth middleware configuration. 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/0` | `foobar` |
| `traefik/http/middlewares/Middleware05/compress/excludedContentTypes/1` | `foobar` | | `traefik/http/middlewares/Middleware05/compress/excludedContentTypes/1` | `foobar` |
| `traefik/http/middlewares/Middleware05/compress/minResponseBodyBytes` | `42` | | `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/headerField` | `foobar` |
| `traefik/http/middlewares/Middleware07/digestAuth/realm` | `foobar` | | `traefik/http/middlewares/Middleware07/digestAuth/realm` | `foobar` |
| `traefik/http/middlewares/Middleware07/digestAuth/removeHeader` | `true` | | `traefik/http/middlewares/Middleware07/digestAuth/removeHeader` | `true` |

View file

@ -17,7 +17,7 @@
"traefik.http.middlewares.middleware05.compress": "true", "traefik.http.middlewares.middleware05.compress": "true",
"traefik.http.middlewares.middleware05.compress.excludedcontenttypes": "foobar, foobar", "traefik.http.middlewares.middleware05.compress.excludedcontenttypes": "foobar, foobar",
"traefik.http.middlewares.middleware05.compress.minresponsebodybytes": "42", "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.headerfield": "foobar",
"traefik.http.middlewares.middleware07.digestauth.realm": "foobar", "traefik.http.middlewares.middleware07.digestauth.realm": "foobar",
"traefik.http.middlewares.middleware07.digestauth.removeheader": "true", "traefik.http.middlewares.middleware07.digestauth.removeheader": "true",

View file

@ -185,19 +185,9 @@ spec:
type: object type: object
contentType: contentType:
description: ContentType holds the content-type middleware configuration. description: ContentType holds the content-type middleware configuration.
This middleware exists to enable the correct behavior until at least This middleware sets the `Content-Type` header value to the media
the default one can be changed in a future version. type detected from the response content, when it is not set by the
properties: backend.
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
type: object type: object
digestAuth: digestAuth:
description: 'DigestAuth holds the digest auth middleware configuration. description: 'DigestAuth holds the digest auth middleware configuration.

View file

@ -762,19 +762,9 @@ spec:
type: object type: object
contentType: contentType:
description: ContentType holds the content-type middleware configuration. description: ContentType holds the content-type middleware configuration.
This middleware exists to enable the correct behavior until at least This middleware sets the `Content-Type` header value to the media
the default one can be changed in a future version. type detected from the response content, when it is not set by the
properties: backend.
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
type: object type: object
digestAuth: digestAuth:
description: 'DigestAuth holds the digest auth middleware configuration. description: 'DigestAuth holds the digest auth middleware configuration.

View file

@ -21,32 +21,12 @@
[http.routers] [http.routers]
[http.routers.router1] [http.routers.router1]
service = "service1" service = "service1"
rule = "PathPrefix(`/css/ct/nomiddleware`) || PathPrefix(`/pdf/ct/nomiddleware`)" rule = "PathPrefix(`/`)"
[http.routers.router2] [http.routers.router2]
service = "service1" service = "service1"
middlewares = ["autodetect"] middlewares = ["autodetect"]
rule = "PathPrefix(`/css/ct/middlewareauto`) || PathPrefix(`/pdf/ct/middlewareauto`)" rule = "PathPrefix(`/autodetect`)"
[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`)"
[http.services] [http.services]
[http.services.service1] [http.services.service1]
@ -56,7 +36,3 @@
url = "{{ .Server }}" url = "{{ .Server }}"
[http.middlewares.autodetect.contentType] [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) { func (s *SimpleSuite) TestContentTypeDisableAutoDetect(c *check.C) {
srv1 := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { srv1 := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.Header()["Content-Type"] = nil rw.Header()["Content-Type"] = nil
switch req.URL.Path[:4] { path := strings.TrimPrefix(req.URL.Path, "/autodetect")
switch path[:4] {
case "/css": case "/css":
if !strings.Contains(req.URL.Path, "noct") { if strings.Contains(req.URL.Path, "/ct") {
rw.Header().Set("Content-Type", "text/css") rw.Header().Set("Content-Type", "text/css")
} }
@ -1177,7 +1178,7 @@ func (s *SimpleSuite) TestContentTypeDisableAutoDetect(c *check.C) {
_, err := rw.Write([]byte(".testcss { }")) _, err := rw.Write([]byte(".testcss { }"))
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
case "/pdf": case "/pdf":
if !strings.Contains(req.URL.Path, "noct") { if strings.Contains(req.URL.Path, "/ct") {
rw.Header().Set("Content-Type", "application/pdf") 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")) 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) 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) 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) 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)) err = try.GetRequest("http://127.0.0.1:8000/css/noct", time.Second, func(res *http.Response) error {
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 {
if ct, ok := res.Header["Content-Type"]; ok { if ct, ok := res.Header["Content-Type"]; ok {
return fmt.Errorf("should have no content type and %s is present", ct) 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) 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 { if ct, ok := res.Header["Content-Type"]; ok {
return fmt.Errorf("should have no content type and %s is present", ct) return fmt.Errorf("should have no content type and %s is present", ct)
} }
return nil return nil
}) })
c.Assert(err, checker.IsNil) 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) { 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"` 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"` 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"` 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"` 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"` 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 // +k8s:deepcopy-gen=true
// ContentType holds the content-type middleware configuration. // 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. // This middleware sets the `Content-Type` header value to the media type detected from the response content,
type ContentType struct { // when it is not set by the backend.
// AutoDetect specifies whether to let the `Content-Type` header, if it has not been set by the backend, type ContentType struct{}
// 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"`
}
// +k8s:deepcopy-gen=true // +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, Attempts: 42,
InitialInterval: 42, InitialInterval: 42,
}, },
ContentType: &dynamic.ContentType{ ContentType: &dynamic.ContentType{},
AutoDetect: true,
},
Plugin: map[string]dynamic.PluginConf{ Plugin: map[string]dynamic.PluginConf{
"foo": { "foo": {
"answer": struct{ Answer int }{ "answer": struct{ Answer int }{

View file

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

View file

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

View file

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

View file

@ -22,6 +22,7 @@ import (
"github.com/traefik/traefik/v2/pkg/ip" "github.com/traefik/traefik/v2/pkg/ip"
"github.com/traefik/traefik/v2/pkg/logs" "github.com/traefik/traefik/v2/pkg/logs"
"github.com/traefik/traefik/v2/pkg/middlewares" "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/forwardedheaders"
"github.com/traefik/traefik/v2/pkg/middlewares/requestdecorator" "github.com/traefik/traefik/v2/pkg/middlewares/requestdecorator"
"github.com/traefik/traefik/v2/pkg/safe" "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 = http.AllowQuerySemicolons(handler)
handler = contenttype.DisableAutoDetection(handler)
if withH2c { if withH2c {
handler = h2c.NewHandler(handler, &http2.Server{ handler = h2c.NewHandler(handler, &http2.Server{
MaxConcurrentStreams: uint32(configuration.HTTP2.MaxConcurrentStreams), MaxConcurrentStreams: uint32(configuration.HTTP2.MaxConcurrentStreams),