Sunday 23 August 2015

Springboot mutlipart upload

The upload works very well for many use cases but I want to handle the upload with an AJAX client. As soon as the upload exceeds the limits the embedded servlet container fires an unhandled exception and the Javascript client doesn't receive any suitable answer from the server.

This happens with servlet containers implementing Servlet 3.0+ API. Starting from Servlet API 3.0 the container has to implement the multiple parts upload. To enable the feature the application has to provide the multiple parts upload configuration and a servlet annotated with javax.servlet.annotation.MultipartConfig.

Spring Boot registers the dispatcher servlet as multiple parts upload servlet. So the implementation depends from the servlet container and not from Spring. This way if something goes wrong the client becomes the standard answer of the servlet container.

Using Jetty the standard answer isn't really suitable for an AJAX client if something goes wrong. What I'm experienced with Springboot 1.2.5 is a timeout (no answer at all).

To change this behavior it would be necessary to register a customized error handler at least for Jetty. This is not my favorite solution and a like to work with the @ControlerAdvice annotation inside of Spring Boot.

In the past years I did use with success the upload package of Apache Commons together with Spring MVC. So I add the dependency to my maven POM:

. . .
  <dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.3.1</version>
  </dependency>
. . .

To avoid conflict I disable the multiple part automatic configuration:

@EnableAutoConfiguration(exclude={MultipartAutoConfiguration.class})
@ComponentScan
public class Application {
  public static void main(String[] args) throws Exception {
    SpringApplication.run(Application.class, args);
  }
}

Now I have to set up the a bean called "multipartResolver" to enable the upload feature:

@Configuration
public class UploadConfiguration {

    private static final Logger LOGGER = LoggerFactory.getLogger(UploadConfiguration.class);

    @Value("${multipart.file-size-threshold:-1}")
    private int maxInMemorySize;
    
    @Value("${multipart.max-file-size:10240}")
    private String uploadMaxFileSize;
    
    @Value("${multipart.location}")
    private String uploadTempDir;

    @Bean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
    public CommonsMultipartResolver multipartResolver() {
        final CommonsMultipartResolver commonsMultipartResolver = new CommonsMultipartResolver();
        if (StringUtils.isNotBlank(uploadTempDir)) {
            try {
                commonsMultipartResolver.setUploadTempDir(new FileSystemResource(uploadTempDir));
            }
            catch (IOException e) {
                LOGGER.warn(String.format("Illegal or not existing folder %s (temporary upload directory)!", uploadTempDir), e);
            }
        }
        commonsMultipartResolver.setMaxUploadSize(parseSize(uploadMaxFileSize));
        commonsMultipartResolver.setMaxInMemorySize(maxInMemorySize);
        return commonsMultipartResolver;
    }
  
    long parseSize(String size) {
        size = size.toUpperCase();
        if (size.endsWith("KB")) {
            return Long.valueOf(size.substring(0, size.length() - 2)) * 1024;
        }
        if (size.endsWith("MB")) {
            return Long.valueOf(size.substring(0, size.length() - 2)) * 1024 * 1024;
        }
        return Long.valueOf(size);
    }
}

This class uses the standard Spring Boot configuration/conventions as far as possible. The next step is to handle the exceptions thrown if the upload size exceeds the max number of bytes configured or if something goes wrong during the upload.

Since I use a AJAX client I need a simple and efficient way to detect the error and the cause. I use two simple message classes to accomplish this task:

public class JsonError {
   public String message;
}

public class JsonResponse {
   public JsonError error;
   public String action;
}

Now I can add the exception handler to my application:

@ControllerAdvice
public class JsonMultipartUploadExceptionHandler {
    
    private static final Logger LOGGER = LoggerFactory.getLogger(JsonMultipartUploadExceptionHandler.class);

    @ExceptionHandler(MaxUploadSizeExceededException.class)
    @ResponseStatus(value = HttpStatus.PRECONDITION_FAILED)
    @ResponseBody
    protected JsonResponse handleMaxUploadSizeExceededException(final HttpServletRequest request,
            final HttpServletResponse response, final Throwable e)
            throws IOException
    {
        LOGGER.warn(e.getMessage());
        return new JsonResponse("NOP").setError(new JsonError("Max file size exceeded"));
    }

    @ExceptionHandler(MultipartException.class)
    @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
    @ResponseBody
    protected JsonResponse handleGenericMultipartException(final HttpServletRequest request,
            final HttpServletResponse response, final Throwable e)
            throws IOException
    {
        Throwable rootCause = e;
        Throwable cause = e.getCause();
        while (cause != null && !cause.equals(rootCause)) {
            rootCause = cause;
            cause = cause.getCause();
        }
        LOGGER.error(rootCause.getMessage());
        return new JsonResponse("NOP").setError(new JsonError(rootCause.getMessage()));
    }
}

Now if something goes wrong my AJAX client can handle the error.

No comments:

Post a Comment