Although most Grails controllers render HTML, JSON or XML output it is possible to use them to render binary data as well. We use a controller to render images uploaded by editors into our content management interface. The theory is simple enough, instead of using the render
dynamic method or returning a model the controller action just writes bytes directly to the HTTP response stream. Our action looked something like this:
def show = {
def image = Image.read(params.id)
if (image) {
response.contentType = image.contentType
response.outputStream.withStream { stream ->
stream << image.bytes
}
} else {
response.sendError SC_NOT_FOUND
}
}
This seemed to work well enough. However when writing a test I noticed an odd thing. I was using RESTClient to scrape resource URLs out of and make a HEAD request against them to ensure the URLs were valid. Javascript and CSS files were working fine but all the non-static images in the page were getting 404s. Initially I suspected a data setup problem and spent some time ensuring my test was setting data up properly. It was only once I put some debug logging in the controller action that I saw that the controller was actually loading images. The 404 was not coming from the else block in the action as I initially assumed. I tried changing the RESTClient call from head to get and suddenly the image URLs started working!
Once I did that I realised what the problem was. An HTTP HEAD request does not expect a response, in fact a server receiving a HEAD request must not return a response. The response stream that our controller action is writing to is, when the request method is HEAD, actually a no-op stream. When the action completes Grails checks to see if anything has been committed to the response stream and since it has not assumes that we want to render a view by convention. You can probably see where this is going now. The convention is that the request gets forwarded to grails-app/views/<controller>/<action>.gsp
which of course does not exist. The forwarded request sets a response code of 404 because there is no GSP!
We caught this bug in our app completely by accident but it could actually have been quite serious. Caching proxies and CDNs may well use a HEAD request to revalidate content and on getting a 404 assume that the URL is no longer valid. If the 404 response itself then gets cached we could get broken images on our site because the CDN tells the client browser there's nothing there.
The solution is simple enough. I changed the controller action to simply set a 200 response code when it gets a HEAD request for a valid image:
def show = {
def image = Image.read(params.id)
if (image) {
if (request.method == "HEAD") {
render SC_OK
} else {
response.contentType = image.contentType
response.outputStream.withStream { stream ->
stream << image.bytes
}
}
} else {
response.sendError SC_NOT_FOUND
}
}
A neater solution might be to use Grails’ support for mapping actions to request methods so that GET and HEAD requests dispatch to different actions.