Repository: traefik/traefik (language: go)

Issue:
Compression breaks transmission of files with incorrect Content-Type
### Welcome!

- [x] Yes, I've searched similar issues on [GitHub](https://github.com/traefik/traefik/issues) and didn't find any.
- [x] Yes, I've searched similar issues on the [Traefik community forum](https://community.traefik.io) and didn't find any.

### What did you do?

I have a backend service that returns incorrect `Content-Type` headers with double charset:

```
HTTP/1.1 200 OK
Content-Disposition: inline; filename=main.js
Content-Type: text/javascript; charset=UTF-8; charset=utf-8
Content-Length: 31340
Last-Modified: Sat, 16 Nov 2024 06:21:28 GMT
Cache-Control: no-cache
ETag: "1731738088.0-31340-228396532"
Date: Sat, 08 Feb 2025 12:21:10 GMT
Content-Security-Policy: default-src 'self' 'unsafe-inline' 'unsafe-eval'; font-src 'self' data:; img-src 'self' data:; object-src 'none';
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
Strict-Transport-Security: max-age=31536000
```

When accessing the service through Traefik v3.3.3 with the compression middleware, I fail to download any javascript file when using compression.

Disabling the compression middleware fixes the problem.

I understand that the backend service is using a malformed `Content-Type`, but the compression middleware should be more robust when handling these cases for example by skipping compression.

See the related bug report of the backend service: [https://github.com/crocodilestick/Calibre-Web-Automated/issues/217](https://github.com/crocodilestick/Calibre-Web-Automated/issues/217)

### What did you see instead?

When using compression, download of javascript files fail:

```
$ curl -kv --compressed   https://cwa.example.com/static/js/main.js

> GET /static/js/main.js HTTP/2
> Host: cwa.example.com
> User-Agent: curl/8.9.1
> Accept: */*
> Accept-Encoding: deflate, gzip, br
> 
* Request completely sent off
* HTTP/2 stream 1 was not closed cleanly: INTERNAL_ERROR (err 2)
* Connection #0 to host cwa.example.com left intact
curl: (92) HTTP/2 stream 1 was not closed cleanly: INTERNAL_ERROR (err 2)
```


### What version of Traefik are you using?

```
Version:      3.3.3
Codename:     saintnectaire
Go version:   go1.23.5
Built:        2025-01-31T14:55:01Z
OS/Arch:      linux/amd64
```

### What is your environment & configuration?

static conf:

```yaml
global:
  checkNewVersion: true
  sendAnonymousUsage: false

entryPoints:
  web:
    address: :80
  websecure:
    address: :443
    http3: {}
    transport:
      respondingTimeouts:
        readTimeout: 300
  traefik:
    address: :8080

log:
  level: INFO

accessLog:
  filePath: /var/log/traefik/access.log

api: {}

ping: {}

metrics:
  prometheus: {}

providers:
  file:
    directory: /etc/traefik/conf.d
    watch: true
  docker:
    exposedByDefault: false
    endpoint: 'tcp://podman-socket-proxy:2375'
    network: traefik

certificatesResolvers:
  le-prod:
    acme:
      email: XXXXX
      storage: /certs/le-prod.json
      dnsChallenge:
        provider: cloudflare
  le-staging:
    acme:
      email: XXXXX
      storage: /certs/le-staging.json
      caServer: "https://acme-staging-v02.api.letsencrypt.org/directory"
      dnsChallenge:
        provider: cloudflare
  step-ca:
    acme:
      email: XXXX
      storage: /certs/step-ca.json
      caServer: "https://XXXXX"
      httpChallenge:
        entryPoint: web
```

Dynamic config:

```
http:
  routers:
    api:
      entryPoints:
        - traefik
      rule: Host(`traefik.example.com`)
      service: api@internal
      tls:
        certResolver: step-ca
      middlewares:
        - restrict-to-internal
        - forward-auth

    public-http:
      entryPoints:
        - web
      rule: PathPrefix(`/`)
      priority: 1
      service: noop@internal
      middlewares:
        - external-middlewares

  middlewares:
    forward-auth:
      forwardAuth:
        address: http://authelia:9091/api/authz/forward-auth
        trustForwardHeader: true
        preserveLocationHeader: true
        authResponseHeaders:
          - Remote-User
          - Remote-Groups
          - Remote-Email
          - Remote-Name

    internal-middlewares:
      chain:
        middlewares:
          - restrict-to-internal
          - redirect-to-https
          - internal-headers
          - compress

    external-middlewares:
      chain:
        middlewares:
          - redirect-to-https
          - external-headers
          - custom-error-pages
          - compress

    external-headers:
      headers:
        contentTypeNosniff: true                          # X-Content-Type-Options=nosniff
        referrerPolicy: "strict-origin-when-cross-origin"
        stsIncludeSubdomains: true                        # Add includeSubdomains to the Strict-Transport-Security header
        stsPreload: true                                  # Add preload flag appended to the Strict-Transport-Security header
        stsSeconds: 63072000                              # Set the max-age of the Strict-Transport-Security header (63072000 = 2 years)
        frameDeny: true
        browserXssFilter: true
        customResponseHeaders:
          server: "" # Remove header
          X-Powered-By: "" # Remove header
          X-Robots-Tag: "noindex, nofollow"

    internal-headers:
      headers:
        contentTypeNosniff: true
        referrerPolicy: "strict-origin-when-cross-origin"
        customFrameOptionsValue: "SAMEORIGIN"
        browserXssFilter: true

    compress:
      compress: {}

    redirect-to-https:
      redirectScheme:
        scheme: https
        permanent: true

    custom-error-pages:
      errors:
        status:
          - "404"
          - "500"
          - "502"
          - "503"
          - "504"
        service: nginx@docker
        query: "/errors/{status}.html"

tls:
  options:
    default:
      cipherSuites:
        - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
        - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
        - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
        - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
        - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305
        - TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
      minVersion: VersionTLS12
      sniStrict: true

```

Test container uses these labels:

```
          traefik.enable: "true"
          traefik.docker.network: cwa-test
          traefik.http.routers.cwa-test.rule: Host(`cwa.example.com`)
          traefik.http.routers.cwa-test.entrypoints: websecure
          traefik.http.routers.cwa-test.middlewares: internal-middlewares@file
          traefik.http.routers.cwa-test.tls: "true"
          traefik.http.routers.cwa-test.tls.certresolver: step-ca
```

Add more configuration information here.


### If applicable, please paste the log output in DEBUG level

This is logged in Traefik when requesting the javascript file using compression:

```
2025-02-08T11:57:30Z DBG github.com/traefik/traefik/v3/pkg/middlewares/compress/compress.go:127 > Unable to parse MIME type error="mime: no media type" middlewareName=compress@file middlewareType=Compress
```


Discussion / hints:
I've been doing additional testing and I see that the compression error only happens with the `br` and `zstd` formats.

I've created the following `docker-compose.yaml` to reproduce the issue (note that I'm using podman, so it'll need some adjustments for docker):

```yaml
version: "3"
networks:
  traefik:
    name: traefik
services:
  traefik:
    image: docker.io/library/traefik:v3.3.4
    container_name: traefik
    command:
      - "--log.level=DEBUG"
      - "--api.insecure=true"
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--providers.docker.endpoint=unix:///run/podman/podman.sock"
      - "--entryPoints.web.address=:8080"
      - "--entryPoints.traefik.address=:8081"
    security_opt:
      - "label=disable"
    ports:
      - "8080:8080"
      - "8081:8081"
    volumes:
      - "/run/podman/podman.sock:/run/podman/podman.sock:ro"
    networks:
      - traefik
  calibre-web:
    image: ghcr.io/linuxserver/calibre-web:version-0.6.24
    container_name: calibre-web
    environment:
      - PUID=1000
      - PGID=1000
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=traefik"
      - "traefik.http.middlewares.test-compress.compress=true"
      - "traefik.http.routers.calibre-web.entrypoints=web"
      - "traefik.http.routers.calibre-web.rule=PathPrefix(`/`)"
      - "traefik.http.routers.calibre-web.service=calibre-web"
      - "traefik.http.routers.calibre-web.middlewares=test-compress@docker"
      - "traefik.http.services.calibre-web.loadbalancer.server.port=8083"
      - "traefik.http.services.calibre-web.loadbalancer.server.scheme=http"
    networks:
      - traefik
```

Requesting the gzip compression works well and we can download the file:

```
# podman run -ti --network host docker.io/curlimages/curl -v --compressed -H "Accept-Encoding: gzip" -o /dev/null http://127.0.0.1:8080/static/js/main.js
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0*   Trying 127.0.0.1:8080...
* Connected to 127.0.0.1 (127.0.0.1) port 8080
* using HTTP/1.x
> GET /static/js/main.js HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/8.12.1
> Accept: */*
> Accept-Encoding: gzip
> 
* Request completely sent off
< HTTP/1.1 200 OK
< Cache-Control: no-cache
< Content-Disposition: inline; filename=main.js
< Content-Encoding: gzip
< Content-Security-Policy: default-src 'self' 'unsafe-inline' 'unsafe-eval'; font-src 'self' data:; img-src 'self' data:; object-src 'none';
< Content-Type: text/javascript; charset=UTF-8; charset=utf-8
< Date: Sun, 23 Mar 2025 11:38:58 GMT
< Etag: "1731738088.0-31340-228396532"
< Last-Modified: Sat, 16 Nov 2024 06:21:28 GMT
< Strict-Transport-Security: max-age=31536000
< Vary: Accept-Encoding
< X-Content-Type-Options: nosniff
< X-Frame-Options: SAMEORIGIN
< X-Xss-Protection: 1; mode=block
< Transfer-Encoding: chunked
< 
{ [3468 bytes data]
100  7267    0  7267    0     0  1990k      0 --:--:-- --:--:-- --:--:-- 2365k
* Connection #0 to host 127.0.0.1 left intact
```

Traefik log for gzip:

```
2025-03-23T11:38:58Z DBG github.com/traefik/traefik/v3/pkg/middlewares/compress/compress.go:127 > Unable to parse MIME type error="mime: no media type" middlewareName=test-compress@docker middlewareType=Compress
2025-03-23T11:38:58Z DBG github.com/traefik/traefik/v3/pkg/server/service/loadbalancer/wrr/wrr.go:213 > Service selected by WRR: http://10.89.1.3:8083
```

However the file download fails when using brotli or zstd compression:

```
# podman run -ti --network host docker.io/curlimages/curl -v  --compressed -H "Accept-Encoding: br" -o /dev/null http://127.0.0.1:8080/static/js/main.js
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0*   Trying 127.0.0.1:8080...
* Connected to 127.0.0.1 (127.0.0.1) port 8080
* using HTTP/1.x
> GET /static/js/main.js HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/8.12.1
> Accept: */*
> Accept-Encoding: br
> 
* Request completely sent off
* Empty reply from server
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
* shutting down connection #0
curl: (52) Empty reply from server
```

In the traefik logs we see this request is aborted:

```
2025-03-23T11:40:27Z DBG github.com/traefik/traefik/v3/pkg/middlewares/compress/compress.go:127 > Unable to parse MIME type error="mime: no media type" middlewareName=test-compress@docker middlewareType=Compress
2025-03-23T11:40:27Z DBG github.com/traefik/traefik/v3/pkg/server/service/loadbalancer/wrr/wrr.go:213 > Service selected by WRR: http://10.89.1.3:8083
2025-03-23T11:40:27Z DBG github.com/traefik/traefik/v3/pkg/middlewares/recovery/recovery.go:45 > Request has been aborted [10.89.1.1:40876 - /static/js/main.js]: net/http: abort Handler middlewareName=traefik-internal-recovery middlewareType=Recovery
```

Fix patch:
diff --git a/pkg/middlewares/compress/compression_handler.go b/pkg/middlewares/compress/compression_handler.go
index 215607ccde..07583028b9 100644
--- a/pkg/middlewares/compress/compression_handler.go
+++ b/pkg/middlewares/compress/compression_handler.go
@@ -233,8 +233,12 @@ func (r *responseWriter) Write(p []byte) (int, error) {
 	// Disable compression according to user wishes in excludedContentTypes or includedContentTypes.
 	if ct := r.rw.Header().Get(contentType); ct != "" {
 		mediaType, params, err := mime.ParseMediaType(ct)
+		// To align the behavior with the klauspost handler for Gzip,
+		// if the MIME type is not parsable the compression is disabled.
 		if err != nil {
-			return 0, fmt.Errorf("parsing content-type media type: %w", err)
+			r.compressionDisabled = true
+			r.rw.WriteHeader(r.statusCode)
+			return r.rw.Write(p)
 		}
 
 		if len(r.includedContentTypes) > 0 {


Test patch:
diff --git a/pkg/middlewares/compress/compression_handler_test.go b/pkg/middlewares/compress/compression_handler_test.go
index b078ed71f3..89bbe062ba 100644
--- a/pkg/middlewares/compress/compression_handler_test.go
+++ b/pkg/middlewares/compress/compression_handler_test.go
@@ -577,6 +577,11 @@ func Test_ExcludedContentTypes(t *testing.T) {
 			contentType:    "",
 			expCompression: true,
 		},
+		{
+			desc:           "MIME malformed",
+			contentType:    "application/json;charset=UTF-8;charset=utf-8",
+			expCompression: false,
+		},
 		{
 			desc:                 "MIME match",
 			contentType:          "application/json",
@@ -687,6 +692,11 @@ func Test_IncludedContentTypes(t *testing.T) {
 			contentType:    "",
 			expCompression: true,
 		},
+		{
+			desc:           "MIME malformed",
+			contentType:    "application/json;charset=UTF-8;charset=utf-8",
+			expCompression: false,
+		},
 		{
 			desc:                 "MIME match",
 			contentType:          "application/json",