grails-core icon indicating copy to clipboard operation
grails-core copied to clipboard

Can't handle FileUploadBase$FileSizeLimitExceededException

Open ppazos opened this issue 1 year ago • 6 comments

Expected Behavior

The documentation for Grails 3.3.10 (similar in newer versions) in Uploading Files > Increase Upload Max File Size (http://docs.grails.org/3.3.10/guide/single.html#7.1.9%20Uploading%20Files) mentions:

Grails default size for file uploads is 128000 (~128KB). When this limit is exceeded you’ll see the following exception:

`org.springframework.web.multipart.MultipartException: Could not parse multipart servlet request; nested exception is java.lang.IllegalStateException: org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException`

You can configure the limit in your application.yml as follows:
...

Though increasing the limit doesn't solve the problem of handling the exception, since, for any configured value, there could be a bigger file uploaded, then the exception keeps happening.

The issue is the exception seems to happen at the Tomcat level and doesn't reach the controllers, where the exception could be handled gracefully and the app can retrieve a friendly error message to the user, instead the user gets a 500 and a stack trace.

I have tried several solutions given in StackOverflow, like configuring a custom multipart resolver, adding URL mappings for 500 with the exception class, or creating exception handlers in the controller. The last one doesn't work because the controller is not even reached when the exceptions is thrown. The custom multipart resolver is being executed but I lose all the multipart parameters and some are needed for the controller action so the controller knows which error is happening.

The official documentation doesn't have a solution for this and can't find any solution online that works 1. handling the exception, 2. forwarding the request to a specific action with parameters so the action knows what is going on and can respond accordingly.

The expected behavior would be to have a mechanism that allows handling this exception gracefully and have full control over the request, request parameters, dispatch to a controller/action, and response.

Actual Behaviour

2022-07-24 03:55:35.319 ERROR --- [nio-8095-exec-9] o.g.web.errors.GrailsExceptionResolver   : FileSizeLimitExceededException occurred when processing request: [POST] /operationalTemplate/upload
The field file exceeds its maximum permitted size of 500000 bytes.. Stacktrace follows:

org.springframework.web.multipart.MultipartException: Could not parse multipart servlet request; nested exception is java.lang.IllegalStateException: org.apache.tomcat.util.http.fileupload.FileUploadBase$FileSizeLimitExceededException: The field file exceeds its maximum permitted size of 500000 bytes.
        at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.parseRequest(StandardMultipartHttpServletRequest.java:116)
        at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.<init>(StandardMultipartHttpServletRequest.java:90)
        at org.springframework.web.multipart.support.StandardServletMultipartResolver.resolveMultipart(StandardServletMultipartResolver.java:81)
        at org.springframework.web.servlet.DispatcherServlet.checkMultipart(DispatcherServlet.java:1104)
        at org.grails.web.servlet.mvc.GrailsDispatcherServlet.checkMultipart(GrailsDispatcherServlet.groovy:75)
        at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:936)
        at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:901)
        at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970)
        at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:872)
        at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846)
        at org.springframework.boot.web.filter.ApplicationContextHeaderFilter.doFilterInternal(ApplicationContextHeaderFilter.java:55)
        at org.grails.web.servlet.mvc.GrailsWebRequestFilter.doFilterInternal(GrailsWebRequestFilter.java:77)
        at org.grails.web.filters.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:67)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at java.lang.Thread.run(Thread.java:748)
Caused by: java.lang.IllegalStateException: org.apache.tomcat.util.http.fileupload.FileUploadBase$FileSizeLimitExceededException: The field file exceeds its maximum permitted size of 500000 bytes.
        at org.grails.web.filters.HiddenHttpMethodFilter.getHttpMethodOverride(HiddenHttpMethodFilter.java:71)
        at org.grails.web.filters.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:60)
        ... 3 common frames omitted
Caused by: org.apache.tomcat.util.http.fileupload.FileUploadBase$FileSizeLimitExceededException: The field file exceeds its maximum permitted size of 500000 bytes.
        at java.io.FilterInputStream.read(FilterInputStream.java:107)
        ... 5 common frames omitted

2022-07-24 03:55:35.337 ERROR --- [nio-8095-exec-9] .a.c.c.C.[.[.[.[grailsDispatcherServlet] : Servlet.service() for servlet [grailsDispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.web.multipart.MultipartException: Could not parse multipart servlet request; nested exception is java.lang.IllegalStateException: org.apache.tomcat.util.http.fileupload.FileUploadBase$FileSizeLimitExceededException: The field file exceeds its maximum permitted size of 500000 bytes.] with root cause

org.apache.tomcat.util.http.fileupload.FileUploadBase$FileSizeLimitExceededException: The field file exceeds its maximum permitted size of 500000 bytes.
        at java.io.FilterInputStream.read(FilterInputStream.java:107)
        at org.grails.web.filters.HiddenHttpMethodFilter.getHttpMethodOverride(HiddenHttpMethodFilter.java:71)
        at org.grails.web.filters.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:60)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at java.lang.Thread.run(Thread.java:748)

Steps To Reproduce

  1. Set grails.controllers.upload..maxFileSize to any value like 500000
  2. Upload a file that is bigger than that
  3. Exception is thrown and user gets it in the browser (above)

Environment Information

$ grails -version | Grails Version: 3.3.10 | JVM Version: 1.8.0_181

Linux Mint 19.3

Example Application

need to build one, this is a private app I'm working on

Version

3.3.10

ppazos avatar Jul 24 '22 07:07 ppazos

Hi, I found a solution to this problem using a multipart resolver:

package demo.controller

import groovy.transform.CompileStatic
import org.apache.tomcat.util.http.fileupload.impl.SizeLimitExceededException
import org.springframework.util.LinkedMultiValueMap
import org.springframework.web.multipart.MaxUploadSizeExceededException
import org.springframework.web.multipart.MultipartFile
import org.springframework.web.multipart.MultipartHttpServletRequest
import org.springframework.web.multipart.support.DefaultMultipartHttpServletRequest
import org.springframework.web.multipart.support.StandardServletMultipartResolver

import javax.servlet.http.HttpServletRequest

@CompileStatic
class MaxFileUploadSizeResolver extends StandardServletMultipartResolver {

    static final String FILE_SIZE_EXCEEDED_ERROR = "demo.fileSizeExceeded"

    @Override
    MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) {

        try {
            return super.resolveMultipart(request)
        }
        catch (MaxUploadSizeExceededException e) {

            request.setAttribute(FILE_SIZE_EXCEEDED_ERROR, e.cause.cause as SizeLimitExceededException)
            return new DefaultMultipartHttpServletRequest(request, new LinkedMultiValueMap<String, MultipartFile>(), new LinkedHashMap<String, String[]>(), new LinkedHashMap<String, String>())
        }
    }
}

and using it by declaring it as a spring bean (conf/spring/resources.groovy):

import demo.controller.MaxFileUploadSizeResolver

beans = {
     multipartResolver(MaxFileUploadSizeResolver)
}

Now, in the controller, you can check "demo.fileSizeExceeded" in the request attributes and act the way you need.

matrei avatar Aug 08 '22 12:08 matrei

@matrei which version of grails are you using?

ppazos avatar Aug 08 '22 13:08 ppazos

@ppazos Grails 5 for this project.

matrei avatar Aug 08 '22 14:08 matrei

@matrei I'm on 3.3.10, not sure if that will work but I'll test it. Tried something similar in the past but wasn't grabbing the exception.

ppazos avatar Aug 08 '22 14:08 ppazos

There will not be any patches for Grails 3, so you could verify that the above solution from @matrei works with Grails 5?

puneetbehl avatar Aug 10 '22 04:08 puneetbehl

@puneetbehl nope, the project I'm maintaining is Grails 3.3.10, can't test on 5.

ppazos avatar Aug 15 '22 21:08 ppazos

Please upgrade to Grails 5 and use the solution suggested in the previous comments.

puneetbehl avatar Jan 10 '23 09:01 puneetbehl

@puneetbehl again, the project I'm maintaining is Grails 3.3.x, can't upgrade to 5.

ppazos avatar Jan 30 '23 02:01 ppazos

@ppazos I tried with Grails 3.3.16 and it works if you subclass CommonsMultipartResolver instead of StandardServletMultipartResolver and set the max file size in the bean config.

package grails3.upload.exception.test

import org.springframework.util.LinkedMultiValueMap
import org.springframework.web.multipart.MaxUploadSizeExceededException
import org.springframework.web.multipart.MultipartFile
import org.springframework.web.multipart.MultipartHttpServletRequest
import org.springframework.web.multipart.commons.CommonsMultipartResolver
import org.springframework.web.multipart.support.DefaultMultipartHttpServletRequest

import javax.servlet.http.HttpServletRequest

class MaxFileUploadSizeResolver extends CommonsMultipartResolver {

    static final String FILE_SIZE_EXCEEDED_EXCEPTION = "demo.fileSizeExceeded"

    @Override
    MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) {
        try {
            return super.resolveMultipart(request)
        } catch (MaxUploadSizeExceededException e) {

            request.setAttribute(FILE_SIZE_EXCEEDED_EXCEPTION, e)
            return new DefaultMultipartHttpServletRequest(request, new LinkedMultiValueMap<String, MultipartFile>(), new LinkedHashMap<String, String[]>(), new LinkedHashMap<String, String>())
        }
    }

}
import grails3.upload.exception.test.MaxFileUploadSizeResolver

// Place your Spring DSL code here
beans = {
    multipartResolver(MaxFileUploadSizeResolver) {
        maxUploadSize = 1024
    }
}

matrei avatar Jan 30 '23 08:01 matrei