I have a ReST API to an application with all controllers found under /api/
, these all return JSON responses with a @ControllerAdvice
which handles all exceptions to map to JSON formatted results.
This works great as of spring 4.0 @ControllerAdvice
now supports matching on annotations. What I can't work out is how to return a JSON result for a 401 - Unauthenticated and 400 - Bad Request responses.
Instead Spring is simply returning the response to the container (tomcat) which renders this as HTML. How can I intercept this and render a JSON result using the same technique that my @ControllerAdvice
is using.
security.xml
<bean id="xBasicAuthenticationEntryPoint"
class="com.example.security.XBasicAuthenticationEntryPoint">
<property name="realmName" value="com.example.unite"/>
</bean>
<security:http pattern="/api/**"
create-session="never"
use-expressions="true">
<security:http-basic entry-point-ref="xBasicAuthenticationEntryPoint"/>
<security:session-management />
<security:intercept-url pattern="/api/**" access="isAuthenticated()"/>
</security:http>
XBasicAuthenticationEntryPoint
public class XBasicAuthenticationEntryPoint extends BasicAuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException)
throws IOException, ServletException {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED,
authException.getMessage());
}
}
I can solve 401 by using the BasicAuthenticationEntryPoint
to write directly to the output stream, but I'm not sure it's the best approach.
public class XBasicAuthenticationEntryPoint extends BasicAuthenticationEntryPoint {
private final ObjectMapper om;
@Autowired
public XBasicAuthenticationEntryPoint(ObjectMapper om) {
this.om = om;
}
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException)
throws IOException, ServletException {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
om.writeValue(httpResponse.getOutputStream(),
new ApiError(request.getRequestURI(),
HttpStatus.SC_UNAUTHORIZED,
"You must sign in or provide basic authentication."));
}
}
I am yet to figure out how to handle 400 though, I once tried a catch all controller which did work, but it seemed that sometimes it would have odd conflicting behaviour with other controllers that I don't want to revisit.
My ControllerAdvice
implementation has a catch all which if spring throws any exception for bad request (400) it should in theory capture it, but it does not:
@ControllerAdvice(annotations = {RestController.class})
public class ApiControllerAdvisor {
@ExceptionHandler(Throwable.class)
public ResponseEntity<ApiError> exception(Throwable exception,
WebRequest request,
HttpServletRequest req) {
ApiError err = new ApiError(req.getRequestURI(), exception);
return new ResponseEntity<>(err, err.getStatus());
}
}
I have solved this by implementing my own version of HandlerExceptionResolver and subclassing DefaultHandlerExceptionResolver. It took a bit to work this out and you must override most methods, though the following has done exactly what I'm after.
Firstly the basics are to create an implementation.
And now register it in your servlet context.
This now does nothing different, but will be the first HandlerExceptionResolver to be tried and will match any request starting with a context path of
/api
(note: you could make this a configurable parameter).Next, we now can override any method that spring encounters errors on, there are 15 on my count.
What I have found is that I can write to the response directly and return an empty ModelAndView object or return null if my method did not eventually handle the fault which causes the next exception resolver to be tried.
As an example to handle situations where a request binding fault occurred I have done the following:
The only disadvantage here is that because I'm writing to the stream myself I'm not using any message converters to handle writing the response, so if I wanted to support XML and JSON API's at the same time it would not be possible, fortunately I'm only interested in supporting JSON, but this same technique could be enhanced to use view resolvers to determine what to render in etc.
If anyone has a better approach I'd still be interested in knowing how to deal with this.
You should look out to set error code in ResponseEntity, as below:
Simply catch the exception in your controller and return response you want to send.
I actually found myself asking the very same question a few weeks ago - As Dirk pointed out in the comments, @ControllerAdvice will only kick in if an exception is thrown from within a controller method, so will inevitably not catch all things (I was actually trying to solve the case for a JSON response for a 404 error).
The solution I settled on, although not entirely pleasant (hopefully if you get a better answer I will change my approach) is handling the error mapping in the Web.xml - I added the following, which will override the tomcat default error pages with specific URL mappings:
Now, if any page returns a 404, it is handled by my error controller something like this:
It all still feels pretty grim - and of course is a global handling of errors - so if you didn't always want a 404 response to be json (if you were serving a normal webapp of the same application) then it doesnt work so well. But like I said, its what I settled on to get moving, here's hoping there is a nicer way!