← Back
deployment blocker seeded

A markdown helper passes CI then crashes only inside the edge runtime

July 9, 2026

Symptom

The Worker deploys cleanly and the health check stays green, but the first request to `/legal/terms` returns HTTP 500 in production. Local Node.js tests and CI pass. Cloudflare logs show `Error: No such module "fs"` from a dependency path that is never exercised by the public-route smoke tests.

Root Cause

The route imported `gray-matter` safely, but the deployed-only code path called `matter.read("./content/terms.md")`. That helper lazily reaches for Node's `fs.readFileSync` at request time instead of during module import, so Vite, Wrangler bundling, and CI all completed. The Cloudflare Workers isolate was running without the `nodejs_compat` compatibility flag, and the runtime had no `fs` binding, so the first request that tried to read the Markdown file threw `Error: No such module "fs"` after the bundle was already live.

Diagnosis Steps

  1. Open Cloudflare Workers Logs for the failing request and copy the first runtime exception, including the module name in `Error: No such module "fs"`.
  2. Search the bundled route for `matter.read`, `readFileSync`, `require("fs")`, and `node:fs` rather than only checking top-level imports.
  3. Run the exact failing route under `wrangler dev --remote` so the request executes in a Workers-like isolate instead of plain Node.js.
  4. Inspect `wrangler.toml` for `compatibility_flags = ["nodejs_compat"]` and record the configured `compatibility_date` before changing anything.
  5. Replace the file-reading helper with a string-based parse path and redeploy a preview Worker, then hit `/legal/terms` before promoting it.

Fix

Stopped calling `gray-matter`'s Node file helper in request handling. The Markdown is now imported as bundled raw text during build and passed to `matter(rawTermsMarkdown)`, so parsing still uses `gray-matter` but file I/O never happens inside the Worker. The team also added a preview check that runs the legal route with `wrangler dev --remote`; `nodejs_compat` is reserved for audited packages instead of being used as a blanket workaround for accidental Node APIs.

Prevention

Run edge-targeted smoke tests for every deployed route, ban `fs`, `node:fs`, `process.nextTick`, and Node `crypto` imports from Worker bundles unless explicitly approved, and prefer Web APIs or build-time asset imports for edge code.

Stack

Cloudflare Workers Wrangler 3 Vite 5 Node.js 20 gray-matter 4.0.3 Miniflare 3

Tags

deployment edge-runtime cloudflare-workers node-compat fs

Date

July 9, 2026

Error: No such module "fs" appeared in Cloudflare Workers Logs for the first real request to /legal/terms, which returned HTTP 500. Local Node.js runs never produced the same clue:

GET /legal/terms -> 500
Uncaught Error: No such module "fs"
  at node_modules/gray-matter/lib/utils.js
  at renderTermsPage

The misleading part was that nothing failed early. npm test ran on Node.js 20. The CI smoke test touched /health and /api/session, neither of which rendered Markdown. wrangler deploy accepted the bundle because the bad API was behind a helper call, not a top-level import that the bundler had to execute.

The production-only request did this:

const page = matter.read("./content/terms.md");

That line is convenient in a server process with a disk. In a Worker isolate, it asks a package to cross the edge boundary and open a local file at request time. There is no local project tree there, and in this deployment there was not even a Node fs module because compatibility_flags = ["nodejs_compat"] was absent.

Adding nodejs_compat would have changed the error shape, but it would not have made request-time disk reads a good deployment contract. The durable fix was to move the file boundary to build time:

import rawTermsMarkdown from "../content/terms.md?raw";

const page = matter(rawTermsMarkdown);

After that change, the Worker receives ordinary JavaScript text in its bundle. gray-matter still parses frontmatter, but the edge runtime no longer has to pretend to be a Node server with a filesystem.