You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@struts.apache.org by lu...@apache.org on 2022/12/29 09:53:17 UTC

[struts] branch WW-5273-servlet-upload updated (f92e3b945 -> 99e3992ed)

This is an automated email from the ASF dual-hosted git repository.

lukaszlenart pushed a change to branch WW-5273-servlet-upload
in repository https://gitbox.apache.org/repos/asf/struts.git


 discard f92e3b945 WW-5273 Supports file upload using Servlet API 3.1
     new 99e3992ed WW-5273 Supports file upload using Servlet API 3.1

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (f92e3b945)
            \
             N -- N -- N   refs/heads/WW-5273-servlet-upload (99e3992ed)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../java/org/apache/struts2/dispatcher/filter/StrutsPrepareFilter.java | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)


[struts] 01/01: WW-5273 Supports file upload using Servlet API 3.1

Posted by lu...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

lukaszlenart pushed a commit to branch WW-5273-servlet-upload
in repository https://gitbox.apache.org/repos/asf/struts.git

commit 99e3992ed27bdbb82a902537ae9df07e7d65a9c1
Author: Lukasz Lenart <lu...@apache.org>
AuthorDate: Thu Dec 29 10:36:45 2022 +0100

    WW-5273 Supports file upload using Servlet API 3.1
---
 apps/showcase/{README.txt => README.md}            |  11 +-
 apps/showcase/pom.xml                              |   4 -
 apps/showcase/src/main/webapp/WEB-INF/web.xml      |  12 +
 .../org/apache/struts2/dispatcher/Dispatcher.java  |   4 +-
 .../dispatcher/filter/FileUploadSupport.java       |  65 ++++++
 .../dispatcher/filter/StrutsExecuteFilter.java     |  17 +-
 .../filter/StrutsPrepareAndExecuteFilter.java      |   7 +-
 .../dispatcher/filter/StrutsPrepareFilter.java     |   2 +-
 .../multipart/AbstractMultiPartRequest.java        |   6 +-
 .../multipart/JakartaMultiPartRequest.java         |   2 +
 .../multipart/JakartaStreamMultiPartRequest.java   |   2 +
 .../dispatcher/multipart/MultiPartRequest.java     |   8 +-
 .../multipart/ServletMultiPartRequest.java         | 246 +++++++++++++++++++++
 .../dispatcher/servlet/FileUploadServlet.java      |  74 +++++++
 .../struts2/interceptor/FileUploadInterceptor.java |  15 +-
 .../org/apache/struts2/default.properties          |   2 +-
 core/src/main/resources/struts-beans.xml           |   2 +
 ...rutsPrepareAndExecuteFilterIntegrationTest.java |  31 ++-
 .../dispatcher/TwoFilterIntegrationTest.java       |  55 +++--
 .../multipart/ServletMultiPartRequestTest.java     |  78 +++++++
 .../interceptor/FileUploadInterceptorTest.java     |  20 +-
 21 files changed, 597 insertions(+), 66 deletions(-)

diff --git a/apps/showcase/README.txt b/apps/showcase/README.md
similarity index 74%
rename from apps/showcase/README.txt
rename to apps/showcase/README.md
index 8e6cbb03c..a1b828042 100644
--- a/apps/showcase/README.txt
+++ b/apps/showcase/README.md
@@ -1,14 +1,11 @@
-README.txt - showcase
+# Showcase App
 
-Showcase is a collection of examples with code that you might be adopt and 
-adapt in your own applications. 
+Showcase is a collection of examples with code that you might be adopted and adapt in your own applications. 
 
-For more on getting started with Struts, see 
+For more on getting started with Struts, see https://struts.apache.org/getting-started/
 
-* http://cwiki.apache.org/WW/home.html
+## I18N
 
-I18N:
-=====
 Please note that this project was created with the assumption that it will be run
 in an environment where the default locale is set to English. This means that
 the default messages defined in package.properties are in English. If the default
diff --git a/apps/showcase/pom.xml b/apps/showcase/pom.xml
index cc6d51d5e..9329b7968 100644
--- a/apps/showcase/pom.xml
+++ b/apps/showcase/pom.xml
@@ -140,10 +140,6 @@
             <groupId>org.directwebremoting</groupId>
             <artifactId>dwr</artifactId>
         </dependency>
-        <dependency>
-            <groupId>commons-fileupload</groupId>
-            <artifactId>commons-fileupload</artifactId>
-        </dependency>
 
        <dependency>
            <groupId>junit</groupId>
diff --git a/apps/showcase/src/main/webapp/WEB-INF/web.xml b/apps/showcase/src/main/webapp/WEB-INF/web.xml
index 1bcfa4184..63059a08b 100644
--- a/apps/showcase/src/main/webapp/WEB-INF/web.xml
+++ b/apps/showcase/src/main/webapp/WEB-INF/web.xml
@@ -114,6 +114,12 @@
         <load-on-startup>1</load-on-startup>
     </servlet>
 
+    <servlet>
+        <servlet-name>fileUploadServlet</servlet-name>
+        <servlet-class>org.apache.struts2.dispatcher.servlet.FileUploadServlet</servlet-class>
+        <load-on-startup>2</load-on-startup>
+    </servlet>
+
     <servlet>
         <servlet-name>strutsServlet</servlet-name>
         <servlet-class>org.apache.struts2.dispatcher.servlet.StrutsServlet</servlet-class>
@@ -142,6 +148,12 @@
         <load-on-startup>4</load-on-startup>
     </servlet>
 
+    <servlet-mapping>
+        <servlet-name>fileUploadServlet</servlet-name>
+        <url-pattern>/fileupload/*</url-pattern>
+        <url-pattern>/tags/ui/*</url-pattern>
+    </servlet-mapping>
+
     <servlet-mapping>
         <servlet-name>dwr</servlet-name>
         <url-pattern>/dwr/*</url-pattern>
diff --git a/core/src/main/java/org/apache/struts2/dispatcher/Dispatcher.java b/core/src/main/java/org/apache/struts2/dispatcher/Dispatcher.java
index 43794c1c5..be1dba13c 100644
--- a/core/src/main/java/org/apache/struts2/dispatcher/Dispatcher.java
+++ b/core/src/main/java/org/apache/struts2/dispatcher/Dispatcher.java
@@ -941,7 +941,7 @@ public class Dispatcher {
      * @return false if disabled
      * @since 2.5.11
      */
-    protected boolean isMultipartSupportEnabled(HttpServletRequest request) {
+    public boolean isMultipartSupportEnabled(HttpServletRequest request) {
         return multipartSupportEnabled;
     }
 
@@ -952,7 +952,7 @@ public class Dispatcher {
      * @return true if it is a multipart request
      * @since 2.5.11
      */
-    protected boolean isMultipartRequest(HttpServletRequest request) {
+    public boolean isMultipartRequest(HttpServletRequest request) {
         String httpMethod = request.getMethod();
         String contentType = request.getContentType();
 
diff --git a/core/src/main/java/org/apache/struts2/dispatcher/filter/FileUploadSupport.java b/core/src/main/java/org/apache/struts2/dispatcher/filter/FileUploadSupport.java
new file mode 100644
index 000000000..5c245ace2
--- /dev/null
+++ b/core/src/main/java/org/apache/struts2/dispatcher/filter/FileUploadSupport.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.struts2.dispatcher.filter;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.struts2.dispatcher.Dispatcher;
+import org.apache.struts2.dispatcher.multipart.MultiPartRequest;
+import org.apache.struts2.dispatcher.multipart.MultiPartRequestWrapper;
+import org.apache.struts2.dispatcher.multipart.ServletMultiPartRequest;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Detects if request is a multipart request with file upload, it works
+ * only when combined with {@link StrutsExecuteFilter} or {@link StrutsPrepareAndExecuteFilter}
+ * and {@link org.apache.struts2.dispatcher.servlet.FileUploadServlet}
+ */
+interface FileUploadSupport {
+
+    Logger LOG = LogManager.getLogger(FileUploadSupport.class);
+
+    default boolean isFileUploadRequest(HttpServletRequest request) throws ServletException {
+        Dispatcher dispatcher = Dispatcher.getInstance(request.getServletContext());
+
+        if (dispatcher == null) {
+            throw new ServletException("Dispatcher is not initialised!");
+        }
+
+        // TODO: remove this once the old implementations of MultiPartRequest have been removed
+        boolean isMultipartRequest = request instanceof MultiPartRequestWrapper;
+        MultiPartRequest multiPartRequest = dispatcher.getContainer().getInstance(MultiPartRequest.class);
+        boolean isServletMultipartRequest = multiPartRequest instanceof ServletMultiPartRequest;
+
+        if (isMultipartRequest && isServletMultipartRequest) {
+            LOG.debug("Using the new Servlet API 3.1 based file upload support");
+            if (dispatcher.isMultipartSupportEnabled(request) && dispatcher.isMultipartRequest(request)) {
+                LOG.debug("The file upload request is going to be handled by servlet");
+                return true;
+            }
+        } else {
+            LOG.debug("Skipping processing request as other implementation of: {} is used: {}", MultiPartRequest.class, multiPartRequest.getClass());
+            LOG.warn("Avoid using this Servlet with other implementations of: {} as it was designed to only work with Servlet API 3.1 based implementation", MultiPartRequest.class);
+        }
+        return false;
+    }
+
+}
diff --git a/core/src/main/java/org/apache/struts2/dispatcher/filter/StrutsExecuteFilter.java b/core/src/main/java/org/apache/struts2/dispatcher/filter/StrutsExecuteFilter.java
index 0e479c449..c07617897 100644
--- a/core/src/main/java/org/apache/struts2/dispatcher/filter/StrutsExecuteFilter.java
+++ b/core/src/main/java/org/apache/struts2/dispatcher/filter/StrutsExecuteFilter.java
@@ -20,12 +20,17 @@ package org.apache.struts2.dispatcher.filter;
 
 import org.apache.struts2.StrutsStatics;
 import org.apache.struts2.dispatcher.Dispatcher;
-import org.apache.struts2.dispatcher.mapper.ActionMapping;
 import org.apache.struts2.dispatcher.ExecuteOperations;
 import org.apache.struts2.dispatcher.InitOperations;
 import org.apache.struts2.dispatcher.PrepareOperations;
+import org.apache.struts2.dispatcher.mapper.ActionMapping;
 
-import javax.servlet.*;
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
@@ -34,7 +39,7 @@ import java.io.IOException;
  * Executes the discovered request information.  This filter requires the {@link StrutsPrepareFilter} to have already
  * been executed in the current chain.
  */
-public class StrutsExecuteFilter implements StrutsStatics, Filter {
+public class StrutsExecuteFilter implements FileUploadSupport, StrutsStatics, Filter {
     protected PrepareOperations prepare;
     protected ExecuteOperations execute;
 
@@ -53,7 +58,6 @@ public class StrutsExecuteFilter implements StrutsStatics, Filter {
             prepare = new PrepareOperations(dispatcher);
             execute = new ExecuteOperations(dispatcher);
         }
-
     }
 
     public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
@@ -61,6 +65,11 @@ public class StrutsExecuteFilter implements StrutsStatics, Filter {
         HttpServletRequest request = (HttpServletRequest) req;
         HttpServletResponse response = (HttpServletResponse) res;
 
+        if (isFileUploadRequest(request)) {
+            chain.doFilter(request, response);
+            return;
+        }
+
         if (excludeUrl(request)) {
             chain.doFilter(request, response);
             return;
diff --git a/core/src/main/java/org/apache/struts2/dispatcher/filter/StrutsPrepareAndExecuteFilter.java b/core/src/main/java/org/apache/struts2/dispatcher/filter/StrutsPrepareAndExecuteFilter.java
index 54ee6883d..0e9c62d67 100644
--- a/core/src/main/java/org/apache/struts2/dispatcher/filter/StrutsPrepareAndExecuteFilter.java
+++ b/core/src/main/java/org/apache/struts2/dispatcher/filter/StrutsPrepareAndExecuteFilter.java
@@ -44,7 +44,7 @@ import java.util.regex.Pattern;
  * Handles both the preparation and execution phases of the Struts dispatching process.  This filter is better to use
  * when you don't have another filter that needs access to action context information, such as Sitemesh.
  */
-public class StrutsPrepareAndExecuteFilter implements StrutsStatics, Filter {
+public class StrutsPrepareAndExecuteFilter implements FileUploadSupport, StrutsStatics, Filter {
 
     private static final Logger LOG = LogManager.getLogger(StrutsPrepareAndExecuteFilter.class);
 
@@ -117,6 +117,11 @@ public class StrutsPrepareAndExecuteFilter implements StrutsStatics, Filter {
         HttpServletRequest request = (HttpServletRequest) req;
         HttpServletResponse response = (HttpServletResponse) res;
 
+        if (isFileUploadRequest(request)) {
+            chain.doFilter(request, response);
+            return;
+        }
+
         try {
             String uri = RequestUtils.getUri(request);
             if (excludedPatterns != null && prepare.isUrlExcluded(request, excludedPatterns)) {
diff --git a/core/src/main/java/org/apache/struts2/dispatcher/filter/StrutsPrepareFilter.java b/core/src/main/java/org/apache/struts2/dispatcher/filter/StrutsPrepareFilter.java
index b35d6cb10..a43b713a6 100644
--- a/core/src/main/java/org/apache/struts2/dispatcher/filter/StrutsPrepareFilter.java
+++ b/core/src/main/java/org/apache/struts2/dispatcher/filter/StrutsPrepareFilter.java
@@ -67,7 +67,7 @@ public class StrutsPrepareFilter implements StrutsStatics, Filter {
     /**
      * Callback for post initialization
      *
-     * @param dispatcher the dispatcher
+     * @param dispatcher   the dispatcher
      * @param filterConfig the filter config
      */
     protected void postInit(Dispatcher dispatcher, FilterConfig filterConfig) {
diff --git a/core/src/main/java/org/apache/struts2/dispatcher/multipart/AbstractMultiPartRequest.java b/core/src/main/java/org/apache/struts2/dispatcher/multipart/AbstractMultiPartRequest.java
index 700364047..9984d2dc9 100644
--- a/core/src/main/java/org/apache/struts2/dispatcher/multipart/AbstractMultiPartRequest.java
+++ b/core/src/main/java/org/apache/struts2/dispatcher/multipart/AbstractMultiPartRequest.java
@@ -44,7 +44,7 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest {
     public static final int BUFFER_SIZE = 10240;
 
     /**
-     * Internal list of raised errors to be passed to the the Struts2 framework.
+     * Internal list of raised errors to be passed to the Struts2 framework.
      */
     protected List<LocalizedMessage> errors = new ArrayList<>();
 
@@ -134,9 +134,9 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest {
         int forwardSlash = fileName.lastIndexOf('/');
         int backwardSlash = fileName.lastIndexOf('\\');
         if (forwardSlash != -1 && forwardSlash > backwardSlash) {
-            fileName = fileName.substring(forwardSlash + 1, fileName.length());
+            fileName = fileName.substring(forwardSlash + 1);
         } else {
-            fileName = fileName.substring(backwardSlash + 1, fileName.length());
+            fileName = fileName.substring(backwardSlash + 1);
         }
         return fileName;
     }
diff --git a/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequest.java b/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequest.java
index c629ec043..1a3a37e22 100644
--- a/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequest.java
+++ b/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequest.java
@@ -39,7 +39,9 @@ import java.util.*;
 
 /**
  * Multipart form data request adapter for Jakarta Commons Fileupload package.
+ * @deprecated since Struts 6.2.0 - please use {@link ServletMultiPartRequest} instead
  */
+@Deprecated
 public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
 
     static final Logger LOG = LogManager.getLogger(JakartaMultiPartRequest.class);
diff --git a/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaStreamMultiPartRequest.java b/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaStreamMultiPartRequest.java
index 2d016f56b..2b069a6a9 100644
--- a/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaStreamMultiPartRequest.java
+++ b/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaStreamMultiPartRequest.java
@@ -40,7 +40,9 @@ import java.util.*;
  *
  * @author Chris Cranford
  * @since 2.3.18
+ * @deprecated since Struts 6.2.0, please use {@link ServletMultiPartRequest} instead
  */
+@Deprecated
 public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
 
     static final Logger LOG = LogManager.getLogger(JakartaStreamMultiPartRequest.class);
diff --git a/core/src/main/java/org/apache/struts2/dispatcher/multipart/MultiPartRequest.java b/core/src/main/java/org/apache/struts2/dispatcher/multipart/MultiPartRequest.java
index 445dcd9a2..f21a01e51 100644
--- a/core/src/main/java/org/apache/struts2/dispatcher/multipart/MultiPartRequest.java
+++ b/core/src/main/java/org/apache/struts2/dispatcher/multipart/MultiPartRequest.java
@@ -18,11 +18,9 @@
  */
 package org.apache.struts2.dispatcher.multipart;
 
-import javax.servlet.http.HttpServletRequest;
-
 import org.apache.struts2.dispatcher.LocalizedMessage;
 
-import java.io.File;
+import javax.servlet.http.HttpServletRequest;
 import java.io.IOException;
 import java.util.Enumeration;
 import java.util.List;
@@ -33,7 +31,7 @@ import java.util.List;
 public interface MultiPartRequest {
 
     void parse(HttpServletRequest request, String saveDir) throws IOException;
-    
+
     /**
      * Returns an enumeration of the parameter names for uploaded files
      *
@@ -48,7 +46,7 @@ public interface MultiPartRequest {
      *
      * @param fieldName input field name
      * @return an array of content encoding for the specified input field name or <tt>null</tt> if
-     *         no content type was specified.
+     * no content type was specified.
      */
     String[] getContentType(String fieldName);
 
diff --git a/core/src/main/java/org/apache/struts2/dispatcher/multipart/ServletMultiPartRequest.java b/core/src/main/java/org/apache/struts2/dispatcher/multipart/ServletMultiPartRequest.java
new file mode 100644
index 000000000..0ec967564
--- /dev/null
+++ b/core/src/main/java/org/apache/struts2/dispatcher/multipart/ServletMultiPartRequest.java
@@ -0,0 +1,246 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.struts2.dispatcher.multipart;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.struts2.dispatcher.LocalizedMessage;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.Part;
+import java.io.File;
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * Pure Servlet API 3.1 based implementation
+ */
+public class ServletMultiPartRequest extends AbstractMultiPartRequest {
+
+    private static final Logger LOG = LogManager.getLogger(ServletMultiPartRequest.class);
+
+    private Map<String, List<FileData>> uploadedFiles = new HashMap<>();
+    private Map<String, List<String>> parameters = new HashMap<>();
+
+    @Override
+    public void parse(HttpServletRequest request, String saveDir) throws IOException {
+        try {
+            if (isSizeLimitExceeded(request)) {
+                applySizeLimitExceededError(request);
+                return;
+            }
+            parseParts(request, saveDir);
+        } catch (ServletException e) {
+            LOG.warn("Error occurred during parsing of multi part request", e);
+            LocalizedMessage errorMessage = buildErrorMessage(e, new Object[]{e.getMessage()});
+            if (!errors.contains(errorMessage)) {
+                errors.add(errorMessage);
+            }
+        }
+    }
+
+    private void parseParts(HttpServletRequest request, String saveDir) throws IOException, ServletException {
+        Collection<Part> parts = request.getParts();
+        if (parts.isEmpty()) {
+            LocalizedMessage error = buildErrorMessage(new IOException(), new Object[]{"No boundary defined!"});
+            if (!errors.contains(error)) {
+                errors.add(error);
+            }
+            return;
+        }
+        for (Part part : parts) {
+            if (part.getSubmittedFileName() == null) { // normal field
+                LOG.debug("Ignoring a normal form field: {}", part.getName());
+            } else { // file upload
+                LOG.debug("Storing file: {} in save dir: {}", part.getSubmittedFileName(), saveDir);
+                parseFile(part, saveDir);
+            }
+        }
+    }
+
+    private boolean isSizeLimitExceeded(HttpServletRequest request) {
+        if (request.getContentLength() > -1) {
+            return maxSizeProvided && request.getContentLength() > maxSize;
+        } else {
+            LOG.debug("Request Content Length is: {} which means the size overflows 2 GB!", request.getContentLength());
+            return true;
+        }
+    }
+
+    private void applySizeLimitExceededError(HttpServletRequest request) {
+        String exceptionMessage = "Request size: " + request.getContentLength() + " exceeded maximum size limit: " + maxSize;
+        SizeLimitExceededException exception = new SizeLimitExceededException(exceptionMessage);
+        LocalizedMessage message = buildErrorMessage(exception, new Object[]{request.getContentLength(), maxSize});
+        if (!errors.contains(message)) {
+            errors.add(message);
+        }
+    }
+
+    private void parseFile(Part part, String saveDir) throws IOException {
+        File file = extractFile(part, saveDir);
+        List<FileData> data = uploadedFiles.get(part.getName());
+        if (data == null) {
+            data = new ArrayList<>();
+        }
+        data.add(new FileData(file, part.getContentType(), part.getSubmittedFileName()));
+        uploadedFiles.put(part.getName(), data);
+    }
+
+    private File extractFile(Part part, String saveDir) throws IOException {
+        String name = part.getSubmittedFileName()
+            .substring(part.getSubmittedFileName().lastIndexOf('/') + 1)
+            .substring(part.getSubmittedFileName().lastIndexOf('\\') + 1);
+
+        String prefix = name;
+        String suffix = "";
+
+        if (name.contains(".")) {
+            prefix = name.substring(0, name.lastIndexOf('.'));
+            suffix = name.substring(name.lastIndexOf('.'));
+        }
+
+        if (prefix.length() < 3) {
+            prefix = UUID.randomUUID().toString();
+        }
+
+        File tempFile = File.createTempFile(prefix + "_", suffix, new File(saveDir));
+        LOG.debug("Stored file: {} as temporary file: {}", part.getSubmittedFileName(), tempFile.getName());
+        return tempFile;
+    }
+
+    @Override
+    public Enumeration<String> getFileParameterNames() {
+        return Collections.enumeration(uploadedFiles.keySet());
+    }
+
+    @Override
+    public String[] getContentType(String fieldName) {
+        List<FileData> fileData = uploadedFiles.get(fieldName);
+        if (fileData == null) {
+            LOG.debug("No file data for: {}", fieldName);
+            return null;
+        }
+        return fileData.stream().map(FileData::getContentType).toArray(String[]::new);
+    }
+
+    @Override
+    public UploadedFile[] getFile(String fieldName) {
+        List<FileData> fileData = uploadedFiles.get(fieldName);
+        if (fileData == null) {
+            LOG.debug("No file data for: {}", fieldName);
+            return null;
+        }
+
+        return fileData.stream().map(data -> new StrutsUploadedFile(data.getFile())).toArray(StrutsUploadedFile[]::new);
+    }
+
+    @Override
+    public String[] getFileNames(String fieldName) {
+        List<FileData> fileData = uploadedFiles.get(fieldName);
+        if (fileData == null) {
+            LOG.debug("No file data for: {}", fieldName);
+            return null;
+        }
+
+        return fileData.stream().map(FileData::getOriginalName).toArray(String[]::new);
+    }
+
+    @Override
+    public String[] getFilesystemName(String fieldName) {
+        List<FileData> fileData = uploadedFiles.get(fieldName);
+        if (fileData == null) {
+            LOG.debug("No file data for: {}", fieldName);
+            return null;
+        }
+
+        return fileData.stream().map(data -> data.getFile().getName()).toArray(String[]::new);
+    }
+
+    @Override
+    public String getParameter(String name) {
+        List<String> params = parameters.get(name);
+        if (params != null && params.size() > 0) {
+            return params.get(0);
+        }
+        LOG.debug("Ignoring parameter: {}", name);
+        return null;
+    }
+
+    @Override
+    public Enumeration<String> getParameterNames() {
+        return Collections.enumeration(parameters.keySet());
+    }
+
+    @Override
+    public String[] getParameterValues(String name) {
+        List<String> v = parameters.get(name);
+        if (v != null && v.size() > 0) {
+            return v.toArray(new String[0]);
+        }
+
+        LOG.debug("Ignoring values for parameter: {}", name);
+        return null;
+    }
+
+    @Override
+    public void cleanUp() {
+        uploadedFiles = new HashMap<>();
+        parameters = new HashMap<>();
+    }
+
+    public static class FileData implements Serializable {
+
+        private final File file;
+        private final String contentType;
+        private final String originalName;
+
+        public FileData(File file, String contentType, String originalName) {
+            this.file = file;
+            this.contentType = contentType;
+            this.originalName = originalName;
+        }
+
+        public File getFile() {
+            return file;
+        }
+
+        public String getContentType() {
+            return contentType;
+        }
+
+        public String getOriginalName() {
+            return originalName;
+        }
+    }
+
+    public static class SizeLimitExceededException extends Exception {
+        public SizeLimitExceededException(String message) {
+            super(message);
+        }
+    }
+}
diff --git a/core/src/main/java/org/apache/struts2/dispatcher/servlet/FileUploadServlet.java b/core/src/main/java/org/apache/struts2/dispatcher/servlet/FileUploadServlet.java
new file mode 100644
index 000000000..ca420b557
--- /dev/null
+++ b/core/src/main/java/org/apache/struts2/dispatcher/servlet/FileUploadServlet.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.struts2.dispatcher.servlet;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.struts2.RequestUtils;
+import org.apache.struts2.dispatcher.Dispatcher;
+import org.apache.struts2.dispatcher.ExecuteOperations;
+import org.apache.struts2.dispatcher.PrepareOperations;
+import org.apache.struts2.dispatcher.mapper.ActionMapping;
+
+import javax.servlet.ServletException;
+import javax.servlet.annotation.MultipartConfig;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+@MultipartConfig
+public class FileUploadServlet extends HttpServlet {
+
+    private static final Logger LOG = LogManager.getLogger(FileUploadServlet.class);
+
+    @Override
+    public void service(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
+        Dispatcher dispatcher = Dispatcher.getInstance(request.getServletContext());
+        if (dispatcher == null) {
+            throw new ServletException("No Dispatchers instance, servlet used out of the Struts filters!");
+        }
+
+        if (!dispatcher.isMultipartSupportEnabled(request)) {
+            throw new ServletException("Support for file upload is disabled!");
+        }
+
+        PrepareOperations prepare = new PrepareOperations(dispatcher);
+        ExecuteOperations execute = new ExecuteOperations(dispatcher);
+
+        String uri = RequestUtils.getUri(request);
+
+        if (dispatcher.isMultipartRequest(request)) {
+            prepare.setEncodingAndLocale(request, response);
+            prepare.createActionContext(request, response);
+            prepare.assignDispatcherToThread();
+            HttpServletRequest wrappedRequest = prepare.wrapRequest(request);
+            ActionMapping mapping = prepare.findActionMapping(wrappedRequest, response, true);
+            if (mapping == null) {
+                throw new ServletException(String.format("Cannot find mapping for %s, passing to other filters", uri));
+            } else {
+                LOG.trace("Found mapping {} for {}", mapping, uri);
+                execute.executeAction(wrappedRequest, response, mapping);
+            }
+        } else {
+            LOG.debug("Not a file upload request, ignoring uri: {}", uri);
+        }
+    }
+
+}
diff --git a/core/src/main/java/org/apache/struts2/interceptor/FileUploadInterceptor.java b/core/src/main/java/org/apache/struts2/interceptor/FileUploadInterceptor.java
index bb77ea093..482ab15b3 100644
--- a/core/src/main/java/org/apache/struts2/interceptor/FileUploadInterceptor.java
+++ b/core/src/main/java/org/apache/struts2/interceptor/FileUploadInterceptor.java
@@ -266,10 +266,10 @@ public class FileUploadInterceptor extends AbstractInterceptor {
         }
 
         // bind allowed Files
-        Enumeration fileParameterNames = multiWrapper.getFileParameterNames();
+        Enumeration<String> fileParameterNames = multiWrapper.getFileParameterNames();
         while (fileParameterNames != null && fileParameterNames.hasMoreElements()) {
             // get the value of this input tag
-            String inputName = (String) fileParameterNames.nextElement();
+            String inputName = fileParameterNames.nextElement();
 
             // get the content type
             String[] contentType = multiWrapper.getContentTypes(inputName);
@@ -298,9 +298,9 @@ public class FileUploadInterceptor extends AbstractInterceptor {
 
                         if (!acceptedFiles.isEmpty()) {
                             Map<String, Parameter> newParams = new HashMap<>();
-                            newParams.put(inputName, new Parameter.File(inputName, acceptedFiles.toArray(new UploadedFile[acceptedFiles.size()])));
-                            newParams.put(contentTypeName, new Parameter.File(contentTypeName, acceptedContentTypes.toArray(new String[acceptedContentTypes.size()])));
-                            newParams.put(fileNameName, new Parameter.File(fileNameName, acceptedFileNames.toArray(new String[acceptedFileNames.size()])));
+                            newParams.put(inputName, new Parameter.File(inputName, acceptedFiles.toArray(new UploadedFile[0])));
+                            newParams.put(contentTypeName, new Parameter.File(contentTypeName, acceptedContentTypes.toArray(new String[0])));
+                            newParams.put(fileNameName, new Parameter.File(fileNameName, acceptedFileNames.toArray(new String[0])));
                             ac.getParameters().appendAll(newParams);
                         }
                     }
@@ -430,9 +430,10 @@ public class FileUploadInterceptor extends AbstractInterceptor {
 
     private boolean isNonEmpty(Object[] objArray) {
         boolean result = false;
-        for (int index = 0; index < objArray.length && !result; index++) {
-            if (objArray[index] != null) {
+        for (Object o : objArray) {
+            if (o != null) {
                 result = true;
+                break;
             }
         }
         return result;
diff --git a/core/src/main/resources/org/apache/struts2/default.properties b/core/src/main/resources/org/apache/struts2/default.properties
index 5847db3be..2b53d6438 100644
--- a/core/src/main/resources/org/apache/struts2/default.properties
+++ b/core/src/main/resources/org/apache/struts2/default.properties
@@ -64,7 +64,7 @@ struts.objectFactory.spring.enableAopSupport = false
 # struts.multipart.parser=cos
 # struts.multipart.parser=pell
 # struts.multipart.parser=jakarta-stream
-struts.multipart.parser=jakarta
+struts.multipart.parser=servlet
 ### Uses javax.servlet.context.tempdir by default
 struts.multipart.saveDir=
 struts.multipart.maxSize=2097152
diff --git a/core/src/main/resources/struts-beans.xml b/core/src/main/resources/struts-beans.xml
index eac1ff8be..87a517667 100644
--- a/core/src/main/resources/struts-beans.xml
+++ b/core/src/main/resources/struts-beans.xml
@@ -88,6 +88,8 @@
           class="org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest" scope="prototype"/>
     <bean type="org.apache.struts2.dispatcher.multipart.MultiPartRequest" name="jakarta-stream"
           class="org.apache.struts2.dispatcher.multipart.JakartaStreamMultiPartRequest" scope="prototype"/>
+    <bean type="org.apache.struts2.dispatcher.multipart.MultiPartRequest" name="servlet"
+          class="org.apache.struts2.dispatcher.multipart.ServletMultiPartRequest" scope="prototype"/>
 
     <bean type="org.apache.struts2.views.TagLibraryModelProvider" name="s"
           class="org.apache.struts2.views.DefaultTagLibrary"/>
diff --git a/core/src/test/java/org/apache/struts2/dispatcher/StrutsPrepareAndExecuteFilterIntegrationTest.java b/core/src/test/java/org/apache/struts2/dispatcher/StrutsPrepareAndExecuteFilterIntegrationTest.java
index a5419cd8b..f7212f22d 100644
--- a/core/src/test/java/org/apache/struts2/dispatcher/StrutsPrepareAndExecuteFilterIntegrationTest.java
+++ b/core/src/test/java/org/apache/struts2/dispatcher/StrutsPrepareAndExecuteFilterIntegrationTest.java
@@ -26,6 +26,7 @@ import org.springframework.mock.web.MockFilterChain;
 import org.springframework.mock.web.MockFilterConfig;
 import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.mock.web.MockServletContext;
 
 import javax.servlet.ServletException;
 import javax.servlet.ServletRequest;
@@ -42,9 +43,10 @@ import java.util.ArrayList;
 public class StrutsPrepareAndExecuteFilterIntegrationTest extends TestCase {
 
     public void test404() throws ServletException, IOException {
-        MockHttpServletRequest request = new MockHttpServletRequest();
+        MockServletContext context = new MockServletContext();
+        MockHttpServletRequest request = new MockHttpServletRequest(context);
         MockHttpServletResponse response = new MockHttpServletResponse();
-        MockFilterConfig filterConfig = new MockFilterConfig();
+        MockFilterConfig filterConfig = new MockFilterConfig(context);
         MockFilterChain filterChain = new MockFilterChain() {
             @Override
             public void doFilter(ServletRequest req, ServletResponse res) {
@@ -62,9 +64,10 @@ public class StrutsPrepareAndExecuteFilterIntegrationTest extends TestCase {
     }
 
     public void test200() throws ServletException, IOException {
-        MockHttpServletRequest request = new MockHttpServletRequest();
+        MockServletContext context = new MockServletContext();
+        MockHttpServletRequest request = new MockHttpServletRequest(context);
         MockHttpServletResponse response = new MockHttpServletResponse();
-        MockFilterConfig filterConfig = new MockFilterConfig();
+        MockFilterConfig filterConfig = new MockFilterConfig(context);
         MockFilterChain filterChain = new MockFilterChain() {
             @Override
             public void doFilter(ServletRequest req, ServletResponse res) {
@@ -82,9 +85,10 @@ public class StrutsPrepareAndExecuteFilterIntegrationTest extends TestCase {
     }
 
     public void testActionMappingLookup() throws ServletException, IOException {
-        MockHttpServletRequest request = new MockHttpServletRequest();
+        MockServletContext context = new MockServletContext();
+        MockHttpServletRequest request = new MockHttpServletRequest(context);
         MockHttpServletResponse response = new MockHttpServletResponse();
-        MockFilterConfig filterConfig = new MockFilterConfig();
+        MockFilterConfig filterConfig = new MockFilterConfig(context);
         MockFilterChain filterChain = new MockFilterChain() {
             @Override
             public void doFilter(ServletRequest req, ServletResponse res) {
@@ -116,9 +120,10 @@ public class StrutsPrepareAndExecuteFilterIntegrationTest extends TestCase {
     }
 
     public void testUriPatternExclusion() throws ServletException, IOException {
-        MockHttpServletRequest request = new MockHttpServletRequest();
+        MockServletContext context = new MockServletContext();
+        MockHttpServletRequest request = new MockHttpServletRequest(context);
         MockHttpServletResponse response = new MockHttpServletResponse();
-        MockFilterConfig filterConfig = new MockFilterConfig();
+        MockFilterConfig filterConfig = new MockFilterConfig(context);
         MockFilterChain filterChain = new MockFilterChain() {
             @Override
             public void doFilter(ServletRequest req, ServletResponse res) {
@@ -142,9 +147,10 @@ public class StrutsPrepareAndExecuteFilterIntegrationTest extends TestCase {
     }
 
     public void testStaticFallthrough() throws ServletException, IOException {
-        MockHttpServletRequest request = new MockHttpServletRequest();
+        MockServletContext context = new MockServletContext();
+        MockHttpServletRequest request = new MockHttpServletRequest(context);
         MockHttpServletResponse response = new MockHttpServletResponse();
-        MockFilterConfig filterConfig = new MockFilterConfig();
+        MockFilterConfig filterConfig = new MockFilterConfig(context);
         MockFilterChain filterChain = new MockFilterChain() {
             @Override
             public void doFilter(ServletRequest req, ServletResponse res) {
@@ -169,9 +175,10 @@ public class StrutsPrepareAndExecuteFilterIntegrationTest extends TestCase {
     }
 
     public void testStaticExecute() throws ServletException, IOException {
-        MockHttpServletRequest request = new MockHttpServletRequest();
+        MockServletContext context = new MockServletContext();
+        MockHttpServletRequest request = new MockHttpServletRequest(context);
         MockHttpServletResponse response = new MockHttpServletResponse();
-        MockFilterConfig filterConfig = new MockFilterConfig();
+        MockFilterConfig filterConfig = new MockFilterConfig(context);
         MockFilterChain filterChain = new MockFilterChain() {
             @Override
             public void doFilter(ServletRequest req, ServletResponse res) {
diff --git a/core/src/test/java/org/apache/struts2/dispatcher/TwoFilterIntegrationTest.java b/core/src/test/java/org/apache/struts2/dispatcher/TwoFilterIntegrationTest.java
index edc88ba17..7075c7b68 100644
--- a/core/src/test/java/org/apache/struts2/dispatcher/TwoFilterIntegrationTest.java
+++ b/core/src/test/java/org/apache/struts2/dispatcher/TwoFilterIntegrationTest.java
@@ -20,44 +20,60 @@ package org.apache.struts2.dispatcher;
 
 import com.opensymphony.xwork2.ActionContext;
 import junit.framework.TestCase;
-import org.apache.struts2.dispatcher.Dispatcher;
-import org.apache.struts2.dispatcher.PrepareOperations;
 import org.apache.struts2.dispatcher.filter.StrutsExecuteFilter;
 import org.apache.struts2.dispatcher.filter.StrutsPrepareFilter;
-import org.springframework.mock.web.*;
+import org.springframework.mock.web.MockFilterChain;
+import org.springframework.mock.web.MockFilterConfig;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.mock.web.MockServletContext;
 
-import javax.servlet.*;
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
 import java.io.IOException;
-import java.util.LinkedList;
 import java.util.Arrays;
+import java.util.LinkedList;
 
 /**
  * Integration tests for the filter
  */
 public class TwoFilterIntegrationTest extends TestCase {
-    StrutsExecuteFilter filterExecute;
-    StrutsPrepareFilter filterPrepare;
-    Filter failFilter;
+
+    private StrutsExecuteFilter filterExecute;
+    private StrutsPrepareFilter filterPrepare;
+    private Filter failFilter;
     private Filter stringFilter;
 
     public void setUp() {
         filterPrepare = new StrutsPrepareFilter();
         filterExecute = new StrutsExecuteFilter();
         failFilter = new Filter() {
-            public void init(FilterConfig filterConfig) throws ServletException {}
+            public void init(FilterConfig filterConfig) throws ServletException {
+            }
+
             public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
                 fail("Should never get here");
             }
-            public void destroy() {}
+
+            public void destroy() {
+            }
         };
         stringFilter = new Filter() {
-            public void init(FilterConfig filterConfig) throws ServletException {}
+            public void init(FilterConfig filterConfig) throws ServletException {
+            }
+
             public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
                 response.getWriter().write("content");
                 assertNotNull(ActionContext.getContext());
                 assertNotNull(Dispatcher.getInstance());
             }
-            public void destroy() {}
+
+            public void destroy() {
+            }
         };
     }
 
@@ -86,7 +102,9 @@ public class TwoFilterIntegrationTest extends TestCase {
 
     public void testFilterInMiddle() throws ServletException, IOException {
         Filter middle = new Filter() {
-            public void init(FilterConfig filterConfig) throws ServletException {}
+            public void init(FilterConfig filterConfig) throws ServletException {
+            }
+
             public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
                 assertNotNull(ActionContext.getContext());
                 assertNotNull(Dispatcher.getInstance());
@@ -94,7 +112,9 @@ public class TwoFilterIntegrationTest extends TestCase {
                 chain.doFilter(request, response);
                 assertEquals("hello", ActionContext.getContext().getActionInvocation().getProxy().getActionName());
             }
-            public void destroy() {}
+
+            public void destroy() {
+            }
         };
         MockHttpServletResponse response = run("/hello.action", filterPrepare, middle, filterExecute, failFilter);
         assertEquals(200, response.getStatus());
@@ -102,9 +122,12 @@ public class TwoFilterIntegrationTest extends TestCase {
 
     private MockHttpServletResponse run(String uri, final Filter... filters) throws ServletException, IOException {
         final LinkedList<Filter> filterList = new LinkedList<>(Arrays.asList(filters));
-        MockHttpServletRequest request = new MockHttpServletRequest();
+
+        MockServletContext context = new MockServletContext();
+        MockHttpServletRequest request = new MockHttpServletRequest(context);
         MockHttpServletResponse response = new MockHttpServletResponse();
-        MockFilterConfig filterConfig = new MockFilterConfig();
+        MockFilterConfig filterConfig = new MockFilterConfig(context);
+
         MockFilterChain filterChain = new MockFilterChain() {
             @Override
             public void doFilter(ServletRequest req, ServletResponse res) {
diff --git a/core/src/test/java/org/apache/struts2/dispatcher/multipart/ServletMultiPartRequestTest.java b/core/src/test/java/org/apache/struts2/dispatcher/multipart/ServletMultiPartRequestTest.java
new file mode 100644
index 000000000..b036477b2
--- /dev/null
+++ b/core/src/test/java/org/apache/struts2/dispatcher/multipart/ServletMultiPartRequestTest.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.struts2.dispatcher.multipart;
+
+import org.apache.struts2.dispatcher.LocalizedMessage;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.springframework.mock.web.DelegatingServletInputStream;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class ServletMultiPartRequestTest {
+
+    private ServletMultiPartRequest multiPart;
+    private Path tempDir;
+
+    @Before
+    public void initialize() {
+        multiPart = new ServletMultiPartRequest();
+        tempDir = Paths.get("target", "multi-part-test");
+    }
+
+    /**
+     * Number of bytes in files greater than 2GB overflow the {@code int} primitive.
+     * The {@link HttpServletRequest#getContentLength()} returns {@literal -1}
+     * when the header is not present, or the size is greater than {@link Integer#MAX_VALUE}.
+     */
+    @Test
+    public void unknownContentLength() throws IOException {
+        HttpServletRequest request = mock(HttpServletRequest.class);
+        when(request.getContentType()).thenReturn("multipart/form-data; charset=utf-8; boundary=__X_BOUNDARY__");
+        when(request.getMethod()).thenReturn("POST");
+        when(request.getContentLength()).thenReturn(-1);
+
+        String entity = "\r\n--__X_BOUNDARY__\r\n" +
+            "Content-Disposition: form-data; name=\"upload\"; filename=\"test.csv\"\r\n" +
+            "Content-Type: text/csv\r\n\r\n1,2\r\n\r\n" +
+            "--__X_BOUNDARY__\r\n" +
+            "Content-Disposition: form-data; name=\"upload2\"; filename=\"test2.csv\"\r\n" +
+            "Content-Type: text/csv\r\n\r\n3,4\r\n\r\n" +
+            "--__X_BOUNDARY__--\r\n";
+        when(request.getInputStream()).thenReturn(new DelegatingServletInputStream(new ByteArrayInputStream(entity.getBytes(StandardCharsets.UTF_8))));
+
+        multiPart.setMaxSize("4");
+
+        multiPart.parse(request, tempDir.toString());
+        LocalizedMessage next = multiPart.getErrors().iterator().next();
+
+        assertEquals(next.getTextKey(), "struts.messages.upload.error.SizeLimitExceededException");
+    }
+}
diff --git a/core/src/test/java/org/apache/struts2/interceptor/FileUploadInterceptorTest.java b/core/src/test/java/org/apache/struts2/interceptor/FileUploadInterceptorTest.java
index b6a41010e..366db033e 100644
--- a/core/src/test/java/org/apache/struts2/interceptor/FileUploadInterceptorTest.java
+++ b/core/src/test/java/org/apache/struts2/interceptor/FileUploadInterceptorTest.java
@@ -29,11 +29,13 @@ import org.apache.struts2.ServletActionContext;
 import org.apache.struts2.StrutsInternalTestCase;
 import org.apache.struts2.TestAction;
 import org.apache.struts2.dispatcher.HttpParameters;
-import org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest;
+import org.apache.struts2.dispatcher.multipart.ServletMultiPartRequest;
 import org.apache.struts2.dispatcher.multipart.StrutsUploadedFile;
 import org.apache.struts2.dispatcher.multipart.MultiPartRequestWrapper;
 import org.apache.struts2.dispatcher.multipart.UploadedFile;
+import org.springframework.http.MediaType;
 import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockPart;
 
 import javax.servlet.http.HttpServletRequest;
 import java.io.File;
@@ -225,7 +227,7 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase {
     public void testInvalidContentTypeMultipartRequest() throws Exception {
         MockHttpServletRequest req = new MockHttpServletRequest();
 
-        req.setContentType("multipart/form-data"); // not a multipart contentype
+        req.setContentType("multipart/form-data"); // not a multipart content type
         req.setMethod("post");
 
         MyFileupAction action = container.inject(MyFileupAction.class);
@@ -279,6 +281,9 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase {
                 "\r\n" +
                 "-----1234--\r\n");
         req.setContent(content.getBytes("US-ASCII"));
+        MockPart part = new MockPart("file", "deleteme.txt", "Unit test of FileUploadInterceptor".getBytes());
+        part.getHeaders().setContentType(MediaType.TEXT_HTML);
+        req.addPart(part);
 
         MyFileupAction action = new MyFileupAction();
 
@@ -338,6 +343,15 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase {
         content.append("--");
         content.append(endline);
         req.setContent(content.toString().getBytes());
+        MockPart part1 = new MockPart("file", "test.html", plainContent.getBytes());
+        part1.getHeaders().setContentType(MediaType.TEXT_PLAIN);
+        req.addPart(part1);
+        MockPart part2 = new MockPart("file", "test1.html", htmlContent.getBytes());
+        part2.getHeaders().setContentType(MediaType.TEXT_HTML);
+        req.addPart(part2);
+        MockPart part3 = new MockPart("file", "test2.html", htmlContent.getBytes());
+        part3.getHeaders().setContentType(MediaType.TEXT_HTML);
+        req.addPart(part3);
 
         assertTrue(ServletFileUpload.isMultipartContent(req));
 
@@ -430,7 +444,7 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase {
     }
 
     private MultiPartRequestWrapper createMultipartRequest(HttpServletRequest req, int maxsize) throws IOException {
-        JakartaMultiPartRequest jak = new JakartaMultiPartRequest();
+        ServletMultiPartRequest jak = new ServletMultiPartRequest();
         jak.setMaxSize(String.valueOf(maxsize));
         return new MultiPartRequestWrapper(jak, req, tempDir.getAbsolutePath(), new DefaultLocaleProvider());
     }