Skip to content

Commit 1b6a376

Browse files
mcollovatiArtur-
andauthored
fix: refresh error views automatically on hotswap (#23272) (#23311)
When a class breaks during development, Flow shows an error view. Previously, when the class was fixed and hotswapped, the error view remained visible because the hotswap system only refreshed views that used the changed class in their route chain. This fix adds ErrorViewHotswapper plugin that detects when an error view is displayed (by checking if the current view implements HasErrorParameter) and triggers a browser refresh on any hotswap. The refresh re-navigates to the original URL, allowing the fixed code to be properly loaded. Adds unit tests to verify that ErrorViewHotswapper correctly: - Triggers UI refresh when an error view is shown during hotswap - Does not trigger refresh for normal views during hotswap Co-authored-by: Artur Signell <artur@vaadin.com>
1 parent 9b6ab40 commit 1b6a376

File tree

5 files changed

+198
-0
lines changed

5 files changed

+198
-0
lines changed

flow-server/src/main/java/com/vaadin/flow/component/internal/UIInternals.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1203,6 +1203,21 @@ public void refreshCurrentRoute(boolean refreshRouteChain) {
12031203
}
12041204
}
12051205

1206+
/**
1207+
* Checks if an error view is currently being displayed. An error view is a
1208+
* component that implements HasErrorParameter.
1209+
*
1210+
* @return true if showing an error view, false otherwise
1211+
*/
1212+
public boolean isShowingErrorView() {
1213+
if (routerTargetChain.isEmpty()) {
1214+
return false;
1215+
}
1216+
// The first element in the chain is the actual view component
1217+
HasElement target = routerTargetChain.get(0);
1218+
return target instanceof com.vaadin.flow.router.HasErrorParameter;
1219+
}
1220+
12061221
/**
12071222
* Check if we have already started navigation to some location on this
12081223
* roundtrip.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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.hotswap.impl;
17+
18+
import jakarta.annotation.Priority;
19+
20+
import com.vaadin.flow.component.UI;
21+
import com.vaadin.flow.hotswap.HotswapClassSessionEvent;
22+
import com.vaadin.flow.hotswap.UIUpdateStrategy;
23+
import com.vaadin.flow.hotswap.VaadinHotswapper;
24+
import com.vaadin.flow.server.VaadinSession;
25+
26+
/**
27+
* Triggers UI refresh when hotswap occurs while an error view is displayed.
28+
* This ensures that fixing a broken class during development will refresh the
29+
* error page and attempt to re-navigate to the original location.
30+
* <p>
31+
* For internal use only. May be renamed or removed in a future release.
32+
*
33+
* @since 25.0
34+
*/
35+
@Priority(100)
36+
public class ErrorViewHotswapper implements VaadinHotswapper {
37+
38+
@Override
39+
public void onClassesChange(HotswapClassSessionEvent event) {
40+
// Only process redefined classes (not first-time loads)
41+
if (!event.isRedefined()) {
42+
return;
43+
}
44+
45+
VaadinSession session = event.getVaadinSession();
46+
47+
// Check each UI in the session
48+
for (UI ui : session.getUIs()) {
49+
if (ui.isClosing()) {
50+
continue;
51+
}
52+
53+
// If showing error view, trigger refresh to re-attempt navigation
54+
if (ui.getInternals().isShowingErrorView()) {
55+
event.triggerUpdate(ui, UIUpdateStrategy.REFRESH);
56+
}
57+
}
58+
}
59+
}

flow-server/src/main/resources/META-INF/services/com.vaadin.flow.hotswap.VaadinHotswapper

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ com.vaadin.flow.component.internal.StyleSheetHotswapper
1818
com.vaadin.flow.internal.ReflectionCacheHotswapper
1919
com.vaadin.flow.i18n.DefaultTranslationsHotswapper
2020
com.vaadin.flow.router.internal.RouteRegistryHotswapper
21+
com.vaadin.flow.hotswap.impl.ErrorViewHotswapper
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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.hotswap.impl;
17+
18+
import java.util.Collections;
19+
import java.util.Set;
20+
21+
import org.junit.Assert;
22+
import org.junit.Before;
23+
import org.junit.Test;
24+
import org.mockito.Mockito;
25+
26+
import com.vaadin.flow.component.Component;
27+
import com.vaadin.flow.component.Tag;
28+
import com.vaadin.flow.hotswap.HotswapClassSessionEvent;
29+
import com.vaadin.flow.hotswap.UIUpdateStrategy;
30+
import com.vaadin.flow.internal.CurrentInstance;
31+
import com.vaadin.flow.router.BeforeEnterEvent;
32+
import com.vaadin.flow.router.ErrorParameter;
33+
import com.vaadin.flow.router.HasErrorParameter;
34+
import com.vaadin.flow.router.Location;
35+
import com.vaadin.flow.server.MockVaadinServletService;
36+
import com.vaadin.flow.server.VaadinSession;
37+
import com.vaadin.tests.util.AlwaysLockedVaadinSession;
38+
import com.vaadin.tests.util.MockUI;
39+
40+
public class ErrorViewHotswapperTest {
41+
42+
private ErrorViewHotswapper hotswapper;
43+
private MockVaadinServletService service;
44+
private VaadinSession session;
45+
private MockUI ui;
46+
47+
@Before
48+
public void setUp() {
49+
CurrentInstance.clearAll();
50+
service = new MockVaadinServletService();
51+
session = new AlwaysLockedVaadinSession(service);
52+
ui = new MockUI(session);
53+
ui.doInit(null, 42, "test");
54+
session.addUI(ui);
55+
hotswapper = new ErrorViewHotswapper();
56+
}
57+
58+
private Location createMockLocation(String path) {
59+
Location location = Mockito.mock(Location.class);
60+
Mockito.when(location.getPath()).thenReturn(path);
61+
return location;
62+
}
63+
64+
@Test
65+
public void onClassesChange_errorViewShown_redefined_triggersRefresh() {
66+
// Simulate an error view being displayed
67+
TestErrorView errorView = new TestErrorView();
68+
ui.getInternals().showRouteTarget(createMockLocation("error"),
69+
errorView, Collections.emptyList());
70+
71+
// Verify error view is actually showing
72+
Assert.assertTrue("Error view should be showing",
73+
ui.getInternals().isShowingErrorView());
74+
75+
// Simulate a class being redefined (hotswap)
76+
var event = new HotswapClassSessionEvent(service, session,
77+
Set.of(String.class), true);
78+
hotswapper.onClassesChange(event);
79+
80+
// Verify refresh was triggered
81+
Assert.assertEquals("Should trigger refresh when error view is shown",
82+
UIUpdateStrategy.REFRESH,
83+
event.getUIUpdateStrategy(ui).orElse(null));
84+
}
85+
86+
@Test
87+
public void onClassesChange_normalViewShown_redefined_noRefresh() {
88+
// Simulate a normal view being displayed
89+
TestNormalView normalView = new TestNormalView();
90+
ui.getInternals().showRouteTarget(createMockLocation("normal"),
91+
normalView, Collections.emptyList());
92+
93+
// Verify error view is not showing
94+
Assert.assertFalse("Normal view should not be an error view",
95+
ui.getInternals().isShowingErrorView());
96+
97+
// Simulate a class being redefined (hotswap)
98+
var event = new HotswapClassSessionEvent(service, session,
99+
Set.of(String.class), true);
100+
hotswapper.onClassesChange(event);
101+
102+
// Verify refresh was not triggered
103+
Assert.assertFalse("Should not trigger refresh for normal view",
104+
event.getUIUpdateStrategy(ui).isPresent());
105+
}
106+
107+
// Test classes
108+
109+
@Tag("div")
110+
public static class TestErrorView extends Component
111+
implements HasErrorParameter<Exception> {
112+
@Override
113+
public int setErrorParameter(BeforeEnterEvent event,
114+
ErrorParameter<Exception> parameter) {
115+
return 500;
116+
}
117+
}
118+
119+
@Tag("div")
120+
public static class TestNormalView extends Component {
121+
}
122+
}

flow-test-generic/src/main/java/com/vaadin/flow/testutil/ClassesSerializableTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ protected Stream<String> getExcludedPatterns() {
125125
"com\\.vaadin\\.flow\\.hotswap\\.Hotswap.*Event(\\$.*)?",
126126
"com\\.vaadin\\.flow\\.hotswap\\.Hotswapper",
127127
"com\\.vaadin\\.flow\\.hotswap\\.VaadinHotswapper",
128+
"com\\.vaadin\\.flow\\.hotswap\\.impl\\.ErrorViewHotswapper",
128129
"com\\.vaadin\\.flow\\.i18n\\.DefaultTranslationsHotswapper",
129130
"com\\.vaadin\\.flow\\.internal\\.BrowserLiveReloadAccessor",
130131
"com\\.vaadin\\.flow\\.internal\\.BrowserLiveReloadAccess",

0 commit comments

Comments
 (0)