Repository: vert-x3/vertx-web
Issue: fileUploads are un-ordered

### Questions

`fileUploads` are stored in an `HashSet`, thus we lose the order they were sent from.

### Version

4.3.0-SNAPSHOT

### Context

I have a route which handle file uploads and it needs to retrieves the file names in the order they were sent.

### Do you have a reproducer?

The `HashSet` is created here:
https://github.com/vert-x3/vertx-web/blob/a60906bf3a88f944c47f324aa1f847b8cc367fea/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextImpl.java#L538-L543

Could a `LinkedHashSet` be used here instead?



Changed files:
vertx-web-validation/src/main/java/io/vertx/ext/web/validation/RequestPredicate.java
vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextDecorator.java
vertx-web-api-contract/src/main/java/io/vertx/ext/web/api/validation/impl/BaseValidationHandler.java
vertx-web/src/main/java/io/vertx/ext/web/RoutingContext.java
vertx-web/src/test/java/io/vertx/ext/web/handler/BodyHandlerTest.java
vertx-web/src/main/java/examples/WebExamples.java
vertx-web-api-contract/src/main/java/io/vertx/ext/web/api/contract/openapi3/impl/OpenAPI3RequestValidationHandlerImpl.java
vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextImpl.java
vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextWrapper.java
vertx-web/src/main/java/io/vertx/ext/web/handler/impl/BodyHandlerImpl.java

diff --git a/vertx-web-api-contract/src/main/java/io/vertx/ext/web/api/contract/openapi3/impl/OpenAPI3RequestValidationHandlerImpl.java b/vertx-web-api-contract/src/main/java/io/vertx/ext/web/api/contract/openapi3/impl/OpenAPI3RequestValidationHandlerImpl.java
index d8428e232..5992cc391 100644
--- a/vertx-web-api-contract/src/main/java/io/vertx/ext/web/api/contract/openapi3/impl/OpenAPI3RequestValidationHandlerImpl.java
+++ b/vertx-web-api-contract/src/main/java/io/vertx/ext/web/api/contract/openapi3/impl/OpenAPI3RequestValidationHandlerImpl.java
@@ -44,7 +44,7 @@ public class OpenAPI3RequestValidationHandlerImpl extends HTTPOperationRequestVa
       this.isOptional = isOptional;
     }
 
-    private boolean existFileUpload(Set<FileUpload> files, String name, Pattern contentType) {
+    private boolean existFileUpload(List<FileUpload> files, String name, Pattern contentType) {
       for (FileUpload f : files) {
         if (f.name().equals(name) && contentType.matcher(f.contentType()).matches()) return true;
       }
diff --git a/vertx-web-api-contract/src/main/java/io/vertx/ext/web/api/validation/impl/BaseValidationHandler.java b/vertx-web-api-contract/src/main/java/io/vertx/ext/web/api/validation/impl/BaseValidationHandler.java
index 4c83a015c..4c14462ba 100644
--- a/vertx-web-api-contract/src/main/java/io/vertx/ext/web/api/validation/impl/BaseValidationHandler.java
+++ b/vertx-web-api-contract/src/main/java/io/vertx/ext/web/api/validation/impl/BaseValidationHandler.java
@@ -275,7 +275,7 @@ public abstract class BaseValidationHandler implements ValidationHandler {
     return parsedParams;
   }
 
-  private boolean existFileUpload(Set<FileUpload> files, String name, Pattern contentType) {
+  private boolean existFileUpload(List<FileUpload> files, String name, Pattern contentType) {
     for (FileUpload f : files) {
       if (f.name().equals(name) && contentType.matcher(f.contentType()).matches()) return true;
     }
@@ -283,7 +283,7 @@ public abstract class BaseValidationHandler implements ValidationHandler {
   }
 
   private void validateFileUpload(RoutingContext routingContext) throws ValidationException {
-    Set<FileUpload> fileUploads = routingContext.fileUploads();
+    List<FileUpload> fileUploads = routingContext.fileUploads();
     for (Map.Entry<String, Pattern> expectedFile : multipartFileRules.entrySet()) {
       if (!existFileUpload(fileUploads, expectedFile.getKey(), expectedFile.getValue()))
         throw ValidationException.ValidationExceptionFactory.generateFileNotFoundValidationException(expectedFile
diff --git a/vertx-web-validation/src/main/java/io/vertx/ext/web/validation/RequestPredicate.java b/vertx-web-validation/src/main/java/io/vertx/ext/web/validation/RequestPredicate.java
index 6b04a31e6..e408838d7 100644
--- a/vertx-web-validation/src/main/java/io/vertx/ext/web/validation/RequestPredicate.java
+++ b/vertx-web-validation/src/main/java/io/vertx/ext/web/validation/RequestPredicate.java
@@ -5,7 +5,7 @@ import io.vertx.core.http.HttpHeaders;
 import io.vertx.ext.web.FileUpload;
 import io.vertx.ext.web.RoutingContext;
 
-import java.util.Set;
+import java.util.List;
 import java.util.function.Function;
 import java.util.regex.Pattern;
 
@@ -40,7 +40,7 @@ public interface RequestPredicate extends Function<RoutingContext, RequestPredic
         rc.request().headers().contains(HttpHeaders.CONTENT_TYPE) &&
         rc.request().getHeader(HttpHeaders.CONTENT_TYPE).contains("multipart/form-data")
       ) {
-        Set<FileUpload> files = rc.fileUploads();
+        List<FileUpload> files = rc.fileUploads();
         for (FileUpload f : files) {
           if (f.name().equals(propertyName) && contentType.matcher(f.contentType()).matches()) return success();
         }
diff --git a/vertx-web/src/main/java/examples/WebExamples.java b/vertx-web/src/main/java/examples/WebExamples.java
index 9dd9ad7f9..52c09446b 100644
--- a/vertx-web/src/main/java/examples/WebExamples.java
+++ b/vertx-web/src/main/java/examples/WebExamples.java
@@ -696,7 +696,7 @@ public class WebExamples {
 
     router.post("/some/path/uploads").handler(ctx -> {
 
-      Set<FileUpload> uploads = ctx.fileUploads();
+      List<FileUpload> uploads = ctx.fileUploads();
       // Do something with uploads....
 
     });
diff --git a/vertx-web/src/main/java/io/vertx/ext/web/RoutingContext.java b/vertx-web/src/main/java/io/vertx/ext/web/RoutingContext.java
index 9af928097..2fd2b1afe 100644
--- a/vertx-web/src/main/java/io/vertx/ext/web/RoutingContext.java
+++ b/vertx-web/src/main/java/io/vertx/ext/web/RoutingContext.java
@@ -378,10 +378,10 @@ public interface RoutingContext {
   RequestBody body();
 
   /**
-   * @return a set of fileuploads (if any) for the request. The context must have first been routed to a
+   * @return a list of {@link FileUpload} (if any) for the request. The context must have first been routed to a
    * {@link io.vertx.ext.web.handler.BodyHandler} for this to work.
    */
-  Set<FileUpload> fileUploads();
+  List<FileUpload> fileUploads();
 
   /**
    * Get the session. The context must have first been routed to a {@link io.vertx.ext.web.handler.SessionHandler}
diff --git a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/BodyHandlerImpl.java b/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/BodyHandlerImpl.java
index 9a286199f..0269e9cca 100644
--- a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/BodyHandlerImpl.java
+++ b/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/BodyHandlerImpl.java
@@ -17,7 +17,7 @@
 package io.vertx.ext.web.handler.impl;
 
 import java.io.File;
-import java.util.Set;
+import java.util.List;
 import java.util.UUID;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -210,7 +210,7 @@ public class BodyHandlerImpl implements BodyHandler {
         initBodyBuffer();
       }
 
-      Set<FileUpload> fileUploads = context.fileUploads();
+      List<FileUpload> fileUploads = context.fileUploads();
 
       final String contentType = context.request().getHeader(HttpHeaders.CONTENT_TYPE);
       if (contentType == null) {
diff --git a/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextDecorator.java b/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextDecorator.java
index 819e11e39..9a2820107 100644
--- a/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextDecorator.java
+++ b/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextDecorator.java
@@ -133,7 +133,7 @@ public class RoutingContextDecorator implements RoutingContextInternal {
   }
 
   @Override
-  public Set<FileUpload> fileUploads() {
+  public List<FileUpload> fileUploads() {
     return decoratedContext.fileUploads();
   }
 
diff --git a/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextImpl.java b/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextImpl.java
index 79413cb55..d7b7a1ae4 100644
--- a/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextImpl.java
+++ b/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextImpl.java
@@ -27,7 +27,6 @@ import io.vertx.core.impl.ContextInternal;
 import io.vertx.ext.auth.User;
 import io.vertx.ext.web.RequestBody;
 import io.vertx.ext.web.FileUpload;
-import io.vertx.ext.web.RequestBody;
 import io.vertx.ext.web.RoutingContext;
 import io.vertx.ext.web.Session;
 import io.vertx.ext.web.handler.HttpException;
@@ -67,7 +66,7 @@ public class RoutingContextImpl extends RoutingContextImplBase {
   private String acceptableContentType;
   private ParsableHeaderValuesContainer parsedHeaders;
 
-  private Set<FileUpload> fileUploads;
+  private List<FileUpload> fileUploads;
   private Session session;
   private User user;
 
@@ -304,8 +303,11 @@ public class RoutingContextImpl extends RoutingContextImplBase {
   }
 
   @Override
-  public Set<FileUpload> fileUploads() {
-    return getFileUploads();
+  public List<FileUpload> fileUploads() {
+    if (fileUploads == null) {
+      fileUploads = new ArrayList<>();
+    }
+    return fileUploads;
   }
 
   @Override
@@ -535,13 +537,6 @@ public class RoutingContextImpl extends RoutingContextImplBase {
     return endHandlers;
   }
 
-  private Set<FileUpload> getFileUploads() {
-    if (fileUploads == null) {
-      fileUploads = new HashSet<>();
-    }
-    return fileUploads;
-  }
-
   private void doFail() {
     this.iter = router.iterator();
     currentRoute = null;
diff --git a/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextWrapper.java b/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextWrapper.java
index 22dca1361..4a4bfe546 100644
--- a/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextWrapper.java
+++ b/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextWrapper.java
@@ -279,7 +279,7 @@ public class RoutingContextWrapper extends RoutingContextImplBase {
   }
 
   @Override
-  public Set<FileUpload> fileUploads() {
+  public List<FileUpload> fileUploads() {
     return inner.fileUploads();
   }
 
diff --git a/vertx-web/src/test/java/io/vertx/ext/web/handler/BodyHandlerTest.java b/vertx-web/src/test/java/io/vertx/ext/web/handler/BodyHandlerTest.java
index f49853c37..ba298bc1f 100644
--- a/vertx-web/src/test/java/io/vertx/ext/web/handler/BodyHandlerTest.java
+++ b/vertx-web/src/test/java/io/vertx/ext/web/handler/BodyHandlerTest.java
@@ -36,7 +36,7 @@ import java.io.File;
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.util.Base64;
-import java.util.Set;
+import java.util.List;
 import java.util.function.Function;
 
 /**
@@ -247,7 +247,7 @@ public class BodyHandlerTest extends WebTestBase {
     String contentType = "application/octet-stream";
     Buffer fileData = TestUtils.randomBuffer(size);
     router.route().handler(rc -> {
-      Set<FileUpload> fileUploads = rc.fileUploads();
+      List<FileUpload> fileUploads = rc.fileUploads();
       assertNotNull(fileUploads);
       assertEquals(1, fileUploads.size());
       FileUpload upload = fileUploads.iterator().next();
@@ -830,7 +830,7 @@ public class BodyHandlerTest extends WebTestBase {
     String contentType = "application/octet-stream";
     Buffer fileData = TestUtils.randomBuffer(50);
     router.route().handler(rc -> {
-      Set<FileUpload> fileUploads = rc.fileUploads();
+      List<FileUpload> fileUploads = rc.fileUploads();
       assertNotNull(fileUploads);
       assertEquals(1, fileUploads.size());
       FileUpload upload = fileUploads.iterator().next();