From db287c4d316d81c71075f66c8ea28a5df0bee268 Mon Sep 17 00:00:00 2001 From: Simon Delicata Date: Tue, 29 Nov 2022 11:48:05 +0100 Subject: [PATCH] Disable Content-Type auto-detection by default --- docs/content/middlewares/http/contenttype.md | 57 ++++--------- docs/content/migration/v2-to-v3.md | 5 ++ .../dynamic-configuration/docker-labels.yml | 2 +- .../reference/dynamic-configuration/file.toml | 1 - .../reference/dynamic-configuration/file.yaml | 3 +- .../kubernetes-crd-definition-v1.yml | 16 +--- .../reference/dynamic-configuration/kv-ref.md | 2 +- .../marathon-labels.json | 2 +- .../traefik.containo.us_middlewares.yaml | 16 +--- integration/fixtures/k8s/01-traefik-crd.yml | 16 +--- integration/fixtures/simple_contenttype.toml | 28 +------ integration/simple_test.go | 51 +++++------- pkg/config/dynamic/middlewares.go | 14 +--- pkg/middlewares/contenttype/content_type.go | 46 +++++++++++ .../contenttype/content_type_test.go | 79 +++++++++++++++++++ pkg/redactor/redactor_config_test.go | 4 +- .../testdata/anonymized-dynamic-config.json | 4 +- .../testdata/secured-dynamic-config.json | 4 +- pkg/server/middleware/middlewares.go | 8 +- pkg/server/server_entrypoint_tcp.go | 3 + 20 files changed, 193 insertions(+), 168 deletions(-) create mode 100644 pkg/middlewares/contenttype/content_type.go create mode 100644 pkg/middlewares/contenttype/content_type_test.go diff --git a/docs/content/middlewares/http/contenttype.md b/docs/content/middlewares/http/contenttype.md index 252a3fda6..38072958a 100644 --- a/docs/content/middlewares/http/contenttype.md +++ b/docs/content/middlewares/http/contenttype.md @@ -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. +``` \ No newline at end of file diff --git a/docs/content/migration/v2-to-v3.md b/docs/content/migration/v2-to-v3.md index 4a8a9bf24..a574bf5e7 100644 --- a/docs/content/migration/v2-to-v3.md +++ b/docs/content/migration/v2-to-v3.md @@ -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. diff --git a/docs/content/reference/dynamic-configuration/docker-labels.yml b/docs/content/reference/dynamic-configuration/docker-labels.yml index 37c7917b2..e58a1d46a 100644 --- a/docs/content/reference/dynamic-configuration/docker-labels.yml +++ b/docs/content/reference/dynamic-configuration/docker-labels.yml @@ -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" diff --git a/docs/content/reference/dynamic-configuration/file.toml b/docs/content/reference/dynamic-configuration/file.toml index 76d27806f..41994814d 100644 --- a/docs/content/reference/dynamic-configuration/file.toml +++ b/docs/content/reference/dynamic-configuration/file.toml @@ -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"] diff --git a/docs/content/reference/dynamic-configuration/file.yaml b/docs/content/reference/dynamic-configuration/file.yaml index fd04e88d5..bfecd9fd2 100644 --- a/docs/content/reference/dynamic-configuration/file.yaml +++ b/docs/content/reference/dynamic-configuration/file.yaml @@ -141,8 +141,7 @@ http: - foobar minResponseBodyBytes: 42 Middleware06: - contentType: - autoDetect: true + contentType: {} Middleware07: digestAuth: users: diff --git a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml index 0681cb3d3..0a8f01823 100644 --- a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml +++ b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml @@ -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. diff --git a/docs/content/reference/dynamic-configuration/kv-ref.md b/docs/content/reference/dynamic-configuration/kv-ref.md index 2a1310905..ca40ac8a4 100644 --- a/docs/content/reference/dynamic-configuration/kv-ref.md +++ b/docs/content/reference/dynamic-configuration/kv-ref.md @@ -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` | diff --git a/docs/content/reference/dynamic-configuration/marathon-labels.json b/docs/content/reference/dynamic-configuration/marathon-labels.json index 01c2af0fc..99cfaefb9 100644 --- a/docs/content/reference/dynamic-configuration/marathon-labels.json +++ b/docs/content/reference/dynamic-configuration/marathon-labels.json @@ -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", diff --git a/docs/content/reference/dynamic-configuration/traefik.containo.us_middlewares.yaml b/docs/content/reference/dynamic-configuration/traefik.containo.us_middlewares.yaml index f332eb5f6..6b9a4cac9 100644 --- a/docs/content/reference/dynamic-configuration/traefik.containo.us_middlewares.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.containo.us_middlewares.yaml @@ -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. diff --git a/integration/fixtures/k8s/01-traefik-crd.yml b/integration/fixtures/k8s/01-traefik-crd.yml index 0681cb3d3..0a8f01823 100644 --- a/integration/fixtures/k8s/01-traefik-crd.yml +++ b/integration/fixtures/k8s/01-traefik-crd.yml @@ -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. diff --git a/integration/fixtures/simple_contenttype.toml b/integration/fixtures/simple_contenttype.toml index 2743cd659..0c68e3788 100644 --- a/integration/fixtures/simple_contenttype.toml +++ b/integration/fixtures/simple_contenttype.toml @@ -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 diff --git a/integration/simple_test.go b/integration/simple_test.go index 8738699a1..22327142f 100644 --- a/integration/simple_test.go +++ b/integration/simple_test.go @@ -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) { diff --git a/pkg/config/dynamic/middlewares.go b/pkg/config/dynamic/middlewares.go index e8d2a1491..52d349c4d 100644 --- a/pkg/config/dynamic/middlewares.go +++ b/pkg/config/dynamic/middlewares.go @@ -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 diff --git a/pkg/middlewares/contenttype/content_type.go b/pkg/middlewares/contenttype/content_type.go new file mode 100644 index 000000000..bfe5a5c48 --- /dev/null +++ b/pkg/middlewares/contenttype/content_type.go @@ -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) + } +} diff --git a/pkg/middlewares/contenttype/content_type_test.go b/pkg/middlewares/contenttype/content_type_test.go new file mode 100644 index 000000000..f134994c0 --- /dev/null +++ b/pkg/middlewares/contenttype/content_type_test.go @@ -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")) + }) + } +} diff --git a/pkg/redactor/redactor_config_test.go b/pkg/redactor/redactor_config_test.go index 88b507252..beb7ed5b1 100644 --- a/pkg/redactor/redactor_config_test.go +++ b/pkg/redactor/redactor_config_test.go @@ -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 }{ diff --git a/pkg/redactor/testdata/anonymized-dynamic-config.json b/pkg/redactor/testdata/anonymized-dynamic-config.json index 4a7d86e2c..f5ce9308f 100644 --- a/pkg/redactor/testdata/anonymized-dynamic-config.json +++ b/pkg/redactor/testdata/anonymized-dynamic-config.json @@ -302,9 +302,7 @@ "attempts": 42, "initialInterval": "42ns" }, - "contentType": { - "autoDetect": true - }, + "contentType": {}, "plugin": { "foo": { "answer": {} diff --git a/pkg/redactor/testdata/secured-dynamic-config.json b/pkg/redactor/testdata/secured-dynamic-config.json index 02547cbb9..05efe1420 100644 --- a/pkg/redactor/testdata/secured-dynamic-config.json +++ b/pkg/redactor/testdata/secured-dynamic-config.json @@ -305,9 +305,7 @@ "attempts": 42, "initialInterval": "42ns" }, - "contentType": { - "autoDetect": true - }, + "contentType": {}, "plugin": { "foo": { "answer": {} diff --git a/pkg/server/middleware/middlewares.go b/pkg/server/middleware/middlewares.go index 18c1f25ba..6d1790838 100644 --- a/pkg/server/middleware/middlewares.go +++ b/pkg/server/middleware/middlewares.go @@ -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) } } diff --git a/pkg/server/server_entrypoint_tcp.go b/pkg/server/server_entrypoint_tcp.go index 2fd4380f8..87c0a77ef 100644 --- a/pkg/server/server_entrypoint_tcp.go +++ b/pkg/server/server_entrypoint_tcp.go @@ -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),