Brain Flush

December 4, 2008

How To Gracefully Recover From File Upload Errors In Grails

Filed under: Software Development & Programming — Tags: , , — Matthias @ 4:45 pm

Problem statement

It is often desirable to let users upload files to your website, such as custom profile images. This is not a trivial task however, since it is prone to errors (e.g. file sizes exceeding maximum values, or certain file types not being allowed by the system).

Grails supports file uploads through its Spring layer, which in turn uses the Apache Commons FileUpload library, a Java based library that brings support for handling HTTP requests of the multipart/form-data MIME type (that’s the format of requests submitted from an HTML form). However, there is one major problem: Any exceptions that occur while uploading files (e.g. due to oversize files) are thrown in a Servlet specific to that library, and the request never reaches any of your Grails controllers. This means that if the user tries to upload a file that is too large, the backend will bail out with an internal exception and the container will display a 500 to the user.

That’s obviously not what we want. Consider for example a signup page where the user can upload a custom profile picture. If the maximum allowed size for that picture is, say, 1MB, we don’t want the user to face a 500, but instead gracefully handle that error and set an error flag on the signup data (e.g. using Spring’s Errors.reject()).

Solution

Grails manages a Spring bean for handling file uploads called ‘multipartResolver’, which is an instance of Spring’s CommonsMultipartResolver. We can set e.g. the maximum allowed size for uploaded files on that bean using the maxUploadSize property (you could do that e.g. in BootStrap.groovy), but that alone does not solve the problem mentioned above. What we want to do is subclass CommonsMultipartResolver, handle any exceptions we do not want to reach the user, and redirect to our own error handling logic. In the case for handling exceeding file sizes, our custom resolver could look like this:

public class CustomMultipartResolver extends CommonsMultipartResolver {

    static final String FILE_SIZE_EXCEEDED_ERROR = "fileSizeExceeded"

    public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) {

        try {
            return super.resolveMultipart(request)
        } catch (MaxUploadSizeExceededException e) {
            request.setAttribute(FILE_SIZE_EXCEEDED_ERROR, true)
            return new DefaultMultipartHttpServletRequest(request, [:], [:])
        }
    }
}

In the controller responsible for processing the request, we can then do as follows:


def handleRequest = {
    def user = new User(params)
    if (request.getAttribute(CustomMultipartResolver.FILE_SIZE_EXCEEDED_ERROR)) {
        user.errors.reject("user.picture.fileSizeTooLarge")
        return [user: user]
    } else {
        // OK, continue processing
    }
}

Of course, since we replaced the implementation of Grails’ multipartResolver, we have to tell Grails we’re using a custom class. We do that via Spring in resources.groovy or resources.xml:

<beans xmlns="...">
    <bean id="multipartResolver" class="com.example.CustomMultipartResolver">
        <!-- limit uploads to 1MB -->
<property name="maxUploadSize" value="1048576" />
    </bean>
</beans>

And that’s it. If the user now uploads an image which is larger than 1MB, the website will display a validation error not visually distinguishable from other validation errors which are actually produced by Spring.

References:
http://blog.bruary.net/2008/03/grails-ajax-file-upload-progressbar.html

Advertisements

5 Comments »

  1. Wow, thanks for this post ! I extended your solution because I wanted for the controller to still receive request parameters, at least non file ones. It’s a dirty hack because commons-fileupload doesn’t provide the right extension points for doing this cleanly but it works quite well. I replace line 11 of your CustomMultipartResolver with the following lines:

    String encoding = determineEncoding(request);
    FileUpload fileUpload = newFileUpload(getFileItemFactory())
    fileUpload.setHeaderEncoding(encoding)
    def fileItems = ((ServletFileUpload)fileUpload).parseRequest(request);
    def parsingResult = parseFileItems(fileItems, encoding)
    return new DefaultMultipartHttpServletRequest(request, [:], parsingResult.multipartParameters)

    Luis

    Comment by Luis Arias — February 18, 2009 @ 1:19 pm

  2. Thanks for this blog, I ran into exactly the same problem and your solution works perfectly :-)

    Also Luis’ enhancement was important too, otherwise the controller would have missed the request parameters like Luis mentioned.

    For those of you who have moved to grails 1.1.1, the DSL definition for the spring bean looks like (in resource.groovy)

    beans = {
    multipartResolver(com.example.CustomMultipartResolver) {
    maxUploadSize = 1048576
    }
    }

    Comment by Jim Kuo — July 11, 2009 @ 7:08 pm

  3. For Grails 1.2 (Spring 3.0), I had to replace line 11 of CustomMultipartResolver with:

    return new DefaultMultipartHttpServletRequest(request, new LinkedMultiValueMap(), [:])

    Comment by Arash — February 24, 2010 @ 8:26 am

  4. This code works great and solves my problem for the most part. I am using this on a Flash uploader so when the error hits the controller, I would like to do a response.sendError() so that Flash can pick it up and read it. Unfortunately something about this code has made it so that I cannot do a sendError(). Here is the exception I am getting. Do I need to override the response as well to solve this?

    2010-08-10 10:46:05,423 [26975173@qtp0-40] ERROR errors.GrailsExceptionResolver – org.mortbay.jetty.EofException
    org.codehaus.groovy.runtime.InvokerInvocationException: org.mortbay.jetty.EofException
    at org.jsecurity.web.servlet.JSecurityFilter.doFilterInternal(JSecurityFilter.java:384)
    at org.jsecurity.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:183)
    Caused by: org.mortbay.jetty.EofException
    at UploadController$_closure2.doCall(UploadController:43)
    at UploadController$_closure2.doCall(UploadController)
    … 2 more
    Caused by: java.nio.channels.ClosedChannelException
    … 4 more

    Comment by Jared — August 10, 2010 @ 3:54 pm

  5. Sorry, to clarify, the exception above is being caused by this line:

    response.sendError(554)

    The response works fine when I test it in other actions, but not in the case of upload.

    Comment by Jared — August 10, 2010 @ 3:55 pm


RSS feed for comments on this post. TrackBack URI

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Create a free website or blog at WordPress.com.

%d bloggers like this: