I’ve been working on a Symfony2 application whose user interface is presented as a single page application that makes heavy use of JavaScript. The app sends XHR requests to the Symfony2 backend API to retrieve and modify data, and it uses Symfony’s authorisation/authentication functionality to protect resources so they are only accessible to logged in users.
In an ideal world every request made by the application will succeed, but in reality things often go wrong; the user may have provided invalid data, their session may have expired meaning they are no longer logged in, or we may have a bug in our API code which causes a server error. With an out-of-the-box installation of Symfony2, each of these scenarios will result in the default error handler rendering a HTML page which isn’t much use to our client JavaScript application – instead we want the server to issue a JSON response to our AJAX request, complete with some contextual error details, with which we can give the user a meaningful error message.
I did a bit of Googling and came across a few posts which dealt with some of these challenges, and by putting them all together I arrived at a solution which I’ll detail here in case it’s of use to anyone else.
I’ve put together a demo app which hopefully illustrates everything. Have a play with it (installation instructions are in the README), then read on
The app consists of a few components:
Client side:
- A jQuery global AJAX error handler
- A login form – I have two, a Symfony2/Twig-based HTML login page which the user is redirected to if they try to access the app without a valid session, and a JavaScript login dialog which we will display if the server rejects an AJAX request due to authentication/authorisation failure, allowing the user to reauthenticate without leaving the app
Server side:
- An authentication failure handler
- An authentication success handler
- A kernel exception listener
As far as the rest of the app goes, I’m using the FOSUserBundle with a standard configuration, which supplies the rest of the auth functionality, including the HTML login page.
Let’s start with the server components – three classes and a bit of config:
The XHRAuthenticationSuccessHandler
and XHRAuthenticationFailureHandler
class’ code is triggered when the user logs in, or fails to log in, respectively. We check to see if the original request was an AJAX (XHR) one, and if so we respond with JSON. If it’s a failure we’re dealing with, we also include the exception message to give the user some feedback – note that this is a quick solution; it might be prudent to vet the message, or to translate it. I’ve also included a ‘success’ property which is a JavaScript convention that some frameworks (like ExtJS) use to execute appropriate callbacks. Finally, we wire up these handlers with the security component in our application’s security configuration.
The XHRCoreExceptionListener
class code is triggered whenever a kernel exception occurs – check the service definition for this listener, you’ll see we tell Symfony to call its onCoreException
method whenever an exception event is fired. Again, we only want to act if the request that caused the exception was an AJAX one. Assuming it is, we try to work out the status code to return from the exception code – if it’s a valid HTTP code, we use that, otherwise we assume a server error (500). As before, we’re reusing the exception message to provide context – but in this case, where the exception could relate to anything in the system (like a database query) we’d really want to be cautious about exposing it to the user, so this code certainly isn’t suitable for a production environment.
That’s it for the server, now for the client. All the functionality is in src/Acme/DemoBundle/Resources/views/Welcome/index.html.js
.
First we define a global error handler, which will be fired whenever an uncaught AJAX error occurs:
If the problem is that the user does not have a valid authentication token, we invite them to log in.
The login process is interesting – the modal login form is essentially a duplicate of the HTML one, but lacks the CSRF token which is automatically injected into the HTML form by Symfony. This helps prevent spoofing so I didn’t want to remove it, so the approach I took was to request the HTML login page, capture the value of the CSRF field in the response, then use that in a second request to actually authenticate the user. This meant I could reuse the same authentication code on the server.
The demo app also includes two other demonstrations – making a valid request (which will fail, and force a login, if the user does not have a valid session) and an invalid one (which displays the error message returned by the server). There’s not much to say about those, the code should be self explanatory.
That’s it really. The code is a little rough and ready, but hopefully it’ll give you enough to go on if you’re trying to do something similar!