withastro / astro · Issue No. 18089
Astro v7.3.3
Vite v8.3.0
Node v24.15.0
System macOS (arm64)
Package Manager npm
Output static
Adapter none
Integrations none
Not browser related.
Follow-up to #18078, where I was asked for a bug report with a real use case.
In astro dev, a direct request and a rewrite to the same path resolve to different routes when two dynamic route patterns match that path. The direct request finds the route that owns the path. The rewrite commits to the first pattern match and throws NoMatchingStaticPathFound. astro build and astro preview resolve both correctly.
I maintain Astro-AEO (GitHub, astro-aeo on npm), an integration that serves a Markdown companion for every page: GET /about.md is answered by middleware that calls context.rewrite('/about/') and converts the rendered HTML to Markdown. It uses a rewrite, not an HTTP fetch, so the project's own middleware, authentication, status and cookies apply exactly as they do for the HTML page.
One site using it, zaai.com (Astro 7.2.8, static output, trailingSlash: 'always'), has a common layout: CMS pages in a catch-all next to a yearly blog archive.
src/pages/[...slug].astro // getStaticPaths() -> about, pricing, ...
src/pages/[year]/index.astro // getStaticPaths() -> 2025, 2026
Both routes are prerendered and every URL is owned by exactly one of them. This is a valid project: Astro builds it without warnings and serves every page.
The reproduction contains no integration, only the two routes above and this middleware:
// src/middleware.js
export const onRequest = (context, next) => {
const match = context.url.pathname.match(/^\/(.+)\.md$/);
if (match) return context.rewrite(`/${match[1]}/`);
return next();
};
Run astro dev and request the four URLs below.
| Request | Route selected | Status |
|---|---|---|
GET /2026/ |
[year]/index.astro |
200 |
GET /about/ |
[...slug].astro |
200 |
GET /2026.md (rewrite to /2026/) |
[year]/index.astro |
200 |
GET /about.md (rewrite to /about/) |
[year]/index.astro (wrong) |
500 |
[NoMatchingStaticPathFound] A `getStaticPaths()` route pattern was matched, but no matching
static path was found for requested path `/about/`.
Hint:
Possible dynamic routes being matched: src/pages/[year]/index.astro.
Every page owned by [...slug].astro can be requested directly but cannot be the target of a rewrite, only in development.
/about/ give 200 for a request and 500 for a rewrite. The error says no static path matches /about/, while the server has just rendered that static path.astro build and astro preview select [...slug].astro for the same rewrite. Code that works in production fails only on the developer's machine.route.distURL, which only a build populates, so the accepted fix never runs in astro dev. This report is the uncovered half of that fix, not a new behaviour request./[year] sorts ahead of /[...slug], and its pattern also matches /about/.
matchRoute (packages/astro/src/core/routing/dev.ts). It collects every route whose pattern matches, calls getProps on each, and continues past NoMatchingStaticPathFound until one owns the path.findRouteToRewrite (packages/astro/src/core/routing/rewrite.ts). It takes the first route whose pattern matches. Its only way to skip a route that does not produce the path is the route.distURL comparison from #13697. In astro dev distURL is empty, the check is skipped, and the first match wins.#17778 / #17799 look like the same first-match selection, reached through the i18n fallback and worked around there with a try/catch.
Measured with the same fixture:
| Astro | context.rewrite('/about/') in dev |
|---|---|
| 7.2.8 | throws |
| 7.3.1 | throws |
| 7.3.3 | throws |
main @ db2eaf17c |
throws |
src/pages/[id].astro next to [...slug].astro fails the same way.[...slug].astro as the only dynamic route, the rewrite returns 200 on every version above.trailingSlash: 'ignore' does not change the result.astro build emits /2025/, /2026/, /about/ and /pricing/ correctly.src/pages/blog/[year]/index.astro, so the patterns no longer overlap, makes the rewrite work. That changes public URLs, so a site cannot always apply it.context.rewrite('/about/') resolves to the route that produces /about/ ([...slug].astro) and returns 200: the same route a direct GET /about/ selects in the same dev server, and the same result astro build and astro preview already give.
https://github.com/ZAAI-com/astro-rewrite-repro
Opens in StackBlitz: https://stackblitz.com/github/ZAAI-com/astro-rewrite-repro
main).Relay reads this issue against the repository's contribution signals: the files it is likely to touch, how the maintainers triage work this size, and what the first contribution would exercise.
The full analysis for this issue is still being assembled. Until then, the description above and the thread on GitHub are the most reliable context.