TanStack / TanStack/router

Prerender crawler corrupts query strings and loops forever when links carry search params

Open Beginner friendly
#7,837 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

information needed
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:

  1. A page renders an anchor whose href carries a search param, such as /posts?page=2.
  2. The crawler requests withTrailingSlash("/posts?page=2") => /posts?page=2/.
  3. The server parses page=2/ (the trailing slash is now part of the value) and renders a page whose own links serialise page=2%2F.
  4. extractLinks picks 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

  1. Create a TanStack Start app with tanstackStart({ prerender: { enabled: true } }).
  2. 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">).
  3. Run pnpm build.
  4. Observe the prerender log crawling the same path with an ever-growing %2F suffix 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 on main
  • 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.