Here's my magic formula for doing this. Maybe there's a better way, but this one works and ensures the same 404 view renders whether you generate the 404 or grails does it internally (no controller found, for example).
First, create a View class that extends AbstractView:
class NotFoundView extends AbstractView {
@Override
protected void renderMergedOutputModel(Map<String, Object> model,
HttpServletRequest request, HttpServletResponse response) {
response.sendError(HttpServletResponse.SC_NOT_FOUND)
}
}
Next, create an error controller:
class ErrorController {
def notFound = {
return render(view: '/error/notFound')
}
}
Now create your error view under views/error/notFound.gsp:
<g:applyLayout name="main">
<!doctype html>
<html>
<head>
<title>Oops! Not found!</title>
</head>
<body>
<h1>Not Found</h1>
<section id="page-body">
<p>Nothing was found at your URI!</p>
</section>
</body>
</html>
</g:applyLayout>
It's crucial that you use the <g:applyLayout> tag. If you use your layout will render twice and nest itself.
Now for the URL mapping:
"404"(controller: 'error', action: 'notFound')
You're all set now to send that 404 from your controller:
def myAction = {
Thing thing = Thing.get(params.id)
if (!thing) {
return new ModelAndView(new NotFoundView())
}
}
This approach also lets you easily log the 404, try to resolve it and send a 301, or whatever you want to do.