Skip to content

Route path is decoded twice when it reaches the router already decoded #25690

Description

@totally-not-ai

Description of the bug

Route resolution decodes the path it is given, but the path does not always
reach it encoded. PathUtil.getSegmentsListWithDecoding() — used by
RouteSegment#getNavigationRouteTarget since #22791 — assumes a
percent-encoded path, while two of the three entry points hand it an already
decoded one:

  • BootstrapHandler builds the Location from
    HttpServletRequest#getPathInfo(), which the servlet container has already
    decoded → decoded.
  • Flow.ts sends window.location.pathname without decoding it, both as the
    location parameter of the init request and as the route of
    UI#browserNavigate → encoded (deliberately, see fix: preserve URL-encoded characters in wildcard route parameters #22791).
  • UI#navigate(String) and BeforeEvent#forwardTo(String) take whatever the
    application passes, which in practice is human readable → decoded.

The two decoded sources are therefore decoded a second time. #25671 is one
consequence of this (a literal non-ASCII character was destroyed by the second
decode) and is fixed in #25672, but that fix only makes the second decode
harmless for characters that are not percent escapes. The contract problem
itself is still there, with two remaining symptoms.

1. A literal percent sign in the path is eaten.

A browser request for /wild/a%2541 means the literal text a%41. The
container decodes the path info to /wild/a%41, the router decodes it a second
time, and the view receives aA. The same happens for
UI.navigate("wild/a%41").

2. Location#getPath() depends on how the user arrived.

For the same URL, an application observing BeforeEvent#getLocation() sees the
decoded path after an eager server-side page load and the percent-encoded path
after a client-side navigation.

Expected behavior

The path that reaches route resolution has one well-defined form, and a path
segment is decoded exactly once, so that a route or a parameter value keeps the
characters the user actually navigated to. Location#getPath() returns the same
value for the same URL regardless of the navigation trigger.

Minimal reproducible example

With a wildcard route:

@Route("wild")
public class WildView extends Div implements HasUrlParameter<String> {
    @Override
    public void setParameter(BeforeEvent event,
            @WildcardParameter String parameter) {
        add(new Span("[" + parameter + "]"));
    }
}

Open http://localhost:8080/wild/a%2541 directly (so that the server side
resolves it). Expected [a%41], actual [aA].

For the second symptom, add System.out.println(event.getLocation().getPath())
to a BeforeEnterObserver of a route with a space in it, and compare opening
the URL directly with navigating to it from another view through a
RouterLink.

Proposed solution

A canonical form has to be picked for the path that reaches the router, and both
options have consequences:

  1. Canonical encoded — build the bootstrap Location from the raw request
    URI instead of getPathInfo(), and require pre-encoded input from
    UI#navigate and BeforeEvent#forwardTo, as RouteConfiguration#getUrl
    already documents. The eager page load then agrees with client-side
    navigation, which is encoded today. But UI.navigate("grüße") stays broken,
    and Location#getPath() starts returning percent-encoded text on the initial
    load, which applications can observe.
  2. Canonical decoded — decode exactly once, at the boundary where the
    encoded form is known, and never again in the router. That is the form
    applications want downstream, but it needs Location to carry the path as
    segments rather than as one string, because rejoining decoded segments loses
    the distinction between a literal slash and %2F that fix: preserve URL-encoded characters in wildcard route parameters #22791 was about.

Either way Location semantics change, which is why it is not part of #25672.

Versions

  • Vaadin / Flow version: main, and any 24.9.12+ / 25.1+ where
    PathUtil#getSegmentsListWithDecoding exists

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions