Router Exception Handling

There is special support for navigation targets that are activated for an unhandled exception thrown during navigation to show an “error view” to the user.

These targets generally work in the same way as regular navigation targets, although they do typically not have any specific @Route as they are shown for arbitrary URLs.

Error navigation is resolved to a target based on the exception type thrown during navigation.

At startup all classes implementing the interface HasErrorParameter<T extends Exception> will be collected to be used as exception targets during navigation.

For instance here is the default target for the NotFoundException that will be shown when there is no target for the given url.

RouteNotFoundError for NotFoundException during routing

  1. @Tag(Tag.DIV)
  2. public class RouteNotFoundError extends Component
  3. implements HasErrorParameter<NotFoundException> {
  4. @Override
  5. public int setErrorParameter(BeforeEnterEvent event,
  6. ErrorParameter<NotFoundException> parameter) {
  7. getElement().setText("Could not navigate to '"
  8. + event.getLocation().getPath() + "'");
  9. return HttpServletResponse.SC_NOT_FOUND;
  10. }
  11. }

This will return an http response of 404 and show the set text to the user.

Exception matching will run first by exception cause and then by exception super type.

Default exceptions that are implemented are RouteNotFoundError for NotFoundException with a 404 and InternalServerError for java.lang.Exception with a 500.

The default exception handlers can be overridden by extending them like:

Custom route not found that is using our application layout

  1. @ParentLayout(MainLayout.class)
  2. public class CustomNotFoundTarget extends RouteNotFoundError {
  3. @Override
  4. public int setErrorParameter(BeforeEnterEvent event,
  5. ErrorParameter<NotFoundException> parameter) {
  6. getElement().setText("My custom not found class!");
  7. return HttpServletResponse.SC_NOT_FOUND;
  8. }
  9. }

As a more complex sample we could have a Dashboard that collects and shows widgets to the user and can have widgets that should not be shown for unauthenticated users. For some reason a ProtectedWidget is loaded for an un-authenticated user.

The collection should have caught the protected widget, but for some reason instantiates it, but luckily the widget checks authentication on creation and throws and AccessDeniedException.

This unhandled exception propagates during navigation and is handled by the AccessDeniedExceptionHandler that still keeps the MainLayout with its menu bar, but displays information that an exception has happened.

Access denied exception sample when protected widget is loaded by mistake

  1. @Route(value = "dashboard", layout = MainLayout.class)
  2. @Tag(Tag.DIV)
  3. public class Dashboard extends Component {
  4. public Dashboard() {
  5. init();
  6. }
  7. private void init() {
  8. getWidgets().forEach(this::addWidget);
  9. }
  10. public void addWidget(Widget widget) {
  11. // Implementation omitted
  12. }
  13. private Stream<Widget> getWidgets() {
  14. // Implementation omitted, gets faulty state widget
  15. return Stream.of(new ProtectedWidget());
  16. }
  17. }
  18. public class ProtectedWidget extends Widget {
  19. public ProtectedWidget() {
  20. if (!AccessHandler.getInstance().isAuthenticated()) {
  21. throw new AccessDeniedException("Unauthorized widget access");
  22. }
  23. // Implementation omitted
  24. }
  25. }
  26. @Tag(Tag.DIV)
  27. public abstract class Widget extends Component {
  28. public boolean isProtected() {
  29. // Implementation omitted
  30. return true;
  31. }
  32. }
  33. @Tag(Tag.DIV)
  34. @ParentLayout(MainLayout.class)
  35. public class AccessDeniedExceptionHandler extends Component
  36. implements HasErrorParameter<AccessDeniedException> {
  37. @Override
  38. public int setErrorParameter(BeforeEnterEvent event,
  39. ErrorParameter<AccessDeniedException> parameter) {
  40. getElement().setText(
  41. "Tried to navigate to a view without correct access rights");
  42. return HttpServletResponse.SC_FORBIDDEN;
  43. }
  44. }
Note
Exception targets may define ParentLayouts and BeforeNavigationEvent and AfterNavigationEvent will be sent as for normal navigation.
Note
One exception may only have one exception handler (only extending instances are allowed).

Rerouting to an error view

It is possible to reroute to an error view registered for an exception from the BeforeEnterEvent and BeforeLeaveEvent.

The rerouting is done by using one of the overloads for rerouteToError with only the exception class to target or with an added custom error message.

Reroute to error view

  1. public class AuthenticationHandler implements BeforeEnterObserver {
  2. @Override
  3. public void beforeEnter(BeforeEnterEvent event) {
  4. Class<?> target = event.getNavigationTarget();
  5. if (!currentUserMayEnter(target)) {
  6. event.rerouteToError(AccessDeniedException.class);
  7. }
  8. }
  9. private boolean currentUserMayEnter(Class<?> target) {
  10. // implementation omitted
  11. return false;
  12. }
  13. }
Note
In cases where the rerouting method catches an exception and there is a need to add a custom message it is possible to use the rerouteToError(Exception, String) method to set a custom message.

Blog sample error view with a custom message

  1. @Tag(Tag.DIV)
  2. public class BlogPost extends Component implements HasUrlParameter<Long> {
  3. @Override
  4. public void setParameter(BeforeEvent event, Long parameter) {
  5. removeAll();
  6. Optional<BlogRecord> record = getRecord(parameter);
  7. if (!record.isPresent()) {
  8. event.rerouteToError(IllegalArgumentException.class,
  9. getTranslation("blog.post.not.found",
  10. event.getLocation().getPath()));
  11. } else {
  12. displayRecord(record.get());
  13. }
  14. }
  15. private void removeAll() {
  16. // NO-OP
  17. }
  18. private void displayRecord(BlogRecord record) {
  19. // NO-OP
  20. }
  21. public Optional<BlogRecord> getRecord(Long id) {
  22. // Implementation omitted
  23. return Optional.empty();
  24. }
  25. }
  26. @Tag(Tag.DIV)
  27. public class FaultyBlogPostHandler extends Component
  28. implements HasErrorParameter<IllegalArgumentException> {
  29. @Override
  30. public int setErrorParameter(BeforeEnterEvent event,
  31. ErrorParameter<IllegalArgumentException> parameter) {
  32. Label message = new Label(parameter.getCustomMessage());
  33. getElement().appendChild(message.getElement());
  34. return HttpServletResponse.SC_NOT_FOUND;
  35. }
  36. }