【Spring Boot】 MultipartException的异常处理

在Spring Boot的默认的异常处理中,是不会对 MultipartException作处理的,因此MultipartException会返回(如果文件超过大小):

{
    "timestamp": "2019-12-30T11:54:39.036+0000",
    "status": 500,
    "error": "Internal Server Error",
    "message": "Maximum upload size exceeded; nested exception is java.lang.IllegalStateException: org.apache.tomcat.util.http.fileupload.FileUploadBase$FileSizeLimitExceededException: The field file exceeds its maximum permitted size of 1048576 bytes.",
    "path": "/upload"
}

但我认为,这个异常不应该返回500的状态码,因为是客户端发送了一个过大的文件,或者客户端的请求不是一个正常的 MultipartRequest造成的,因此返回400更合适。

要返回400也简单,只需要增加一个额外的异常处理器(模仿DefaultHandlerExceptionResolver):

MyConfigurer implements WebMvcConfigurer {

	@Override
	public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
		resolvers.add(new HandlerExceptionResolver() {

			@Override
			public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response,
					Object handler, Exception ex) {
				if (ex instanceof MaxUploadSizeExceededException) {
					try {
						response.sendError(HttpServletResponse.SC_BAD_REQUEST);
					} catch (IOException e) {
						
					}
					return new ModelAndView();
				}
				return null;
			}

		});
	}

}

但是增加了一个这样的处理器之后,虽然可以返回期望的400错误码,但是错误信息却是一片空白,看了下DispatcherServlet之后发现,是在对MultipartRequest的处理导致了问题,DispatcherServlet通过checkMultipart(request)方法来处理请求,此时如果上传文件过大,直接就抛出了MultipartException,然后processDispatchResult对请求进行处理,将请求转发到 ErrorController来输出异常信息,但当转发之后,再次通过checkMultipart(request)方法处理了请求,此时再次触发了MultipartException,导致没有异常信息。所以实际上这个请求中MultipartException被处理了两次。

checkMultipart处理逻辑如下:

protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
      if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
          if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) {
              if (request.getDispatcherType().equals(DispatcherType.REQUEST)) {
              }
          }
          else if (hasMultipartException(request)) {
          }
          else {
              try {
                  return this.multipartResolver.resolveMultipart(request);
              }
              catch (MultipartException ex) {
                  if (request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) != null) {
                      
                  }
                  else {
                      throw ex;
                  }
              }
          }
      }
      // If not returned before: return original request.
      return request;
  }

要想解决这种情况,第一种就是修改对MultipartException的处理,例如:

if (ex instanceof MaxUploadSizeExceededException) {
    response.setStatus(400);
    return new ModelAndView(new MappingJackson2JsonView()).addObject("error",
            "max upload size exceeded");
}

这样就绕过ErrorController的处理,第二种就是 延迟对MultipartRequest的处理,在application.properties中添加或修改如下:

spring.servlet.multipart.resolve-lazily=true

这样在转发到ErrorController之后,就可以通过如下的判断,从而避免再次抛出MultipartException

if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) {
}

最后不要试图在错误处理中对请求直接设置 WebUtils.ERROR_EXCEPTION_ATTRIBUTE 属性,这将始终返回 500错误码,例如:

if (ex instanceof MaxUploadSizeExceededException) {
  request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, ex);
  try {
    response.sendError(HttpServletResponse.SC_BAD_REQUEST);
  } catch (IOException e) {

  }
  return new ModelAndView();
}