'com.google.cloud.functions.HttpRequest instance does not support form-data with more than 1 file on the same field

I am creating a gcloud http Cloud Function which must receive a "form-data" form with a "medias" field which can contain up to 3 files, here is the basic code:

gcloud Java function :

package functions;

import com.google.cloud.functions.HttpFunction;
import com.google.cloud.functions.HttpRequest;
import com.google.cloud.functions.HttpResponse;
import java.io.BufferedWriter;
import java.io.IOException;

public class HelloWorld implements HttpFunction {
  @Override
  public void service(HttpRequest request, HttpResponse response)
      throws IOException {
    var parts = request.getParts();

    parts.forEach(
            (e1, e2) -> {
                System.out.println(e1);
                System.out.println(e2.getFileName().get());
            });


  }
}

My simple request :

curl --location --request POST 'http://localhost:8080/' \
--form 'medias=@"/file1.jpeg"' \
--form 'medias=@"/file2.jpeg"'

Oddly, "request.getParts();" returns a "Map<String, HttpPart>" and therefore I don't see how to retrieve multiple files of the same submitted parameter. In debugging, I get a nice:

"java.lang.IllegalStateException: Duplicate key medias (attempted merging values com.google.cloud.functions.invoker.http.HttpRequestImpl$HttpPartImpl@49e848cf

and

com.google.cloud.functions.invoker.http .HttpRequestImpl$HttpPartImpl@6d5d7015)"

The same query works without problem with "Springboot" by specifying this as a parameter:

@RequestParam(value = "medias", required = false) MultipartFile[] medias

So do you think it's a bug with this dependency?

<dependency>
    <groupId>com.google.cloud.functions</groupId>
    <artifactId>functions-framework-api</artifactId>
    <version>1.0.4</version>
    <scope>provided</scope>
</dependency>


Solution 1:[1]

As mentioned in this GCP docs, you can process the data with a multipart/form-data content type by using the sample code below.

for (HttpRequest.HttpPart httpPart : request.getParts().values()) {
      String filename = httpPart.getFileName().orElse(null);
      if (filename == null) {
        continue;
      }
      
      //do something here
}

I've done some tests using your scenario and tried manipulating the getParts() method using java.util.stream.Collectors to come up with a solution like Map<String, List<HttpPart>> but failed. It seems that it's not possible to use the same parameter key for each form-data.

I also checked the source code of the HttpRequest.getParts() method and found out that it has its own way of collecting the stream from the form-data that is failing when there's only a single key for each data as seen below:

public Map<String, HttpRequest.HttpPart> getParts() {
    String contentType = this.request.getContentType();
    if (contentType == null || this.request.getContentType().startsWith("multipart/form-data"))
        throw new IllegalStateException("Content-Type must be multipart/form-data: " + contentType);
     try {
         return (Map<String, HttpRequest.HttpPart>)this.request.getParts().stream().collect(Collectors.toMap(Part::getName, x -> new HttpPartImpl(x)));
     } catch (IOException e) {
         throw new UncheckedIOException(e);
     } catch (ServletException e) {
         throw new RuntimeException(e.getMessage(), e);
     }
}

Finally, my suggestion is to use "unique" parameter key for each form-data.

curl --location --request POST 'http://localhost:8080/' --form 'medias[item][email protected]' --form 'medias[item1][email protected]'

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1