Skip to content

Fix issue with dynamic routes in complex projects using workerd#16720

Open
thomas-callahan-collibra wants to merge 3 commits into
withastro:mainfrom
thomas-callahan-collibra:tc/feature--fix-node-detection
Open

Fix issue with dynamic routes in complex projects using workerd#16720
thomas-callahan-collibra wants to merge 3 commits into
withastro:mainfrom
thomas-callahan-collibra:tc/feature--fix-node-detection

Conversation

@thomas-callahan-collibra
Copy link
Copy Markdown

@thomas-callahan-collibra thomas-callahan-collibra commented May 12, 2026

Changes

Fixes dynamic routes returning the string [object Object] under the following conditions:

  1. when used with Cloudflare Workers with the nodejs_compat flag. The affected projects all use compatibility dates of 2025-04-01 or newer, not sure if older dates are affected.
  2. only for large, complex projects where Rollup splits code in a way that triggers it. The exact level of complexity/size required is unknown. The fix should work for projects of all sizes though.

This was a very difficult issue to track down. Static routes were fine, and in the Worker logs I could see that the dynamic routes were being processed correctly (I see things like logs indicating success for loading external content and other expected internal logging from the build process) but the returned response was just the string [object Object] regardless of which page or what content was expected -- no error messages of any kind.

I did not open an issue for this because by the time I had enough detail to do so, Claude Code had provided a fix, so I'm just going straight to a PR.

See included Claude Code analysis below for full details.

Testing

I did not add tests because I'm not sure how to provide a sufficiently complicated test project -- this problem only surfaces in larger projects. I'm happy to assist adding tests if I can get some help.

I do have this fix running in 3 separate projects ranging in size and complexity from a stripped down copy of the Astro blog starter template for Workers (where the problem never exhibited but this fix does not cause any issues), up to a 1000-page hybrid site with upwards of 100 Astro components and a mix of dynamic and static routes (where the problem surfaced and the fix works).

Claude Code's explanation of the issue and proposed fix were arrived at after several rounds of analysis and refinements, and its explanation makes sense and matches what I've been experiencing. I specifically asked it to check that the changes do not affect anything for Deno or Node prerendering environments, only workerd.

Docs

I don't believe docs updates are needed because this fixes an issue with expected functionality with no changes needed by the user.

Claude's analysis, RCA, and proposed fix, which I've implemented in this PR

If you're interested, here's what my AI agent found This analysis was done with Astro 6.2.x and Cloudflare 13.3.x, but I've confirmed the problem still exists in the current versions. I'm barred by work policy from installing packages newer than 7 days so I cannot test the latest versions directly, but the problem code still exists in the latest version, unchanged.

This analysis was done by comparing two projects:

  • astro-blog-starter-template which is a copy of the Astro starter for Workers, stripped of all but one dynamic and one static test page
  • web-dev-collibra-astro which is our largest, most complicated project where the problem surfaced

This is its final summary, verbatim:

Fix Summary — Astro isNode false-positive in workerd

Symptom

In an Astro 6.2.x project deployed to Cloudflare Workers via @astrojs/cloudflare 13.3.x with nodejs_compat, any route with prerender: false returns an HTTP 200, content-type: text/html response whose body is the 15-byte string [object Object]. Static / prerendered routes are unaffected. Reproducible locally with astro build && astro preview.

Root cause

astro/dist/runtime/server/render/util.js defines:

const isNode = typeof process !== "undefined"
  && Object.prototype.toString.call(process) === "[object process]";

astro/dist/runtime/server/render/page.js then chooses the response body shape:

if (isNode && !isDeno) {
  body = await renderToAsyncIterable(...);   // Node-only async iterable
} else {
  body = await renderToReadableStream(...);  // standard ReadableStream
}

With nodejs_compat, workerd's process reports Object.prototype.toString.call(process) === "[object process]" until the @cloudflare/unenv-preset polyfill replaces it with a plain object. The output of renderToAsyncIterable is an object exposing Symbol.asyncIterator. Node's undici accepts async iterables as a Response body; workerd's Response does not, and falls back to String(body)"[object Object]".

Why the bug surfaces in some projects but not others

The bug is a module-initialization-order race that only manifests once Rollup decides to code-split Astro's shared runtime.

Both the failing project and the working starter contain the same two pieces of code in the worker bundle:

// A — unenv polyfill assignment (replaces workerd's raw process)
globalThis.process = _process;

// B — Astro's isNode evaluation (reads process)
const isNode = typeof process !== "undefined"
  && Object.prototype.toString.call(process) === "[object process]";

The order in which A and B run depends on whether Rollup keeps them in one chunk or splits them across two:

astro-blog-starter-template (2 pages, almost no Astro internals reused) — Rollup keeps everything in one chunk, worker-entry_DEEjL72T.mjs:

line  558:  globalThis.process = _process;       ← A runs first
line 5864:  const isNode = ...                   ← B sees the unenv polyfill

Single module body, top-down execution → process is the plain-object unenv polyfill by the time isNode runs → isNode = falserenderToReadableStream → works.

web-dev-collibra-astro (many pages and components importing from astro/runtime/server/*) — Rollup factors Astro's shared runtime into its own chunk:

transition_DTdhAtUt.mjs:814    const isNode = ...                ← B
worker-entry_2ArJYird.mjs:1282 globalThis.process = _process;    ← A

worker-entry imports transition. ES Module spec executes imported module bodies before the importer's body, so B runs before A. isNode is evaluated against workerd's raw nodejs_compat process, latches to true, and renderToAsyncIterable is selected → broken response.

The threshold is whatever pushes Rollup over its code-splitting heuristic for shared dependencies. Any project with enough reuse of astro/runtime/server/* across pages or components will eventually trip it — it's not specific to Vue, custom integrations, or middleware.

I confirmed module init order is the differentiator by logging Object.prototype.toString.call(process) at module init vs. request time in the failing project: "[object process]" at init, "[object Object]" at request time — proving the polyfill assignment happens between the two, after isNode has already been frozen.

The fix

One line in packages/astro/src/runtime/server/render/util.ts:

const isNode = typeof process !== "undefined"
  && Object.prototype.toString.call(process) === "[object process]"
  && !(typeof navigator !== "undefined" && navigator.userAgent === "Cloudflare-Workers");

Workerd's navigator.userAgent === "Cloudflare-Workers" is documented, stable, and present from t=0 — so the check is immune to the module-init-order race that broke the original process-only detection.

Behavior matrix

Environment process toString navigator.userAgent isNode before isNode after Behavior change
Node.js "[object process]" undefined / Node ua true true None
Deno "[object process]" (Node compat shim) "Deno/x.y.z" true true None — also gated by !isDeno in renderPage
Browser undefined various, never "Cloudflare-Workers" false false None
Cloudflare Workers (workerd) without nodejs_compat undefined "Cloudflare-Workers" false false None
Cloudflare Workers (workerd) with nodejs_compat, polyfill already installed before isNode eval "[object Object]" "Cloudflare-Workers" false false None (incidentally-correct case — the starter)
Cloudflare Workers (workerd) with nodejs_compat, polyfill not yet installed "[object process]" "Cloudflare-Workers" true (wrong) false (correct) Bug fixed

The change only flips behavior in the row where the original heuristic is wrong.

Cross-checks against the prerender flow

@astrojs/cloudflare exposes prerenderEnvironment: "node" | "workerd" (default "workerd").

  • prerenderEnvironment: "node": prerender runs in real Node. No navigator global, original isNode = true preserved. Same as before. ✓
  • prerenderEnvironment: "workerd" (default): prerender runs in workerd. New check correctly returns false. The renderToReadableStream path produces a ReadableStream body, which Astro's prerender pipeline consumes the same way it consumes an async iterable. ✓

Verification performed

  1. Reproduced locally on web-dev-collibra-astro with astro build + astro previewcurl /dev/dynamic returns [object Object] (15 bytes).
  2. Confirmed the path by logging isNode, streaming, and body.constructor.name inside the bundled renderPageisNode=true, body constructor is Object (the async iterable), not ReadableStream.
  3. Confirmed module-init-order theory by logging Object.prototype.toString.call(process) both at module init ("[object process]") and at request time ("[object Object]") — proves the polyfill assignment happens between the two.
  4. Confirmed astro-blog-starter-template evaluates isNode = false because the unenv polyfill assignment (line 558) precedes the isNode evaluation (line 5864) in the same chunk.
  5. Applied the patch via build-scripts/patch-astro-renderer.mjs; re-ran preview; dynamic route returns <!DOCTYPE html>...<div>TEST</div>, static route unchanged.
Details

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 12, 2026

🦋 Changeset detected

Latest commit: 1a86c67

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 421 packages
Name Type
astro Patch
@e2e/astro-linked-lib Patch
@e2e/actions-blog Patch
@e2e/actions-react-19 Patch
@e2e/astro-component Patch
@e2e/astro-envs Patch
@e2e/astro-island-hydration-error Patch
@test/astro-cloudflare-node-prerender-mdx Patch
@test/astro-cloudflare Patch
@e2e/content-collections Patch
@e2e/csp-server-islands Patch
@e2e/css Patch
@test/custom-client-directives Patch
@e2e/dev-toolbar Patch
@e2e/error-cyclic Patch
@e2e/error-sass Patch
@e2e/errors Patch
@e2e/hydration-race Patch
@e2e/i18n Patch
@test/nested-style-bug-e22e Patch
@e2e/preact-compat-component Patch
@e2e/preact-component Patch
@e2e/preact-lazy-component Patch
@e2e/prefetch Patch
@e2e/react-component Patch
@e2e/server-islands-key Patch
@e2e/server-islands Patch
@e2e/solid-circular Patch
@e2e/solid-component Patch
@e2e/solid-recurse Patch
@e2e/svelte-component Patch
@e2e/e2e-tailwindcss Patch
@e2e/ts-resolution Patch
@e2e/view-transitions Patch
@e2e/vite-virtual-modules Patch
@e2e/vue-component Patch
@performance/md Patch
@performance/mdoc Patch
@performance/mdx Patch
@test/0-css Patch
fake-astro-library Patch
@test/actions Patch
@test/alias-path-alias-style Patch
@test/ts-paths-no-baseurl Patch
@test/aliases-tsconfig Patch
@test/aliases Patch
@test/api-routes Patch
@test/asset-query-params-chunks Patch
@test/asset-url-base Patch
@test/astro-pages Patch
@test/astro-assets-dir Patch
@test/astro-assets-prefix Patch
@test/astro-assets Patch
@test/astro-basic Patch
@test/astro-check-errors Patch
@test/astro-check-no-errors Patch
@test/astro-check-watch Patch
@test/astro-children Patch
@test/astro-client-only Patch
@test/astro-component-bundling Patch
@test/astro-component-code Patch
@test/astro-cookies Patch
@test/astro-css-bundling Patch
@test/astro-dev-headers Patch
@test/astro-dev-http2 Patch
@test/astro-directives Patch
@test/astro-doctype Patch
@test/astro-dynamic Patch
@test/astro-env-content-collections Patch
@test/astro-env-required-public Patch
@test/astro-env-server-fail Patch
@test/astro-env-server-secret Patch
@test/astro-env Patch
@test/astro-envs Patch
@test/astro-expr Patch
@test/astro-get-static-paths Patch
@test/astro-global Patch
@test/astro-head Patch
@test/astro-jsx Patch
@test/astro-manifest-client-script Patch
@test/astro-manifest-invalid Patch
@test/astro-manifest Patch
@test/astro-markdown-frontmatter-injection Patch
@test/astro-markdown-plugins Patch
@test/astro-markdown-remarkRehype Patch
@test/astro-markdown-skiki-default-color Patch
@test/astro-markdown-skiki-langs Patch
@test/astro-markdown-skiki-themes-custom Patch
@test/astro-markdown-skiki-themes-integrated Patch
@test/astro-markdown-skiki-wrap-false Patch
@test/astro-markdown-skiki-wrap-null Patch
@test/astro-markdown-skiki-wrap-true Patch
@test/astro-markdown-url Patch
@test/astro-markdown Patch
@test/astro-mode Patch
@test/astro-not-response Patch
@test/astro-page-directory-url Patch
@test/astro-partial-html Patch
@test/astro-preview-allowed-hosts Patch
@test/astro-preview-headers Patch
@test/astro-public Patch
@test/astro-script-template-dedup Patch
@test/astro-scripts Patch
@test/astro-slots-nested Patch
@test/astro-slots Patch
@test/concurrency Patch
@test/build-readonly-file Patch
@test/cache-memory-query-include Patch
@test/cache-memory-query Patch
@test/cache-memory Patch
@test/cache-route Patch
@test/client-address-node Patch
@test/client-address Patch
@test/code-component Patch
@test/component-library Patch
@test/config-vite-css-target Patch
@test/config-vite Patch
@test/react-container Patch
@test/content-with-spaces-in-folder-name Patch
@test/content-collection-picture-render Patch
@example/content-collection-references Patch
@test/content-collection-tla-svg Patch
@test/content-collections-base Patch
@test/content-collections-empty-dir Patch
@test/content-collections-empty-md-file Patch
@test/content-collections-mutation Patch
@test/content-collections-number-id Patch
@test/content-collections-type-inference Patch
@test/content-collections-with-config-mjs Patch
@test/content-collections Patch
@test/content-frontmatter Patch
@test/content-intellisense Patch
@test/content-layer-loader-schema-function Patch
@test/content-layer-remark-plugins Patch
@test/content-layer Patch
@test/content-ssr-integration Patch
@test/content-static-paths-integration Patch
@test/content Patch
@test/core-image-data-url Patch
@test/core-image-deletion-ssr Patch
@test/core-image-deletion Patch
@test/core-image-errors Patch
@test/core-image-fs-config Patch
@test/core-image-remark-infersize Patch
@test/core-image-layout Patch
@test/core-image-picture-emit-file Patch
@test/core-image-remark-imgattr Patch
@test/core-image-ssg Patch
@test/core-image-ssr Patch
@test/core-image-svg-in-client Patch
@test/core-image-svg Patch
@test/core-image-unconventional-settings Patch
@test/core-image Patch
@test/csp-adapter Patch
@test/csp-fonts Patch
@test/csp Patch
@test/css-assets Patch
@test/css-dangling-references Patch
@test/css-deduplication Patch
@test/css-double-bundle Patch
@test/css-dynamic-import-dev Patch
@test/css-import-as-inline Patch
@test/css-inline-stylesheets Patch
@test/css-no-code-split Patch
@test/custom-404-injected-from-dep Patch
@test/custom-404-pkg Patch
@test/custom-500-failing Patch
@test/custom-500 Patch
@test/custom-assets-name Patch
custom-fetch-error-pages Patch
@test/custom-renderer Patch
@test/data-collections-schema Patch
@test/data-collections Patch
@test/debug-component Patch
@test/dev-container Patch
@test/dev-render Patch
@test/dev-request-url Patch
@test/dynamic-endpoint-collision Patch
@test/dynamic-route-build-file Patch
@test/endpoint-routing Patch
@test/entry-file-names Patch
@test/error-bad-js Patch
@test/error-build-location Patch
@test/error-non-error Patch
@test/extension-matching Patch
@test/fetch Patch
@test/fonts Patch
@test/astro-fontsource-package Patch
@test/get-static-paths-pages Patch
@test/glob-pages-css Patch
@test/head-propagation-prerender-env Patch
@test/hmr-markdown Patch
@test/hmr-new-page Patch
@test/hmr-slots-render Patch
@test/hoisted-imports Patch
@test/html-component Patch
@test/html-escape Patch
@test/html-page Patch
@test/html-slots Patch
@test/hydration-race Patch
@test/i18n-css-leak-basic Patch
@test/import-ts-with-js Patch
@test/impostor-md-file Patch
@test/integration-add-page-extension Patch
@test/integration-server-setup Patch
@test/jsx-queue-rendering Patch
@test/large-array-solid Patch
@test/legacy-collections-backwards-compat Patch
@test/lightningcss-scoped-nesting Patch
@test/live-loaders Patch
@test/markdown Patch
@test/middleware-dev Patch
@test/middleware-full-ssr Patch
@test/middleware-no-user-middlewaqre Patch
@test/middleware-sequence-rewrite Patch
@test/middleware-tailwind Patch
@test/minification-html-jsx Patch
@test/minification-html Patch
@test/non-ascii-path Patch
@test/non-html-pages Patch
@test/page-format Patch
@test/page-level-styles Patch
@test/parallel-components Patch
@test/partials-css-boundary Patch
@test/partials Patch
@test/passthrough-image-service Patch
@test/postcss Patch
@test/preact-compat-component Patch
@test/preact-component Patch
@test/queue-rendering Patch
@test/remote-css Patch
@test/request-signal Patch
@test/reuse-injected-entrypoint Patch
@test/rewrite-runtime-error-custom500 Patch
@test/rewrite-runtime-error Patch
@test/reroute-server Patch
@test/rewrite-trailing-slash-never Patch
@test/rewrite-with-base Patch
@test/root-srcdir-css Patch
@test/scoped-style-strategy Patch
@test/server-entry-fake-adapter Patch
@test/server-entry Patch
@test/server-islands-hybrid Patch
@test/server-islands-ssr Patch
@test/sessions Patch
@test/slots-preact Patch
@test/slots-react Patch
@test/slots-solid Patch
@test/slots-svelte Patch
@test/slots-vue Patch
@test/solid-component Patch
@test/sourcemap Patch
@test/space-in-folder-name Patch
@test/special-chars-in-component-imports Patch
@test/ssr-api-route Patch
@test/ssr-assets Patch
@test/ssr-dynamic Patch
@test/ssr-partytown Patch
@test/ssr-prerender-get-static-paths Patch
@test/ssr-prerender Patch
@test/ssr-preview Patch
@test/ssr-renderers-static-vue Patch
@test/ssr-request Patch
@test/ssr-hoisted-script Patch
@test/ssr-scripts Patch
@test/static-build-code-component Patch
@test/static-build-dir Patch
@test/static-build-frameworks Patch
@test/static-build-page-url-format Patch
@test/static-build-ssr Patch
@test/static-build Patch
@test/static-redirect Patch
@test/streaming Patch
@test/svelte-component Patch
@test/svg-deduplication Patch
@test/tailwindcss Patch
@e2e/third-party-astro Patch
@test/url-import-suffix Patch
@test/view-transitions Patch
@test/virtual-astro-file Patch
@test/vitest Patch
@test/vue-component Patch
@test/vue-with-multi-renderer Patch
@test/db-aliases Patch
@test/db-db-in-src Patch
@test/error-handling Patch
@test/db-integration-only Patch
@test/db-integration Patch
@test/db-libsql-remote Patch
@test/db-local-prod Patch
@test/db-no-apptoken Patch
@test/db-no-seed Patch
@test/recipes Patch
@test/db-static-remote Patch
eventbrite-from-scratch Patch
@test/alpinejs-basics Patch
@test/alpinejs-directive Patch
@test/alpinejs-plugin-script-import Patch
@test/astro-cloudflare-allowed-hosts Patch
@test/astro-cloudflare-astro-dev-platform Patch
@test/astro-cloudflare-astro-env Patch
@test/astro-cloudflare-binding-image-service Patch
@test/astro-cloudflare-cache-provider-wait-until Patch
@test/astro-cloudflare-client-address Patch
@test/astro-cloudflare-compile-image-service Patch
@test/astro-cloudflare-custom-entryfile Patch
@test/astro-cloudflare-dev-image-endpoint Patch
@test/astro-cloudflare-external-image-service Patch
@test/astro-cloudflare-external-redirects Patch
@test/astro-cloudflare-internal-redirects Patch
@test/astro-cloudflare-no-output Patch
@test/astro-cloudflare-prerender-node-env Patch
@test/astro-cloudflare-prerender-queue-consumers Patch
@test/astro-cloudflare-prerender-styles Patch
@test/astro-cloudflare-prerenderer-errors Patch
@test/routing-priority-cloudflare Patch
@test/cf-server-entry Patch
@test/astro-cloudflare-server-island-prerender-framework Patch
@test/astro-cloudflare-sql-import Patch
@test/cf-ssr-deps Patch
@test/astro-cloudflare-static Patch
@test/astro-cloudflare-svelte-rune-deps Patch
@test/astro-cloudflare-top-level-return Patch
@test/astro-cloudflare-vite-plugin Patch
@test/astro-cloudflare-with-base Patch
@test/astro-cloudflare-with-react Patch
@test/astro-cloudflare-with-solid-js Patch
@test/astro-cloudflare-with-svelte Patch
@test/astro-cloudflare-with-vue Patch
@test/astro-cloudflare-wrangler-preview-platform Patch
@test/markdoc-content-collections Patch
@test/content-layer-markdoc Patch
@test/headings-custom Patch
@test/headings Patch
@test/image-assets-custom Patch
@test/image-assets Patch
@test/markdoc-propagated-assets Patch
@test/markdoc-render-with-space Patch
@test/markdoc-render-html Patch
@test/markdoc-render-null Patch
@test/markdoc-render-partials Patch
@test/markdoc-render-simple Patch
@test/markdoc-render-table-attrs Patch
@test/markdoc-render-typographer Patch
@test/markdoc-render-with-components Patch
@test/markdoc-render-with-config Patch
@test/markdoc-render-with-extends-components Patch
@test/markdoc-render-with-indented-components Patch
@test/markdoc-render-with-transform Patch
@test/markdoc-variables Patch
@test/content-layer-rendering Patch
@test/mdx-css-head-mdx Patch
@test/image-remark-imgattr Patch
@test/mdx-astro-container-escape Patch
@test/mdx-frontmatter-injection Patch
@test/netlify-skew-protection Patch
@test/netlify-hosted-astro-project Patch
@test/nodejs-api-route Patch
@test/nodejs-badurls Patch
@test/nodejs-encoded Patch
@test/nodejs-errors Patch
@test/nodejs-headers Patch
@test/nodejs-image Patch
@test/locals Patch
@test/node-middleware Patch
@test/nodejs-prerender-404-500 Patch
@test/nodejs-prerender Patch
@test/nodejs-prerendered-error-page-fetch Patch
@test/nodejs-preview-headers Patch
@test/redirects Patch
@test/node-sessions Patch
@test/ssr-assets-middleware Patch
@test/node-static-headers Patch
@test/node-trailingslash Patch
@test/url Patch
@test/well-known-locations Patch
@test/react-component Patch
@test/sitemap-chunks Patch
@test/sitemap-dynamic Patch
@test/sitemap-i18n-fallback Patch
@test/sitemap-ssr Patch
@test/sitemap-static Patch
@test/sitemap-trailing-slash Patch
async-rendering Patch
conditional-rendering Patch
@test/empty-class Patch
svelte-prop-types Patch
@test/astro-vercel-basic Patch
@test/astro-vercel-image Patch
@test/astro-vercel-integration-assets Patch
@test/vercel-isr Patch
@test/vercel-max-duration Patch
@test/vercel-edge-middleware-with-edge-file Patch
@test/vercel-edge-middleware-without-edge-file Patch
@test/astro-vercel-no-output Patch
@test/astro-vercel-prerendered-error-pages Patch
@test/astro-vercel-redirects-serverless Patch
@test/astro-vercel-redirects Patch
@test/vercel-server-islands Patch
@test/astro-vercel-serverless-prerender Patch
@test/astro-vercel-serverless-with-dynamic-routes Patch
@test/astro-vercel-static-assets Patch
@test/vercel-static-headers Patch
@test/astro-vercel-static Patch
@test/vercel-streaming Patch
@test/astro-vercel-with-web-analytics-enabled-output-as-static Patch
vercel-hosted-astro-project Patch
@test/vue-app-entrypoint-async Patch
@test/vue-app-entrypoint-css Patch
@test/vue-app-entrypoint-no-export-default Patch
@test/vue-app-entrypoint-relative Patch
@test/vue-app-entrypoint-src-absolute Patch
@test/vue-app-entrypoint Patch
@test/vue-basics Patch
vue-prop-types Patch
astro-benchmark Patch
@benchmark/adapter Patch
@benchmark/timer Patch
benchmark-build-hybrid Patch
benchmark-build-server Patch
benchmark-build-static Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions Bot added the pkg: astro Related to the core `astro` package (scope) label May 12, 2026
@ematipico
Copy link
Copy Markdown
Member

You burned tons of tokens to emit this dissertation for a mere 3 lines of code, and you didn't even have the decency to test the changes yourself (the app doesn't build).

🤦

@thomas-callahan-collibra
Copy link
Copy Markdown
Author

A ton of tokens maybe, but also almost an entire working day trying to track this issue down on my own. Does it matter that it's 3 lines of code? It did build locally, but I dropped an export statement by accident when cleaning up. Sorry for the inconvenience. It's fixed now, the build has completed, waiting for tests to complete.

This kind of response is why people hesitate to try to contribute to OS. I'm trying to help fix a bug in your project that has cost me considerable time and effort (on top of another bug I reported and helped test a week or two ago). I'm doing my best with a project whose code I don't know and a very complex build/CI setup. Your own project guidelines say:

We welcome contributions of any size and skill level. As an open source project, we believe in giving back to our contributors and are happy to help with guidance on PRs, technical writing, and turning any feature idea into a reality.

If there's anything still not right with this PR, I'm happy to help further if you have any additional constructive feedback.

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 13, 2026

Merging this PR will not alter performance

✅ 18 untouched benchmarks


Comparing thomas-callahan-collibra:tc/feature--fix-node-detection (1a86c67) with main (9446049)

Open in CodSpeed

@ematipico
Copy link
Copy Markdown
Member

ematipico commented May 13, 2026

Not all contributions are the same. Here's why I think this contribution triggered me on a personal level and I found it not respectful towards me:

  • It deviated from the requested template by adding a wall of text generated from a tool.
  • The wall of text is too much to read, and you didn't hide it. You could have hidden and say "If you're interested, here's what my AI agent found" and then a maintainer opens it if they deem it useful.
  • You didn't explain the solution of the PR. I don't care what the tool did, I'm interested to the what the you think, not what a tool did. These tools aren't perfect and can make mistakes.
  • You claim that you tested the change, but you didn't explain how, if you didn't compile the code.
  • More than once you say "Claude fix", so I infer that you don't understand the fix and can't explain it.

I am not keen in accepting a fix if the contributor can't explain it.

@matt-trem
Copy link
Copy Markdown

As I read it (and it was not that long of a read), "Claude fix" => here's what's happening does not mean he's not understanding it, on the contrary. It's clear he understands it and it's a legit bug. And instead of tangling up Enterprise support, he's contributing a fix for your benefit, the project, and its owner.

And you talk of decency. I hate to quote that guy but did you even say thank you once?

@ematipico
Copy link
Copy Markdown
Member

And you talk of decency. I hate to quote that guy but did you even say thank you once?

#16619 (review)

@thomas-callahan-collibra
Copy link
Copy Markdown
Author

thomas-callahan-collibra commented May 13, 2026

  1. It does follow the requested template, but I added additional documentation. I've never once had someone complain about something being over-documented.

  2. Point taken, fine, hidden.

  3. Believe me I know they're not perfect and can make mistakes. I've been very slow to start using them myself because of that. In this case after spending literally hours between two members of my team trying to find the problem in our own projects' code, I had it do multiple analyses of builds in different projects, and it found this problem not in my code, but Astro's, and based the solution on things it found in the source of workerd itself. This is not the kind of deep analysis of multiple unrelated codebases that I as a user could reasonably do on my own, and frankly I'm not sure how any one human could. Once it found the solution, the first fix wasn't perfect -- I pointed out the mistakes and it revised. It took three revisions like that to get to a solution that works for Node, Deno, and workerd. No, I as an end user don't have time or resources to stand up multiple test projects including multiple hosting providers to verify every path myself, but I'll point back to the contributor guidelines where it says "help can be provided for those".

I'll also point out that every existing test passes, the only untested path is a project sufficiently complex to test the specific issue this fixes, which I can't imagine how you would do in a CI test runner. What are you going to do, set up a project with dozens of components and a bunch of imported libs just for a CI test?

  1. Again I did compile the code but removed a comment before pushing, mistakenly removed the export statement, and pushed without recompiling. It's literally a typo, not some fundamental problem with the code or project. I was working well beyond my normal working day, pushed the change, submitted the PR, and went offline. Have you never made a simple mistake in a rush? This is a PR after all, that's why there are checks and approvals required, it's not like it went to production. All you had to do was point out that it didn't compile in CI, or I would have noticed myself when I got the failure notice this morning when I came back online.

  2. You infer incorrectly. This isn't just being thrown out there. Astro is currently setting isNode to true in a workerd environment when it should be false if the project is sufficiently complicated to trigger Rollup's code splitting -- the fix alters the calculation of isNode to take into account details from the workerd user agent string to fix the false positive. While no I don't know all of the internal workings of either Astro or workerd, and maybe there's a better way to do this, the changes make sense, and matches with both the source code the bot refers to and behavior I'm seeing in multiple projects both local dev and in CF Workers.

This isn't some AI slop bug report and fix. If you're going to reject it because AI was involved then I don't know what to say. I couldn't have found it without it, and nobody else has apparently found this either. The issue does take a specific set of circumstances to trigger, but it is real.

I opened this other issue a few weeks ago, your own project's AI bot suggested a fix, someone from your team had me test, I confirmed the fix, and the change was merged earlier this week. How is this any different except that I did the analysis on my own and contributed a change rather than just opening an issue? If there is serious doubt from the team that this is correct, then close this and I'll just open an issue while I continue using the post-install patch so I can get this project moving again.

@ematipico
Copy link
Copy Markdown
Member

Regarding the issue

The affected projects all use compatibility dates of 2025-04-01 or newer, not sure if older dates are affected.

I believe we have encountered this issue a few months ago where all our tests started to fail. This was an issue on CF side. Once we they fixed it, everything was green. Is there a particular reason why you need to stick to this compatibility date?

@thomas-callahan-collibra
Copy link
Copy Markdown
Author

No, but the complex project that actually triggers this issue uses 2025-09-15. I said 2025-04-01 or newer because one of the other projects I tested this with uses that date. But there's no specific reason I can't move to a newer date as long as it doesn't break anything. I'll test with the latest in a few minutes.

@thomas-callahan-collibra
Copy link
Copy Markdown
Author

So that is interesting -- the problem does go away with 2026-04-21. I ran a bunch of local builds and was able to narrow it down to 2026-02-10 -- works in that version and newer, fails with older ones.

For our projects, I can bump our compatibility dates (we're migrating from Astro 5->6 so they all have dates older than this), but I'm not sure everybody has that liberty. If that's the fix, it should probably be mentioned in the migration docs though, and maybe the CF adapter docs? I can redo this PR to drop the code change and change it to just a docs update if you want.

I did do some more investigation though. I logged the values that are used by the isNode check and resulting value of isNode through all stages of multiple builds -- once with a "bad" compatibility date and once with a "good" one, first without the patch applied, and then with the patch applied.

Test results
Iteration 1 — compat 2025-09-15, patch DISABLED — ❌ content rendering FAILURE (HTTP 200, 15-byte body [object Object])
Phase stringified-process userAgent isNode
build [object process] Node.js/24 true
prerender [object Object] Cloudflare-Workers false
prerender [object process] Cloudflare-Workers true
runtime access (preview start, Node) [object process] Node.js/24 true
runtime access (request, workerd) [object process] Cloudflare-Workers true
Iteration 2 — compat 2026-04-21, patch DISABLED — ✅ content rendered correctly (HTTP 200, 1.15 MB HTML)
Phase stringified-process userAgent isNode
build [object process] Node.js/24 true
prerender [object process] Cloudflare-Workers true
prerender [object process] Cloudflare-Workers true
runtime access (preview start, Node) [object process] Node.js/24 true
runtime access (request, workerd) [object process] Cloudflare-Workers true
Iteration 3 — compat 2025-09-15, patch ENABLED — ✅ content rendered correctly (HTTP 200, 1.15 MB HTML)
Phase stringified-process userAgent isNode
build [object process] Node.js/24 true
prerender [object Object] Cloudflare-Workers false
prerender [object process] Cloudflare-Workers false
runtime access (preview start, Node) [object process] Node.js/24 true
runtime access (request, workerd) [object process] Cloudflare-Workers false
Iteration 4 — compat 2026-04-21, patch ENABLED — ✅ content rendered correctly (HTTP 200, 1.15 MB HTML)
Phase stringified-process userAgent isNode
build [object process] Node.js/24 true
prerender [object process] Cloudflare-Workers false
prerender [object process] Cloudflare-Workers false
runtime access (preview start, Node) [object process] Node.js/24 true
runtime access (request, workerd) [object process] Cloudflare-Workers false

I can see the difference that results in this problem -- when using a "bad" compatibility date without the patch applied, there are two instances where isNode is set to true even though it's running workerd.

This is also true when the compatibilty date is "good" -- in fact it ALWAYS evaluates to true, but workerd seems to be able to handle being treated like Node now.

With the patch applied, isNode is always accurate to the environment. This might no longer be necessary because of changes to workerd, but it is technically correct and works for all compatibility dates.

But assuming you want to maintain compatibility with older workerd dates, updating the isNode check is still correct. In fact it probably isn't necessary to check the string value of Object.prototype.toString.call(process) at all. A revised version of this check that would work regardless of runtime could rely solely on the userAgent string, like this:

export const isNode = typeof process !== "undefined" && !(typeof navigator !== "undefined" && navigator.userAgent === "Cloudflare-Workers");

or just in case the string changes slightly in the future?

export const isNode = typeof process !== "undefined" && !(typeof navigator !== "undefined" && navigator.userAgent?.toLowerCase().includes("cloudflare"));

I wouldn't ordinarily rely on a userAgent string because of its history of unreliability in browsers, but in workerd I'd expect it to be stable and reliable.

I don't love the double negative logic either, but flipping that would mean changing the variable name to isWorker or something, which would require more changes elsewhere.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pkg: astro Related to the core `astro` package (scope)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants