Skip to content

Fix: getQueryArgs discards everything after the second = in a query argument value - #81066

Merged
hbhalodia merged 9 commits into
WordPress:trunkfrom
hbhalodia:fix/issue-81062
Aug 12, 2026
Merged

Fix: getQueryArgs discards everything after the second = in a query argument value#81066
hbhalodia merged 9 commits into
WordPress:trunkfrom
hbhalodia:fix/issue-81062

Conversation

@hbhalodia

Copy link
Copy Markdown
Contributor

What?

Closes #81062

Why?

  • PR resolve the ambiguity in parsing the URL query param in function getQueryArgs from @wordpress/url package.

How?

  • Update the function to parse the = sign correctly.

Testing Instructions

  1. Open page/post.
  2. Open console and make sure you are on top frame.
  3. In console, run wp.url.getQueryArgs( 'https://example.com/?a=1&b=x=y&c=3' ) or multiple cases mentioned in issue.

Testing Instructions for Keyboard

  • None

Screenshots or screencast

  • None

Use of AI Tools

  • Claude Code Opus 5.
  • Used for implementation draft and final code reviwed by me.

AI Summary

What?

Closes #81062

getQueryArgs() splits each query argument on the first = only. Previously it split on every = and kept just the first two segments, silently discarding everything after the second one.

Why?

packages/url/src/get-query-args.ts parsed each key=value pair with:

const [ key, value = '' ] = keyValue
	.split( '=' )
	.filter( Boolean )
	.map( safeDecodeURIComponent );

'b=x=y'.split( '=' ) yields [ 'b', 'x', 'y' ], and the destructuring reads only the first two entries — so 'y' was dropped. Any value legitimately containing an unencoded = was truncated:

Input Before After
?b=x=y { b: 'x' } { b: 'x=y' }
?token=eyJhbGciOiJIUzI1NiJ9== { token: 'eyJhbGciOiJIUzI1NiJ9' } { token: 'eyJhbGciOiJIUzI1NiJ9==' }
?redirect=/watch?v=abc { redirect: '/watch?v' } { redirect: '/watch?v=abc' }
?=orphan { orphan: '' } {}

The blast radius is wider than one function, because getQueryArgs() is the shared parser:

  • addQueryArgs() merges existing args via getQueryArgs(), so appending a parameter to a URL rewrote the truncated value back out — silent data loss rather than a read-only glitch.
  • @wordpress/api-fetch calls addQueryArgs() from its default user-locale middleware, so every request whose URL carried a = in a value was affected.
  • getQueryArg(), hasQueryArg(), and removeQueryArgs() all delegate to it too.

Base64 (== padding), JWTs, and redirect targets are exactly the values that carry =, so this hit auth tokens and redirect URLs hardest.

The last row is the secondary defect from the issue: .filter( Boolean ) dropped the empty string from [ '', 'orphan' ], leaving [ 'orphan' ], which promoted a keyless value into a key.

How?

Find the first = with indexOf and slice around it, instead of splitting on all of them:

const separatorIndex = keyValue.indexOf( '=' );
const hasValue = separatorIndex !== -1;
const key = safeDecodeURIComponent(
	hasValue ? keyValue.slice( 0, separatorIndex ) : keyValue
);

if ( key ) {
	const value = hasValue
		? safeDecodeURIComponent( keyValue.slice( separatorIndex + 1 ) )
		: '';
	const segments = key.replace( /\]/g, '' ).split( '[' );
	setPath( accumulator, segments, value );
}

Dropping .filter( Boolean ) is what fixes the orphan case: an empty key now reaches the pre-existing if ( key ) guard and the malformed pair is skipped. value moved inside that guard because it is only needed there.

File Change
packages/url/src/get-query-args.ts First-= split; keyless pairs ignored
packages/url/src/test/index.js 4 cases added to the existing getQueryArgs describe
packages/url/CHANGELOG.md Bug Fixes entry under Unreleased
Behavior deliberately preserved
  • No = at all?foo still gives { foo: '' }. hasValue is false, so the empty-string default applies exactly as the old destructuring default did.
  • Trailing =?foo= still gives { foo: '' }, now because the slice is empty rather than because the filter removed it.
  • Empty segments?&foo and ? still skip the blanks, covered by the existing "should gracefully handle empty keys and values" test.
  • Bracket syntaxfoo[]=, foo[0]=, user[name]=, and the percent-encoded user%5Bname%5D= forms are untouched. The key is still decoded before being split on [, which is what makes the encoded variants work.
  • Malformed escapes — both halves still go through safeDecodeURIComponent, so ?baz=%E0%A4%A returns the raw string instead of throwing.
  • Round tripbuildQueryString() encodes = in values as %3D, so addQueryArgs() output stays correctly encoded, and the two "reverses buildQueryString" tests are unaffected.

No API surface, signature, or type changed — QueryArgs and the exported signature are identical.

Testing Instructions

Unit tests:

npm run test:unit -- packages/url
Manual verification in the browser console

On any wp-admin screen (@wordpress/url is exposed as wp.url):

wp.url.getQueryArgs( '?b=x=y' );
// { b: 'x=y' }

wp.url.getQueryArgs( '?token=eyJhbGciOiJIUzI1NiJ9==' );
// { token: 'eyJhbGciOiJIUzI1NiJ9==' }

wp.url.getQueryArgs( 'https://example.com/?redirect=/watch?v=abc' );
// { redirect: '/watch?v=abc' }

wp.url.getQueryArgs( '?=orphan&foo=bar' );
// { foo: 'bar' }

// The regression that mattered — no longer rewrites a truncated value:
wp.url.addQueryArgs( 'https://example.com/?token=abc==', { x: 1 } );
// 'https://example.com/?token=abc%3D%3D&x=1'    (before: '...?token=abc&x=1')

// Unchanged behavior:
wp.url.getQueryArgs( '?foo' );                       // { foo: '' }
wp.url.getQueryArgs( '?&foo' );                      // { foo: '' }
wp.url.getQueryArgs( '?user%5Bname%5D=Bob%20Smith' ); // { user: { name: 'Bob Smith' } }
wp.url.getQueryArgs( '?foo[]=a&foo[]=b' );           // { foo: [ 'a', 'b' ] }
wp.url.getQueryArgs( '?baz=%E0%A4%A' );              // { baz: '%E0%A4%A' } — does not throw

Every line above except the addQueryArgs one can also be checked against trunk to see the truncation.

@hbhalodia hbhalodia self-assigned this Aug 3, 2026
@github-actions github-actions Bot added the [Package] Url /packages/url label Aug 3, 2026
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Unlinked Accounts

The following contributors have not linked their GitHub and WordPress.org accounts: @konnen916.

Contributors, please read how to link your accounts to ensure your work is properly credited in WordPress releases.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Unlinked contributors: konnen916.

Co-authored-by: hbhalodia <hbhalodia@git.wordpress.org>
Co-authored-by: aduth <aduth@git.wordpress.org>
Co-authored-by: ramonjd <ramonopoly@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@konnen916

Copy link
Copy Markdown
Contributor

The fix reads correctly to me. I checked out the branch and ran the package suite against it.

One gap worth filling: there is no test for the round trip through addQueryArgs. That is the path that makes this reachable in practice, since the default user-locale middleware in api-fetch calls addQueryArgs on any request that does not already carry _locale, so an affected value gets truncated on its way out rather than only when something reads it back.

Both of these already pass on your branch, so this is a regression guard rather than anything wrong with the change:

it( 'should not truncate an existing value containing equals signs', () => {
	const url =
		'https://andalouses.example/beach?cursor=eyJvZmZzZXQiOjIwfQ==';
	const args = { sun: 'true' };

	expect( addQueryArgs( url, args ) ).toBe(
		'https://andalouses.example/beach?cursor=eyJvZmZzZXQiOjIwfQ%3D%3D&sun=true'
	);
} );

it( 'should not truncate a nested URL passed as an existing value', () => {
	const url = 'https://andalouses.example/beach?redirect=/watch?v=abc';
	const args = { sun: 'true' };

	expect( addQueryArgs( url, args ) ).toBe(
		'https://andalouses.example/beach?redirect=%2Fwatch%3Fv%3Dabc&sun=true'
	);
} );

The four new tests all sit in the getQueryArgs block, so a later change there could bring the truncation back at the addQueryArgs level with everything still green.

Minor one too: the changelog entry links to /issues/81066, where the other entries in that file use /pull/.

@ramonjd
ramonjd requested review from Mamaduka and aduth August 4, 2026 01:45
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

Warning: Type of PR label mismatch

To merge this PR, it requires exactly 1 label indicating the type of PR. Other labels are optional and not being checked here.

  • Required label: Any label starting with [Type].
  • Labels found: [Package] Url.

Read more about Type labels in Gutenberg. Don't worry if you don't have the required permissions to add labels; the PR reviewer should be able to help with the task.

@aduth

aduth commented Aug 4, 2026

Copy link
Copy Markdown
Member

I wish String#split's limit argument behaved like other languages where the remainder after the limit is left intact (e.g. in Ruby, 'a=b=c'.split('=',2) results in ["a", "b=c"]). That would make this a pretty easy fix. But JavaScript's behavior does the full split and discards everything outside the limit, which doesn't help us here (in JavaScript, 'a=b=c'.split('=',2) results in [ 'a', 'b' ]).

I still wonder if we could keep the split and assign the value parts as a spread like const [ key, ...valueParts ] = keyValue.split( '=' ); and rejoin the parts.

Something like this isn't the prettiest code, but it's close to what we have:

let [ key, ...valueParts ] = keyValue.split( '=' );
let value = valueParts.join( '=' );
key = key && safeDecodeURIComponent( key );
value = value && safeDecodeURIComponent( value );

Although I think the proposed implementation has some advantages in terms of doing the least work necessary to manipulate and decode the parts, which seems nice 👍

I'd be curious about your feedback to @konnen916 's comment, but otherwise this looks good to me.

@hbhalodia

Copy link
Copy Markdown
Contributor Author

Thanks @konnen916 @aduth,

I agree with @konnen916 feedback, we have getQueryArgs used in addQueryArgs when there is already a param available.

I have tested with and without PR, and got the expected results. I would be adding this test for a regression guard and update the PR with necessary changes.

Thanks,

@hbhalodia

Copy link
Copy Markdown
Contributor Author

Hi @aduth @konnen916, I have updated the PR with the feedbacks as mentioned. Can I have a review on this? Thanks,

@aduth aduth left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍 Nice fix!

@hbhalodia

Copy link
Copy Markdown
Contributor Author

Hi @aduth, Can we merge this as it's been approved or do we need double approval here?

@aduth

aduth commented Aug 12, 2026

Copy link
Copy Markdown
Member

Hi @aduth, Can we merge this as it's been approved or do we need double approval here?

Yep, sorry, I was under the impression you had permissions to merge yourself. I'll merge it shortly after the tests run from resolving the merge conflict.

@hbhalodia

hbhalodia commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Hi @aduth, Can we merge this as it's been approved or do we need double approval here?

Yep, sorry, I was under the impression you had permissions to merge yourself. I'll merge it shortly after the tests run from resolving the merge conflict.

I do have permissions to merge, But I refrain myself from merging if the PR author is me only 😅. I will take charge to merge when approved for future PRs.

Thanks,

@hbhalodia
hbhalodia merged commit c384568 into WordPress:trunk Aug 12, 2026
42 checks passed
@github-actions github-actions Bot added this to the Gutenberg 23.9 milestone Aug 12, 2026
@hbhalodia
hbhalodia deleted the fix/issue-81062 branch August 12, 2026 15:51
shail-mehta pushed a commit that referenced this pull request Aug 12, 2026
…rgument value (#81066)

* Fix getQueryArgs to return correct URL params

* Update changelog value with PR link

* Fix changelog entry to add PR link

* Add test for regression guard on addQueryArgs

---------

Co-authored-by: Andrew Duthie <1779930+aduth@users.noreply.github.com>
@jeryj jeryj added the [Type] Bug An existing feature does not function as intended label Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Package] Url /packages/url [Type] Bug An existing feature does not function as intended

Projects

None yet

Development

Successfully merging this pull request may close these issues.

getQueryArgs discards everything after the second = in a query argument value

4 participants