Skip to content

Commit e21f537

Browse files
authored
feat: add UI.ensureCurrent() to provide better error message if UI is not available (#22665)
Add a new UI.ensureCurrent() method that returns a non-null UI instance or throws IllegalStateException with a helpful error message guiding developers to use UI.access() for background threads. This provides a cleaner alternative to the UI.getCurrent() + null check pattern and makes code more explicit about requiring an active UI context.
1 parent 4e1b59c commit e21f537

File tree

10 files changed

+70
-27
lines changed

10 files changed

+70
-27
lines changed

flow-server/src/main/java/com/vaadin/flow/component/ComponentUtil.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -536,11 +536,7 @@ public static <T extends Component> T componentFromElement(Element element,
536536

537537
try {
538538
Component.elementToMapTo.set(wrapData);
539-
UI ui = UI.getCurrent();
540-
if (ui == null) {
541-
throw new IllegalStateException("UI instance is not available. "
542-
+ "It looks like you are trying to execute UI code outside the UI/Servlet dispatching thread");
543-
}
539+
UI ui = UI.ensureCurrent();
544540
Instantiator instantiator = Instantiator.get(ui);
545541
return instantiator.createComponent(componentType);
546542
} finally {

flow-server/src/main/java/com/vaadin/flow/component/UI.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,33 @@ public static UI getCurrent() {
322322
return CurrentInstance.get(UI.class);
323323
}
324324

325+
/**
326+
* Gets the currently used UI, throwing an exception if none is available.
327+
* Use this method when the code must run within an active UI context.
328+
* <p>
329+
* This method is useful when code must execute within a UI context and
330+
* cannot handle the case where no UI is available. If the code can work
331+
* without a UI, use {@link #getCurrent()} instead and check for null.
332+
* <p>
333+
* The UI is stored using a weak reference to avoid leaking memory in case
334+
* it is not explicitly cleared.
335+
*
336+
* @return the current UI instance, never <code>null</code>
337+
* @throws IllegalStateException
338+
* if no UI is bound to the current thread
339+
* @see #getCurrent()
340+
* @see #access(Command)
341+
*/
342+
public static UI ensureCurrent() {
343+
UI ui = getCurrent();
344+
if (ui == null) {
345+
throw new IllegalStateException(
346+
"No currently active UI found. This code must be run within a UI context. "
347+
+ "If you are running this from a background thread, wrap the call in UI.access().");
348+
}
349+
return ui;
350+
}
351+
325352
/**
326353
* Marks this UI to be {@link #onDetach(DetachEvent) detached} from the
327354
* session at the end of the current request, or the next request if there

flow-server/src/main/java/com/vaadin/flow/component/page/WebStorage.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public static void setItem(String key, String value) {
9090
* the value
9191
*/
9292
public static void setItem(Storage storage, String key, String value) {
93-
setItem(UI.getCurrent(), storage, key, value);
93+
setItem(UI.ensureCurrent(), storage, key, value);
9494
}
9595

9696
/**
@@ -132,7 +132,7 @@ public static void removeItem(String key) {
132132
* the key to be deleted
133133
*/
134134
public static void removeItem(Storage storage, String key) {
135-
removeItem(UI.getCurrent(), storage, key);
135+
removeItem(UI.ensureCurrent(), storage, key);
136136
}
137137

138138
/**
@@ -165,7 +165,7 @@ public static void clear() {
165165
* the storage
166166
*/
167167
public static void clear(Storage storage) {
168-
clear(UI.getCurrent(), storage);
168+
clear(UI.ensureCurrent(), storage);
169169
}
170170

171171
/**
@@ -207,7 +207,7 @@ public static void getItem(String key, Callback callback) {
207207
* available.
208208
*/
209209
public static void getItem(Storage storage, String key, Callback callback) {
210-
getItem(UI.getCurrent(), storage, key, callback);
210+
getItem(UI.ensureCurrent(), storage, key, callback);
211211
}
212212

213213
/**
@@ -274,7 +274,7 @@ public static CompletableFuture<String> getItem(String key) {
274274
*/
275275
public static CompletableFuture<String> getItem(Storage storage,
276276
String key) {
277-
return getItem(UI.getCurrent(), storage, key);
277+
return getItem(UI.ensureCurrent(), storage, key);
278278
}
279279

280280
/**

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ public StreamRegistration registerResource(
124124
public StreamRegistration registerResource(
125125
ElementRequestHandler elementRequestHandler) {
126126
return registerResource(elementRequestHandler,
127-
UI.getCurrent().getElement());
127+
UI.ensureCurrent().getElement());
128128
}
129129

130130
/**

flow-server/src/main/java/com/vaadin/flow/server/communication/StreamRequestHandler.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,8 +244,8 @@ private PathData parsePath(String pathInfo) {
244244
* @return generated URI string
245245
*/
246246
public static String generateURI(String name, String id) {
247-
return DYN_RES_PREFIX + UI.getCurrent().getUIId() + PATH_SEPARATOR + id
248-
+ PATH_SEPARATOR + UrlUtil.encodeURIComponent(name);
247+
return DYN_RES_PREFIX + UI.ensureCurrent().getUIId() + PATH_SEPARATOR
248+
+ id + PATH_SEPARATOR + UrlUtil.encodeURIComponent(name);
249249
}
250250

251251
private static Optional<URI> getPathUri(String path) {

flow-server/src/main/java/com/vaadin/flow/server/communication/WebComponentBootstrapHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,7 @@ protected void writeBootstrapPage(String contentType,
441441
BootstrapHandler
442442
.getStylesheetLinks(context, "document.css",
443443
frontendDirectory)
444-
.forEach(link -> UI.getCurrent().getPage().executeJs(
444+
.forEach(link -> UI.ensureCurrent().getPage().executeJs(
445445
BootstrapHandler.SCRIPT_TEMPLATE_FOR_STYLESHEET_LINK_TAG,
446446
modifyPath(serviceUrl, link)));
447447
}

flow-server/src/test/java/com/vaadin/flow/component/UITest.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,6 +1028,36 @@ public void accessLaterRunnable_detachedUi_detachHandlerCalled() {
10281028
assertEquals("Handler should have run once", 1, runCount.get());
10291029
}
10301030

1031+
@Test
1032+
public void ensureCurrent_withCurrentUI_returnsUI() {
1033+
UI ui = createTestUI();
1034+
UI.setCurrent(ui);
1035+
1036+
UI result = UI.ensureCurrent();
1037+
1038+
assertSame("ensureCurrent should return the current UI", ui, result);
1039+
}
1040+
1041+
@Test(expected = IllegalStateException.class)
1042+
public void ensureCurrent_withoutCurrentUI_throws() {
1043+
CurrentInstance.clearAll();
1044+
UI.ensureCurrent();
1045+
}
1046+
1047+
@Test
1048+
public void ensureCurrent_withoutCurrentUI_throwsWithHelpfulMessage() {
1049+
CurrentInstance.clearAll();
1050+
try {
1051+
UI.ensureCurrent();
1052+
Assert.fail("Should have thrown IllegalStateException");
1053+
} catch (IllegalStateException e) {
1054+
assertTrue("Exception message should mention UI context",
1055+
e.getMessage().contains("UI context"));
1056+
assertTrue("Exception message should mention UI.access()",
1057+
e.getMessage().contains("UI.access()"));
1058+
}
1059+
}
1060+
10311061
@Test
10321062
public void csrfToken_differentUIs_shouldBeUnique() {
10331063
String token1 = new UI().getCsrfToken();

vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/VaadinRouteScope.java

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -455,12 +455,7 @@ private static String getWindowName(UI ui) {
455455
}
456456

457457
private static UI getUI() {
458-
UI ui = UI.getCurrent();
459-
if (ui == null) {
460-
throw new IllegalStateException(
461-
"There is no UI available. The route scope is not active");
462-
}
463-
return ui;
458+
return UI.ensureCurrent();
464459
}
465460

466461
private static UI findPreservingUI(UI ui) {

vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/VaadinUIScope.java

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -121,11 +121,6 @@ protected BeanStore getBeanStore() {
121121
}
122122

123123
private UI getUI() {
124-
UI ui = UI.getCurrent();
125-
if (ui == null) {
126-
throw new IllegalStateException(
127-
"There is no UI available. The UI scope is not active");
128-
}
129-
return ui;
124+
return UI.ensureCurrent();
130125
}
131126
}

vaadin-spring/src/main/java/com/vaadin/flow/spring/security/AuthenticationContext.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ public boolean isAuthenticated() {
131131
* {@link org.springframework.security.web.authentication.logout.LogoutHandler}.
132132
*/
133133
public void logout() {
134-
final UI ui = UI.getCurrent();
134+
final UI ui = UI.ensureCurrent();
135135
boolean pushWebsocketConnected = ui.getPushConfiguration()
136136
.getTransport() == Transport.WEBSOCKET
137137
&& ui.getInternals().getPushConnection().isConnected();

0 commit comments

Comments
 (0)