Repository: traefik/traefik (language: go)

Issue:
Option to configure the size HTTP Request Header Fields (MaxHeaderBytes)
### 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.containo.us) and didn't find any.

### What did you expect to see?

## Option to configure the size HTTP Request Header Fields (MaxHeaderBytes)

### Description of the new feature/enhancement
Traefik uses the Go net/http module to handle request. This module has a default value for the maximum permitted size of the headers in an HTTP request. By default the DefaultMaxHeaderBytes value is set at 1MB (const DefaultMaxHeaderBytes = 1 << 20 // 1 MB). 
 
An request with header size larger than 1MB will result in “_431 Request Header Fields Too Large_”. In our use case large headers are used in OIDC authentication/authorization flow with Keyclock. For this we should be able to configure the MaxHeaderBytes (and go beyond the default of 1MB).  

### How to test / reproduce
Simply add a random header to a get request with a value greater than 1MB. 

### Related questions on community forums
https://community.traefik.io/t/what-is-the-maxheaderbytes-length-in-traefik-v2/10502
https://community.traefik.io/t/431-request-header-fields-too-large/11047/1

### Additional context
We are in the process to transition from NGINX Ingress Controller to Traefik. With NGINX we are able to extend the header size, we are using the following settings: 
•	http2-max-field-size 2m 
•	http2-max-header-size 2m
•	large-client-header-buffers 4 2m
•	proxy-body-size 10m

Kind Regards, 
Erwin Kersten 



Discussion / hints:
I've been facing the same issue when trying to put certain docker containers behind traefik + authelia.
Is there any update on it?
Hello @ren0d1 ,

From experience (we had to solve the issue for some customers), we know that large headers lead to really bad latency issues, which is why we're pretty reluctant to allow this kind of configuration.

So we're waiting to see how much traction this proposal gets. We wouldn't want to add an option (maintenance cost) that 1) in general adds latency, and 2) is a very niche use case that is requested by only very few users.


I need to be able to put some apps behind authelia, and some behind server-auth using organizr api, this is affecting me dearly.
While I recognize not wanting to introduce latency, we need to have the option. Surely sane defaults address the latency concern, don't they? It's configurable in nginx, caddy, apace, etc.
This looks to be impacting us as well but showing a different error:
```
"'500 Internal Server Error' caused by: http2: request header list larger than peer's advertised limit"
``` 

In our case, the offending cookies don't actually need to get down to our underlying apps (they're for the security layer that exists over top our cluster) so we should have the option to strip them out. Would it be possible to use [a middleware to do this](https://doc.traefik.io/traefik/middlewares/http/headers/#adding-and-removing-headers)? Or does this error get encountered before even getting to the middlewares phase of the process?

Any insight would be greatly appreciated!
> While I recognize not wanting to introduce latency, we need to have the option. Surely sane defaults address the latency concern, don't they? It's configurable in nginx, caddy, apace, etc.

Exactly.

> is a very niche use case that is requested by only very few users.

I suppose it is not a very rare case, many cases are concerned with OIDC and cookie headers. (I have the same error while using Casdoor as my OIDC provider and trying to use custom error pages).


I've been on Traefik 2.10.7 since it released without any issues and now I am getting this daily.  If I clear the cookies it solves it until tomorrow when oauth would normally renew.  Using Google OAuth,.
Hello,

Thank you for your contributions on this topic.

We agree that the feature makes sense.
Unfortunately, this would not make it to our roadmap for a while as we are focused elsewhere.

If any community member would like to build it, let us know, and we will work with you to make sure you have all the information needed so that it can be merged.
> Hello,
> 
> Thank you for your contributions on this topic.
> 
> We agree that the feature makes sense. Unfortunately, this would not make it to our roadmap for a while as we are focused elsewhere.
> 
> If any community member would like to build it, let us know, and we will work with you to make sure you have all the information needed so that it can be merged.

Hi @nmengin,

I’m interested in working on this feature and have submitted a [draft PR](https://github.com/traefik/traefik/pull/10995). I’ve reproduced the header size limitation locally and added both unit and integration tests.

Please let me know if there are any design considerations or additional details needed before I finalize the PR.

Thanks!
is possible to increase DefaultMaxHeaderBytes ?

Fix patch:
diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md
index b0c02c5653..6173592163 100644
--- a/docs/content/reference/static-configuration/cli-ref.md
+++ b/docs/content/reference/static-configuration/cli-ref.md
@@ -141,6 +141,9 @@ HTTP configuration.
 `--entrypoints.<name>.http.encodequerysemicolons`:  
 Defines whether request query semicolons should be URLEncoded. (Default: ```false```)
 
+`--entrypoints.<name>.http.maxheaderbytes`:  
+Maximum size of request headers in bytes. (Default: ```1048576```)
+
 `--entrypoints.<name>.http.middlewares`:  
 Default middlewares for the routers linked to the entry point.
 
diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md
index 2f4c19971a..045da78309 100644
--- a/docs/content/reference/static-configuration/env-ref.md
+++ b/docs/content/reference/static-configuration/env-ref.md
@@ -150,6 +150,9 @@ UDP port to advertise, on which HTTP/3 is available. (Default: ```0```)
 `TRAEFIK_ENTRYPOINTS_<NAME>_HTTP_ENCODEQUERYSEMICOLONS`:  
 Defines whether request query semicolons should be URLEncoded. (Default: ```false```)
 
+`TRAEFIK_ENTRYPOINTS_<NAME>_HTTP_MAXHEADERBYTES`:  
+Maximum size of request headers in bytes. (Default: ```1048576```)
+
 `TRAEFIK_ENTRYPOINTS_<NAME>_HTTP_MIDDLEWARES`:  
 Default middlewares for the routers linked to the entry point.
 
diff --git a/docs/content/reference/static-configuration/file.toml b/docs/content/reference/static-configuration/file.toml
index f92cd37631..8f899b211b 100644
--- a/docs/content/reference/static-configuration/file.toml
+++ b/docs/content/reference/static-configuration/file.toml
@@ -51,6 +51,7 @@
     [entryPoints.EntryPoint0.http]
       middlewares = ["foobar", "foobar"]
       encodeQuerySemicolons = true
+      maxHeaderBytes = 42
       [entryPoints.EntryPoint0.http.redirections]
         [entryPoints.EntryPoint0.http.redirections.entryPoint]
           to = "foobar"
diff --git a/docs/content/reference/static-configuration/file.yaml b/docs/content/reference/static-configuration/file.yaml
index 12b5efccc0..31b8e485fa 100644
--- a/docs/content/reference/static-configuration/file.yaml
+++ b/docs/content/reference/static-configuration/file.yaml
@@ -80,6 +80,7 @@ entryPoints:
               - foobar
               - foobar
       encodeQuerySemicolons: true
+      maxHeaderBytes: 42
     http2:
       maxConcurrentStreams: 42
     http3:
diff --git a/integration/fixtures/simple_max_header_size.toml b/integration/fixtures/simple_max_header_size.toml
new file mode 100644
index 0000000000..afd1dd7e27
--- /dev/null
+++ b/integration/fixtures/simple_max_header_size.toml
@@ -0,0 +1,25 @@
+[global]
+  checkNewVersion = false
+  sendAnonymousUsage = false
+
+[entryPoints]
+  [entryPoints.web]
+    address = ":8000"
+  [entryPoints.web.http]
+    maxHeaderBytes = 1310720
+
+[providers.file]
+  filename = "{{ .SelfFilename }}"
+
+## dynamic configuration ##
+
+[http.routers]
+  [http.routers.test-router]
+    entryPoints = ["web"]
+    service = "test-service"
+    rule = "Host(`127.0.0.1`)"
+
+[http.services]
+  [http.services.test-service]
+    [[http.services.test-service.loadBalancer.servers]]
+      url = "{{ .TestServer }}"
diff --git a/pkg/config/static/entrypoints.go b/pkg/config/static/entrypoints.go
index 9b48ddce42..bc3f3c30c6 100644
--- a/pkg/config/static/entrypoints.go
+++ b/pkg/config/static/entrypoints.go
@@ -3,6 +3,7 @@ package static
 import (
 	"fmt"
 	"math"
+	"net/http"
 	"strings"
 
 	ptypes "github.com/traefik/paerser/types"
@@ -53,6 +54,8 @@ func (ep *EntryPoint) SetDefaults() {
 	ep.ForwardedHeaders = &ForwardedHeaders{}
 	ep.UDP = &UDPConfig{}
 	ep.UDP.SetDefaults()
+	ep.HTTP = HTTPConfig{}
+	ep.HTTP.SetDefaults()
 	ep.HTTP2 = &HTTP2Config{}
 	ep.HTTP2.SetDefaults()
 }
@@ -63,6 +66,12 @@ type HTTPConfig struct {
 	Middlewares           []string      `description:"Default middlewares for the routers linked to the entry point." json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty" export:"true"`
 	TLS                   *TLSConfig    `description:"Default TLS configuration for the routers linked to the entry point." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
 	EncodeQuerySemicolons bool          `description:"Defines whether request query semicolons should be URLEncoded." json:"encodeQuerySemicolons,omitempty" toml:"encodeQuerySemicolons,omitempty" yaml:"encodeQuerySemicolons,omitempty"`
+	MaxHeaderBytes        int           `description:"Maximum size of request headers in bytes." json:"maxHeaderBytes,omitempty" toml:"maxHeaderBytes,omitempty" yaml:"maxHeaderBytes,omitempty" export:"true"`
+}
+
+// SetDefaults sets the default values.
+func (c *HTTPConfig) SetDefaults() {
+	c.MaxHeaderBytes = http.DefaultMaxHeaderBytes
 }
 
 // HTTP2Config is the HTTP2 configuration of an entry point.
diff --git a/pkg/server/server_entrypoint_tcp.go b/pkg/server/server_entrypoint_tcp.go
index ca901c90f9..e402442198 100644
--- a/pkg/server/server_entrypoint_tcp.go
+++ b/pkg/server/server_entrypoint_tcp.go
@@ -633,11 +633,12 @@ func createHTTPServer(ctx context.Context, ln net.Listener, configuration *stati
 	}
 
 	serverHTTP := &http.Server{
-		Handler:      handler,
-		ErrorLog:     stdlog.New(logs.NoLevel(log.Logger, zerolog.DebugLevel), "", 0),
-		ReadTimeout:  time.Duration(configuration.Transport.RespondingTimeouts.ReadTimeout),
-		WriteTimeout: time.Duration(configuration.Transport.RespondingTimeouts.WriteTimeout),
-		IdleTimeout:  time.Duration(configuration.Transport.RespondingTimeouts.IdleTimeout),
+		Handler:        handler,
+		ErrorLog:       stdlog.New(logs.NoLevel(log.Logger, zerolog.DebugLevel), "", 0),
+		ReadTimeout:    time.Duration(configuration.Transport.RespondingTimeouts.ReadTimeout),
+		WriteTimeout:   time.Duration(configuration.Transport.RespondingTimeouts.WriteTimeout),
+		IdleTimeout:    time.Duration(configuration.Transport.RespondingTimeouts.IdleTimeout),
+		MaxHeaderBytes: configuration.HTTP.MaxHeaderBytes,
 	}
 	if debugConnection || (configuration.Transport != nil && (configuration.Transport.KeepAliveMaxTime > 0 || configuration.Transport.KeepAliveMaxRequests > 0)) {
 		serverHTTP.ConnContext = func(ctx context.Context, c net.Conn) context.Context {


Test patch:
diff --git a/integration/simple_test.go b/integration/simple_test.go
index 9aae5c1b2d..e57fb2c9a4 100644
--- a/integration/simple_test.go
+++ b/integration/simple_test.go
@@ -1511,3 +1511,63 @@ func (s *SimpleSuite) TestDenyFragment() {
 	require.NoError(s.T(), err)
 	assert.Equal(s.T(), http.StatusBadRequest, resp.StatusCode)
 }
+
+func (s *SimpleSuite) TestMaxHeaderBytes() {
+	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.WriteHeader(http.StatusOK)
+	})
+
+	listener, err := net.Listen("tcp", "127.0.0.1:9000")
+	require.NoError(s.T(), err)
+
+	ts := &httptest.Server{
+		Listener: listener,
+		Config: &http.Server{
+			Handler:        handler,
+			MaxHeaderBytes: 1.25 * 1024 * 1024, // 1.25 MB
+		},
+	}
+	ts.Start()
+	defer ts.Close()
+
+	// The test server and traefik config file both specify a max request header size of 1.25 MB.
+	file := s.adaptFile("fixtures/simple_max_header_size.toml", struct {
+		TestServer string
+	}{ts.URL})
+
+	s.traefikCmd(withConfigFile(file))
+
+	testCases := []struct {
+		name           string
+		headerSize     int
+		expectedStatus int
+	}{
+		{
+			name:           "1.25MB header",
+			headerSize:     int(1.25 * 1024 * 1024),
+			expectedStatus: http.StatusOK,
+		},
+		{
+			name:           "1.5MB header",
+			headerSize:     int(1.5 * 1024 * 1024),
+			expectedStatus: http.StatusRequestHeaderFieldsTooLarge,
+		},
+		{
+			name:           "500KB header",
+			headerSize:     int(500 * 1024),
+			expectedStatus: http.StatusOK,
+		},
+	}
+
+	for _, test := range testCases {
+		s.Run(test.name, func() {
+			req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000", nil)
+			require.NoError(s.T(), err)
+
+			req.Header.Set("X-Large-Header", strings.Repeat("A", test.headerSize))
+
+			err = try.Request(req, 2*time.Second, try.StatusCodeIs(test.expectedStatus))
+			require.NoError(s.T(), err)
+		})
+	}
+}