File upload in Spring Boot: Uploading, validation, and exception handling
<p>I would like to be able to upload images to a server, handling errors and exceptions gracefully, with error messages displayed to the user in the form, and ideally only using an existing barebones Spring Boot and Thymeleaf install. </p>

<p>Using the example project <a href="https://github.com/spring-guides/gs-uploading-files" rel="noreferrer">gs-uploading-files</a> I can upload files to a server using Spring Boot and Thymeleaf. In application.properties I set <code>spring.http.multipart.max-file-size=1MB</code> and 
<code>spring.http.multipart.max-request-size=1MB</code>. 
 However several security and validation issues are unresolved when I upload files larger than 1MB.</p>

<ol>
<li><p>Any file can be uploaded. For example, an html file could be uploaded and thus hosted on the server. How can files be restricted by type? Can they be validated in the page before the request is sent? If I have multiple ways of uploading images how can I validate all MultipartFiles?</p></li>
<li><p>Users can attempt to uploade large files, beyond the default limits of Spring and the embedded Tomcat. This causes a <code>org.springframework.web.multipart.MultipartException</code> is not handled by Spring. How can the file size be validated before the upload attempt? In case this is bypassesed can any file upload exceptions caught by Spring, so that a nice error message is displayed?</p></li>
<li><p>The default Spring error page is not used as a fallback for all exceptions. The MultipartException returns a Tomcat exception page with a full stacktrace (see Log 1).</p></li>
</ol>

<hr>

<p>I've searched to try and find and implement a set of solutions.</p>

<p>A step towards fixing number 1 is by amending to <code>handleFileUpload</code> check the content type, rejecting files which fail this:  <code>!file.getContentType().toLowerCase().startsWith("image")</code>. Will this always be valid? Can a malicious user bypass this? And how can I check every MultipartFile, to save having to remember to add this every time?</p>

<pre><code>@PostMapping("/")
public String handleFileUpload(@RequestParam("file") MultipartFile file, RedirectAttributes redirectAttributes)
        throws MultipartException, IllegalStateException {

    if (file != null &amp;&amp; file.getContentType() != null &amp;&amp; !file.getContentType().toLowerCase().startsWith("image"))
        throw new MultipartException("not img");

    storageService.store(file);
    redirectAttributes.addFlashAttribute("message",
            "You successfully uploaded " + file.getOriginalFilename() + "!");

    return "redirect:/";
}
</code></pre>

<p>Adding an <code>@ExceptionHandler</code> doesn't work, it simply never gets called.</p>

<pre><code>@ExceptionHandler({ SizeLimitExceededException.class, MultipartException.class,
        java.lang.IllegalStateException.class })
public ModelAndView handleError(HttpServletRequest req, Exception e) {
    // error("Request: " + req.getRequestURL() + " raised " + ex);

    ModelAndView mav = new ModelAndView();
    mav.addObject("exception", e);
    mav.addObject("url", req.getRequestURL());
    mav.addObject("timestamp", new Date());
    mav.addObject("error", e.getClass());
    mav.addObject("message", e.getMessage());
    mav.addObject("status", HttpStatus.INTERNAL_SERVER_ERROR);
    mav.setViewName("error");
    return mav;
}
</code></pre>

<p>Number 3 can be resolved by a global exception handler on all Exceptions. (explained in detail in <a href="https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc" rel="noreferrer">this post</a>). However, I'm concerned that it may overrule a Controller level handler.</p>

<pre><code>package hello;

import java.util.Date;

import javax.servlet.http.HttpServletRequest;

import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.ModelAndView;

@ControllerAdvice
class GlobalDefaultExceptionHandler {
    public static final String DEFAULT_ERROR_VIEW = "error";

    @ExceptionHandler(value = Exception.class)
    public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception {
        // If the exception is annotated with @ResponseStatus rethrow it and let
        // the framework handle it - like the OrderNotFoundException example
        // at the start of this post.
        // AnnotationUtils is a Spring Framework utility class.
        if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null)
            throw e;

        // Otherwise setup and send the user to a default error-view.
        ModelAndView mav = new ModelAndView();
        mav.addObject("exception", e);
        mav.addObject("url", req.getRequestURL());
        mav.addObject("timestamp", new Date());
        mav.addObject("error", e.getClass());
        mav.addObject("message", e.getMessage());
        mav.addObject("status", HttpStatus.INTERNAL_SERVER_ERROR);
        mav.setViewName(DEFAULT_ERROR_VIEW);
        return mav;
    }
}
</code></pre>

<p>I've attempted <a href="https://stackoverflow.com/questions/29363705/handling-multipartexception-with-spring-boot-and-display-error-page">this answer</a>, which handles the exception but returns an error page. I'd like to return to the original page and display a nice error message. </p>

<hr>

<p>Log 1:</p>

<pre><code>HTTP Status 500 - 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$SizeLimitExceededException: the request was rejected because its size (1292555) exceeds the configured maximum (1048576)

type Exception report

message 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$SizeLimitExceededException: the request was rejected because its size (1292555) exceeds the configured maximum (1048576)

description The server encountered an internal error that prevented it from fulfilling this request.

exception

org.springframework.web.util.NestedServletException: 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$SizeLimitExceededException: the request was rejected because its size (1292555) exceeds the configured maximum (1048576)
    org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:982)
    org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:872)
    javax.servlet.http.HttpServlet.service(HttpServlet.java:648)
    org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846)
    javax.servlet.http.HttpServlet.service(HttpServlet.java:729)
    org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
    org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    org.springframework.web.filter.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:89)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:77)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
root cause

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: the request was rejected because its size (1292555) exceeds the configured maximum (1048576)
    org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.parseRequest(StandardMultipartHttpServletRequest.java:111)
    org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.&lt;init&gt;(StandardMultipartHttpServletRequest.java:85)
    org.springframework.web.multipart.support.StandardServletMultipartResolver.resolveMultipart(StandardServletMultipartResolver.java:76)
    org.springframework.web.servlet.DispatcherServlet.checkMultipart(DispatcherServlet.java:1099)
    org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:932)
    org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:897)
    org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970)
    org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:872)
    javax.servlet.http.HttpServlet.service(HttpServlet.java:648)
    org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846)
    javax.servlet.http.HttpServlet.service(HttpServlet.java:729)
    org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
    org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    org.springframework.web.filter.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:89)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:77)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
root cause

java.lang.IllegalStateException: org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (1292555) exceeds the configured maximum (1048576)
    org.apache.catalina.connector.Request.parseParts(Request.java:2871)
    org.apache.catalina.connector.Request.parseParameters(Request.java:3176)
    org.apache.catalina.connector.Request.getParameter(Request.java:1110)
    org.apache.catalina.connector.RequestFacade.getParameter(RequestFacade.java:381)
    org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:70)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
root cause

org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (1292555) exceeds the configured maximum (1048576)
    org.apache.tomcat.util.http.fileupload.FileUploadBase$FileItemIteratorImpl.&lt;init&gt;(FileUploadBase.java:811)
    org.apache.tomcat.util.http.fileupload.FileUploadBase.getItemIterator(FileUploadBase.java:256)
    org.apache.tomcat.util.http.fileupload.FileUploadBase.parseRequest(FileUploadBase.java:280)
    org.apache.catalina.connector.Request.parseParts(Request.java:2801)
    org.apache.catalina.connector.Request.parseParameters(Request.java:3176)
    org.apache.catalina.connector.Request.getParameter(Request.java:1110)
    org.apache.catalina.connector.RequestFacade.getParameter(RequestFacade.java:381)
    org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:70)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
note The full stack trace of the root cause is available in the Apache Tomcat/8.5.5 logs.

Apache Tomcat/8.5.5
</code></pre>

<hr>