Skip to content

fix(utils.url): keep the hash out of the query in withQuery - #615

Open
vidigoat wants to merge 1 commit into
unjs:mainfrom
vidigoat:fix-with-query-hash
Open

fix(utils.url): keep the hash out of the query in withQuery#615
vidigoat wants to merge 1 commit into
unjs:mainfrom
vidigoat:fix-with-query-hash

Conversation

@vidigoat

@vidigoat vidigoat commented Jul 26, 2026

Copy link
Copy Markdown

withQuery treats the whole input as path[?query], so a URL that carries a fragment ends up mangled. The hash is the last component of a URL, so the query has to be spliced in before it.

There are two failure modes, both reachable through $fetch since fetch.ts calls withQuery(context.request, context.options.query) for any string request:

When the URL has no ?, the added parameters are appended after the # and become part of the fragment, so they are never sent. When the URL already has a ?, everything after it (including the fragment) is parsed as the query string, so the last existing parameter's value silently absorbs #section as %23section.

This is a regression against v1, which used ufo's withQuery. Minimal reproduction against the published packages:

import { createServer } from "node:http";
import { $fetch } from "ofetch";            // 2.0.0-alpha.3
import { $fetch as $fetch1 } from "ofetch1"; // 1.5.1

const server = createServer((req, res) => {
  res.setHeader("content-type", "application/json");
  res.end(JSON.stringify({ seen: req.url }));
}).listen(0);
const base = `http://localhost:${server.address().port}`;

console.log(await $fetch(`${base}/api#section`, { query: { foo: "1" } }));
console.log(await $fetch1(`${base}/api#section`, { query: { foo: "1" } }));
console.log(await $fetch(`${base}/api?a=1#section`, { query: { b: "2" } }));
console.log(await $fetch1(`${base}/api?a=1#section`, { query: { b: "2" } }));

server.close();
2.0.0-alpha.3: { seen: '/api' }              <- foo=1 lost
1.5.1        : { seen: '/api?foo=1' }
2.0.0-alpha.3: { seen: '/api?a=1%23section&b=2' } <- a corrupted
1.5.1        : { seen: '/api?a=1&b=2' }

The fix splits the fragment off before touching the query and re-appends it at the end. I checked the patched withQuery against ufo@1.6.4 over the hash edge cases (no hash, empty hash, hash before/after an existing query, hash-only input, query that resolves to empty) and the output matches.

The regression test goes through the existing /echo route, which reports pathname + search, so it asserts what the server actually receives rather than what the helper returns; the last assertion uses an onResponse hook to check that the fragment itself survives in the final request URL. It fails on main with expected '/echo' to equal '/echo?foo=1'.

pnpm lint and vitest run both pass

Summary by CodeRabbit

  • Bug Fixes

    • Preserved URL hash fragments when adding, updating, or removing query parameters.
    • Ensured query parameters are placed before the fragment, maintaining correct request URLs.
  • Tests

    • Added coverage for query handling on URLs with hash fragments.

The hash is the last component of a URL, so query parameters have to be
inserted before it. `withQuery` did not account for it, which either
dropped the added parameters into the fragment or folded the fragment
into an existing parameter's value.
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 66478816-c088-4bcf-8cb1-e5f0c6d0dda4

📥 Commits

Reviewing files that changed from the base of the PR and between 1dbc37f and 46aafdc.

📒 Files selected for processing (2)
  • src/utils.url.ts
  • test/index.test.ts

📝 Walkthrough

Walkthrough

withQuery now keeps URL fragments separate while adding or updating query parameters, then restores them at the end. A $fetch regression test covers URLs with fragments and existing query parameters.

Changes

URL Fragment Preservation

Layer / File(s) Summary
Fragment-safe query composition
src/utils.url.ts, test/index.test.ts
withQuery processes query parameters before URL fragments and reattaches the fragment in all return paths. $fetch tests verify query merging and request paths containing #hash.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main fix in withQuery: preserving the hash fragment outside the query string.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

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