Skip to content

Commit 79595e6

Browse files
Merge pull request #194 from 0xPolygon/fix/cors-preflight-worker
fix(deploy): answer CORS preflight OPTIONS from a worker script
2 parents a57a5e4 + a2729c3 commit 79595e6

8 files changed

Lines changed: 90 additions & 15 deletions

File tree

CLAUDE.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@ Two public surfaces driven from one source JSON tree at the repo-root
2626
the same JSON files served verbatim from Cloudflare Workers static
2727
assets. `scripts/assemble-cdn.sh` (`pnpm run build:cdn`) stages
2828
`network/`, `index.html`, and `public/_headers` into `dist/`, which
29-
`wrangler.toml` serves directly (no worker script). CORS and caching
30-
live in `public/_headers`.
29+
`wrangler.toml` serves directly. `worker/worker.ts` answers CORS
30+
preflight OPTIONS (the asset layer serves GET/HEAD only and 405s
31+
everything else, which blocks maticjs in browsers); response CORS
32+
and caching for asset hits live in `public/_headers`.
3133

3234
`deploy.yml` is trunk-based: every push to `master` deploys
3335
**staging** (`static-cf.polygon.technology`, a `custom_domain` on a

eslint.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ import { recommended, typescript } from '@polygonlabs/apps-team-lint';
44

55
export default defineConfig([
66
...recommended({ globals: 'node' }),
7-
...typescript(),
7+
...typescript({ tsconfigRootDir: import.meta.dirname }),
88
{ ignores: ['.claude/**', '**/dist/**', 'packages/meta/src/generated/**'] }
99
]);

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"devDependencies": {
3333
"@changesets/changelog-github": "^0.7.0",
3434
"@changesets/cli": "^2.31.0",
35+
"@cloudflare/workers-types": "^4.20260702.1",
3536
"@commitlint/cli": "^19.0.0",
3637
"@commitlint/config-conventional": "^19.0.0",
3738
"@polygonlabs/apps-team-lint": "^2.0.0",

pnpm-lock.yaml

Lines changed: 11 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

public/_headers

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1-
# Cloudflare static-assets header rules.
1+
# Cloudflare static-assets header rules — applied to asset (GET/HEAD)
2+
# responses only. Preflight OPTIONS never reaches the asset layer; it is
3+
# answered by worker/worker.ts, which owns the Allow-Headers/Max-Age surface.
24
#
3-
# This is a read-only public store, so the CORS surface reflects what it
4-
# actually does: cross-origin GETs of JSON. The previous nginx origin also
5-
# advertised POST and Authorization/Content-Type, but nothing here accepts a
6-
# write or reads credentials — that was cargo-cult and is dropped.
5+
# The nginx origin's Authorization/Content-Type allow-header was NOT
6+
# cargo-cult (an earlier revision dropped it as such): maticjs sends
7+
# Content-Type on its GETs, which makes browsers preflight, and the missing
8+
# preflight handling broke every browser consumer of the SDK. Allow-headers
9+
# only matter on the preflight response, so they live in the worker, not here.
710
#
811
# Cache-Control is new: the store is fetched uncached at every maticjs client
912
# init, so a short shared-cache TTL cuts repeated edge/origin load without
1013
# risking staleness for the rare network/ABI additions.
1114
/*
1215
Access-Control-Allow-Origin: *
13-
Access-Control-Allow-Methods: GET, OPTIONS
16+
Access-Control-Allow-Methods: GET, HEAD, OPTIONS
1417
Cache-Control: public, max-age=300

worker/tsconfig.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"compilerOptions": {
3+
"target": "esnext",
4+
"module": "esnext",
5+
"moduleResolution": "bundler",
6+
"lib": ["esnext"],
7+
"types": ["@cloudflare/workers-types"],
8+
"strict": true,
9+
"noEmit": true,
10+
"noUncheckedSideEffectImports": true
11+
},
12+
"include": ["worker.ts"]
13+
}

worker/worker.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// CORS preflight shim in front of the static-assets bundle.
2+
//
3+
// Cloudflare's asset layer serves GET/HEAD only; every other method routes to
4+
// the worker script — or returns 405 when none exists. That 405 broke maticjs
5+
// in every browser: it sends Content-Type on its GET of
6+
// /network/<network>/<version>/index.json, browsers therefore preflight with
7+
// OPTIONS, and a failed preflight blocks the GET entirely. This worker answers
8+
// OPTIONS and delegates everything else to the asset layer. run_worker_first
9+
// is required (see wrangler.toml): with default routing the asset layer 405s
10+
// OPTIONS on asset-matched paths itself and the worker never sees it.
11+
interface Env {
12+
ASSETS: Fetcher;
13+
}
14+
15+
export default {
16+
async fetch(request, env): Promise<Response> {
17+
if (request.method === 'OPTIONS') {
18+
return new Response(null, {
19+
status: 204,
20+
headers: {
21+
'Access-Control-Allow-Origin': '*',
22+
'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS',
23+
// Content-Type is what maticjs sends (see above); Authorization
24+
// restores the old nginx origin's allowance for clients that
25+
// attach it.
26+
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
27+
'Access-Control-Max-Age': '86400'
28+
}
29+
});
30+
}
31+
// Everything else defers to the asset layer, which applies _headers and
32+
// not_found_handling as if the worker weren't here.
33+
return env.ASSETS.fetch(request);
34+
}
35+
} satisfies ExportedHandler<Env>;

wrangler.toml

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
# Cloudflare Workers static-assets deployment for the static.polygon.technology
2-
# HTTP endpoint. No worker script — Cloudflare serves the assembled dist/ bundle
3-
# (network/ JSON tree + health-check index.html + _headers) directly from its
4-
# edge. Replaces the previous nginx-on-ECS origin; the npm package
5-
# (@polygonlabs/meta, packages/meta/) is a separate surface and unaffected.
2+
# HTTP endpoint. Cloudflare serves the assembled dist/ bundle (network/ JSON
3+
# tree + health-check index.html + _headers) directly from its edge; the tiny
4+
# worker script exists only because the asset layer serves GET/HEAD and 405s
5+
# every other method — including the OPTIONS preflights browsers send for
6+
# maticjs's Content-Type-bearing GETs (see worker/worker.ts). Replaces the
7+
# previous nginx-on-ECS origin; the npm package (@polygonlabs/meta,
8+
# packages/meta/) is a separate surface and unaffected.
69
name = "polygon-static"
10+
main = "worker/worker.ts"
711
compatibility_date = "2026-06-24"
812
placement = { mode = "smart" }
913
workers_dev = false
@@ -15,6 +19,14 @@ directory = "./dist"
1519
# Missing paths return a real 404 (a JSON CDN, not an SPA). The old nginx
1620
# try_files fell back to index.html, masking 404s as 200+HTML.
1721
not_found_handling = "none"
22+
# Lets the worker delegate non-OPTIONS requests back to the asset layer.
23+
binding = "ASSETS"
24+
# Required for the worker to see OPTIONS at all: without it the asset layer
25+
# answers non-GET/HEAD on asset-matched paths itself (405) — verified with
26+
# `wrangler dev`; the docs' "other methods route to the Worker" doesn't hold
27+
# for matched paths. GET/HEAD delegated via the binding still get _headers
28+
# and not_found_handling applied.
29+
run_worker_first = true
1830

1931
[build]
2032
command = "pnpm run build:cdn"

0 commit comments

Comments
 (0)