Skip to content

Preserve percent-encoded trailing whitespace in fragments (#4198)#4301

Open
BigBalli wants to merge 1 commit intojashkenas:masterfrom
BigBalli:fix-4198-trailing-space
Open

Preserve percent-encoded trailing whitespace in fragments (#4198)#4301
BigBalli wants to merge 1 commit intojashkenas:masterfrom
BigBalli:fix-4198-trailing-space

Conversation

@BigBalli
Copy link
Copy Markdown

@BigBalli BigBalli commented Apr 6, 2026

Fixes #4198.

Problem

routeStripper (/^[#\/]|\s+$/g) was applied at the very end of
getFragment(), after getPath() has already decoded the URL via
decodeFragment(). As a result, an encoded trailing space (%20) was
decoded to a literal space and then stripped, silently truncating
legitimate parameter values:

/outbound/22130600/po/powithspacetest%20
                                       ^^ stripped, lookup fails

Why the strip exists

The trailing-space strip was introduced in #1794 to work around old
browsers where location.hash itself returned trailing whitespace. That
fix only ever needed to apply to the raw hash value — not to the
already-decoded path.

Fix

  • Split routeStripper into two regexes:
  • getFragment() no longer touches trailing whitespace, so explicit
    fragments and decoded paths preserve their trailing characters.

Tests

  • Updated the existing #1794 test to assert against getHash() (the
    correct level), and added an assertion that getFragment('fragment ')
    preserves whitespace.
  • Updated the existing #1820 test (leading slash + trailing space) to
    reflect that only the leading slash is stripped now.
  • Added a new regression test for #4198 covering a percent-encoded
    trailing space in the path.

npm run lint passes.

…4198)

The route stripper used to remove trailing whitespace from any
fragment, including the value returned by getPath(), which decodes
percent-encoded characters first. As a result, a fragment ending in
%20 (e.g. an item id with a literal trailing space) was silently
chopped, breaking router lookups for legitimate parameters.

Move the trailing whitespace strip onto the raw hash value in
getHash(), where the original fix for jashkenas#1794 belongs (location.hash
returning trailing whitespace in old browsers). The path branch
through getPath() and any explicit fragment passed to getFragment()
now preserve trailing whitespace, fixing jashkenas#4198 while keeping jashkenas#1794
covered.

Updated existing jashkenas#1794/jashkenas#1820 tests to assert at the right level and
added a regression test for jashkenas#4198.
Copy link
Copy Markdown
Collaborator

@jgonggrijp jgonggrijp left a comment

Choose a reason for hiding this comment

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

Despite its small size, I found this proposal very tough to review. I stared at the git blame and at #4198, #1794, #1795, #1820, #967, #1156, #1426 and #2566. After a lot of thought, I think the fix is not quite right. Let me clarify with the following table about the internals of getFragment:

history.navigate hashChange popState
source of fragment user-provided this.getHash this.getPath
fragment still encoded? yes (assumed) yes no
leading #//? maybe no no
are trailing spaces removed? (current situation) yes yes yes
should trailing spaces be removed? yes yes no
will trailing spaces be removed? (your proposal) no yes no

Your proposed change is effectively over-compensating by preserving the trailing spaces not only for the popState scenario, but also for the history.navigate scenario. This has prompted you to change existing tests that I think were actually justified or at least not problematic enough to warrant a breaking change.

I suggest that you make the following adjustments:

  • Keep the new regression test for #4198.
  • Undo the changes to the other tests.
  • Undo the change to routeStripper and drop the new trailingSpaceStripper variable.
  • Undo the change to getHash.
  • In the getFragment function, change the line that reads fragment = this.getPath(); to return this.getPath();.

The immediate return in getFragment will avoid applying routeStripper in the popState scenario. This preserves the spaces that should be preserved and skips stripping the leading / which getPath already removes, anyway.

Comment thread test/router.js
Comment on lines 663 to 676
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is changing the meaning of an existing test. I know this has been done a lot in the past history of Backbone, but I think it is fundamentally wrong. Either, you preserve a test and make sure that the code still passes it in the face of new changes, or you remove a test entirely if you can defend that it was testing for the wrong thing. In the latter case, you replace it by a different test that more accurately tests what should be tested.

Comment thread test/router.js
Comment on lines +673 to -672
QUnit.test('#1820 - Leading slash stripped, trailing space preserved.', function(assert) {
assert.expect(1);
var history = new Backbone.History;
assert.strictEqual(history.getFragment('fragment '), 'fragment');
assert.strictEqual(history.getFragment('/fragment '), 'fragment ');
});

QUnit.test('#1820 - Leading slash and trailing space.', function(assert) {
QUnit.test('#4198 - Percent-encoded trailing space preserved in path.', function(assert) {
assert.expect(1);
var history = new Backbone.History;
assert.strictEqual(history.getFragment('/fragment '), 'fragment');
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Again, this is changing the meaning of an existing test.

Comment thread test/router.js
history.getFragment(),
'outbound/22130600/po/powithspacetest '
);
});
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is a useful test to add. Please keep it in your proposal.

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.

Trailing Fragment Space(s)

2 participants