Skip to content

Commit 9d3ca55

Browse files
authored
feat(router): make header size configurable (#1457)
1 parent 41022cd commit 9d3ca55

File tree

8 files changed

+62
-11
lines changed

8 files changed

+62
-11
lines changed

router-tests/graphql_over_http_test.go

+42
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@ package integration
22

33
import (
44
"bytes"
5+
56
"io"
67
"net/http"
8+
"strings"
79
"testing"
810

911
"github.com/stretchr/testify/require"
1012
"github.com/wundergraph/cosmo/router-tests/testenv"
13+
"github.com/wundergraph/cosmo/router/core"
14+
"github.com/wundergraph/cosmo/router/pkg/config"
1115
)
1216

1317
func TestGraphQLOverHTTPCompatibility(t *testing.T) {
@@ -196,5 +200,43 @@ func TestGraphQLOverHTTPCompatibility(t *testing.T) {
196200
require.NoError(t, err)
197201
require.Equal(t, `{"data":{"findEmployees":[{"id":1,"details":{"forename":"Jens","surname":"Neuse"}},{"id":2,"details":{"forename":"Dustin","surname":"Deus"}},{"id":4,"details":{"forename":"Björn","surname":"Schwenzer"}},{"id":11,"details":{"forename":"Alexandra","surname":"Neuse"}}]}}`, string(data))
198202
})
203+
t.Run("request with long header should return 431 response", func(t *testing.T) {
204+
header := http.Header{}
205+
206+
// the limit actually behaves a bit weird in the http library. It's not exactly 1<<20 (1MiB)
207+
// It also has some threshold added, and when running all tests at the same time the limit goes even higher.
208+
// I couldn't figure out why, so I just go way beyond the limit to make sure it fails.
209+
header.Add("X-Long-Header", strings.Repeat("abc", http.DefaultMaxHeaderBytes)) // 3MB
210+
211+
body := []byte(`{"query":"query Find($criteria: SearchInput!) {findEmployees(criteria: $criteria){id details {forename surname}}}","variables":{"criteria":{"nationality":"GERMAN"}},"operationName":"Find"}`)
212+
res, err := xEnv.MakeRequest("POST", "/graphql", header, bytes.NewReader(body))
213+
require.NoError(t, err)
214+
require.Equal(t, http.StatusRequestHeaderFieldsTooLarge, res.StatusCode)
215+
})
216+
})
217+
218+
t.Run("request with long header and updated max size should return 200 response", func(t *testing.T) {
219+
t.Parallel()
220+
testenv.Run(t, &testenv.Config{
221+
RouterOptions: []core.Option{
222+
core.WithRouterTrafficConfig(&config.RouterTrafficConfiguration{
223+
MaxHeaderBytes: 4 << 20, // 4MiB
224+
MaxRequestBodyBytes: 5 << 20, // 5MiB
225+
}),
226+
},
227+
}, func(t *testing.T, xEnv *testenv.Environment) {
228+
header := http.Header{
229+
"Content-Type": []string{"application/json"},
230+
"Accept": []string{"application/json"},
231+
}
232+
233+
header.Add("X-Long-Header", strings.Repeat("abc", http.DefaultMaxHeaderBytes)) // 3MB
234+
235+
body := []byte(`{"query":"query Find($criteria: SearchInput!) {findEmployees(criteria: $criteria){id details {forename surname}}}","variables":{"criteria":{"nationality":"GERMAN"}},"operationName":"Find"}`)
236+
237+
res, err := xEnv.MakeRequest("POST", "/graphql", header, bytes.NewReader(body))
238+
require.NoError(t, err)
239+
require.Equal(t, http.StatusOK, res.StatusCode)
240+
})
199241
})
200242
}

router/core/http_server.go

+5-3
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type httpServerOptions struct {
2929
tlsServerConfig *tls.Config
3030
healthcheck health.Checker
3131
baseURL string
32+
maxHeaderBytes int
3233
}
3334

3435
func newServer(opts *httpServerOptions) *server {
@@ -37,9 +38,10 @@ func newServer(opts *httpServerOptions) *server {
3738
ReadTimeout: 60 * time.Second,
3839
// Disable write timeout to keep the connection open until the client closes it
3940
// This is required for SSE (Server-Sent-Events) subscriptions to work correctly
40-
WriteTimeout: 0,
41-
ErrorLog: zap.NewStdLog(opts.logger),
42-
TLSConfig: opts.tlsServerConfig,
41+
WriteTimeout: 0,
42+
ErrorLog: zap.NewStdLog(opts.logger),
43+
TLSConfig: opts.tlsServerConfig,
44+
MaxHeaderBytes: opts.maxHeaderBytes,
4345
}
4446

4547
n := &server{

router/core/router.go

+2-6
Original file line numberDiff line numberDiff line change
@@ -710,6 +710,7 @@ func (r *Router) NewServer(ctx context.Context) (Server, error) {
710710
tlsServerConfig: r.tlsServerConfig,
711711
healthcheck: r.healthcheck,
712712
baseURL: r.baseURL,
713+
maxHeaderBytes: int(r.routerTrafficConfig.MaxHeaderBytes.Uint64()),
713714
})
714715

715716
// Start the server with the static config without polling
@@ -1044,6 +1045,7 @@ func (r *Router) Start(ctx context.Context) error {
10441045
tlsServerConfig: r.tlsServerConfig,
10451046
healthcheck: r.healthcheck,
10461047
baseURL: r.baseURL,
1048+
maxHeaderBytes: int(r.routerTrafficConfig.MaxHeaderBytes.Uint64()),
10471049
})
10481050

10491051
// Start the server with the static config without polling
@@ -1779,12 +1781,6 @@ func WithCacheWarmupConfig(cfg *config.CacheWarmupConfiguration) Option {
17791781
}
17801782
}
17811783

1782-
func WithHostName(hostName string) Option {
1783-
return func(r *Router) {
1784-
r.hostName = hostName
1785-
}
1786-
}
1787-
17881784
type ProxyFunc func(req *http.Request) (*url.URL, error)
17891785

17901786
func newHTTPTransport(opts TransportTimeoutOptions, proxy ProxyFunc) *http.Transport {

router/pkg/config/config.go

+2
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,8 @@ type FileUpload struct {
152152
type RouterTrafficConfiguration struct {
153153
// MaxRequestBodyBytes is the maximum size of the request body in bytes
154154
MaxRequestBodyBytes BytesString `yaml:"max_request_body_size" envDefault:"5MB"`
155+
// MaxHeaderBytes is the maximum size of the request headers in bytes
156+
MaxHeaderBytes BytesString `yaml:"max_header_bytes" envDefault:"0MiB" env:"MAX_HEADER_BYTES"`
155157
}
156158

157159
type GlobalSubgraphRequestRule struct {

router/pkg/config/config.schema.json

+6
Original file line numberDiff line numberDiff line change
@@ -1162,6 +1162,12 @@
11621162
"minimum": "1MB"
11631163
},
11641164
"description": "The maximum request body size. The size is specified as a string with a number and a unit, e.g. 10KB, 1MB, 1GB. The supported units are 'KB', 'MB', 'GB'."
1165+
},
1166+
"max_header_bytes": {
1167+
"type": "string",
1168+
"description": "The maximum size of the request headers. Setting this to 0 uses the default value from the http standard lib, which is 1MiB.",
1169+
"default": "0MiB",
1170+
"bytes": {}
11651171
}
11661172
}
11671173
},

router/pkg/config/fixtures/full.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ traffic_shaping:
146146
router:
147147
# Is the maximum size of the request body in MB, mib
148148
max_request_body_size: 5MB
149+
max_header_bytes: 4MiB
149150
all: # Rules are applied to all subgraph requests.
150151
# Subgraphs transport options
151152
request_timeout: 60s

router/pkg/config/testdata/config_defaults.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,8 @@
118118
"KeepAliveProbeInterval": 30000000000
119119
},
120120
"Router": {
121-
"MaxRequestBodyBytes": 5000000
121+
"MaxRequestBodyBytes": 5000000,
122+
"MaxHeaderBytes": 0
122123
},
123124
"Subgraphs": null
124125
},

router/pkg/config/testdata/config_full.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,8 @@
242242
"KeepAliveProbeInterval": 30000000000
243243
},
244244
"Router": {
245-
"MaxRequestBodyBytes": 5000000
245+
"MaxRequestBodyBytes": 5000000,
246+
"MaxHeaderBytes": 4194304
246247
},
247248
"Subgraphs": {
248249
"products": {

0 commit comments

Comments
 (0)