Skip to content

fix: serve dotted paths through the Netlify edge function - #244

Closed
rturnq wants to merge 10 commits into
mainfrom
claude/agent-feedback-triage-hy7p57
Closed

fix: serve dotted paths through the Netlify edge function#244
rturnq wants to merge 10 commits into
mainfrom
claude/agent-feedback-triage-hy7p57

Conversation

@rturnq

@rturnq rturnq commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Description

@marko/run-adapter-netlify: the default edge entry declared config = { pattern: "^[^.]*$" }, so Netlify only invoked the function for URLs with zero dots. Any dotted URL bypassed the app entirely and 404'd from the static CDN — a catch-all serving report.2024.pdf, a $handle segment holding jane.doe, a handler emitting .xml — even though the router matches those paths in dev, on the node adapter, and on the functions adapter.

That pattern was a crude "is this a file?" heuristic, standing in for the fact that an edge function runs before Netlify's static handling. Instead of heuristics, the adapter now declares the app's actual routes:

{ "path": ["/", "/reports/*"], "excludedPath": ["/assets/*"] }

routesGenerated converts the route table into Netlify path declarations ($name:name, $$rest*, plus a trailing-slash variant per route so the trailingSlashes option still sees those requests). The function then runs only for paths the app serves, and everything else — published files included — stays with Netlify's own static handling. The handler is back to its original one-liner: route, and fall through on a decline.

Route paths reach the entry through a virtual module served by the adapter's Vite plugin. The CLI evaluates the Vite config to resolve the adapter and the build evaluates it again, so the instance contributing that plugin is not the one receiving routesGenerated; the declaration is passed between them keyed by root, and the loader awaits a this.load of the router so the route build has definitely run.

Motivation and Context

Dynamic routes containing dots silently 404 in production on Netlify edge while working on every other target.

Two behavior consequences, both documented in the adapter README:

  • A catch-all route claims published files under it. /reports/* matches /reports/pinned.txt, and the function runs before static handling, so the route wins. A handler can throw null to hand such a path back to Netlify — the fixture covers this. The build's assets directory is excluded outright, since a root catch-all would otherwise shadow /assets/* and break hydration; that is the case the old dot heuristic was quietly protecting.
  • Unmatched paths get Netlify's 404, not the app's +404 page, because they never reach the app. Publishing a 404.html customizes that.

Testing: the edge preview needs the Netlify CLI and Deno, so the netlify-adapter-edge-paths fixture serves the built dist/index.mjs through a local stand-in for the platform (same custom-adapter pattern as the adapter-is-entry fixture) that honors path/pattern/excludedPath the way Netlify does. It covers dotted catch-all routing, a published file outside any route, an extensionless published file, the excluded assets path, POST routing, and the decline-to-platform fallthrough. Reverting the entry fails the preview run while dev still passes — the same divergence the bug describes.

Screenshots (if appropriate):

Checklist:

  • I have updated/added documentation affected by my changes.
  • I have added tests to cover my changes.

@changeset-bot

changeset-bot Bot commented Aug 7, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 75f0500

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

This PR includes changesets to release 1 package
Name Type
@marko/run-adapter-netlify 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

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

The Netlify edge entry now matches all paths, checks static files for GET and HEAD requests, and routes dotted paths through the application router with 404 fallback. A new fixture validates static-file precedence, dotted catch-all routing, POST handling, and unmatched-path responses. Documentation and release metadata describe the updated behavior.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 63.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: serving dotted paths through the Netlify edge function.
Description check ✅ Passed The description directly explains the Netlify routing fix, implementation, documentation, tests, and motivation.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/agent-feedback-triage-hy7p57

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/run/src/__tests__/fixtures/netlify-adapter-edge-paths/test.config.ts (1)

30-38: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Cover the untested routing branches.

Add a +404 fixture page and assert its rendered content for an unmatched dotted URL. The current assertion only proves that the fallback response has status 404.

Add HEAD static-file coverage and POST routing coverage. The current steps only exercise GET, so they do not verify the separate GET/HEAD static branch or router-first behavior for other methods.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/run/src/__tests__/fixtures/netlify-adapter-edge-paths/test.config.ts`
around lines 30 - 38, Add a +404 fixture page and update unmatchedPathIs404 to
request an unmatched dotted URL and assert both the 404 status and rendered
fallback content. Extend the steps around staticFileWins and dottedPathRoutes
with HEAD static-file coverage and POST routing coverage, ensuring the
assertions verify the separate GET/HEAD static behavior and router-first
handling for non-GET methods.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@packages/run/src/__tests__/fixtures/netlify-adapter-edge-paths/test.config.ts`:
- Around line 30-38: Add a +404 fixture page and update unmatchedPathIs404 to
request an unmatched dotted URL and assert both the 404 status and rendered
fallback content. Extend the steps around staticFileWins and dottedPathRoutes
with HEAD static-file coverage and POST routing coverage, ensuring the
assertions verify the separate GET/HEAD static behavior and router-first
handling for non-GET methods.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7e33c849-25ba-41eb-97a8-17dabdc54297

📥 Commits

Reviewing files that changed from the base of the PR and between a387c7b and 55aa2ac.

⛔ Files ignored due to path filters (2)
  • packages/run/src/__tests__/fixtures/netlify-adapter-edge-paths/__snapshots__/dev.expected.md is excluded by !**/__snapshots__/** and included by **
  • packages/run/src/__tests__/fixtures/netlify-adapter-edge-paths/__snapshots__/preview.expected.md is excluded by !**/__snapshots__/** and included by **
📒 Files selected for processing (8)
  • packages/run/src/__tests__/fixtures/netlify-adapter-edge-paths/.marko-run/routes.d.ts
  • packages/run/src/__tests__/fixtures/netlify-adapter-edge-paths/previewAdapter.ts
  • packages/run/src/__tests__/fixtures/netlify-adapter-edge-paths/public/reports/pinned.txt
  • packages/run/src/__tests__/fixtures/netlify-adapter-edge-paths/src/routes/+page.marko
  • packages/run/src/__tests__/fixtures/netlify-adapter-edge-paths/src/routes/reports/$$rest/+page.marko
  • packages/run/src/__tests__/fixtures/netlify-adapter-edge-paths/test.config.ts
  • packages/run/src/__tests__/fixtures/netlify-adapter-edge-paths/tsconfig.json
  • packages/run/src/__tests__/fixtures/netlify-adapter-edge-paths/vite.config.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/adapters/netlify/src/default-edge-entry.ts`:
- Around line 19-23: Update the fallback logic near eagerFallback so
context.next() is used whenever fetch returns no response, regardless of method,
while restricting the response.status === 404 fallback to GET and HEAD requests
through mayBeStatic. Add a regression test covering a declined non-GET/HEAD
request and verify it returns the CDN response rather than the synthetic 404.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1ba986e6-3ae2-4e22-99c9-37db50a6d98f

📥 Commits

Reviewing files that changed from the base of the PR and between 55aa2ac and 1302f70.

⛔ Files ignored due to path filters (2)
  • packages/run/src/__tests__/fixtures/netlify-adapter-edge-paths/__snapshots__/dev.expected.md is excluded by !**/__snapshots__/** and included by **
  • packages/run/src/__tests__/fixtures/netlify-adapter-edge-paths/__snapshots__/preview.expected.md is excluded by !**/__snapshots__/** and included by **
📒 Files selected for processing (8)
  • .changeset/eight-frogs-report.md
  • packages/adapters/netlify/README.md
  • packages/adapters/netlify/src/default-edge-entry.ts
  • packages/run/src/__tests__/fixtures/netlify-adapter-edge-paths/.marko-run/routes.d.ts
  • packages/run/src/__tests__/fixtures/netlify-adapter-edge-paths/public/notes
  • packages/run/src/__tests__/fixtures/netlify-adapter-edge-paths/src/routes/+404.marko
  • packages/run/src/__tests__/fixtures/netlify-adapter-edge-paths/src/routes/reports/$$rest/+handler.ts
  • packages/run/src/__tests__/fixtures/netlify-adapter-edge-paths/test.config.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • .changeset/eight-frogs-report.md
  • packages/adapters/netlify/README.md

Comment thread packages/adapters/netlify/src/default-edge-entry.ts Outdated
@rturnq
rturnq force-pushed the claude/agent-feedback-triage-hy7p57 branch from 1302f70 to 1dcd70b Compare August 7, 2026 21:14

rturnq commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

test: node@22 (ubuntu-latest) failed on 1dcd70b, but not on anything in this diff — it timed out in the dev-add-layout fixture from #242 (Timed out waiting for removed layout to stop wrapping the page). node@24 and node@26 on ubuntu passed the same commit in the same run, and this PR only touches the Netlify edge adapter, its new fixture, and cspell.json.

It did not reproduce locally: 12 isolated runs of the fixture, 6 isolated runs under CPU saturation, and 2 full-suite runs under saturation were all green. Since the fixture polls for 8s and never recovered — and the cold add step had already passed in the same run — this looks like permanent staleness rather than slowness, most likely a @marko/vite transform already in flight repopulating cachedSources with the pre-rewrite source after the synthetic invalidation dropped it.

Rather than change plugin.ts inside an unrelated diff, I've recorded it in agent-feedback/bugs.md (e426a88) with the analysis and two suggested fix directions.


Generated by Claude Code

rturnq and others added 10 commits August 10, 2026 23:01
The default edge entry matched only dot-free paths (pattern ^[^.]*$),
so any route whose URL contained a dot -- a catch-all serving
report.2024.pdf, a $handle segment holding jane.doe, a handler emitting
.xml -- never reached the router and 404'd from the static CDN. The
entry now runs on every path: GET/HEAD requests first let Netlify serve
a matching static file (mirroring the functions adapter's preferStatic
and the static-first order of the dev and node targets), then fall back
to the app's router, which also lets the app's +404 page apply to
unmatched paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V888Mxok4otffCprmvyiKU
…er unit test

The adapter package had no test setup of its own; the run package's
fixture harness is where adapter behavior is exercised. The fixture
builds with the netlify edge adapter and previews the built edge
function through a local stand-in for the platform: the function runs
only for the paths its exported config selects, and its next() answers
from the published static dir. That covers the declaration itself, which
a stubbed unit test could not -- reverting the entry fails the fixture's
preview on the dotted path while dev still passes, which is exactly the
divergence the bug described.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V888Mxok4otffCprmvyiKU
Asking Netlify for a static file before routing cost every request an
extra pass through the platform. Only GET/HEAD requests can be answered
by a file, and a request whose path carries no dot is almost never one,
so the lookup now runs up front -- raced against the router, so it adds
no latency -- only for dotted paths, and otherwise waits until the
router has no answer. A published file still wins wherever one exists,
including extensionless files the path gives no hint about.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V888Mxok4otffCprmvyiKU
A handler that declines (throw null) a POST or other non-GET/HEAD
request dead-ended in a synthetic 404 instead of reaching the rest of
the platform chain, which the pre-fix entry forwarded to. The fallback
now asks the platform whenever the router has no response, keeping the
404-triggered retry limited to GET/HEAD. Also anchors the fixture's
static-body assertions with regexes so Windows CRLF checkouts compare
equal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V888Mxok4otffCprmvyiKU
The default export leads the file and helper detail follows, and
comments are trimmed to two lines, matching the conventions AGENTS.md
now records.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V888Mxok4otffCprmvyiKU
The edge entry asked the platform for a static file before routing, so
every request paid an extra pass through the platform and the handler
carried the static-vs-route precedence itself. The adapter now converts
the route table into Netlify path declarations, so the function only
runs for paths the app actually serves and everything else -- published
files included -- stays with the platform's static handling. The
handler goes back to routing and falling through on a decline.

Route paths reach the entry through a virtual module the adapter's
plugin serves. The CLI and the build each evaluate the vite config, so
the adapter instance contributing that plugin is not the one receiving
routesGenerated; the declaration is passed between them keyed by root.

Two behavior notes, both documented in the README: a catch-all route now
claims published files under it (a handler can throw null to hand one
back), and unmatched paths get Netlify's 404 rather than the app's +404
page. The build's assets directory is excluded so a catch-all cannot
claim it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V888Mxok4otffCprmvyiKU
The plugin side is handed the CLI's cwd and the adapter side a path the
plugin already normalized. Those are the same string everywhere but
windows, where the lookup missed and the entry shipped an empty
declaration -- so no path ran the edge function and every route 404'd.
Both sides now derive the key the same way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V888Mxok4otffCprmvyiKU
Declaring every route as a Netlify path declaration meant generating
them at build time, handing them to the entry through a virtual module,
and bridging two adapter instances that the CLI and the build create
separately -- which is also where the windows path-separator bug came
from. Running on every path and asking the platform only once the router
has no answer needs none of that: the entry keeps routing first, and a
404 (or a declined request) is what hands the path to the platform.

The router answers unmatched paths with a truthy 404 response, so a
plain `||` fallthrough never reaches next() and every published file
404s -- the case the old dot pattern was really guarding. Falling
through on the status instead serves those files, and keeping the app's
response when the platform also has nothing preserves its +404 page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V888Mxok4otffCprmvyiKU
Deferred by the maintainer while fixing the Netlify edge adapter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V888Mxok4otffCprmvyiKU
@rturnq
rturnq force-pushed the claude/agent-feedback-triage-hy7p57 branch from 93337fc to 75f0500 Compare August 10, 2026 23:04

rturnq commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Status after the rebase onto cc6098f: 7 of 8 checks green on 75f0500, including all three Windows jobs — everything in this PR's diff passes everywhere. The one red check (test: node@22... node@26 ubuntu) is the dev-add-layout flake from #242 again (Timed out waiting for removed layout to stop wrapping the page, 342 passing around it) — its sixth occurrence, tracked in agent-feedback/bugs.md with analysis and fix directions.


Generated by Claude Code

@rturnq rturnq closed this Aug 11, 2026
rturnq added a commit that referenced this pull request Aug 11, 2026
PR #244 closed without merging, taking two entries that were recorded
on its branch and never rejected: the dev-add-layout CI flake (six
observed occurrences with analysis) and the fetch void/404 routing
design the maintainer deferred. Restored against main's current state;
the netlify dotted-paths entry itself never left main.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V888Mxok4otffCprmvyiKU
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant