master
go 175 lines 6.32 KB
Raw
1 package cli
2
3 import (
4 "net/http"
5 "testing"
6 "time"
7
8 "github.com/ipfs/kubo/config"
9 "github.com/ipfs/kubo/test/cli/harness"
10 "github.com/stretchr/testify/assert"
11 )
12
13 // TestGatewayLimits tests the gateway request limiting and timeout features.
14 // These are basic integration tests that verify the configuration works.
15 // For comprehensive tests, see:
16 // - github.com/ipfs/boxo/gateway/middleware_retrieval_timeout_test.go
17 // - github.com/ipfs/boxo/gateway/middleware_ratelimit_test.go
18 func TestGatewayLimits(t *testing.T) {
19 t.Parallel()
20
21 t.Run("RetrievalTimeout", func(t *testing.T) {
22 t.Parallel()
23
24 // Create a node with a short retrieval timeout
25 node := harness.NewT(t).NewNode().Init()
26 node.UpdateConfig(func(cfg *config.Config) {
27 // Set a 1 second timeout for retrieval
28 cfg.Gateway.RetrievalTimeout = config.NewOptionalDuration(1 * time.Second)
29 })
30 node.StartDaemon()
31 defer node.StopDaemon()
32
33 // Add content that can be retrieved quickly
34 cid := node.IPFSAddStr("test content")
35
36 client := node.GatewayClient()
37
38 // Normal request should succeed (content is local)
39 resp := client.Get("/ipfs/" + cid)
40 assert.Equal(t, http.StatusOK, resp.StatusCode)
41 assert.Equal(t, "test content", resp.Body)
42
43 // Request for non-existent content should timeout
44 // Using a CID that has no providers (generated with ipfs add -n)
45 nonExistentCID := "bafkreif6lrhgz3fpiwypdk65qrqiey7svgpggruhbylrgv32l3izkqpsc4"
46
47 // Create a client with longer timeout than the gateway's retrieval timeout
48 // to ensure we get the gateway's 504 response
49 clientWithTimeout := &harness.HTTPClient{
50 Client: &http.Client{
51 Timeout: 5 * time.Second,
52 },
53 BaseURL: client.BaseURL,
54 }
55
56 resp = clientWithTimeout.Get("/ipfs/" + nonExistentCID)
57 assert.Equal(t, http.StatusGatewayTimeout, resp.StatusCode, "Expected 504 Gateway Timeout for stuck retrieval")
58 assert.Contains(t, resp.Body, "Unable to retrieve content within timeout period")
59 })
60
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
102 t.Run("MaxConcurrentRequests", func(t *testing.T) {
103 t.Parallel()
104
105 // Create a node with a low concurrent request limit
106 node := harness.NewT(t).NewNode().Init()
107 node.UpdateConfig(func(cfg *config.Config) {
108 // Allow only 1 concurrent request to make test deterministic
109 cfg.Gateway.MaxConcurrentRequests = config.NewOptionalInteger(1)
110 // Set retrieval timeout so blocking requests don't hang forever
111 cfg.Gateway.RetrievalTimeout = config.NewOptionalDuration(2 * time.Second)
112 })
113 node.StartDaemon()
114 defer node.StopDaemon()
115
116 // Add some content - use a non-existent CID that will block during retrieval
117 // to ensure we can control timing
118 blockingCID := "bafkreif6lrhgz3fpiwypdk65qrqiey7svgpggruhbylrgv32l3izkqpsc4"
119 normalCID := node.IPFSAddStr("test content for concurrent request limiting")
120
121 client := node.GatewayClient()
122
123 // First, verify single request succeeds
124 resp := client.Get("/ipfs/" + normalCID)
125 assert.Equal(t, http.StatusOK, resp.StatusCode)
126
127 // Now test deterministic 429 response:
128 // Start a blocking request that will occupy the single slot,
129 // then make another request that MUST get 429
130
131 blockingStarted := make(chan bool)
132 blockingDone := make(chan bool)
133
134 // Start a request that will block (searching for non-existent content)
135 go func() {
136 blockingStarted <- true
137 // This will block until timeout looking for providers
138 client.Get("/ipfs/" + blockingCID)
139 blockingDone <- true
140 }()
141
142 // Wait for blocking request to start and occupy the slot
143 <-blockingStarted
144 time.Sleep(1 * time.Second) // Ensure it has acquired the semaphore
145
146 // This request MUST get 429 because the slot is occupied
147 resp = client.Get("/ipfs/" + normalCID + "?must-get-429=true")
148 assert.Equal(t, http.StatusTooManyRequests, resp.StatusCode, "Second request must get 429 when slot is occupied")
149
150 // Verify 429 response headers
151 retryAfter := resp.Headers.Get("Retry-After")
152 assert.NotEmpty(t, retryAfter, "Retry-After header must be set on 429 response")
153 assert.Equal(t, "60", retryAfter, "Retry-After must be 60 seconds")
154
155 cacheControl := resp.Headers.Get("Cache-Control")
156 assert.Equal(t, "no-store", cacheControl, "Cache-Control must be no-store on 429 response")
157
158 assert.Contains(t, resp.Body, "Too many requests", "429 response must contain error message")
159
160 // Clean up: wait for blocking request to timeout (it will timeout due to gateway retrieval timeout)
161 select {
162 case <-blockingDone:
163 // Good, it completed
164 case <-time.After(10 * time.Second):
165 // Give it more time if needed
166 }
167
168 // Wait a bit more to ensure slot is fully released
169 time.Sleep(1 * time.Second)
170
171 // After blocking request completes, new request should succeed
172 resp = client.Get("/ipfs/" + normalCID + "?after-limit-cleared=true")
173 assert.Equal(t, http.StatusOK, resp.StatusCode, "Request must succeed after slot is freed")
174 })
175 }