Skip to content

fix!: auto-apply context:// for @StyleSheet values#24235

Merged
Artur- merged 6 commits into
mainfrom
auto-context-url-rewrite-stylesheet
May 8, 2026
Merged

fix!: auto-apply context:// for @StyleSheet values#24235
Artur- merged 6 commits into
mainfrom
auto-context-url-rewrite-stylesheet

Conversation

@Artur-
Copy link
Copy Markdown
Member

@Artur- Artur- commented May 1, 2026

@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 #22888

@Artur- Artur- marked this pull request as draft May 1, 2026 07:30
@github-actions github-actions Bot added the +0.1.0 label May 1, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 1, 2026

Test Results

 1 405 files  +1   1 405 suites  +1   1h 21m 40s ⏱️ - 1m 2s
10 136 tests +8  10 066 ✅ +8  70 💤 ±0  0 ❌ ±0 
10 611 runs  +8  10 532 ✅ +8  79 💤 ±0  0 ❌ ±0 

Results for commit 94b1eef. ± Comparison against base commit 09ccde4.

♻️ This comment has been updated with latest results.

@Stylesheet currently produces broken <link> elements when the Vaadin
servlet is mapped at a non-root path (vaadin.urlMapping=/ui/* etc.):
the browser resolves the bare relative href against <base> (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 <link>.
- 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 #22888.
@Artur- Artur- force-pushed the auto-context-url-rewrite-stylesheet branch from 3190b0e to 7bc1d84 Compare May 4, 2026 08:47
@Artur- Artur- marked this pull request as ready for review May 4, 2026 08:48
@mshabarov mshabarov self-requested a review May 4, 2026 11:41
…ite-stylesheet

# Conflicts:
#	flow-server/src/test/java/com/vaadin/flow/server/communication/UidlWriterTest.java
@github-actions github-actions Bot added +1.0.0 and removed +0.1.0 labels May 6, 2026
@github-actions github-actions Bot added +0.1.0 and removed +1.0.0 labels May 6, 2026
Copy link
Copy Markdown
Contributor

@mshabarov mshabarov left a comment

Choose a reason for hiding this comment

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

Not yet tested, but I'm pretty sure that the hotswapper related functionality will be broken without changes in StyleSheetHotswapper because of the reasons listed by Claude (mainly because the hotswapper uses the old raw stylesheet urls everywhere).

@mshabarov
Copy link
Copy Markdown
Contributor

The existing test masks the regression:

StyleSheetHotswapperTest#onClassesChange_componentModifyAnnotation_updatesStylesheet passes after the PR for two compounding reasons:

  • Line 340 calls clearPendingSendToClient(), which marks the (un-removed) old context://styles/component.css as already sent. currentStylesheets() only iterates getPendingSendToClient(), so the orphaned old
    URL is invisible.
  • hasStylesheet(String urlPart) uses url.contains(urlPart), so "context://styles/modified-component.css".contains("styles/modified-component.css") returns true.

So assertEquals(initialCount, countStylesheets()) evaluates 1 == 1 even though the dep list actually contains two entries (one stale context://..., one new bare styles/...). The test does not exercise
pendingStyleSheetRemovals for the component branch.

Artur- added 3 commits May 7, 2026 13:28
UIInternals.addComponentDependencies registers stylesheet dependencies
under the resolved (context://) URL, but StyleSheetHotswapper was still
looking up, adding, and tracking by the raw @StyleSheet.value(). This
broke hot-reload of @Stylesheet annotations: removal lookups missed the
existing dependency (leaving the stale <link> in the DOM), the new
dependency was added with a bare relative URL (regressing under
vaadin.urlMapping), and ActiveStyleSheetTracker accumulated duplicate
keys per stylesheet.

Resolve URLs through FrontendDependencyUrlResolver in updateUIs and
lookupUrlsForComponents so all hotswap operations key on the same
canonical form as addComponentDependencies. The appShell- removal id
keeps the raw value because AppShellRegistry keys it that way.

Also rewrite the modify-annotation test to mirror the real hot-reload
sequence (HotswapClassEvent before HotswapClassSessionEvent) so it
exercises the first-time removal path, captures the original dependency
id, and asserts on pendingStyleSheetRemovals — instead of relying on
clearPendingSendToClient to mask the missing removal.
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented May 8, 2026

Copy link
Copy Markdown
Contributor

@mshabarov mshabarov left a comment

Choose a reason for hiding this comment

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

Looks good to me and works as expected - checked that styles are applied on app shell and components level for no context, non-empty context and custom servlet path, hotswapping works as expected.

One small thing is that the traversal warning is reported 3 times in the log. I have a local patch for it, but can push it in a new PR.

@Artur- Artur- enabled auto-merge May 8, 2026 12:05
@Artur- Artur- added this pull request to the merge queue May 8, 2026
Merged via the queue into main with commit 28e1cc9 May 8, 2026
30 of 31 checks passed
@Artur- Artur- deleted the auto-context-url-rewrite-stylesheet branch May 8, 2026 12:26
@vaadin-bot
Copy link
Copy Markdown
Collaborator

This ticket/PR has been released with Vaadin 25.2.0-alpha7.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add "context://" automatically for custom URL mapping

3 participants