2

When a DataProvider fetch or count method throws an exception, e.g. because the user is not authorized, how could I handle these exceptions centrally? I know there is HasErrorParameter interface to show error views when there is an exception thrown when routing. But these error views are not triggered when DataProvider throws the exception.

Example:

  new AbstractBackEndDataProvider<String, Void>() {
        @Override
        protected Stream<String> fetchFromBackEnd(Query<String, Void> query) {
            ...
        }

        @Override
        protected int sizeInBackEnd(Query<String, Void> query) {
            throw new UnsupportedOperationException("test");
        }
    }

@Route("failed")
public class FailView extends VerticalLayout 
         implements HasErrorParameter<UnsupportedOperationException> {...}

Even if I do a try catch within the DataProvider methods, I don't see how I could navigate to the appropriate error view just by using the caught exception and not the view component class (this wouldn't trigger setErrorParameter method).

BTW: I miss the router exception handling topic in Vaadin Flow 13 documentation. I wonder why they removed it.

Steffen Harbich
  • 2,639
  • 2
  • 37
  • 71
  • 1
    This is a known issue/limitation/bug in current HesErrorParameter implementation and there is many issue tickets filed related to this https://github.com/vaadin/flow/issues/4715 https://github.com/vaadin/flow/issues/4549 https://github.com/vaadin/flow/issues/4607 https://github.com/vaadin/flow/issues/3192 – Tatu Lund Mar 18 '19 at 08:43

1 Answers1

4

I believe all Exceptions that don't occur while routing will be given to the ErrorHandler of the VaadinSession the error occured in.

The best way to set the ErrorHandler seems to be to override the sessionInit method in a custom SessionInitListener

You can add a custom SessionInitListener inside the servletInitialized method of a custom VaadinServlet.

class CustomServlet extends VaadinServlet{
    @Override
    protected void servletInitialized() throws ServletException {
        super.servletInitialized();
        getService().addSessionInitListener(new CustomSessionInitListener());
    }
}

And that SessionInitListener (in this example CustomSessionInitListener) has to set the errorHandler of the sessions that get initialized.

class CustomSessionInitListener implements SessionInitListener{
    @Override
    public void sessionInit(SessionInitEvent event) throws ServiceException {
        event.getSession().setErrorHandler(new CustomErrorHandler());
    }
}

For further information on how to create your own Servlet take a look at Vaadin's tutorial page(you need to scroll down to "Customizing Vaadin Servlet")

Edit: To show the error page you need to get Vaadin to reroute to an error. To achieve that we can use an BeforeEnterEvent, BeforeEnterEvents have a rerouteToError method which we can use to let Vaadin show our ErrorView.

But we also want to pass along the Exception instance, so we have to store that as well. I did exactly that with the following class:

@Route("error-view") // Route shown in the user's browser
public class ErrorViewShower extends Div implements BeforeEnterObserver {
    // Class to store the current Exception of each UI in
    private static class UIExceptionContainer extends HashMap<UI, Exception> {

    }

    // Method to call when we want to show an error
    public static void showError(Exception exception) {
        UIExceptionContainer exceptionContainer = VaadinSession.getCurrent().getAttribute(UIExceptionContainer.class);
        // Creating and setting the exceptionContainer in case it hasn't been set yet.
        if (exceptionContainer == null) {
            exceptionContainer = new UIExceptionContainer();
            VaadinSession.getCurrent().setAttribute(UIExceptionContainer.class, exceptionContainer);
        }

        // Storing the exception for the beforeEnter method
        exceptionContainer.put(UI.getCurrent(), exception);

        // Now we navigate to an Instance of this class, to use the BeforeEnterEvent to reroute to the actual error view
        UI.getCurrent().navigate(ErrorViewShower.class);// If this call doesn't work you might want to wrap into UI.access
    }

    @Override
    public void beforeEnter(BeforeEnterEvent event) {
        UIExceptionContainer exceptionContainer = VaadinSession.getCurrent().getAttribute(UIExceptionContainer.class);

        // Retrieving the previously stored exception. You might want to handle if this has been called without setting any Exception.
        Exception exception = exceptionContainer.get(UI.getCurrent());

        //Clearing out the now handled Exception
        exceptionContainer.remove(UI.getCurrent());

        // Using the BeforeEnterEvent to show the error
        event.rerouteToError(exception, "Possible custom message for the ErrorHandler here");
    }

}

Usage of it in combination with the error handler looks like this:

public class CustomErrorHandler implements ErrorHandler {
    @Override
    public void error(ErrorEvent event) {
        // This can easily throw an exception itself, you need to add additional checking before casting.
        // And it's possible that this method is called outside the context of an UI(when a dynamic resource throws an exception for example)
        Exception exception = (Exception) event.getThrowable();
        ErrorViewShower.showError(exception);
    }

}

Edit2: As it turns out that Exceptions occuring inside internal method calls don't get handled by the UI's ErrorHandler or the VaadinSession's ErrorHandler but instead by another error handler which causes the client side to terminate and show the Error Notification,

a solution is to catch the Exceptions inside the methods of the DataProvider and pass them to ErrorViewShower.showError() and still return without any Exception flying the stacktrace upwards. (Or don't throw any Exception yourself and instead simply pass a new to the ErrorViewShower.showError() method).

By returning normally Vaadin doesn't even know something went wrong.
ErrorViewShower.showError() calls ui.navigate, that navigation command seems to get "queued" behind the calls to the DataProvider, meaning the view of the user will change in the same request.

Dataprovider with such an implementation:

new AbstractBackEndDataProvider<String, Void>() {
    @Override
    protected Stream<String> fetchFromBackEnd(Query<String, Void> query) {
        try{
            //Code that can throw an Exception here
        }catch(Exception e){
            ErrorViewShower.showError(e);
            //We have to make sure that query.getLimit and query.getOffset gets called, otherwise Vaadin throws an Exception with the message "the data provider hasn't ever called getLimit() method on the provided query. It means that the the data provider breaks the contract and the returned stream contains unxpected data."
            query.getLimit();
            query.getOffset();
            return Stream.of(); //Stream of empty Array to return without error
        }
    }

    @Override
    protected int sizeInBackEnd(Query<String, Void> query) {
        //Second way i mentioned, but this will not catch any Exception you didn't create, where as the try...catch has no way to let any Exception reach Vaadin.
        if(badThingsHappened){
            ErrorViewShower.showError(new UnsupportedOperationException("Bad things..."));
            return 0;//Exiting without error
        }
    }
}
froemijojo
  • 91
  • 1
  • 4
  • Indeed, the error handler is called, but that doesn't help me much because navigation or `UI.getCurrent().getPage().executeJavaScript("window.location = '/my-error-page'")` does not work within the error handler. My goal is to show an appropriate error page. – Steffen Harbich Mar 26 '19 at 08:51
  • 1
    @SteffenHarbich i edited my answer to include a way to get Vaadin to reroute to an error view. – froemijojo Mar 26 '19 at 11:10
  • Thank you for your answers. So this code is working in your case? I ask because when I call a `UI.getCurrent().navigate` in my error handler nothing happens except the usual Vaadin Flow error panel "Internal Error..take note of unsaved data..." in the upper right corner of the view. No redirect. – Steffen Harbich Mar 26 '19 at 12:21
  • Copied your approach, still getting no redirect to error view. – Steffen Harbich Mar 26 '19 at 12:36
  • Yes it works exactly as expected. (View changes to the class with the appriorate HasErrorParameter, Browser URL changes to /error-view) – froemijojo Mar 26 '19 at 14:22
  • but "Vaadin Flow error panel "Internal Error..take note of unsaved data" sounds like the Exception didn't reach your Errorhandler, but Vaadin's default ErrorHandler, are you sure you set your ErrorHandler right? If you literally copy-pasted my code you need add the @WebServlet annotation to CustomServlet([see here](https://vaadin.com/docs/v13/flow/advanced/tutorial-application-lifecycle.html)) – froemijojo Mar 26 '19 at 14:24
  • I am using Spring Boot, so I coded a component implementing VaadinServiceInitListener to register the SessionInitListener and the ErrorHandler finally. The error handler is invoked as expected and I can step over `UI.getCurrent().navigate(ErrorViewShower.class)` call in debug mode. Even the `beforeEnter` is called. What's your Vaadin version? – Steffen Harbich Mar 26 '19 at 14:53
  • I'm using Vaadin 13.0.2. I just created a stripped down copy of my application, and changed the Vaadin Version to 10.0.11, there it **doesn't** work. But changing nothing except the Vaadin Version to 13.0.2 makes it work. It works because the client side doesn't show the error panel and shuts down, but instead keeps running. [This Vaadin Forum- Thread](https://vaadin.com/forum/thread/17064883/custom-error-handlers) says that the client side terminates when an error occurs, but that was 10 months ago. Seems like they changed that in the newer versions(at least Vaadin 13). – froemijojo Mar 27 '19 at 08:16
  • I am using Vaadin 13.0.2 and am totally clueless, why it works in your application but not in mine... will try to build a minimal example. – Steffen Harbich Mar 27 '19 at 14:52
  • My [minimal example](https://www.dropbox.com/s/pp2729kwuvbesid/my-starter-project_55210726.zip?dl=0) did not reproduce your behavior. Can please have a look into it? It's based on a Vaadin Flow starter project with no spring. Thousand thanks! – Steffen Harbich Mar 27 '19 at 18:34
  • Uhm, i only tested Exceptions outside a Dataprovider, there it does work. Seems like Exceptions inside such internal methods aren't handled the same way than it's calling other listeners. I've edited(or am still editing) my answer to include an idea i've to circumvent that. – froemijojo Mar 27 '19 at 20:48
  • 1
    Nice, that works fine. Let's hope Vaadin will fix the exception handling some day, so this workaround will not be required anymore. Thanks a lot for your help! – Steffen Harbich Mar 28 '19 at 06:52