Summary
convertToQueryString in core/routing/util.ts rebuilds the request query string with raw template concatenation (${key}=${value}), but the request converters feed it already-decoded query values (via getQueryFromSearchParams). When a query value contains a character that is significant in a URL — most visibly #, but also &, +, = — the reconstructed req.url is corrupted.
For #, the decoded value becomes a URL fragment and everything after it is dropped, so the param is lost entirely.
Root cause
- Converter decodes values:
core/overrides/converters/edge.js → getQueryFromSearchParams(new URL(event.url).searchParams) yields decoded values, e.g. q = "#sinners".
requestHandler rebuilds the URL from those decoded values:
// core/requestHandler.ts (setInitialURL override)
req.url = initialURL.pathname + convertToQueryString(routingResult.internalEvent.query);
convertToQueryString does not encode:
queryStrings.push(`${key}=${value}`); // → "q=#sinners"
- The route then does
new URL(req.url) → #sinners is treated as a fragment → searchParams.get("q") === "".
The function comment says "query should be properly encoded before using this function", but the converter hands it decoded values — an internal contract mismatch.
Reproduction
App with a route that reads new URL(request.url).searchParams.get("q"), request:
GET /api/search?q=%23sinners
- Expected:
q === "#sinners"
- Actual:
q === "" (route sees ?q=)
Also reproduces with q=rock%20%26%20roll → route sees q = "rock " (truncated at &).
Reproduces on the OpenNext Cloudflare adapter (@opennextjs/cloudflare) and is present in current main. Tested on @opennextjs/aws@4.0.2.
Suggested fix
Encode keys/values in convertToQueryString (or keep values encoded through the converter):
queryStrings.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
Environment
@opennextjs/cloudflare 1.19.x, @opennextjs/aws 4.0.2
- Next.js 15.5.x, Cloudflare Workers
Summary
convertToQueryStringincore/routing/util.tsrebuilds the request query string with raw template concatenation (${key}=${value}), but the request converters feed it already-decoded query values (viagetQueryFromSearchParams). When a query value contains a character that is significant in a URL — most visibly#, but also&,+,=— the reconstructedreq.urlis corrupted.For
#, the decoded value becomes a URL fragment and everything after it is dropped, so the param is lost entirely.Root cause
core/overrides/converters/edge.js→getQueryFromSearchParams(new URL(event.url).searchParams)yields decoded values, e.g.q = "#sinners".requestHandlerrebuilds the URL from those decoded values:convertToQueryStringdoes not encode:new URL(req.url)→#sinnersis treated as a fragment →searchParams.get("q") === "".The function comment says "query should be properly encoded before using this function", but the converter hands it decoded values — an internal contract mismatch.
Reproduction
App with a route that reads
new URL(request.url).searchParams.get("q"), request:q === "#sinners"q === ""(route sees?q=)Also reproduces with
q=rock%20%26%20roll→ route seesq = "rock "(truncated at&).Reproduces on the OpenNext Cloudflare adapter (
@opennextjs/cloudflare) and is present in currentmain. Tested on@opennextjs/aws@4.0.2.Suggested fix
Encode keys/values in
convertToQueryString(or keep values encoded through the converter):Environment
@opennextjs/cloudflare1.19.x,@opennextjs/aws4.0.2