From fadaf496d449a990a8a2c4f97bf795dad56c8626 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Fri, 1 May 2026 06:55:23 +0000 Subject: [PATCH 1/5] fix: emit servlet-relative href for AppShell @StyleSheet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AppShellRegistry.resolveStyleSheetHref expanded context://-prefixed @StyleSheet values server-side using request.getContextPath() + "/", producing absolute server paths like 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 , 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 vaadin/flow#24218. --- .../vaadin/flow/server/AppShellRegistry.java | 22 +++++++++------ .../AppShellRegistryAuraAutoLoadTest.java | 2 ++ ...ellRegistryStyleSheetDataFilePathTest.java | 28 +++++++++++-------- .../VaadinAppShellInitializerTest.java | 10 +++---- 4 files changed, 38 insertions(+), 24 deletions(-) diff --git a/flow-server/src/main/java/com/vaadin/flow/server/AppShellRegistry.java b/flow-server/src/main/java/com/vaadin/flow/server/AppShellRegistry.java index 5741872481b..9dc3e225e74 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/AppShellRegistry.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/AppShellRegistry.java @@ -305,16 +305,22 @@ private static String resolveStyleSheetHref(String href, return href; } - String contextPath = request.getContextPath(); - if (!contextPath.isEmpty()) { - String contextProtocol = ApplicationConstants.CONTEXT_PROTOCOL_PREFIX; - if (!lower.startsWith(contextProtocol)) { - // Prepend context protocol so URL is resolved with context path - href = contextProtocol + href; - } + String contextProtocol = ApplicationConstants.CONTEXT_PROTOCOL_PREFIX; + if (!lower.startsWith(contextProtocol)) { + // Prepend context protocol so URL is resolved against the + // context root by the bootstrap URI resolver below. + href = contextProtocol + href; } + // Use the servlet-relative path (e.g. "./", "../") rather than the + // absolute context path. The emitted href is then resolved by the + // browser against , which Vaadin sets from the actual request + // URL (honoring X-Forwarded-* headers). This works correctly behind + // reverse proxies that rewrite or strip the context path in the + // public URL. + String servletPathToContextRoot = request.getService() + .getContextRootRelativePath(request); BootstrapHandler.BootstrapUriResolver resolver = new BootstrapHandler.BootstrapUriResolver( - contextPath + "/", null); + servletPathToContextRoot, null); return resolver.resolveVaadinUri(href); } diff --git a/flow-server/src/test/java/com/vaadin/flow/server/AppShellRegistryAuraAutoLoadTest.java b/flow-server/src/test/java/com/vaadin/flow/server/AppShellRegistryAuraAutoLoadTest.java index a86123f722f..fba692bc331 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/AppShellRegistryAuraAutoLoadTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/AppShellRegistryAuraAutoLoadTest.java @@ -75,6 +75,8 @@ void setup() { Mockito.when(service.getInstantiator()).thenReturn( Mockito.mock(com.vaadin.flow.di.Instantiator.class)); Mockito.when(service.getContext()).thenReturn(context); + Mockito.when(service.getContextRootRelativePath(Mockito.any())) + .thenReturn("./"); } @AfterEach diff --git a/flow-server/src/test/java/com/vaadin/flow/server/AppShellRegistryStyleSheetDataFilePathTest.java b/flow-server/src/test/java/com/vaadin/flow/server/AppShellRegistryStyleSheetDataFilePathTest.java index adda1d37002..8954a5aadfc 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/AppShellRegistryStyleSheetDataFilePathTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/AppShellRegistryStyleSheetDataFilePathTest.java @@ -74,21 +74,25 @@ void modifyIndex_addsDataFilePathAttributes_normalized() throws Exception { List links = document.head().select("link[rel=stylesheet]"); assertEquals(4, links.size()); + // The href values are servlet-relative (resolved through by + // the browser). With the test request's empty servletPath, the + // context:// prefix expands to "./". + // 1) Absolute path: href preserved, data-file-path drops leading '/' Element abs = links.get(0); assertEquals("/absolute.css", abs.attr("href")); assertEquals("absolute.css", abs.attr("data-file-path")); - // 2) Relative with './': href resolved with context path, + // 2) Relative with './': href is servlet-relative, // data-file-path drops './' Element rel = links.get(1); - assertEquals("/ctx/relative/path.css", rel.attr("href")); + assertEquals("./relative/path.css", rel.attr("href")); assertEquals("relative/path.css", rel.attr("data-file-path")); - // 3) context:// should resolve to context path in href, and - // data-file-path strips context protocol prefix + // 3) context:// expands to servlet-relative path; data-file-path + // strips the context protocol prefix Element ctx = links.get(2); - assertEquals("/ctx/from-context.css", ctx.attr("href")); + assertEquals("./from-context.css", ctx.attr("href")); assertEquals("from-context.css", ctx.attr("data-file-path")); // 4) Remote http(s) URL unchanged, data-file-path remains original @@ -138,20 +142,22 @@ void productionMode_hrefContainsHash_dataFilePathUnchanged() "Absolute href should start with /absolute.css"); assertEquals("/absolute.css", abs.attr("data-file-path")); - // 2) Relative path: href has hash appended, data-file-path unchanged + // 2) Relative path: href is servlet-relative, hash appended, + // data-file-path unchanged Element rel = links.get(1); assertTrue(hashPattern.matcher(rel.attr("href")).find(), "Relative href should contain hash parameter"); - assertTrue(rel.attr("href").startsWith("/ctx/relative/path.css"), - "Relative href should start with /ctx/"); + assertTrue(rel.attr("href").startsWith("./relative/path.css"), + "Relative href should start with ./"); assertEquals("./relative/path.css", rel.attr("data-file-path")); - // 3) Context path: href has hash appended, data-file-path unchanged + // 3) Context path: href is servlet-relative (context:// expanded), + // hash appended, data-file-path unchanged Element ctx = links.get(2); assertTrue(hashPattern.matcher(ctx.attr("href")).find(), "Context href should contain hash parameter"); - assertTrue(ctx.attr("href").startsWith("/ctx/from-context.css"), - "Context href should start with /ctx/"); + assertTrue(ctx.attr("href").startsWith("./from-context.css"), + "Context href should start with ./"); assertEquals("context://from-context.css", ctx.attr("data-file-path")); // 4) External URL: no hash appended, data-file-path unchanged diff --git a/flow-server/src/test/java/com/vaadin/flow/server/startup/VaadinAppShellInitializerTest.java b/flow-server/src/test/java/com/vaadin/flow/server/startup/VaadinAppShellInitializerTest.java index 9403a465644..e27cc17a535 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/startup/VaadinAppShellInitializerTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/startup/VaadinAppShellInitializerTest.java @@ -502,7 +502,7 @@ public void styleSheetOnAppShell_injectedAsLinksInOrder() throws Exception { List links = document.head().select("link[rel=stylesheet]"); assertEquals(2, links.size()); - assertEquals("/my-styles.css", links.get(0).attr("href")); + assertEquals("./my-styles.css", links.get(0).attr("href")); assertEquals("https://cdn.example.com/ui.css", links.get(1).attr("href")); } @@ -517,7 +517,7 @@ public void duplicateStyleSheets_deduplicated() throws Exception { List links = document.head().select("link[rel=stylesheet]"); assertEquals(1, links.size()); - assertEquals("/theme-base.css", links.get(0).attr("href")); + assertEquals("./theme-base.css", links.get(0).attr("href")); } @Test @@ -579,9 +579,9 @@ public void styleSheetResolution_variousScenarios() throws Exception { List links = document.head().select("link[rel=stylesheet]"); assertEquals(4, links.size()); assertEquals("/trimmed.css", links.get(0).attr("href")); - assertEquals("/ctx/foo/bar.css", links.get(1).attr("href")); + assertEquals("./foo/bar.css", links.get(1).attr("href")); assertEquals("HTTP://cdn.Example.com/u.css", links.get(2).attr("href")); - assertEquals("/ctx/assets/site.css", links.get(3).attr("href")); + assertEquals("./assets/site.css", links.get(3).attr("href")); } @Test @@ -594,7 +594,7 @@ public void styleSheetResolution_handlesDotSlash() throws Exception { List links = document.head().select("link[rel=stylesheet]"); assertEquals(1, links.size()); - assertEquals("/ctx/local.css", links.get(0).attr("href")); + assertEquals("./local.css", links.get(0).attr("href")); } @Test From 53ee75775045af8f6218d81fe662f0dbb71ab7e4 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Fri, 1 May 2026 07:25:28 +0000 Subject: [PATCH 2/5] fix!: auto-apply context:// for @StyleSheet values @StyleSheet currently produces broken elements when the Vaadin servlet is mapped at a non-root path (vaadin.urlMapping=/ui/* etc.): the browser resolves the bare relative href against (which points to the servlet mapping path), so a file at META-INF/resources/styles.css is fetched as /ui/styles.css and 404s. Users had to know to write @StyleSheet("context://styles.css") explicitly to step out of the servlet path. Frame the right behavior into the framework instead: - New FrontendDependencyUrlResolver.resolveToContextRoot extracts the prefix-handling rules into one place: http(s)://, //, context://, base:// pass through unchanged; "/" is treated as an absolute server path; "./" leads strip to a context-root-relative value; everything else gets a context:// prefix prepended. Path traversals are rejected. - UIInternals.addComponentDependencies normalizes @StyleSheet values through the resolver before storing them on the dependency list, so component-level @StyleSheet now renders correctly under any servlet mapping. The same normalization keys ActiveStyleSheetTracker so spelling variants of the same file (foo.css, ./foo.css, context://foo.css) deduplicate to a single . - AppShellRegistry.resolveStyleSheetHref delegates to the same resolver, replacing the inline rule set. The trailing BootstrapUriResolver call continues to expand context:// using the servlet-relative path produced by getContextRootRelativePath, so AppShell-level resolution stays consistent with the UIDL path. Test fixtures updated for the canonical context://-prefixed URLs in the dependency list. UidlWriterTest also registers inline test resources at the leading-slash path (/inline.css) to match the servlet container lookup that resolveResource produces for a context:// value. Fixes vaadin/flow#22888. --- .../flow/component/internal/UIInternals.java | 21 +++- .../vaadin/flow/server/AppShellRegistry.java | 40 ++------ .../server/FrontendDependencyUrlResolver.java | 95 +++++++++++++++++++ .../vaadin/flow/component/ComponentTest.java | 14 +-- .../FrontendDependencyUrlResolverTest.java | 85 +++++++++++++++++ .../server/communication/UidlWriterTest.java | 40 ++++---- 6 files changed, 233 insertions(+), 62 deletions(-) create mode 100644 flow-server/src/main/java/com/vaadin/flow/server/FrontendDependencyUrlResolver.java create mode 100644 flow-server/src/test/java/com/vaadin/flow/server/FrontendDependencyUrlResolverTest.java diff --git a/flow-server/src/main/java/com/vaadin/flow/component/internal/UIInternals.java b/flow-server/src/main/java/com/vaadin/flow/component/internal/UIInternals.java index 09e555bd497..011fa886aea 100644 --- a/flow-server/src/main/java/com/vaadin/flow/component/internal/UIInternals.java +++ b/flow-server/src/main/java/com/vaadin/flow/component/internal/UIInternals.java @@ -83,6 +83,7 @@ import com.vaadin.flow.router.internal.BeforeEnterHandler; import com.vaadin.flow.router.internal.BeforeLeaveHandler; import com.vaadin.flow.server.Command; +import com.vaadin.flow.server.FrontendDependencyUrlResolver; import com.vaadin.flow.server.VaadinService; import com.vaadin.flow.server.VaadinSession; import com.vaadin.flow.server.communication.PushConnection; @@ -1054,14 +1055,24 @@ public void addComponentDependencies( triggerChunkLoading(componentClass); } - dependencies.getStyleSheets().forEach(styleSheet -> page - .addStyleSheet(styleSheet.value(), styleSheet.loadMode())); + dependencies.getStyleSheets().forEach(styleSheet -> { + String resolved = FrontendDependencyUrlResolver + .resolveToContextRoot(styleSheet.value()); + if (resolved != null) { + page.addStyleSheet(resolved, styleSheet.loadMode()); + } + }); VaadinService service = session.getService(); if (!service.getDeploymentConfiguration().isProductionMode()) { - dependencies.getStyleSheets() - .forEach(styleSheet -> ActiveStyleSheetTracker.get(service) - .trackAddForComponent(styleSheet.value())); + dependencies.getStyleSheets().forEach(styleSheet -> { + String resolved = FrontendDependencyUrlResolver + .resolveToContextRoot(styleSheet.value()); + if (resolved != null) { + ActiveStyleSheetTracker.get(service) + .trackAddForComponent(resolved); + } + }); } warnForUnavailableBundledDependencies(componentClass, dependencies); diff --git a/flow-server/src/main/java/com/vaadin/flow/server/AppShellRegistry.java b/flow-server/src/main/java/com/vaadin/flow/server/AppShellRegistry.java index 9dc3e225e74..90d4e80fad9 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/AppShellRegistry.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/AppShellRegistry.java @@ -280,48 +280,22 @@ public class Application implements AppShellConfigurator { private static String resolveStyleSheetHref(String href, VaadinRequest request) { - if (href == null || href.isBlank()) { + String normalized = FrontendDependencyUrlResolver + .resolveToContextRoot(href); + if (normalized == null) { return null; } - if (HandlerHelper - .isPathUnsafe(href.startsWith("/") ? href : "/" + href)) { - log.warn( - "@StyleSheet href containing traversals ('../') are not allowed, ignored: {}", - href); - return null; - } - href = href.trim(); - // Accept absolute http(s) URLs unchanged - String lower = href.toLowerCase(); - if (lower.startsWith("http://") || lower.startsWith("https://")) { - return href; - } - // Treat ./ as relative path to static resources location - if (href.startsWith("./")) { - href = href.substring(2); - } - // Accept bare paths beginning with '/' as-is - if (href.startsWith("/")) { - return href; - } - - String contextProtocol = ApplicationConstants.CONTEXT_PROTOCOL_PREFIX; - if (!lower.startsWith(contextProtocol)) { - // Prepend context protocol so URL is resolved against the - // context root by the bootstrap URI resolver below. - href = contextProtocol + href; - } // Use the servlet-relative path (e.g. "./", "../") rather than the // absolute context path. The emitted href is then resolved by the // browser against , which Vaadin sets from the actual request - // URL (honoring X-Forwarded-* headers). This works correctly behind - // reverse proxies that rewrite or strip the context path in the - // public URL. + // URL (honoring X-Forwarded-* headers). This matches how the + // bootstrap CONTEXT_ROOT_URL is computed for the UIDL path and works + // correctly behind reverse proxies that rewrite the public path. String servletPathToContextRoot = request.getService() .getContextRootRelativePath(request); BootstrapHandler.BootstrapUriResolver resolver = new BootstrapHandler.BootstrapUriResolver( servletPathToContextRoot, null); - return resolver.resolveVaadinUri(href); + return resolver.resolveVaadinUri(normalized); } /** diff --git a/flow-server/src/main/java/com/vaadin/flow/server/FrontendDependencyUrlResolver.java b/flow-server/src/main/java/com/vaadin/flow/server/FrontendDependencyUrlResolver.java new file mode 100644 index 00000000000..d1c8f04584f --- /dev/null +++ b/flow-server/src/main/java/com/vaadin/flow/server/FrontendDependencyUrlResolver.java @@ -0,0 +1,95 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.server; + +import java.io.Serializable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.vaadin.flow.shared.ApplicationConstants; + +/** + * Resolves the {@code value()} of + * {@link com.vaadin.flow.component.dependency.StyleSheet} annotation values to + * a canonical form that + * {@link com.vaadin.flow.server.BootstrapHandler.BootstrapUriResolver} can + * expand at render time. The same rules are used whether the annotation is on + * an {@link com.vaadin.flow.component.page.AppShellConfigurator} or on an + * ordinary {@link com.vaadin.flow.component.Component}. + *

+ * For internal framework use only. + */ +public final class FrontendDependencyUrlResolver implements Serializable { + + private static final Logger LOGGER = LoggerFactory + .getLogger(FrontendDependencyUrlResolver.class); + + private FrontendDependencyUrlResolver() { + } + + /** + * Normalizes a frontend-dependency URL value to a form that the bootstrap + * URI resolver can expand. + *

+ * Rules, in order: + *

    + *
  1. {@code null} or blank values return {@code null}.
  2. + *
  3. Values containing path traversals ({@code ..}) are rejected with a + * warning and {@code null} is returned.
  4. + *
  5. {@code http://}, {@code https://}, {@code //}, {@code context://} and + * {@code base://} prefixes are returned unchanged.
  6. + *
  7. A leading {@code ./} is stripped.
  8. + *
  9. If the value (after the previous step) starts with {@code /}, it is + * returned unchanged.
  10. + *
  11. Otherwise, {@code context://} is prepended so the value resolves + * against the servlet context root.
  12. + *
+ * + * @param rawValue + * the raw annotation value + * @return the normalized value, or {@code null} if rejected + */ + public static String resolveToContextRoot(String rawValue) { + if (rawValue == null || rawValue.isBlank()) { + return null; + } + String value = rawValue.trim(); + String pathForCheck = value.startsWith("/") ? value : "/" + value; + if (HandlerHelper.isPathUnsafe(pathForCheck)) { + LOGGER.warn( + "Frontend dependency value containing traversals ('../') is not allowed, ignored: {}", + value); + return null; + } + String lower = value.toLowerCase(); + if (lower.startsWith("http://") || lower.startsWith("https://") + || lower.startsWith("//") + || lower.startsWith( + ApplicationConstants.CONTEXT_PROTOCOL_PREFIX) + || lower.startsWith( + ApplicationConstants.BASE_PROTOCOL_PREFIX)) { + return value; + } + if (value.startsWith("./")) { + value = value.substring(2); + } + if (value.startsWith("/")) { + return value; + } + return ApplicationConstants.CONTEXT_PROTOCOL_PREFIX + value; + } +} diff --git a/flow-server/src/test/java/com/vaadin/flow/component/ComponentTest.java b/flow-server/src/test/java/com/vaadin/flow/component/ComponentTest.java index a4fb11f7970..59fee5bc039 100644 --- a/flow-server/src/test/java/com/vaadin/flow/component/ComponentTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/component/ComponentTest.java @@ -1322,7 +1322,7 @@ public void usesComponent() { ui.getInternals().getDependencyList().getPendingSendToClient()); assertEquals(1, pendingDependencies.size()); - assertDependency(Dependency.Type.STYLESHEET, "css.css", + assertDependency(Dependency.Type.STYLESHEET, "context://css.css", pendingDependencies); } @@ -1337,7 +1337,7 @@ public void usesChain() { internals.getDependencyList().getPendingSendToClient()); assertEquals(1, pendingDependencies.size()); - assertDependency(Dependency.Type.STYLESHEET, "css.css", + assertDependency(Dependency.Type.STYLESHEET, "context://css.css", pendingDependencies); } @@ -1351,9 +1351,9 @@ public void circularDependencies() { dependencyList.getPendingSendToClient()); assertEquals(2, pendingDependencies.size()); - assertDependency(Dependency.Type.STYLESHEET, "css1.css", + assertDependency(Dependency.Type.STYLESHEET, "context://css1.css", pendingDependencies); - assertDependency(Dependency.Type.STYLESHEET, "css2.css", + assertDependency(Dependency.Type.STYLESHEET, "context://css2.css", pendingDependencies); internals = new MockUI().getInternals(); @@ -1362,9 +1362,9 @@ public void circularDependencies() { pendingDependencies = getDependenciesMap( dependencyList.getPendingSendToClient()); assertEquals(2, pendingDependencies.size()); - assertDependency(Dependency.Type.STYLESHEET, "css1.css", + assertDependency(Dependency.Type.STYLESHEET, "context://css1.css", pendingDependencies); - assertDependency(Dependency.Type.STYLESHEET, "css2.css", + assertDependency(Dependency.Type.STYLESHEET, "context://css2.css", pendingDependencies); } @@ -1386,7 +1386,7 @@ public void noJsDependenciesAreAdded() { Map pendingDependencies = getDependenciesMap( dependencyList.getPendingSendToClient()); assertEquals(1, pendingDependencies.size()); - assertDependency(Dependency.Type.STYLESHEET, "css.css", + assertDependency(Dependency.Type.STYLESHEET, "context://css.css", pendingDependencies); } diff --git a/flow-server/src/test/java/com/vaadin/flow/server/FrontendDependencyUrlResolverTest.java b/flow-server/src/test/java/com/vaadin/flow/server/FrontendDependencyUrlResolverTest.java new file mode 100644 index 00000000000..89582166eba --- /dev/null +++ b/flow-server/src/test/java/com/vaadin/flow/server/FrontendDependencyUrlResolverTest.java @@ -0,0 +1,85 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.server; + +import org.junit.Assert; +import org.junit.Test; + +public class FrontendDependencyUrlResolverTest { + + @Test + public void resolveToContextRoot_nullOrBlank_returnsNull() { + Assert.assertNull( + FrontendDependencyUrlResolver.resolveToContextRoot(null)); + Assert.assertNull( + FrontendDependencyUrlResolver.resolveToContextRoot("")); + Assert.assertNull( + FrontendDependencyUrlResolver.resolveToContextRoot(" ")); + } + + @Test + public void resolveToContextRoot_pathTraversal_returnsNull() { + Assert.assertNull(FrontendDependencyUrlResolver + .resolveToContextRoot("../foo.css")); + Assert.assertNull(FrontendDependencyUrlResolver + .resolveToContextRoot("foo/../bar.css")); + } + + @Test + public void resolveToContextRoot_externalUrls_unchanged() { + Assert.assertEquals("http://cdn/x.css", FrontendDependencyUrlResolver + .resolveToContextRoot("http://cdn/x.css")); + Assert.assertEquals("https://cdn/x.css", FrontendDependencyUrlResolver + .resolveToContextRoot("https://cdn/x.css")); + Assert.assertEquals("//cdn/x.css", FrontendDependencyUrlResolver + .resolveToContextRoot("//cdn/x.css")); + } + + @Test + public void resolveToContextRoot_explicitProtocols_unchanged() { + Assert.assertEquals("context://foo.css", FrontendDependencyUrlResolver + .resolveToContextRoot("context://foo.css")); + Assert.assertEquals("base://foo.css", FrontendDependencyUrlResolver + .resolveToContextRoot("base://foo.css")); + } + + @Test + public void resolveToContextRoot_absoluteServerPath_unchanged() { + Assert.assertEquals("/assets/foo.css", FrontendDependencyUrlResolver + .resolveToContextRoot("/assets/foo.css")); + } + + @Test + public void resolveToContextRoot_dotSlashRelative_strippedAndPrefixed() { + Assert.assertEquals("context://foo.css", FrontendDependencyUrlResolver + .resolveToContextRoot("./foo.css")); + } + + @Test + public void resolveToContextRoot_bareRelative_prefixedWithContext() { + Assert.assertEquals("context://foo.css", + FrontendDependencyUrlResolver.resolveToContextRoot("foo.css")); + Assert.assertEquals("context://styles/foo.css", + FrontendDependencyUrlResolver + .resolveToContextRoot("styles/foo.css")); + } + + @Test + public void resolveToContextRoot_trimsWhitespace() { + Assert.assertEquals("context://foo.css", FrontendDependencyUrlResolver + .resolveToContextRoot(" foo.css ")); + } +} diff --git a/flow-server/src/test/java/com/vaadin/flow/server/communication/UidlWriterTest.java b/flow-server/src/test/java/com/vaadin/flow/server/communication/UidlWriterTest.java index 0538388caae..bd0cf6b3279 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/communication/UidlWriterTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/communication/UidlWriterTest.java @@ -282,13 +282,13 @@ void testComponentInterfaceDependencies_npmMode() throws Exception { .filterLazyLoading(getDependenciesMap(response)); assertEquals(4, dependenciesMap.size()); - assertDependency("childinterface1-" + CSS_STYLE_NAME, CSS_STYLE_NAME, + assertDependency("context://childinterface1-" + CSS_STYLE_NAME, + CSS_STYLE_NAME, dependenciesMap); + assertDependency("context://childinterface2-" + CSS_STYLE_NAME, + CSS_STYLE_NAME, dependenciesMap); + assertDependency("context://child1-" + CSS_STYLE_NAME, CSS_STYLE_NAME, dependenciesMap); - assertDependency("childinterface2-" + CSS_STYLE_NAME, CSS_STYLE_NAME, - dependenciesMap); - assertDependency("child1-" + CSS_STYLE_NAME, CSS_STYLE_NAME, - dependenciesMap); - assertDependency("child2-" + CSS_STYLE_NAME, CSS_STYLE_NAME, + assertDependency("context://child2-" + CSS_STYLE_NAME, CSS_STYLE_NAME, dependenciesMap); } @@ -330,14 +330,14 @@ void checkAllTypesOfDependencies_npmMode() throws Exception { hasSize(0)); ObjectNode eagerDependency = eagerDependencies.get(0); - assertEquals("eager.css", + assertEquals("context://eager.css", eagerDependency.get(Dependency.KEY_URL).textValue()); assertEquals(Dependency.Type.STYLESHEET, Dependency.Type .valueOf(eagerDependency.get(Dependency.KEY_TYPE).textValue())); List lazyDependencies = dependenciesMap.get(LoadMode.LAZY); ObjectNode lazyDependency = lazyDependencies.get(0); - assertEquals("lazy.css", + assertEquals("context://lazy.css", lazyDependency.get(Dependency.KEY_URL).textValue()); assertEquals(Dependency.Type.STYLESHEET, Dependency.Type .valueOf(lazyDependency.get(Dependency.KEY_TYPE).textValue())); @@ -355,9 +355,9 @@ void productionMode_stylesheetDependency_urlContainsHash() // Add resources so hash can be computed. Paths must match what // resolveResource() produces for the @StyleSheet annotation values. - mocks.getServlet().addServletContextResource("eager.css", + mocks.getServlet().addServletContextResource("/eager.css", "body { color: red; }"); - mocks.getServlet().addServletContextResource("lazy.css", + mocks.getServlet().addServletContextResource("/lazy.css", "body { color: blue; }"); UidlWriter uidlWriter = new UidlWriter(); @@ -386,7 +386,7 @@ void productionMode_stylesheetDependency_urlContainsHash() .findFirst().orElse(null); assertNotNull(eagerCss, "Should have an eager stylesheet dependency"); String eagerUrl = eagerCss.get(Dependency.KEY_URL).textValue(); - assertTrue(eagerUrl.matches("eager\\.css\\?" + assertTrue(eagerUrl.matches("context://eager\\.css\\?" + ApplicationConstants.CONTENT_HASH_PARAMETER + "=[0-9a-f]{8}"), "Eager stylesheet URL should contain hash: " + eagerUrl); @@ -400,7 +400,7 @@ void productionMode_stylesheetDependency_urlContainsHash() .findFirst().orElse(null); assertNotNull(lazyCss, "Should have a lazy stylesheet dependency"); String lazyUrl = lazyCss.get(Dependency.KEY_URL).textValue(); - assertTrue(lazyUrl.matches("lazy\\.css\\?" + assertTrue(lazyUrl.matches("context://lazy\\.css\\?" + ApplicationConstants.CONTENT_HASH_PARAMETER + "=[0-9a-f]{8}"), "Lazy stylesheet URL should contain hash: " + lazyUrl); @@ -501,6 +501,11 @@ private UI initializeUIForDependenciesTest(UI ui) throws Exception { for (String type : new String[] { "html", "js", "css" }) { mocks.getServlet().addServletContextResource("inline." + type, "inline." + type); + // After context-root URL normalization the lookup uses a leading + // slash; register both variants so component-level inline content + // can be resolved. + mocks.getServlet().addServletContextResource("/inline." + type, + "inline." + type); } HttpServletRequest servletRequestMock = mock(HttpServletRequest.class); @@ -529,16 +534,17 @@ private void addInitialComponentDependencies(UI ui, UidlWriter uidlWriter) { // UI parent first, then UI, then super component's dependencies, then // the interfaces and then the component - assertDependency("super-" + CSS_STYLE_NAME, CSS_STYLE_NAME, + assertDependency("context://super-" + CSS_STYLE_NAME, CSS_STYLE_NAME, dependenciesMap); - assertDependency("anotherinterface-" + CSS_STYLE_NAME, CSS_STYLE_NAME, - dependenciesMap); + assertDependency("context://anotherinterface-" + CSS_STYLE_NAME, + CSS_STYLE_NAME, dependenciesMap); - assertDependency("interface-" + CSS_STYLE_NAME, CSS_STYLE_NAME, + assertDependency("context://interface-" + CSS_STYLE_NAME, CSS_STYLE_NAME, dependenciesMap); - assertDependency(CSS_STYLE_NAME, CSS_STYLE_NAME, dependenciesMap); + assertDependency("context://" + CSS_STYLE_NAME, CSS_STYLE_NAME, + dependenciesMap); } private Map getDependenciesMap(ObjectNode response) { From 3190b0ee1945c076684aa9001a64826189dd0648 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Fri, 1 May 2026 11:12:06 +0300 Subject: [PATCH 3/5] format --- .../com/vaadin/flow/server/communication/UidlWriterTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flow-server/src/test/java/com/vaadin/flow/server/communication/UidlWriterTest.java b/flow-server/src/test/java/com/vaadin/flow/server/communication/UidlWriterTest.java index bd0cf6b3279..a274c4df7f3 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/communication/UidlWriterTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/communication/UidlWriterTest.java @@ -540,8 +540,8 @@ private void addInitialComponentDependencies(UI ui, UidlWriter uidlWriter) { assertDependency("context://anotherinterface-" + CSS_STYLE_NAME, CSS_STYLE_NAME, dependenciesMap); - assertDependency("context://interface-" + CSS_STYLE_NAME, CSS_STYLE_NAME, - dependenciesMap); + assertDependency("context://interface-" + CSS_STYLE_NAME, + CSS_STYLE_NAME, dependenciesMap); assertDependency("context://" + CSS_STYLE_NAME, CSS_STYLE_NAME, dependenciesMap); From 00a20cc178d5ecadef682c52fd5fcd5239d437e3 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Fri, 1 May 2026 13:41:54 +0000 Subject: [PATCH 4/5] feat: add type attribute to @JavaScript for runtime ES modules Lets a @JavaScript annotation render as a