Skip to content

Commit ebec679

Browse files
Merge origin/master into cf/staging-image-publish (post-wave-6)
2 parents 379c28b + c63951a commit ebec679

2 files changed

Lines changed: 80 additions & 0 deletions

File tree

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// cors_maxage_test.go — pins BUG-API-303 (QA 2026-05-29): the CORS
2+
// preflight response must carry Access-Control-Max-Age so browsers cache
3+
// the preflight result instead of re-issuing one before every CORS
4+
// request. Without this, an SPA making 5 cross-origin API calls fires 5
5+
// extra preflight roundtrips.
6+
//
7+
// We reconstruct just the fiberCORS middleware exactly as router.New
8+
// configures it (same allow-origins / methods / headers / max-age) and
9+
// drive a single OPTIONS preflight through it. The assertion is the live
10+
// Access-Control-Max-Age header on the response.
11+
12+
package router_test
13+
14+
import (
15+
"net/http/httptest"
16+
"testing"
17+
18+
"github.com/gofiber/fiber/v2"
19+
fiberCORS "github.com/gofiber/fiber/v2/middleware/cors"
20+
"github.com/stretchr/testify/assert"
21+
"github.com/stretchr/testify/require"
22+
)
23+
24+
// TestCORSPreflight_HasMaxAgeHeader pins BUG-API-303: the fiberCORS
25+
// preflight response on any cross-origin OPTIONS must carry
26+
// Access-Control-Max-Age=86400 so browsers (and cooperative proxies)
27+
// cache the preflight result.
28+
//
29+
// Mirrors the production fiberCORS config in router.New verbatim. A
30+
// future router.New edit that drops MaxAge regresses BUG-API-303 and
31+
// fails this test.
32+
func TestCORSPreflight_HasMaxAgeHeader(t *testing.T) {
33+
const (
34+
corsAllowOrigins = "https://instanode.dev,https://www.instanode.dev"
35+
corsAllowMethods = "GET,POST,PUT,PATCH,DELETE,OPTIONS"
36+
corsAllowHeaders = "Content-Type,Authorization,X-Request-ID,X-E2E-Test-Token,X-E2E-Source-IP"
37+
corsMaxAgeSeconds = 86400
38+
)
39+
40+
app := fiber.New()
41+
app.Use(fiberCORS.New(fiberCORS.Config{
42+
AllowOrigins: corsAllowOrigins,
43+
AllowMethods: corsAllowMethods,
44+
AllowHeaders: corsAllowHeaders,
45+
ExposeHeaders: "X-Request-ID,X-Instant-Upgrade,X-Instant-Notice",
46+
MaxAge: corsMaxAgeSeconds,
47+
}))
48+
app.Get("/api/v1/whoami", func(c *fiber.Ctx) error { return c.JSON(fiber.Map{"ok": true}) })
49+
50+
req := httptest.NewRequest("OPTIONS", "/api/v1/whoami", nil)
51+
req.Header.Set("Origin", "https://instanode.dev")
52+
req.Header.Set("Access-Control-Request-Method", "GET")
53+
req.Header.Set("Access-Control-Request-Headers", "Content-Type")
54+
55+
resp, err := app.Test(req, -1)
56+
require.NoError(t, err)
57+
defer resp.Body.Close()
58+
59+
// Status — preflight should 204 (or 200) and emit the CORS-allow set.
60+
require.True(t, resp.StatusCode == fiber.StatusNoContent || resp.StatusCode == fiber.StatusOK,
61+
"preflight expected 204/200; got %d", resp.StatusCode)
62+
63+
// BUG-API-303: the Max-Age header is what closes the regression.
64+
maxAge := resp.Header.Get("Access-Control-Max-Age")
65+
assert.Equal(t, "86400", maxAge,
66+
"BUG-API-303: Access-Control-Max-Age must be 86400 (24h) — without it browsers re-preflight every CORS request; got %q", maxAge)
67+
}

internal/router/router.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,10 @@ func NewWithHooks(cfg *config.Config, db *sql.DB, rdb *redis.Client, geoDbs *mid
194194
}
195195
const corsAllowMethods = "GET,POST,PUT,PATCH,DELETE,OPTIONS"
196196
const corsAllowHeaders = "Content-Type,Authorization,X-Request-ID,X-E2E-Test-Token,X-E2E-Source-IP"
197+
// corsMaxAgeSeconds — 24h preflight cache (Firefox/Safari upper bound;
198+
// Chrome will clamp to 2h regardless). BUG-API-303 (QA 2026-05-29):
199+
// without this value the browser re-preflights every CORS request.
200+
const corsMaxAgeSeconds = 86400
197201
// BUG-API-066/067: Fiber's CORS middleware sets Access-Control-Allow-*
198202
// headers but does NOT validate the inbound preflight request — a
199203
// browser asking for TRACE or Cookie still gets a 204 even though
@@ -210,6 +214,15 @@ func NewWithHooks(cfg *config.Config, db *sql.DB, rdb *redis.Client, geoDbs *mid
210214
AllowMethods: corsAllowMethods,
211215
AllowHeaders: corsAllowHeaders,
212216
ExposeHeaders: "X-Request-ID,X-Instant-Upgrade,X-Instant-Notice",
217+
// BUG-API-303 (QA 2026-05-29): without Access-Control-Max-Age the
218+
// browser re-issues an OPTIONS preflight before every CORS request.
219+
// 24h (corsMaxAgeSeconds) is the modern browsers' clamp ceiling —
220+
// Chrome caps at 2h, Firefox 24h, Safari 7d, so the practical
221+
// effect is per-browser but we ask for the maximum standard value
222+
// so cooperative agents (and reverse proxies) cache for the longest
223+
// period. Pairs with the Vary: Origin header already emitted to
224+
// keep per-origin caches safe.
225+
MaxAge: corsMaxAgeSeconds,
213226
}))
214227
app.Use(middleware.GeoEnrich(geoDbs))
215228
app.Use(middleware.Fingerprint())

0 commit comments

Comments
 (0)