Skip to content

Commit edb7056

Browse files
lidelgammazero
andauthored
feat(config): add Gateway.MaxRequestDuration option (#11138)
* feat(config): add Gateway.MaxRequestDuration option exposes the previously hardcoded 1 hour gateway request deadline as a configurable option, allowing operators to adjust it to fit deployment needs. protects gateway from edge cases and slow client attacks. boxo: ipfs/boxo#1079 * test(gateway): add MaxRequestDuration integration test verifies config is wired correctly and 504 is returned when exceeded * docs: add MaxRequestDuration to gateway production guide --------- Co-authored-by: Andrew Gillis <11790789+gammazero@users.noreply.github.com>
1 parent 6983543 commit edb7056

File tree

12 files changed

+82
-10
lines changed

12 files changed

+82
-10
lines changed

config/gateway.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const (
1313

1414
// Gateway limit defaults from boxo
1515
DefaultRetrievalTimeout = gateway.DefaultRetrievalTimeout
16+
DefaultMaxRequestDuration = gateway.DefaultMaxRequestDuration
1617
DefaultMaxConcurrentRequests = gateway.DefaultMaxConcurrentRequests
1718
DefaultMaxRangeRequestFileSize = 0 // 0 means no limit
1819
)
@@ -96,6 +97,14 @@ type Gateway struct {
9697
// A value of 0 disables this timeout.
9798
RetrievalTimeout *OptionalDuration `json:",omitempty"`
9899

100+
// MaxRequestDuration is an absolute deadline for the entire request.
101+
// Unlike RetrievalTimeout (which resets on each data write and catches
102+
// stalled transfers), this is a hard limit on the total time a request
103+
// can take. Returns 504 Gateway Timeout when exceeded.
104+
// This protects the gateway from edge cases and slow client attacks.
105+
// A value of 0 uses the default (1 hour).
106+
MaxRequestDuration *OptionalDuration `json:",omitempty"`
107+
99108
// MaxConcurrentRequests limits concurrent HTTP requests handled by the gateway.
100109
// Requests beyond this limit receive 429 Too Many Requests with Retry-After header.
101110
// A value of 0 disables the limit.

core/corehttp/gateway.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ func Libp2pGatewayOption() ServeOption {
112112
Menu: nil,
113113
// Apply timeout and concurrency limits from user config
114114
RetrievalTimeout: cfg.Gateway.RetrievalTimeout.WithDefault(config.DefaultRetrievalTimeout),
115+
MaxRequestDuration: cfg.Gateway.MaxRequestDuration.WithDefault(config.DefaultMaxRequestDuration),
115116
MaxConcurrentRequests: int(cfg.Gateway.MaxConcurrentRequests.WithDefault(int64(config.DefaultMaxConcurrentRequests))),
116117
MaxRangeRequestFileSize: int64(cfg.Gateway.MaxRangeRequestFileSize.WithDefault(uint64(config.DefaultMaxRangeRequestFileSize))),
117118
DiagnosticServiceURL: "", // Not used since DisableHTMLErrors=true
@@ -272,6 +273,7 @@ func getGatewayConfig(n *core.IpfsNode) (gateway.Config, map[string][]string, er
272273
NoDNSLink: cfg.Gateway.NoDNSLink,
273274
PublicGateways: map[string]*gateway.PublicGateway{},
274275
RetrievalTimeout: cfg.Gateway.RetrievalTimeout.WithDefault(config.DefaultRetrievalTimeout),
276+
MaxRequestDuration: cfg.Gateway.MaxRequestDuration.WithDefault(config.DefaultMaxRequestDuration),
275277
MaxConcurrentRequests: int(cfg.Gateway.MaxConcurrentRequests.WithDefault(int64(config.DefaultMaxConcurrentRequests))),
276278
MaxRangeRequestFileSize: int64(cfg.Gateway.MaxRangeRequestFileSize.WithDefault(uint64(config.DefaultMaxRangeRequestFileSize))),
277279
DiagnosticServiceURL: cfg.Gateway.DiagnosticServiceURL.WithDefault(config.DefaultDiagnosticServiceURL),

docs/changelogs/v0.40.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ This release was brought to you by the [Shipyard](https://ipshipyard.com/) team.
1717
- [Improved `ipfs dag stat` output](#improved-ipfs-dag-stat-output)
1818
- [Skip bad keys when listing](#skip_bad_keys_when_listing)
1919
- [Accelerated DHT Client and Provide Sweep now work together](#accelerated-dht-client-and-provide-sweep-now-work-together)
20+
- [⏱️ Configurable gateway request duration limit](#️-configurable-gateway-request-duration-limit)
2021
- [🔧 Recovery from corrupted MFS root](#-recovery-from-corrupted-mfs-root)
2122
- [📦️ Dependency updates](#-dependency-updates)
2223
- [📝 Changelog](#-changelog)
@@ -97,6 +98,12 @@ Change the `ipfs key list` behavior to log an error and continue listing keys wh
9798

9899
Previously, provide operations could start before the Accelerated DHT Client discovered enough peers, causing sweep mode to lose its efficiency benefits. Now, providing waits for the initial network crawl (about 10 minutes). Your content will be properly distributed across DHT regions after initial DHT map is created. Check `ipfs provide stat` to see when providing begins.
99100

101+
#### ⏱️ Configurable gateway request duration limit
102+
103+
[`Gateway.MaxRequestDuration`](https://github.com/ipfs/kubo/blob/master/docs/config.md#gatewaymaxrequestduration) sets an absolute deadline for gateway requests. Unlike `RetrievalTimeout` (which resets on each data write and catches stalled transfers), this is a hard limit on the total time a request can take.
104+
105+
The default 1 hour limit (previously hardcoded) can now be adjusted to fit your deployment needs. This is a fallback that prevents requests from hanging indefinitely when subsystem timeouts are misconfigured or fail to trigger. Returns 504 Gateway Timeout when exceeded.
106+
100107
#### 🔧 Recovery from corrupted MFS root
101108

102109
If your daemon fails to start because the MFS root is not a directory (due to misconfiguration, operational error, or disk corruption), you can now recover without deleting and recreating your repository in a new `IPFS_PATH`.

docs/config.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ config file at runtime.
6767
- [`Gateway.DisableHTMLErrors`](#gatewaydisablehtmlerrors)
6868
- [`Gateway.ExposeRoutingAPI`](#gatewayexposeroutingapi)
6969
- [`Gateway.RetrievalTimeout`](#gatewayretrievaltimeout)
70+
- [`Gateway.MaxRequestDuration`](#gatewaymaxrequestduration)
7071
- [`Gateway.MaxRangeRequestFileSize`](#gatewaymaxrangerequestfilesize)
7172
- [`Gateway.MaxConcurrentRequests`](#gatewaymaxconcurrentrequests)
7273
- [`Gateway.HTTPHeaders`](#gatewayhttpheaders)
@@ -1178,6 +1179,16 @@ Default: `30s`
11781179

11791180
Type: `optionalDuration`
11801181

1182+
### `Gateway.MaxRequestDuration`
1183+
1184+
An absolute deadline for the entire gateway request. Unlike [`RetrievalTimeout`](#gatewayretrievaltimeout) (which resets on each data write and catches stalled transfers), this is a hard limit on the total time a request can take.
1185+
1186+
Returns 504 Gateway Timeout when exceeded. This protects the gateway from edge cases and slow client attacks.
1187+
1188+
Default: `1h`
1189+
1190+
Type: `optionalDuration`
1191+
11811192
### `Gateway.MaxRangeRequestFileSize`
11821193

11831194
Maximum file size for HTTP range requests on deserialized responses. Range requests for files larger than this limit return 501 Not Implemented.

docs/examples/kubo-as-a-library/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ go 1.25
77
replace github.com/ipfs/kubo => ./../../..
88

99
require (
10-
github.com/ipfs/boxo v0.35.3-0.20251202220026-0842ad274a0c
10+
github.com/ipfs/boxo v0.35.3-0.20260109213916-89dc184784f2
1111
github.com/ipfs/kubo v0.0.0-00010101000000-000000000000
1212
github.com/libp2p/go-libp2p v0.46.0
1313
github.com/multiformats/go-multiaddr v0.16.1

docs/examples/kubo-as-a-library/go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,8 +265,8 @@ github.com/ipfs-shipyard/nopfs/ipfs v0.25.0 h1:OqNqsGZPX8zh3eFMO8Lf8EHRRnSGBMqcd
265265
github.com/ipfs-shipyard/nopfs/ipfs v0.25.0/go.mod h1:BxhUdtBgOXg1B+gAPEplkg/GpyTZY+kCMSfsJvvydqU=
266266
github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs=
267267
github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0=
268-
github.com/ipfs/boxo v0.35.3-0.20251202220026-0842ad274a0c h1:mczpALnNzNhmggehO5Ehr9+Q8+NiJyKJfT4EPwi01d0=
269-
github.com/ipfs/boxo v0.35.3-0.20251202220026-0842ad274a0c/go.mod h1:Abmp1if6bMQG87/0SQPIB9fkxJnZMLCt2nQw3yUZHH0=
268+
github.com/ipfs/boxo v0.35.3-0.20260109213916-89dc184784f2 h1:pRQYSSGnGQa921d8v0uhXg2BGzoSf9ndTWTlR7ImVoo=
269+
github.com/ipfs/boxo v0.35.3-0.20260109213916-89dc184784f2/go.mod h1:Abmp1if6bMQG87/0SQPIB9fkxJnZMLCt2nQw3yUZHH0=
270270
github.com/ipfs/go-bitfield v1.1.0 h1:fh7FIo8bSwaJEh6DdTWbCeZ1eqOaOkKFI74SCnsWbGA=
271271
github.com/ipfs/go-bitfield v1.1.0/go.mod h1:paqf1wjq/D2BBmzfTVFlJQ9IlFOZpg422HL0HqsGWHU=
272272
github.com/ipfs/go-block-format v0.0.3/go.mod h1:4LmD4ZUw0mhO+JSKdpWwrzATiEfM7WWgQ8H5l6P8MVk=

docs/gateway.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,9 @@ When deploying Kubo's gateway in production, be aware of these important conside
109109
110110
> [!IMPORTANT]
111111
> **Timeouts:** Configure [`Gateway.RetrievalTimeout`](config.md#gatewayretrievaltimeout)
112-
> based on your expected content retrieval times.
112+
> to terminate stalled transfers (resets on each data write, catches unresponsive operations),
113+
> and [`Gateway.MaxRequestDuration`](config.md#gatewaymaxrequestduration) as a fallback
114+
> deadline (default: 1 hour, catches cases when other timeouts are misconfigured or fail to fire).
113115
114116
> [!IMPORTANT]
115117
> **Rate Limiting:** Use [`Gateway.MaxConcurrentRequests`](config.md#gatewaymaxconcurrentrequests)

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ require (
2121
github.com/hashicorp/go-version v1.7.0
2222
github.com/ipfs-shipyard/nopfs v0.0.14
2323
github.com/ipfs-shipyard/nopfs/ipfs v0.25.0
24-
github.com/ipfs/boxo v0.35.3-0.20251202220026-0842ad274a0c
24+
github.com/ipfs/boxo v0.35.3-0.20260109213916-89dc184784f2
2525
github.com/ipfs/go-block-format v0.2.3
2626
github.com/ipfs/go-cid v0.6.0
2727
github.com/ipfs/go-cidutil v0.1.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -336,8 +336,8 @@ github.com/ipfs-shipyard/nopfs/ipfs v0.25.0 h1:OqNqsGZPX8zh3eFMO8Lf8EHRRnSGBMqcd
336336
github.com/ipfs-shipyard/nopfs/ipfs v0.25.0/go.mod h1:BxhUdtBgOXg1B+gAPEplkg/GpyTZY+kCMSfsJvvydqU=
337337
github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs=
338338
github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0=
339-
github.com/ipfs/boxo v0.35.3-0.20251202220026-0842ad274a0c h1:mczpALnNzNhmggehO5Ehr9+Q8+NiJyKJfT4EPwi01d0=
340-
github.com/ipfs/boxo v0.35.3-0.20251202220026-0842ad274a0c/go.mod h1:Abmp1if6bMQG87/0SQPIB9fkxJnZMLCt2nQw3yUZHH0=
339+
github.com/ipfs/boxo v0.35.3-0.20260109213916-89dc184784f2 h1:pRQYSSGnGQa921d8v0uhXg2BGzoSf9ndTWTlR7ImVoo=
340+
github.com/ipfs/boxo v0.35.3-0.20260109213916-89dc184784f2/go.mod h1:Abmp1if6bMQG87/0SQPIB9fkxJnZMLCt2nQw3yUZHH0=
341341
github.com/ipfs/go-bitfield v1.1.0 h1:fh7FIo8bSwaJEh6DdTWbCeZ1eqOaOkKFI74SCnsWbGA=
342342
github.com/ipfs/go-bitfield v1.1.0/go.mod h1:paqf1wjq/D2BBmzfTVFlJQ9IlFOZpg422HL0HqsGWHU=
343343
github.com/ipfs/go-block-format v0.0.3/go.mod h1:4LmD4ZUw0mhO+JSKdpWwrzATiEfM7WWgQ8H5l6P8MVk=

test/cli/gateway_limits_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,47 @@ func TestGatewayLimits(t *testing.T) {
5858
assert.Contains(t, resp.Body, "Unable to retrieve content within timeout period")
5959
})
6060

61+
t.Run("MaxRequestDuration", func(t *testing.T) {
62+
t.Parallel()
63+
64+
// Create a node with a short max request duration
65+
node := harness.NewT(t).NewNode().Init()
66+
node.UpdateConfig(func(cfg *config.Config) {
67+
// Set a short absolute deadline (500ms) for the entire request
68+
cfg.Gateway.MaxRequestDuration = config.NewOptionalDuration(500 * time.Millisecond)
69+
// Set retrieval timeout much longer so MaxRequestDuration fires first
70+
cfg.Gateway.RetrievalTimeout = config.NewOptionalDuration(30 * time.Second)
71+
})
72+
node.StartDaemon()
73+
defer node.StopDaemon()
74+
75+
// Add content that can be retrieved quickly
76+
cid := node.IPFSAddStr("test content for max request duration")
77+
78+
client := node.GatewayClient()
79+
80+
// Fast request for local content should succeed (well within 500ms)
81+
resp := client.Get("/ipfs/" + cid)
82+
assert.Equal(t, http.StatusOK, resp.StatusCode)
83+
assert.Equal(t, "test content for max request duration", resp.Body)
84+
85+
// Request for non-existent content should timeout due to MaxRequestDuration
86+
// This CID has no providers and will block during content routing
87+
nonExistentCID := "bafkreif6lrhgz3fpiwypdk65qrqiey7svgpggruhbylrgv32l3izkqpsc4"
88+
89+
// Create a client with a longer timeout than MaxRequestDuration
90+
// to ensure we receive the gateway's 504 response
91+
clientWithTimeout := &harness.HTTPClient{
92+
Client: &http.Client{
93+
Timeout: 5 * time.Second,
94+
},
95+
BaseURL: client.BaseURL,
96+
}
97+
98+
resp = clientWithTimeout.Get("/ipfs/" + nonExistentCID)
99+
assert.Equal(t, http.StatusGatewayTimeout, resp.StatusCode, "Expected 504 when request exceeds MaxRequestDuration")
100+
})
101+
61102
t.Run("MaxConcurrentRequests", func(t *testing.T) {
62103
t.Parallel()
63104

0 commit comments

Comments
 (0)