Conversation
The per-parameter key pattern [^=]+ requires a non-empty key, so "=b"
parsed as { b: "" } losing the value "b", and "==" / "=" were dropped.
WHATWG URLSearchParams keeps the empty key: "=b" -> { "": "b" }.
Switch the key pattern to [^=]* and skip fully-empty parts explicitly,
which matches URLSearchParams across the matrix ("", "=b", "a=1&=b",
"==", "&", "a", "a=", "&&", "=b&=c").
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthrough
ChangesEmpty key parsing in parseQuery
Estimated code review effort🎯 1 (Trivial) | ⏱️ ~3 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
… + empty-key fixes Adopts three upstream unjs/ufo PRs into the fork: - PR unjs#331 (@saripovdenis) \u2014 parseQuery charCode-scan rewrite, ~40% faster on real-world benchmarks (empty +421%, single pair +72%, mixed +36%, repeated keys +43%, encoded +32%, leading-equals +37%, long +22%). - PR unjs#333 (@saripovdenis) \u2014 stringifyQuery single-pass string build. - PR unjs#289 (@pi0) \u2014 prototype pollution guard for the parsed object. Also fixes issue unjs#355 (empty-key parameter loss). Previous regex `/([^=]+)=?(.*)/` required a non-empty key so `=b` was parsed as `{b: ''}` (silently inventing a key). Now `=b` \u2192 `{'': 'b'}`, matching native `URLSearchParams` iteration output. Prototype-pollution guard tightened: previously blocked `__proto__` and `constructor`; now also blocks `prototype` (aligned with owasp guidance). Regression tests added: - Empty-key preservation (`=b`, `a=1&=b`, `==`) - URLSearchParams parity for values containing `=` - null-prototype return object - Dangerous-key filter including `prototype` Tests: 995 pass + 65 xfail (was 991 + 65).
CREDITS.md itemizes every upstream unjs/ufo PR and issue whose work was adopted by this fork, grouped by category: - Perf: @saripovdenis (unjs#331, unjs#333) - Security: @pi0 (unjs#289), @spokodev (unjs#355) - Encoding: converged 11 PRs from 10 authors into one WHATWG-aligned implementation \u2014 @JvanderHeide, @yyz945947732, @byt3m4st3r, @nianqingrenganmane, @ConnorBerghoffer, @terminalchai (x2), @guoyangzhen, @mvtandas, @armorbreak001, @LeSingh1 - Bug fixes: @sandros94 (unjs#237 approach) - Features: @Thy3634 (unjs#243 proposal) Also documents deliberately-rejected PR unjs#350 (@LeSingh1, protocol- relative in withBase) and the v2-deferred surface (unjs#208 native URL, unjs#189 per-protocol trailing slash, unjs#296 bracket->array parseQuery). README top gets a fork banner linking CREDITS.md and enumerating what this fork adds on top of upstream. Full name change to better-ufo lands in the publish-prep commit at end of series.
…paque schemes) Encoding — WHATWG application/x-www-form-urlencoded compliance - encodeQueryValue/encodeQueryKey: byte-for-byte parity with URLSearchParams.toString(); |, `, ^, @, :, ,, ;, =, ? all encoded (adopts consensus from 11 open upstream PRs: unjs#279 unjs#288 unjs#303 unjs#305 unjs#310 unjs#318 unjs#324 unjs#327 unjs#328 unjs#329 unjs#354) - Single-pass replacer; %20 → + rewrite after encodeURI, before map parseQuery — correctness + security - Single-pass charCode scanner (PR unjs#331 @saripovdenis, ~40% faster) - Empty-key preservation: =value → { "": "value" } (PR unjs#355 @spokodev; was silently losing the value with the old /([^=]+)=?(.*)/ regex) - Proto-pollution guard: __proto__ / constructor / prototype blocked (PR unjs#289 @pi0; extended to cover prototype as well) - stringifyQuery: single-pass builder (PR unjs#333 @saripovdenis) parseURL — opaque-scheme URIs (RFC 3986 §3) - mailto:, tel:, urn:, sms: — scheme NOT followed by // → opaque path surfaced in .pathname; query/fragment parsed from the opaque tail - data:, blob: round-trip correctly (tested against WPT urltestdata.json) parseAuth — RFC 3986 §3.2.1 - Split on FIRST colon only; subsequent colons belong to the password (was splitting on all colons, losing interior password chars) parseHost — IPv6 - Brackets retained on hostname to match WHATWG URL.hostname contract - Malformed unclosed bracket returns input verbatim (no silent truncation) - Non-numeric port suffix detected (was silently truncated) cleanDoubleSlashes — query/fragment protection - Double-slash collapse no longer touches query or fragment sections hasProtocol — single-char scheme rejection - Blocks Windows drive letters (C:) and bare-digit prefixes withBase / withoutBase — fragment-boundary fix - Fragment on input (#hash) no longer defeats 'base already present' check (was producing doubled base path on fragment-suffixed inputs) withFragment — empty hash strips existing fragment WPT urltestdata.json ratchet - 100-case special-scheme subset; 65 known-divergent cases run via it.fails (same count as upstream); ratchet trips if a fix lands
package.json - name: ufo → better-ufo; version: 1.6.4-fork.0 - description, keywords (17), homepage, bugs, repository → fork URLs - author: silverfish2525; contributors: upstream credits pointer - publishConfig: access public + provenance true - files: dist + CREDITS.md + SECURITY.md - packageManager: pnpm@10.33.2 (exact, required by corepack) README.md - Title, install commands, import specifiers → better-ufo - Fork banner: what we add (WHATWG compliance, security guards, literal-preserving TS inference, new APIs, CI gates) - Drop-in pnpm overrides recipe for existing ufo consumers - Badge shields point to npmjs.com/package/better-ufo - License section updated; upstream attribution preserved CREDITS.md - Only lists contributors whose work is in the codebase (every entry has a line-level cite in src/); removed rejected PR unjs#350 and v2- deferred issues that were not adopted - Sections: perf (unjs#331 unjs#333), security (unjs#289 unjs#355), encoding (11 PRs), bug fixes (unjs#237), features (unjs#243) SECURITY.md - GitHub private advisory as primary channel (silverfish2525/ufo) - In-scope / out-of-scope tables; supply-chain note; zero-runtime-dep note; vendored punycode identified as only third-party byte AGENTS.md + CONTRIBUTING.md - Updated for current repo shape: tests/ (not test/), tsdown, .ts scripts, @antfu/eslint-config, upstream-reference convention advisor-plans/ - 15 plan files (001–014, 021) documenting the improvement strategy that produced the security/correctness/type/infra changes above - advisor-plans/README.md index
Problem
parseQuerymatches each parameter with/([^=]+)=?(.*)/. The key sub-pattern[^=]+requires at least one non-=character, so for an empty-key parameter the match anchors past the=:=bparses as{ b: "" }: the real value"b"is silently lost and a spurious key"b"is invented.==and a bare=are dropped entirely.This corrupts
parseQuery,getQuery,filterQuery, andnormalizeURLfor any URL carrying an empty-key parameter.WHATWG
URLSearchParamskeeps the empty key:Fix
Change the key pattern
[^=]+to[^=]*so an empty key matches, and add an explicit skip of fully-empty parts (if (!parameter) continue;) to preserve the current behavior of dropping""and&&(with the star pattern the match no longer returns a length-0 array, so the skip has to be explicit).After the fix,
parseQuerymatchesURLSearchParamsacross the matrix:URLSearchParams""{}{}=b{ b: "" }{ "": "b" }a=1&=b{ a: "1", b: "" }{ a: "1", "": "b" }=={}{ "": "=" }&{}{}a{ a: "" }{ a: "" }a={ a: "" }{ a: "" }&&{}{}=b&=c{ b: "", c: "" }{ "": ["b", "c"] }Tests
Added four empty-key cases to
test/query.test.ts. They fail on the current code (red) and pass with the fix (green); the full suite stays green.Summary by CodeRabbit
Bug Fixes
Tests