Prerender crawler corrupts query strings and loops forever when links carry search params
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 15.1k
- Forks
- 1.9k
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 143
Description
Prerender crawler corrupts query strings and loops forever when links carry search params
Which project does this relate to?
Start
Describe the bug
The static prerender crawler in @tanstack/start-plugin-core applies withTrailingSlash to the entire request path including the query string. withTrailingSlash from ufo, called without its respectQueryAndFragment option, appends the slash to the end of the string, so the slash lands inside the last search-param value instead of on the path.
packages/start-plugin-core/src/prerender.ts (line ~140):
const res = await requestWithRedirects(
withTrailingSlash(withBase(page.path, routerBasePath)),
// ...
)
Demonstration in isolation:
import { withTrailingSlash, withBase } from 'ufo'
withTrailingSlash(withBase('/posts?page=2', '/'))
// => "/posts?page=2/" ❌ slash appended to the query value
withTrailingSlash(withBase('/posts?page=2', '/'), true)
// => "/posts/?page=2" ✅ slash on the path, query preserved
When crawlLinks is enabled (the default), this turns into an infinite loop:
- A page renders an anchor whose href carries a search param, such as
/posts?page=2. - The crawler requests
withTrailingSlash("/posts?page=2")=>/posts?page=2/. - The server parses
page=2/(the trailing slash is now part of the value) and renders a page whose own links serialisepage=2%2F. extractLinkspicks that up, the crawler appends another slash =>page=2%2F%2F, and so on.
Every URL is unique, so the crawler's seen set never dedups. The queue grows without bound and the build hangs until it runs out of memory.
This bites any app that has a search param on a route reachable through a crawled <a href>. A common trigger is a root-level search param such as a currency or locale selector rendered in shared page chrome, so every prerendered page emits a self-link carrying it.
Your Example Website or App
Minimal reproduction: any TanStack Start app with prerender.enabled and a route that renders an <a> whose href includes a query string, such as a <Link to="." search={{ page: 2 }}>.
Steps to Reproduce the Bug or Issue
- Create a TanStack Start app with
tanstackStart({ prerender: { enabled: true } }). - Add a search param to a route and render a
<Link>to that route with the search param set (so the prerendered HTML contains<a href="/route?param=value">). - Run
pnpm build. - Observe the prerender log crawling the same path with an ever-growing
%2Fsuffix on the param value, and the build never completing.
[prerender] Crawling: /route?param=value
[prerender] Crawling: /route?param=value%2F
[prerender] Crawling: /route?param=value%2F%2F
...
Expected behavior
The trailing slash should be applied to the path only, leaving the query string intact:
/route?param=value → /route/?param=value
The crawler should then converge and dedup normally rather than generating an unbounded stream of unique URLs.
Screenshots or Videos
N/A
Platform
- Affected package:
@tanstack/start-plugin-core, reproduced on 1.171.20 with the same code onmain - Reproduced on Linux with Node 24
Additional context
The fix is a one-line change: pass respectQueryAndFragment: true to withTrailingSlash. Happy to open a PR (with a changeset).
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in packages/start-plugin-core/src/prerender.ts around line 140 and inspect how the crawler builds the request URL. Run the reported minimal reproduction with pnpm build, then verify that a query such as /route?param=value becomes /route/?param=value, the crawl deduplicates, and the build completes without growing the parameter suffix.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- build-system
- Issue type
- Bug
- Difficulty
- 1/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 82/100