简体   繁体   中英

File Upload and acceptable Error Handling on Undertow/Wildfly wth Spring Boot

We have a project running on Undertow & SpringBoot and are trying to add File Uploads. The first attempts were successful, the Files have been bound to the appropriate Beans by using the StandardServletMultipartResolver and configuring it using the application.properties . However, we ran into terrible difficulties when it came to Error Handling. We found a "solution" by configuring the standard resolver to 100MB and using CommonsMultipartResolver . We then added a filter like this

@Bean
public Filter filter() {
    return new OncePerRequestFilter() {
        @Override
        protected void doFilterInternal(HttpServletRequest request,
                HttpServletResponse response, FilterChain filterChain)
                throws ServletException, IOException {
            try {
                filterChain.doFilter(request, response);
            } catch (ServletException e) {
                if (e.getCause()
                        .getClass()
                        .equals(org.apache.commons.fileupload.FileUploadBase.FileSizeLimitExceededException.class)) {
                    int requestSize = request.getContentLength();
                    Collection<Part> parts = request.getParts();
                    List<String> oversizedFields = new LinkedList<>();
                    long uploadSize = 0;
                    for (Part part : new ArrayList<>(parts)) {
                        if (uploadSize + part.getSize() > MAX_UPLOAD_SIZE) {
                            requestSize -= part.getSize();
                            oversizedFields.add(part.getName());
                            request.getParameterMap()
                                    .remove(part.getName());
                            parts.remove(part);
                        } else {
                            uploadSize += part.getSize();
                        }
                    }
                    request.setAttribute("oversizedFields", oversizedFields);
                    SizeModifyingServletRequestWrapper requestWrapper = new SizeModifyingServletRequestWrapper(
                            request, requestSize, uploadSize);
                    filterChain.doFilter(requestWrapper, response);
                }
            }
        }
    };
}

Requestwrapper:

private static class SizeModifyingServletRequestWrapper extends
        HttpServletRequestWrapper {
    private int size;
    private long sizeLong;

    public SizeModifyingServletRequestWrapper(HttpServletRequest request,
            int size, long sizeLong) {
        super(request);
        this.size = size;
        this.sizeLong = sizeLong;
    }

    @Override
    public int getContentLength() {
        return size;
    }

    @Override
    public long getContentLengthLong() {
        return sizeLong;
    }

    @Override
    public String getHeader(String name) {
        if (FileUploadBase.CONTENT_LENGTH.equals(name)) {
            return Integer.toString(size);
        } else {
            return super.getHeader(name);
        }
    }
}

The @Controller -method then checks for oversized Files and adds the result to the BindingResult , which works great, except for the fact that the files are not bound to the bean. It turns out that the CommonsMultipartResolver , when trying to parse the request, throws a MalformedStreamException in ItemInputStream.makeAvailable() , which always returns the Message String ended unexpectedly .

So we went back to using the StandardServletMultipartResolver , and were able to catch the RuntimeException it throws just fine, however it delivers absolutely no form data when even one file exceeds its size boundaries.

We are absolutely stumped as it is no matter if the Resolver works lazily or not. If anyone has any further ideas how to resolve this matter, be welcome to suggest answers =)

Further Code for reference:

Extract from WebAppInitializer

@Bean(name = "multipartResolver")
public MultipartResolver multipartResolver() {
    StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver();
    multipartResolver.setResolveLazily(true);
    return multipartResolver;
}

@Bean
public MultipartConfigElement multipartConfigElement() {
    MultipartConfigFactory factory = new MultipartConfigFactory();
    factory.setMaxFileSize("2MB");
    factory.setMaxRequestSize("100MB");
    return factory.createMultipartConfig();
}

Extract from Controller:

@RequestMapping(method = { RequestMethod.POST, RequestMethod.PUT })
public String saveOrganizationDetails(
        @PathVariable(PATH_VARIABLE_ORGANIZATION_ID) String organizationId,
        @ModelAttribute @Valid Organization organization,
        BindingResult bindingResult, Model model,
        RedirectAttributes redirectAttributes, WebRequest request) {
checkForOversizedFiles(request, bindingResult);
    Map<String, MultipartFile> files = organization.getStyle().whichFiles();
}

private boolean checkForOversizedFiles(WebRequest request,
        BindingResult bindingResult) {
    if (request.getAttribute("oversizedFields", WebRequest.SCOPE_REQUEST) instanceof LinkedList) {
        @SuppressWarnings("unchecked")
        LinkedList<String> oversizedFiles = (LinkedList<String>) request
                .getAttribute("oversizedFields", WebRequest.SCOPE_REQUEST);
        for (String s : oversizedFiles) {
            String errorCode = KEY_ORGANIZATION_LOGO_OVERSIZED_FILE + s;
            bindingResult.rejectValue(s,
                    errorCode);
        }
        return true;
    } else {
        return false;
    }
}

private void handleUpload(Map<String, MultipartFile> files,
        OrganizationStyle style, BindingResult result) {
    for (String filename : files.keySet()) {
        if (processUpload(files.get(filename), filename)) {
            style.setLogoFlag(filename);
        } else {
            result.reject(KEY_ORGANIZATION_LOGO_UPLOAD_FAILURE);
        }
    }
}

processUpload() so far has no functionality, which is why I'm not including it here.

Extract from Form-Backing Bean:

public class OrganizationStyle {
@Transient
private MultipartFile logoPdf;
@Transient
private MultipartFile logoCustomerArea;
@Transient
private MultipartFile logoAssistant;
@Transient
private MultipartFile logoIdentityArea;

<omitting Getters and setters>

private Map<String, MultipartFile> getAllFiles() {
    Map<String, MultipartFile> files = new HashMap<>();
    files.put("logoPdf", logoPdf);
    files.put("logoCustomerArea", logoCustomerArea);
    files.put("logoAssistant", logoAssistant);
    files.put("logoIdentityArea", logoIdentityArea);
    return files;
}

public Map<String, MultipartFile> whichFiles() {
    Map<String, MultipartFile> whichFiles = new HashMap<>();
    for (String name : getAllFiles().keySet()) {
        MultipartFile file = getAllFiles().get(name);
        if (file != null && !file.isEmpty()) {
            whichFiles.put(name, file);
        }
    }
    return whichFiles;
}
}

This is, as stated, not the entire code, but the necessary code for this particular problem. The Exception thrown when uploading oversized files is either:

(java.io.IOException) java.io.IOException: UT000054: The maximum size 2097152 for an individual file in a multipart request was exceeded

or the mentioned FileUploadBase.FileSizeLimitExceedeException

And last but not least, an extract of the form-page

<div id="layoutOne" class="panel-collapse collapse">
    <div class="panel-body">
        <div class="form-group">
            <label for="logoPdf" class="control-label" th:text="#{organizationcontext.groups.addmodal.logo.form.label}">LOGO-FORM</label>
            <input type="file" th:field="*{style.logoPdf}" accept="image/*" />
        </div>
        <div class="form-group">
            <label for="logoCustomerArea" class="control-label" th:text="#{organizationcontext.groups.addmodal.logo.customer.label}">LOGO-ORGANIZATION</label>
            <input type="file" th:field="*{style.logoCustomerArea}" accept="image/*" />
        </div>
        <div class="form-group">
            <label for="logoAssistant" class="control-label" th:text="#{organizationcontext.groups.addmodal.logo.assistant.label}">LOGO-ASSISTANT</label>
            <input type="file" th:field="*{style.logoAssistant}" accept="image/*" />
        </div>
        <div class="form-group">
            <label for="logoIdentityArea" class="control-label" th:text="#{organizationcontext.groups.addmodal.logo.id.label}">LOGO-ID</label>
            <input type="file" th:field="*{style.logoIdentityArea}" accept="image/*" />
        </div>
        <div class="form-group" th:classappend="${#fields.hasErrors('style.cssUrl')}? has-error">
            <label for="style.cssUrl" class="control-label" th:text="#{organizationcontext.groups.addmodal.css.external.label}">CSS-EXTERNAL</label>
            <input th:field="*{style.cssUrl}" class="form-control" type="text" th:placeholder="#{placeholder.css.external}" />
        </div>
        <div class="form-group" th:classappend="${#fields.hasErrors('style.cssCode')}? has-error">
            <label for="style.cssCode" class="control-label" th:text="#{organizationcontext.groups.addmodal.css.input.label}">CSS</label>
            <textarea th:field="*{style.cssCode}" class="form-control" th:placeholder="#{placeholder.css.input}"></textarea>
        </div>
    </div>
</div>

If you followed the problems down here, you should have realized that we already tried several possible solutions, most being from here. Right now, the Filter catches RuntimeException and checks for IOException as cause, also, the sizes are no longer set within the application.properties

Any help or suggestions at all would be very much appreciated.

More Information

So, I debugged the StandardServletMultipartResolver and found that it uses the ISO-8859-1-charset for its parsing. This does produce the desired effects, even though the pages are UTF-8 encoded and the request object also hast UTF-8-Charset. I have been trying to force the ISO-Charset with a Filter like so

@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public Filter characterEncodingFilter() {
    CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter();
    characterEncodingFilter.setEncoding("ISO-8859-1");
    characterEncodingFilter.setForceEncoding(true);
    return characterEncodingFilter;
}

but, for some reason, the CommonsMultipartResolver finds a UTF-8 Encoded request object, so either this encoding does not work, or I have made another mistake I don't see.

I have also tried to find the exact moment of the thrown Exception, to maybe extend the class myself and make sure the already resolved form-data is kept, so far to no avail.

Even more Information

As suggested by another thread here, I tried to force the ISO-8859-1 charset on the request. At first, this completely bypassed the CommonsMultipartResolver and messed up my text, now it filters to the correct resolver, but this one still states that there are no files within the multipart data. Just for reference, the Filterclass I used:

private class MyMultiPartFilter extends MultipartFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        request.setCharacterEncoding("ISO-8859-1");
        request.getParameterNames();
        super.doFilterInternal(request, response, filterChain);
    }
}

Made a Bean from it and changed name of multipartResolver()-Bean to filterMultipartResolver()

The solution to the problem was found roughly at the time I was looking for it. It has been posted here .

Due to the fact that WildFly and Undertow have difficulties in dealing with the StandardServletMultipartResolver , it is more effective (maybe even necessary) to use the CommonsMultipartResolver . However, this must be called before the rest of the POST-data is processed.
To ensure this, it is necessary to call a MultipartFilter and create a filterMultipartResolver -Bean like this:

@Bean
public CommonsMultipartResolver filterMultipartResolver() {
    return new CommonsMultipartResolver();
}

@Bean
@Order(0)
public MultipartFilter multipartFilter() {
    return new MultipartFilter();
}

This ensures that the filter is called first, and it in turn calls the resolver. The only downside is that there is no out-of-the-box way to limit the individual filesize for uploads. This can be done by setting maxUploadSize(value) , which limits the overall request size.

Final Edit

So, here's what I ended up using, which allows for effective upload and handling of oversized files. I am not sure if this will be as effective when uploading large files, since this handles the oversized files after converting the request to FileItems but before parsing said FileItems .

I extended the CommonsMultipartResolver to override parseRequest like this:

@Override
protected MultipartParsingResult parseRequest(HttpServletRequest request) {

    String encoding = determineEncoding(request);
    FileUpload fileUpload = prepareFileUpload(encoding);

    List<FileItem> fileItems;
    List<String> oversizedFields = new LinkedList<>();

    try {
        fileItems = ((ServletFileUpload) fileUpload).parseRequest(request);
    } catch (FileUploadBase.SizeLimitExceededException ex) {
        fileItems = Collections.emptyList();
        request.setAttribute(ATTR_REQUEST_SIZE_EXCEEDED,
                KEY_REQUEST_SIZE_EXCEEDED);
    } catch (FileUploadException ex) {
        throw new MultipartException(MULTIPART_UPLOAD_ERROR, ex);
    }
    if (maxFileSize > -1) {
        for (FileItem fileItem : fileItems) {
            if (fileItem.getSize() > maxFileSize) {
                oversizedFields.add(fileItem.getFieldName());
                fileItem.delete();
            }
        }
    }
    if (!oversizedFields.isEmpty()) {
        request.setAttribute(ATTR_FIELDS_OVERSIZED, oversizedFields);
    }
    return parseFileItems((List<FileItem>) fileItems, encoding);
}

and added methods to set maxFileSize via bean configuration. Should the request size be exceeded, all values will be dropped, so be careful with that, especially if you use _csrf-token or similar.

In the controller, it is now easy to check for the added attribute and place error messages on the page.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM