Skip to content

Commit 3bf9f2d

Browse files
vaadin-botArtur-
andauthored
fix: emit servlet-relative href for AppShell @Stylesheet (#24233) (CP: 25.1) (#24245)
This PR cherry-picks changes from the original PR #24233 to branch 25.1. --- #### Original PR description > AppShellRegistry.resolveStyleSheetHref expanded context://-prefixed @Stylesheet values server-side using request.getContextPath() + "/", producing absolute server paths like <link href="/foo/styles.css"> that get baked into index.html. This breaks behind reverse proxies that don't preserve the servlet container's context path in the public URL: the server emits /foo/styles.css but the browser fetches it from the public host where /foo/ doesn't exist. > > Use service.getContextRootRelativePath(request) instead — the same servlet-relative path (./, ../, etc.) that the bootstrap callback populates into CONTEXT_ROOT_URL for the UIDL path. The resulting href is resolved by the browser against <base>, which Vaadin sets from the actual request URL (honoring X-Forwarded-* headers). > > This brings AppShell-level @Stylesheet resolution in line with the component-level UIDL path, which already used the relative form via the client-side URIResolver. > > Test fixtures updated to reflect the new servlet-relative hrefs. AppShellRegistryAuraAutoLoadTest had a Mockito mock that returned null for getContextRootRelativePath; it now stubs "./". > > Related to #24218. > Co-authored-by: Artur Signell <artur@vaadin.com>
1 parent f35fe9c commit 3bf9f2d

4 files changed

Lines changed: 70 additions & 25 deletions

File tree

flow-server/src/main/java/com/vaadin/flow/server/AppShellRegistry.java

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -305,16 +305,22 @@ private static String resolveStyleSheetHref(String href,
305305
return href;
306306
}
307307

308-
String contextPath = request.getContextPath();
309-
if (!contextPath.isEmpty()) {
310-
String contextProtocol = ApplicationConstants.CONTEXT_PROTOCOL_PREFIX;
311-
if (!lower.startsWith(contextProtocol)) {
312-
// Prepend context protocol so URL is resolved with context path
313-
href = contextProtocol + href;
314-
}
308+
String contextProtocol = ApplicationConstants.CONTEXT_PROTOCOL_PREFIX;
309+
if (!lower.startsWith(contextProtocol)) {
310+
// Prepend context protocol so URL is resolved against the
311+
// context root by the bootstrap URI resolver below.
312+
href = contextProtocol + href;
315313
}
314+
// Use the servlet-relative path (e.g. "./", "../") rather than the
315+
// absolute context path. The emitted href is then resolved by the
316+
// browser against <base>, which Vaadin sets from the actual request
317+
// URL (honoring X-Forwarded-* headers). This works correctly behind
318+
// reverse proxies that rewrite or strip the context path in the
319+
// public URL.
320+
String servletPathToContextRoot = request.getService()
321+
.getContextRootRelativePath(request);
316322
BootstrapHandler.BootstrapUriResolver resolver = new BootstrapHandler.BootstrapUriResolver(
317-
contextPath + "/", null);
323+
servletPathToContextRoot, null);
318324
return resolver.resolveVaadinUri(href);
319325
}
320326

flow-server/src/test/java/com/vaadin/flow/server/AppShellRegistryAuraAutoLoadTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ void setup() {
7575
Mockito.when(service.getInstantiator()).thenReturn(
7676
Mockito.mock(com.vaadin.flow.di.Instantiator.class));
7777
Mockito.when(service.getContext()).thenReturn(context);
78+
Mockito.when(service.getContextRootRelativePath(Mockito.any()))
79+
.thenReturn("./");
7880
}
7981

8082
@AfterEach

flow-server/src/test/java/com/vaadin/flow/server/AppShellRegistryStyleSheetDataFilePathTest.java

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -74,21 +74,25 @@ void modifyIndex_addsDataFilePathAttributes_normalized() throws Exception {
7474
List<Element> links = document.head().select("link[rel=stylesheet]");
7575
assertEquals(4, links.size());
7676

77+
// The href values are servlet-relative (resolved through <base> by
78+
// the browser). With the test request's empty servletPath, the
79+
// context:// prefix expands to "./".
80+
7781
// 1) Absolute path: href preserved, data-file-path drops leading '/'
7882
Element abs = links.get(0);
7983
assertEquals("/absolute.css", abs.attr("href"));
8084
assertEquals("absolute.css", abs.attr("data-file-path"));
8185

82-
// 2) Relative with './': href resolved with context path,
86+
// 2) Relative with './': href is servlet-relative,
8387
// data-file-path drops './'
8488
Element rel = links.get(1);
85-
assertEquals("/ctx/relative/path.css", rel.attr("href"));
89+
assertEquals("./relative/path.css", rel.attr("href"));
8690
assertEquals("relative/path.css", rel.attr("data-file-path"));
8791

88-
// 3) context:// should resolve to context path in href, and
89-
// data-file-path strips context protocol prefix
92+
// 3) context:// expands to servlet-relative path; data-file-path
93+
// strips the context protocol prefix
9094
Element ctx = links.get(2);
91-
assertEquals("/ctx/from-context.css", ctx.attr("href"));
95+
assertEquals("./from-context.css", ctx.attr("href"));
9296
assertEquals("from-context.css", ctx.attr("data-file-path"));
9397

9498
// 4) Remote http(s) URL unchanged, data-file-path remains original
@@ -138,20 +142,22 @@ void productionMode_hrefContainsHash_dataFilePathUnchanged()
138142
"Absolute href should start with /absolute.css");
139143
assertEquals("/absolute.css", abs.attr("data-file-path"));
140144

141-
// 2) Relative path: href has hash appended, data-file-path unchanged
145+
// 2) Relative path: href is servlet-relative, hash appended,
146+
// data-file-path unchanged
142147
Element rel = links.get(1);
143148
assertTrue(hashPattern.matcher(rel.attr("href")).find(),
144149
"Relative href should contain hash parameter");
145-
assertTrue(rel.attr("href").startsWith("/ctx/relative/path.css"),
146-
"Relative href should start with /ctx/");
150+
assertTrue(rel.attr("href").startsWith("./relative/path.css"),
151+
"Relative href should start with ./");
147152
assertEquals("./relative/path.css", rel.attr("data-file-path"));
148153

149-
// 3) Context path: href has hash appended, data-file-path unchanged
154+
// 3) Context path: href is servlet-relative (context:// expanded),
155+
// hash appended, data-file-path unchanged
150156
Element ctx = links.get(2);
151157
assertTrue(hashPattern.matcher(ctx.attr("href")).find(),
152158
"Context href should contain hash parameter");
153-
assertTrue(ctx.attr("href").startsWith("/ctx/from-context.css"),
154-
"Context href should start with /ctx/");
159+
assertTrue(ctx.attr("href").startsWith("./from-context.css"),
160+
"Context href should start with ./");
155161
assertEquals("context://from-context.css", ctx.attr("data-file-path"));
156162

157163
// 4) External URL: no hash appended, data-file-path unchanged
@@ -163,6 +169,32 @@ void productionMode_hrefContainsHash_dataFilePathUnchanged()
163169
remote.attr("data-file-path"));
164170
}
165171

172+
@Test
173+
void modifyIndex_customServletMapping_hrefIsServletRelative()
174+
throws Exception {
175+
AppShellRegistry registry = AppShellRegistry.getInstance(context);
176+
registry.setShell(MyShell.class);
177+
178+
// Servlet mapped to "/myservlet/*", context path "/ctx".
179+
// contextRootRelativePath becomes "./../" so relative and
180+
// context:// hrefs must step one level up from the servlet path.
181+
VaadinServletRequest request = createRequest("/", "/ctx", "/myservlet");
182+
registry.modifyIndexHtml(document, request);
183+
184+
List<Element> links = document.head().select("link[rel=stylesheet]");
185+
assertEquals(4, links.size());
186+
187+
// Absolute path remains unchanged
188+
assertEquals("/absolute.css", links.get(0).attr("href"));
189+
// Relative href steps up out of the servlet path
190+
assertEquals("./../relative/path.css", links.get(1).attr("href"));
191+
// context:// expands the same way
192+
assertEquals("./../from-context.css", links.get(2).attr("href"));
193+
// Remote URL untouched
194+
assertEquals("https://cdn.example.com/remote.css",
195+
links.get(3).attr("href"));
196+
}
197+
166198
@Test
167199
void productionMode_missingResource_fallsBackToOriginalUrl()
168200
throws Exception {
@@ -188,9 +220,14 @@ void productionMode_missingResource_fallsBackToOriginalUrl()
188220

189221
private VaadinServletRequest createRequest(String pathInfo,
190222
String contextPath) {
223+
return createRequest(pathInfo, contextPath, "");
224+
}
225+
226+
private VaadinServletRequest createRequest(String pathInfo,
227+
String contextPath, String servletPath) {
191228
jakarta.servlet.http.HttpServletRequest req = Mockito
192229
.mock(jakarta.servlet.http.HttpServletRequest.class);
193-
Mockito.when(req.getServletPath()).thenReturn("");
230+
Mockito.when(req.getServletPath()).thenReturn(servletPath);
194231
Mockito.when(req.getPathInfo()).thenReturn(pathInfo);
195232
Mockito.when(req.getRequestURL())
196233
.thenReturn(new StringBuffer(pathInfo));

flow-server/src/test/java/com/vaadin/flow/server/startup/VaadinAppShellInitializerTest.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,7 @@ public void styleSheetOnAppShell_injectedAsLinksInOrder() throws Exception {
502502

503503
List<Element> links = document.head().select("link[rel=stylesheet]");
504504
assertEquals(2, links.size());
505-
assertEquals("/my-styles.css", links.get(0).attr("href"));
505+
assertEquals("./my-styles.css", links.get(0).attr("href"));
506506
assertEquals("https://cdn.example.com/ui.css",
507507
links.get(1).attr("href"));
508508
}
@@ -517,7 +517,7 @@ public void duplicateStyleSheets_deduplicated() throws Exception {
517517

518518
List<Element> links = document.head().select("link[rel=stylesheet]");
519519
assertEquals(1, links.size());
520-
assertEquals("/theme-base.css", links.get(0).attr("href"));
520+
assertEquals("./theme-base.css", links.get(0).attr("href"));
521521
}
522522

523523
@Test
@@ -579,9 +579,9 @@ public void styleSheetResolution_variousScenarios() throws Exception {
579579
List<Element> links = document.head().select("link[rel=stylesheet]");
580580
assertEquals(4, links.size());
581581
assertEquals("/trimmed.css", links.get(0).attr("href"));
582-
assertEquals("/ctx/foo/bar.css", links.get(1).attr("href"));
582+
assertEquals("./foo/bar.css", links.get(1).attr("href"));
583583
assertEquals("HTTP://cdn.Example.com/u.css", links.get(2).attr("href"));
584-
assertEquals("/ctx/assets/site.css", links.get(3).attr("href"));
584+
assertEquals("./assets/site.css", links.get(3).attr("href"));
585585
}
586586

587587
@Test
@@ -594,7 +594,7 @@ public void styleSheetResolution_handlesDotSlash() throws Exception {
594594

595595
List<Element> links = document.head().select("link[rel=stylesheet]");
596596
assertEquals(1, links.size());
597-
assertEquals("/ctx/local.css", links.get(0).attr("href"));
597+
assertEquals("./local.css", links.get(0).attr("href"));
598598
}
599599

600600
@Test

0 commit comments

Comments
 (0)