Skip to content
Metric VaultHelp Center
Open app

The worker (_worker.js)

How the single 28,000-line worker is organized - the flat ordered router, where each family of routes sits, the response helpers, CORS, and how errors surface.

Last updated 2026-08-06

Summary#

_worker.js is a single file, 28,268 lines and 1.71 MB, that contains the entire backend: the router, every /api handler, the inlined sign-in page, all the D1 schema creators, the background jobs and the static-asset fallback. There is no framework, no route table and no module system. Routing is a flat, ordered chain of if statements inside one function, and the first match wins.

That design has one property you must hold in your head at all times: order is behavior. Several routes only work because they are checked before a broader one, and at least two blanket checks sit between families of routes.

Purpose#

The file is large on purpose. There is no build step, so there is no way to split the worker across modules without either adding a bundler or paying an import cost at the edge. Keeping one file means one deploy artefact, one place to search when a route misbehaves, and no possibility of a stale bundle.

The flat if chain is a deliberate trade too. A route table would be tidier, but it would hide the ordering that several routes depend on, and it would add an abstraction that nothing else in the codebase uses. What you lose in elegance you gain in being able to read the dispatch order top to bottom.

Architecture#

Entry point#

js
export default {
  async fetch(request, env, ctx) {
    return mvTrackedFetch(request, env, ctx, mvRouteRequest);
  },
  async scheduled(event, env, ctx) { /* four jobs */ }
};

_worker.js:1333-1365. Two things follow:

  • Every request passes through mvTrackedFetch first. The tracker wraps the router so it can observe the real status and duration of the response rather than guessing at the call site. That is what keeps the Library complete: a newly added route is logged without touching its handler.
  • scheduled() exists but Cloudflare Pages never calls it. Cron is a Workers-only feature. The handler is kept so the same code works if it is ever deployed as a Worker, and the jobs self-throttle in the database so it is safe even if both paths fire. See Background jobs and scheduling.

mvRouteRequest dispatch order#

_worker.js:1372-2439. The chain in order:

#StepLine
1new URL(request.url), stash globalThis.__mvEnv = env1373-1376
2OPTIONS short-circuit for every path (CORS preflight)1379-1387
3Billing: /api/create-checkout, /api/stripe/webhook, /api/billing/sync1389-1400
4Blanket diagnostic gate: /api/dbgdfs or any /api/diag/* requires ?key=MV_INTERNAL_SECRET, else 4031405-1410
5Account, billing, library, chat, translate, tier-2, premium-ai, article, cron1413-1498
6Blog, ordered deliberately: /api/blog/site, /api/blog/sites, /api/blog/role, Higgsfield sign-in and callback, /api/blog/media/file/*, /api/blog/public/*, then the authenticated /api/blog/* catch-all1504-1540
7/api/tools1541
8The inline diagnostic handlers1545-1803
9Analyst, brief, originality, factcheck, schedules, opportunities1805-1832
10Google OAuth, social OAuth and publishing, Search Console1834-1876
11Benchmarks, cited sources, extension quickview, usage1878-1891
12All /api/admin/* routes1892-1959
13API keys and the public API /api/v1/analyze1960-1971
14Team, notifications, prefs, branding, rank alerts, editorial, publish, share, workflow1973-2065
15/share/<slug> public HTML2067
16/blog, /blog/<slug> HTML2071-2110
17Responsive analyzer, 8 routes2112-2135
18Competitor monitor, 10 routes2137-2166
19/sitemap.xml, /robots.txt from inline strings2171-2194
20/legal, /compare (301 to /pricing), /login (inlined HTML), logo PNGs2196-2249
21FREE_REDIRECTS (9 permanent redirects), LEGAL_REDIRECTS (7), /ext-preview.html, FREE_PAGES (9 paths)2252-2328
22The env.ASSETS fallback chain2331-2438

Order-dependent pairs to be careful with:

  • The Stripe webhook is routed at step 3, before anything can read the body, because HMAC verification needs the raw body.
  • /api/blog/site and /api/blog/sites must precede the /api/blog/* catch-all at the end of step 6.
  • The diagnostic gate at step 4 sits above the individual diag handlers at step 8, so a handler's own comment claiming it is safe to call unauthenticated is wrong: every diagnostic route requires ?key=.

Handler conventions#

A handler is a plain async function taking some subset of (request, env, ctx, url) and returning a Response. There is no middleware stack; shared behavior is a function you call at the top of the handler. The canonical shape:

js
async function handleSomething(request, env) {
  try {
    const body = await request.json().catch(() => ({}));
    const { user_email } = body;
    const gate = await enforceAiQuota(env, user_email, { cost: 1 });
    if (gate) return gate;                     // gates return a ready Response
    const db = monRequireDB(env);              // throws if MONITOR_DB is unbound
    await ensureSomethingSchema(db);           // lazy schema, see dev-migrations
    // ... work ...
    return new Response(JSON.stringify({ data }), { headers: corsHeaders() });
  } catch (e) {
    return new Response(JSON.stringify({ error: e.message }), { status: 500, headers: corsHeaders() });
  }
}

Three conventions matter:

  1. Gates return a Response or null. enforceAiQuota, requirePlan, enforceAiRecoQuota, mvVerifyInternalSecret and mvAssertPublicUrl all return a ready-made error Response when they refuse and null when they allow. The caller pattern is always if (x) return x;.
  2. Every handler catches its own errors. There is no global error formatter. mvTrackedFetch rethrows anything the router throws, so an uncaught exception produces Cloudflare's own error page rather than a JSON body. Inside a try, return await a promise instead of returning it bare. A bare return somethingAsync() hands back the promise before it settles, so a rejection skips the handler's own catch and becomes that error page. /api/tools dispatched every technical tool that way until 2026-09-14, and a tool that threw showed visitors Cloudflare error 1101 instead of its own message; tests/tools-dispatch-await.mjs holds the fix.
  3. Schema creation is the handler's job. Call the feature's ensure*Schema before the first query. See Schema migrations.

Response helpers#

HelperAddsLine
corsHeaders()Content-Type: application/json, Access-Control-Allow-Origin: *13789-13794
noCacheJsonHeaders()the above plus Cache-Control: no-store, no-cache, must-revalidate, max-age=0, Pragma: no-cache, CDN-Cache-Control: no-store, Cloudflare-CDN-Cache-Control: no-store28182-28191
corsExtensionHeaders()ACAO: *, Allow-Methods: GET, POST, OPTIONS, Allow-Headers: Content-Type, X-Extension-Version16165-16172
monJson(obj, status)Competitor Monitor JSON with corsHeaders()22993-22998
raJson(obj, status)Responsive Analyzer JSON22155-22157
mvbOk(data, status)Blog: a {data} envelope with noCacheJsonHeaders()24188-24190
mvbFail(status, code, message)Blog: {error:{code,message}}, and rewrites 502 to 503 because Cloudflare replaces a Worker 502 body with its own text/plain page24191-24205

CORS#

The only place CORS is negotiated is the OPTIONS short-circuit at the very top of the router:

js
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization'

corsHeaders() on the actual response sets only the origin. Two gaps follow, and both are real:

  • The blog API uses PATCH, PUT and DELETE, and /api/blog/sites uses PATCH, but the preflight never advertises them. Cross-origin blog writes fail preflight. Same-origin calls from dashboard.html are unaffected because they are not preflighted.
  • The custom headers x-mv-user and x-mv-site are not in Allow-Headers, with the same consequence.

Where a response needs a custom header to be readable by the browser, the handler sets Access-Control-Expose-Headers explicitly. The shared-cache wrapper exposes X-MV-Cache, X-MV-Cache-Age-Days and X-MV-Saved-Id.

Components#

The tracking wrapper#

mvTrackedFetch (_worker.js:17789-17878) runs on every request:

  1. Parses the URL. If that fails it routes untouched.
  2. Looks up a route descriptor with mvActRouteFor(pathname, method). If there is none, it routes untouched. Static assets, reads and pollers take this path.
  3. Buffers the request body only when content-type is application/json and the length is at or below MV_ACT_MAX_BODY (24,000 bytes), then rebuilds an identical Request from the same text. Streaming and multipart bodies, such as blog media uploads, are never buffered.
  4. Calls the router, timing it.
  5. Clones the response synchronously if it is an error or a pending-job response, so the background write cannot race the client's own read.
  6. Writes an activity_log row inside ctx.waitUntil.
  7. Rethrows if the router threw, otherwise returns the response.

The whole recording block is wrapped in try {} catch {} because tracking must never break a response. Status mapping is 429 to warning, any other status at or above 400 to failed, a thrown error to failed, otherwise success.

Routes marked aiCovered (/api/premium-ai, /api/tools, /api/analyst, /api/brief, /api/v1/analyze) log only failures, because successes are already logged by incrementUsage. MV_ACT_QUIET lists pure read and poll endpoints that are never logged at all.

Detail capture is a whitelist of body and query keys, not a blocklist. That is what keeps logo data URLs, generated HTML, webhook URLs and API keys out of the Library by construction rather than by filtering.

The SSRF guard#

mvAssertPublicUrl(rawUrl) (_worker.js:13859-13865) returns null when a URL is safe to fetch, or a ready 400 Response:

ConditionBody
Unparseable{"error":"Invalid URL"}
Protocol not http or https{"error":"Only http(s) URLs are allowed"}
Blocked host{"error":"That host is not allowed"}

mvHostIsBlocked blocks localhost, *.localhost, *.local, *.internal, metadata, metadata.google.internal, the IPv6 loopback and link-local ranges, IPv4-mapped IPv6, and after decoding dotted, decimal, octal, hex and 32-bit integer forms, anything in 0/8, 127/8, 10/8, 169.254/16, 172.16/12, 192.168/16, 100.64/10, 192.0.0.0/24 and everything at or above 224. Malformed input is blocked.

Warning

Warning: DNS rebinding is not caught. A public hostname that resolves to a private address passes this guard. That limitation is stated in the code.

Call sites: /api/tools, /api/monitor/add (the URL and both webhook URLs), blog media import, and the responsive proxy and asset routes.

The inlined login page#

GET /login and GET /login.html never touch env.ASSETS. The worker returns LOGIN_HTML, a template literal defined at _worker.js:75 and roughly 1,258 lines long, with Content-Type: text/html; charset=utf-8, Cache-Control: no-cache, no-store, must-revalidate, Pragma: no-cache and Expires: 0. It was added to dodge a Pages caching bug that served stale or zero-byte HTML.

Important

Important: editing login.html alone changes nothing in production. Run node tests/login-inline-sync.mjs --write and commit both files. The no-argument form is a drift check that exits 1.

The static fallback#

Described in full in System architecture. In short: fetch through env.ASSETS, return non-empty 200s, remap clean URLs to .html for document routes, then serve the branded 404 for extension-less GET paths.

Data flow#

For a metered tool call the worker does the following, in this order:

  1. mvTrackedFetch buffers the body and starts the timer.
  2. mvRouteRequest matches the path.
  3. For /api/premium-ai the match calls mvSaveAroundPremiumAI, the shared-cache wrapper, not the handler directly.
  4. The wrapper reads mv_seo_cache. A fresh hit still runs the quota gate and still charges the credit, then returns the stored body with X-MV-Cache: hit and X-MV-Cache-Age-Days.
  5. On a miss, handlePremiumAI runs the per-tool kill switch (platform_config key tool_off:<type>), the recommendation gate for result_guide, enforceAiQuota, then incrementUsage.
  6. Provider calls run: DataForSEO through callDataForSEOCached, then the LLM through the configured base URL.
  7. The response is written through to mv_seo_cache and saved to tool_results.
  8. mvBumpMetrics records into metrics_hourly; a failure also calls mvRecordError.
  9. mvTrackedFetch writes the activity row in the background and returns.

Every gate, with its exact status and body, is in Request lifecycle.

Failure modes#

FailureWhat the caller seesWhy
Uncaught exception in a handlerCloudflare's own error page, no JSONmvTrackedFetch rethrows and there is no global error formatter
MONITOR_DB unbound500 with the literal string MONITOR_DB not bound. Wrangler may need a redeploy.monRequireDB throws that message and some handlers surface it directly
A route added below a broader matchThe broader handler answers insteadFirst match wins in a flat chain
A path listed in _routes.json excludeThe worker never runs for it/blog.html, /logo-dark.png and /logo-light.png are excluded yet still have handlers in the file. Those handlers are unreachable
Cross-origin PATCH/PUT/DELETEPreflight failureThe OPTIONS response advertises only GET, POST, OPTIONS
A Worker 502 from the blog APICloudflare's text/plain page instead of the JSON errorWhich is exactly why mvbFail rewrites 502 to 503
A literal </script> or </body> inside an inline scriptThe page's script parser terminates early and the whole page breaksCaught by node tests/guard.mjs. This killed production on 2026-04-20

Two verification scripts exist specifically for this file:

  • node tests/guard.mjs bans a literal </body, </html or </script inside any inline <script> block in dashboard.html, index.html, admin.html and login.html.
  • node tests/login-inline-sync.mjs proves the inlined LOGIN_HTML still evaluates and still matches login.html.

Both are described in Verification scripts.

See also

Was this article helpful?