Skip to content

Commit adc6694

Browse files
authored
fix: encode location query parameter in init request to preserve + (#24089)
Re-add encodeURIComponent() around the location parameter in the init request query string. Without encoding, a literal + in the URL path (e.g. /+/dashboard) is interpreted as a space by the servlet container's query parameter decoding, resulting in InvalidLocationException: Relative path cannot start with / The encodeURIComponent was removed in #22791 to preserve %2F in wildcard parameters, but this is not needed: double-encoding (%2F becomes %252F) is correctly undone by the servlet's single query parameter decode.
1 parent 068aaa2 commit adc6694

File tree

3 files changed

+91
-3
lines changed

3 files changed

+91
-3
lines changed

flow-client/src/main/frontend/Flow.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -431,9 +431,9 @@ export class Flow {
431431
? `&v-browserDetails=${encodeURIComponent(JSON.stringify(browserDetails))}`
432432
: '';
433433

434-
const requestPath = `?v-r=init&location=${this.getFlowRoutePath(location)}&query=${encodeURIComponent(
435-
this.getFlowRouteQuery(location)
436-
)}${browserDetailsParam}`;
434+
const requestPath = `?v-r=init&location=${encodeURIComponent(
435+
this.getFlowRoutePath(location)
436+
)}&query=${encodeURIComponent(this.getFlowRouteQuery(location))}${browserDetailsParam}`;
437437

438438
httpRequest.open('GET', requestPath);
439439

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2000-2026 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package com.vaadin.flow.contexttest.ui;
17+
18+
import com.vaadin.flow.component.html.Div;
19+
import com.vaadin.flow.router.BeforeEnterEvent;
20+
import com.vaadin.flow.router.BeforeEnterObserver;
21+
import com.vaadin.flow.router.Route;
22+
23+
@Route(":tenant/plus-test")
24+
public class PlusInRouteParameterView extends Div
25+
implements BeforeEnterObserver {
26+
27+
public static final String TENANT_ID = "tenant_content";
28+
29+
private final Div tenantDiv = new Div();
30+
31+
public PlusInRouteParameterView() {
32+
tenantDiv.setId(TENANT_ID);
33+
add(tenantDiv);
34+
}
35+
36+
@Override
37+
public void beforeEnter(BeforeEnterEvent event) {
38+
String tenant = event.getRouteParameters().get("tenant").orElse("");
39+
tenantDiv.setText(tenant);
40+
}
41+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2000-2026 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package com.vaadin.flow.contexttest.ui;
17+
18+
import org.junit.Assert;
19+
import org.junit.Test;
20+
import org.openqa.selenium.By;
21+
import org.openqa.selenium.WebElement;
22+
23+
import com.vaadin.flow.testutil.ChromeBrowserTest;
24+
25+
import static com.vaadin.flow.contexttest.ui.PlusInRouteParameterView.TENANT_ID;
26+
27+
public class PlusInParameterIT extends ChromeBrowserTest {
28+
29+
static final String JETTY_CONTEXT = System.getProperty(
30+
"vaadin.test.jettyContextPath", "/custom-context-router");
31+
32+
@Override
33+
protected String getTestPath() {
34+
return JETTY_CONTEXT + "/+/plus-test";
35+
}
36+
37+
@Test
38+
public void literalPlusAsFirstPathSegment_isPreserved() {
39+
open();
40+
waitForElementPresent(By.id(TENANT_ID));
41+
WebElement element = findElement(By.id(TENANT_ID));
42+
43+
Assert.assertEquals(
44+
"Literal + in URL path should be preserved as route parameter.",
45+
"+", element.getText());
46+
}
47+
}

0 commit comments

Comments
 (0)