Tobia
Tobia

Reputation: 18811

Send a specific response, or at least a specific HTTP status code, using an exception

In Django, there are a couple of exceptions that are designed to be intercepted by the framework and turned into specific HTTP response codes, such as 404 Not Found and 403 Forbidden.

This is especially useful for request validation, because it allows you to factor out common validation logic into utility functions and cleanup your controller actions.

Whenever the utility functions decide that the current request must be aborted with a specific HTTP error code, they can do so by throwing the relevant exception, without any support code in the controller action, in the form of a return statement or a try/catch.

For example, given a tree of nested REST resources:

static mappings = {
    "/authors" (resources: "author") {
        "/sagas" (resources: "saga") {
            "/books" (resources: "book") {
        }
    }
}

Then the URL pattern for the Book resource is /authors/$authorId/sagas/$sagaId/books/$id, which means that any of the show(), delete(), or update() actions in BookController have this signature and must include some boilerplate validation logic:

def actionName(int authorId, int sagaId, Book book) {

    // -- common validation logic ----------
    // fetch parent objects
    def author = Author.get(authorId)
    def saga = Saga.get(sagaId)
    // check that they exists
    if (author == null || saga == null || book == null) {
        return render(status: NOT_FOUND)
    }
    // check consistency
    if (book.author != author || book.saga != saga || saga.author != author) {
        return render(status: BAD_REQUEST)
    }
    // -- end of commond code --------------

    ...
}

What is the Grails way of factoring this out into a common method, while still allowing it to terminate request processing whenever an exceptional condition occurs?

I would think the best way is a NotFoundException, ForbiddenException, BadRequestException, and so on, or maybe a generic exception that accepts a HTTP status code. Is there anything like it in Grails? If not, where is the best place to add it? A filter?


Edit: I see now that the standard method is to add an error controller with a matching URL pattern, such as:

"500" (controller: "error")

The problem with this is that Grails will still log full stacktraces for all exceptions, including those that are not programming errors. This spams log files with all sorts of useless tracebacks.

Is there a solution?

Upvotes: 2

Views: 1953

Answers (1)

thewidgetsmith
thewidgetsmith

Reputation: 194

You catch the exception in the beforeInterceptor closure of your controller. I resolved this same problem by examining the exception thrown and then acting accordingly. For example:

class BaseController {

/**
 * Define DRA exception handlers. This prevents the default Grails
 * behavior of returning an HTTP 500 error for every exception.
 *
 * Instead the exceptions are intercepted and modified according to
 * the exception that was thrown. These exceptions are not logged
 * whereas application exceptions are.
 */
def beforeInterceptor = {

    request.exceptionHandler = { exception ->

        def cause = exception.cause
        def exceptionBody = [:]

        if(cause.class == BadRequestException) {
            response.setStatus(HttpStatus.BAD_REQUEST.value()) // HTTP 400 BAD REQUEST
            exceptionBody.httpStatus = HttpStatus.BAD_REQUEST.value()
            exceptionBody.error = cause.message
        }

        // render the exception body, the status code is set above.
        render exceptionBody as JSON

        return true

    }

}
}

In order to get this to work you will have to create an ErrorController or something where all server errors are processed and rendered. For example:

class ErrorController {

def serverError() {
    def handler = request.exceptionHandler
    if(handler) {
        request.exceptionHandler = null
        if(handler.call(request.exception)) {
            return
        }
    }
}

I have tested this an it does work. I copied the code from a running project that I have been working on. You can build out the if statement in the beforeInterceptor to catch any type of Exception you wish.

Upvotes: 2

Related Questions