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
|
@ -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.
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -141,8 +141,7 @@ http:
|
||||||
- foobar
|
- foobar
|
||||||
minResponseBodyBytes: 42
|
minResponseBodyBytes: 42
|
||||||
Middleware06:
|
Middleware06:
|
||||||
contentType:
|
contentType: {}
|
||||||
autoDetect: true
|
|
||||||
Middleware07:
|
Middleware07:
|
||||||
digestAuth:
|
digestAuth:
|
||||||
users:
|
users:
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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` |
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
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,
|
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 }{
|
||||||
|
|
|
@ -302,9 +302,7 @@
|
||||||
"attempts": 42,
|
"attempts": 42,
|
||||||
"initialInterval": "42ns"
|
"initialInterval": "42ns"
|
||||||
},
|
},
|
||||||
"contentType": {
|
"contentType": {},
|
||||||
"autoDetect": true
|
|
||||||
},
|
|
||||||
"plugin": {
|
"plugin": {
|
||||||
"foo": {
|
"foo": {
|
||||||
"answer": {}
|
"answer": {}
|
||||||
|
|
|
@ -305,9 +305,7 @@
|
||||||
"attempts": 42,
|
"attempts": 42,
|
||||||
"initialInterval": "42ns"
|
"initialInterval": "42ns"
|
||||||
},
|
},
|
||||||
"contentType": {
|
"contentType": {},
|
||||||
"autoDetect": true
|
|
||||||
},
|
|
||||||
"plugin": {
|
"plugin": {
|
||||||
"foo": {
|
"foo": {
|
||||||
"answer": {}
|
"answer": {}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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),
|
||||||
|
|
Loading…
Reference in a new issue