JSONP & exceptions with Spring web 4

Since Spring 4.1, it is really easy to enable JSONP on an API controller:

@RestController
@RequestMapping(value = "/")
public class MyController {

    @ControllerAdvice
    static class JsonpAdvice extends AbstractJsonpResponseBodyAdvice {
        public JsonpAdvice() {
            super("callback"); // name of the query parameter to use
        }
    }

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public MyAPIResult getStuff(...) {
        ...
    }
}

There is no RFC for JSONP. It is simply a recipe, without any reference document for implementation best practices.

Specifically, error handling can be difficult: if an API return a 4XX or 5XX error, the JSONP callback cannot catch them and will simply never be called, whereas the API actually responded.

In order to fix this, a solution is to make your API handle exceptions differently when a callback parameter is provided, and else to respond with 4XX/5XX HTTP codes as a REST API should do.

For example, in case of an error your API can pass to the Javascript callback an Error instead of a JSON object. The calling code can then react to errors as it wishes:

window.jsonp_callback = function (response) {
    if (response instanceof Error) {
        ...handle API error response
    } else {
        ...handle API successful response
    }
}

On the server side, with Spring, this can be accomplished with an @ExceptionHandler annotation:

@ControllerAdvice // Handle exceptions raised in all controllers
class GlobalControllerExceptionHandler {

    @ExceptionHandler(NotFoundException.class) // 404
    public ResponseEntity handleNotFoundException(NotFoundException exception,
                                                  HttpServletRequest request) {
        LOGGER.error("Error 404", exception);
        return createErrorResponse(HttpStatus.NOT_FOUND,
                                   request.getParameterMap(),
                                   exception.getMessage());
    }

    @ExceptionHandler(Exception.class)  // Catch any other exception -> 500
    public ResponseEntity handleException(Exception exception,
                                          HttpServletRequest request) {
        LOGGER.error("Error 500", exception);
        return createErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR,
                                   request.getParameterMap(),
                                   exception.getMessage());
    }

    static private ResponseEntity createErrorResponse(HttpStatus errorStatus,
                                                      Map<String, String[]> params,
                                                      String errorMsg) {
        if (params.containsKey("callback")) {
            final String jsError = String.format("%s(new Error('%d: %s'))",
                                                 params.get("callback")[0],
                                                 errorStatus.value(),
                                                 escapeEcmaScript(errorMsg));
            return new ResponseEntity<>(jsError, HttpStatus.OK);
        }
        return new ResponseEntity<>(createJsonResponse(errorMsg), errorStatus);
    }

    static private String createJsonResponse(String errorMsg) {
        final Map<String, Object> jsonData = new HashMap<>();
        jsonData.put("error", errorMsg);
        return StringFormatUtils.multilinesPrettyFormat(jsonData);
    }
}