web-dev-qa-db-fra.com

Java 9 HttpClient envoyer une requête multipart / form-data

Voici un formulaire:

<form action="/example/html5/demo_form.asp" method="post" 
enctype=”multipart/form-data”>
   <input type="file" name="img" />
   <input type="text" name=username" value="foo"/>
   <input type="submit" />
</form>

quand soumettra ce formulaire, la demande ressemblera à ceci:

POST /example/html5/demo_form.asp HTTP/1.1
Host: 10.143.47.59:9093
Connection: keep-alive
Content-Length: 326
Accept: application/json, text/javascript, */*; q=0.01
Origin: http://10.143.47.59:9093
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryEDKBhMZFowP9Leno
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.8,zh-CN;q=0.6,zh;q=0.4

Request Payload
------WebKitFormBoundaryEDKBhMZFowP9Leno
Content-Disposition: form-data; name="username"

foo
------WebKitFormBoundaryEDKBhMZFowP9Leno
Content-Disposition: form-data; name="img"; filename="out.txt"
Content-Type: text/plain


------WebKitFormBoundaryEDKBhMZFowP9Leno--

veuillez faire attention à la "Request Payload", vous pouvez voir les deux paramètres dans le formulaire, le nom d'utilisateur et l'img (form-data; name = "img"; filename = "out.txt"), et le nom de fin est le nom de fichier réel (ou chemin d'accès) dans votre système de fichiers, vous recevrez le fichier par nom (et non par nom de fichier) dans votre backend (comme le contrôleur de ressort).
si nous utilisons Apache Httpclient pour simuler la demande, nous écrirons un tel code:

MultipartEntity mutiEntity = newMultipartEntity();
File file = new File("/path/to/your/file");
mutiEntity.addPart("username",new StringBody("foo", Charset.forName("utf-8")));
mutiEntity.addPart("img", newFileBody(file)); //img is name, file is path

Mais en Java 9, nous pourrions écrire un tel code:

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.
        newBuilder(new URI("http:///example/html5/demo_form.asp"))
       .method("post",HttpRequest.BodyProcessor.fromString("foo"))
       .method("post", HttpRequest.BodyProcessor.fromFile(Paths.get("/path/to/your/file")))
       .build();
HttpResponse response = client.send(request, HttpResponse.BodyHandler.asString());
System.out.println(response.body());

Maintenant vous voyez, comment pourrais-je définir le "nom" du param?

11
vicfan

Je voulais le faire pour un projet sans avoir à tirer sur le client Apache, j'ai donc écrit un MultiPartBodyPublisher (Java 11, fyi):

import Java.io.IOException;
import Java.io.InputStream;
import Java.io.UncheckedIOException;
import Java.net.http.HttpRequest;
import Java.nio.charset.StandardCharsets;
import Java.nio.file.Files;
import Java.nio.file.Path;
import Java.util.*;
import Java.util.function.Supplier;

public class MultiPartBodyPublisher {
    private List<PartsSpecification> partsSpecificationList = new ArrayList<>();
    private String boundary = UUID.randomUUID().toString();

    public HttpRequest.BodyPublisher build() {
        if (partsSpecificationList.size() == 0) {
            throw new IllegalStateException("Must have at least one part to build multipart message.");
        }
        addFinalBoundaryPart();
        return HttpRequest.BodyPublishers.ofByteArrays(PartsIterator::new);
    }

    public String getBoundary() {
        return boundary;
    }

    public MultiPartBodyPublisher addPart(String name, String value) {
        PartsSpecification newPart = new PartsSpecification();
        newPart.type = PartsSpecification.TYPE.STRING;
        newPart.name = name;
        newPart.value = value;
        partsSpecificationList.add(newPart);
        return this;
    }

    public MultiPartBodyPublisher addPart(String name, Path value) {
        PartsSpecification newPart = new PartsSpecification();
        newPart.type = PartsSpecification.TYPE.FILE;
        newPart.name = name;
        newPart.path = value;
        partsSpecificationList.add(newPart);
        return this;
    }

    public MultiPartBodyPublisher addPart(String name, Supplier<InputStream> value, String filename, String contentType) {
        PartsSpecification newPart = new PartsSpecification();
        newPart.type = PartsSpecification.TYPE.STREAM;
        newPart.name = name;
        newPart.stream = value;
        newPart.filename = filename;
        newPart.contentType = contentType;
        partsSpecificationList.add(newPart);
        return this;
    }

    private void addFinalBoundaryPart() {
        PartsSpecification newPart = new PartsSpecification();
        newPart.type = PartsSpecification.TYPE.FINAL_BOUNDARY;
        newPart.value = "--" + boundary + "--";
        partsSpecificationList.add(newPart);
    }

    static class PartsSpecification {

        public enum TYPE {
            STRING, FILE, STREAM, FINAL_BOUNDARY
        }

        PartsSpecification.TYPE type;
        String name;
        String value;
        Path path;
        Supplier<InputStream> stream;
        String filename;
        String contentType;

    }

    class PartsIterator implements Iterator<byte[]> {

        private Iterator<PartsSpecification> iter;
        private InputStream currentFileInput;

        private boolean done;
        private byte[] next;

        PartsIterator() {
            iter = partsSpecificationList.iterator();
        }

        @Override
        public boolean hasNext() {
            if (done) return false;
            if (next != null) return true;
            try {
                next = computeNext();
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
            if (next == null) {
                done = true;
                return false;
            }
            return true;
        }

        @Override
        public byte[] next() {
            if (!hasNext()) throw new NoSuchElementException();
            byte[] res = next;
            next = null;
            return res;
        }

        private byte[] computeNext() throws IOException {
            if (currentFileInput == null) {
                if (!iter.hasNext()) return null;
                PartsSpecification nextPart = iter.next();
                if (PartsSpecification.TYPE.STRING.equals(nextPart.type)) {
                    String part =
                            "--" + boundary + "\r\n" +
                            "Content-Disposition: form-data; name=" + nextPart.name + "\r\n" +
                            "Content-Type: text/plain; charset=UTF-8\r\n\r\n" +
                            nextPart.value + "\r\n";
                    return part.getBytes(StandardCharsets.UTF_8);
                }
                if (PartsSpecification.TYPE.FINAL_BOUNDARY.equals(nextPart.type)) {
                    return nextPart.value.getBytes(StandardCharsets.UTF_8);
                }
                String filename;
                String contentType;
                if (PartsSpecification.TYPE.FILE.equals(nextPart.type)) {
                    Path path = nextPart.path;
                    filename = path.getFileName().toString();
                    contentType = Files.probeContentType(path);
                    if (contentType == null) contentType = "application/octet-stream";
                    currentFileInput = Files.newInputStream(path);
                } else {
                    filename = nextPart.filename;
                    contentType = nextPart.contentType;
                    if (contentType == null) contentType = "application/octet-stream";
                    currentFileInput = nextPart.stream.get();
                }
                String partHeader =
                        "--" + boundary + "\r\n" +
                        "Content-Disposition: form-data; name=" + nextPart.name + "; filename=" + filename + "\r\n" +
                        "Content-Type: " + contentType + "\r\n\r\n";
                return partHeader.getBytes(StandardCharsets.UTF_8);
            } else {
                byte[] buf = new byte[8192];
                int r = currentFileInput.read(buf);
                if (r > 0) {
                    byte[] actualBytes = new byte[r];
                    System.arraycopy(buf, 0, actualBytes, 0, r);
                    return actualBytes;
                } else {
                    currentFileInput.close();
                    currentFileInput = null;
                    return "\r\n".getBytes(StandardCharsets.UTF_8);
                }
            }
        }
    }
}

Vous pouvez l'utiliser à peu près ainsi:

MultiPartBodyPublisher publisher = new MultiPartBodyPublisher()
       .addPart("someString", "foo")
       .addPart("someInputStream", () -> this.getClass().getResourceAsStream("test.txt"), "test.txt", "text/plain")
       .addPart("someFile", pathObject);
HttpRequest request = HttpRequest.newBuilder()
       .uri(URI.create("https://www.example.com/dosomething"))
       .header("Content-Type", "multipart/form-data; boundary=" + publisher.getBoundary())
       .timeout(Duration.ofMinutes(1))
       .POST(publisher.build())
       .build();

Notez que addPart pour les flux d'entrée prend en fait Supplier<InputStream> et pas seulement un InputStream.

8
ittupelo

Une direction dans laquelle vous pouvez atteindre un appel multiform-data pourrait être la suivante:

BodyProcessor peut être utilisé avec leurs implémentations par défaut ou bien une implémentation personnalisée peut également être utilisée. Peu de façons de les utiliser sont:

  1. Lisez le processeur via une chaîne comme:

    HttpRequest.BodyProcessor dataProcessor = HttpRequest.BodyProcessor.fromString("{\"username\":\"foo\"}")
    
  2. Création d'un processeur à partir d'un fichier en utilisant son chemin

    Path path = Paths.get("/path/to/your/file"); // in your case path to 'img'
    HttpRequest.BodyProcessor fileProcessor = HttpRequest.BodyProcessor.fromFile(path);
    

OR

  1. Vous pouvez convertir l'entrée de fichier en un tableau d'octets en utilisant le Apache.commons.lang (ou une méthode personnalisée que vous pouvez trouver) pour ajouter un petit util comme:

    org.Apache.commons.fileupload.FileItem file;
    
    org.Apache.http.HttpEntity multipartEntity = org.Apache.http.entity.mime.MultipartEntityBuilder.create()
           .addPart("username",new StringBody("foo", Charset.forName("utf-8")))
           .addPart("img", newFileBody(file))
           .build();
    multipartEntity.writeTo(byteArrayOutputStream);
    byte[] bytes = byteArrayOutputStream.toByteArray();
    

    puis l'octet [] peut être utilisé avec BodyProcessor comme:

    HttpRequest.BodyProcessor byteProcessor = HttpRequest.BodyProcessor.fromByteArray();
    

De plus, vous pouvez créer la demande comme:

HttpRequest request = HttpRequest.newBuilder()
            .uri(new URI("http:///example/html5/demo_form.asp"))
            .headers("Content-Type","multipart/form-data","boundary","boundaryValue") // appropriate boundary values
            .POST(dataProcessor)
            .POST(fileProcessor)
            .POST(byteProcessor) //self-sufficient
            .build();

La réponse pour le même peut être gérée comme un fichier et avec un nouveau HttpClient en utilisant

HttpResponse.BodyHandler bodyHandler = HttpResponse.BodyHandler.asFile(Paths.get("/path"));

HttpClient client = HttpClient.newBuilder().build();

comme:

HttpResponse response = client.send(request, bodyHandler);
System.out.println(response.body());
2
Naman

J'ai lutté avec ce problème pendant un certain temps, même après avoir vu et lu cette page. Mais, en utilisant les réponses sur cette page pour m'orienter dans la bonne direction, en lisant plus sur les formes et les limites en plusieurs parties et en bricolant, j'ai pu créer une solution de travail.

L'essentiel de la solution consiste à utiliser MultipartEntityBuilder d'Apache pour créer l'entité et ses limites (HttpExceptionBuilder est une classe locale):

import Java.io.BufferedInputStream;
import Java.io.File;
import Java.io.FileInputStream;
import Java.io.FileNotFoundException;
import Java.io.IOException;
import Java.io.InputStream;
import Java.util.Optional;
import Java.util.function.Supplier;

import org.Apache.commons.lang3.Validate;
import org.Apache.http.HttpEntity;
import org.Apache.http.entity.BufferedHttpEntity;
import org.Apache.http.entity.ContentType;
import org.Apache.http.entity.mime.MultipartEntityBuilder;

/**
 * Class containing static helper methods pertaining to HTTP interactions.
 */
public class HttpUtils {
    public static final String MULTIPART_FORM_DATA_BOUNDARY = "ThisIsMyBoundaryThereAreManyLikeItButThisOneIsMine";

    /**
     * Creates an {@link HttpEntity} from a {@link File}, loading it into a {@link BufferedHttpEntity}.
     *
     * @param file     the {@link File} from which to create an {@link HttpEntity}
     * @param partName an {@link Optional} denoting the name of the form data; defaults to {@code data}
     * @return an {@link HttpEntity} containing the contents of the provided {@code file}
     * @throws NullPointerException  if {@code file} or {@code partName} is null
     * @throws IllegalStateException if {@code file} does not exist
     * @throws HttpException         if file cannot be found or {@link FileInputStream} cannot be created
     */
    public static HttpEntity getFileAsBufferedMultipartEntity(final File file, final Optional<String> partName) {
        Validate.notNull(file, "file cannot be null");
        Validate.validState(file.exists(), "file must exist");
        Validate.notNull(partName, "partName cannot be null");

        final HttpEntity entity;
        final BufferedHttpEntity bufferedHttpEntity;

        try (final FileInputStream fis = new FileInputStream(file);
                final BufferedInputStream bis = new BufferedInputStream(fis)) {
            entity = MultipartEntityBuilder.create().setBoundary(MULTIPART_FORM_DATA_BOUNDARY)
                    .addBinaryBody(partName.orElse("data"), bis, ContentType.APPLICATION_OCTET_STREAM, file.getName())
                    .setContentType(ContentType.MULTIPART_FORM_DATA).build();

            try {
                bufferedHttpEntity = new BufferedHttpEntity(entity);
            } catch (final IOException e) {
                throw HttpExceptionBuilder.create().withMessage("Unable to create BufferedHttpEntity").withThrowable(e)
                        .build();
            }
        } catch (final FileNotFoundException e) {
            throw HttpExceptionBuilder.create()
                    .withMessage("File does not exist or is not readable: %s", file.getAbsolutePath()).withThrowable(e)
                    .build();
        } catch (final IOException e) {
            throw HttpExceptionBuilder.create()
                    .withMessage("Unable to create multipart entity from file: %s", file.getAbsolutePath())
                    .withThrowable(e).build();
        }

        return bufferedHttpEntity;
    }

    /**
     * Returns a {@link Supplier} of {@link InputStream} containing the content of the provided {@link HttpEntity}. This
     * method closes the {@code InputStream}.
     *
     * @param entity the {@link HttpEntity} from which to get an {@link InputStream}
     * @return an {@link InputStream} containing the {@link HttpEntity#getContent() content}
     * @throws NullPointerException if {@code entity} is null
     * @throws HttpException        if something goes wrong
     */
    public static Supplier<? extends InputStream> getInputStreamFromHttpEntity(final HttpEntity entity) {
        Validate.notNull(entity, "entity cannot be null");

        return () -> {
            try (final InputStream is = entity.getContent()) {
                return is;
            } catch (final UnsupportedOperationException | IOException e) {
                throw HttpExceptionBuilder.create().withMessage("Unable to get InputStream from HttpEntity")
                        .withThrowable(e).build();
            }
        };
    }
}

Et puis une méthode qui utilise ces méthodes d'assistance:

private String doUpload(final File uploadFile, final String filePostUrl) {
    assert uploadFile != null : "uploadFile cannot be null";
    assert uploadFile.exists() : "uploadFile must exist";
    assert StringUtils.notBlank(filePostUrl, "filePostUrl cannot be blank");

    final URI uri = URI.create(filePostUrl);
    final HttpEntity entity = HttpUtils.getFileAsBufferedMultipartEntity(uploadFile, Optional.of("partName"));
    final String response;

    try {
        final Builder requestBuilder = HttpRequest.newBuilder(uri)
                .POST(BodyPublisher.fromInputStream(HttpUtils.getInputStreamFromHttpEntity(entity)))
                .header("Content-Type", "multipart/form-data; boundary=" + HttpUtils.MULTIPART_FORM_DATA_BOUNDARY);

        response = this.httpClient.send(requestBuilder.build(), BodyHandler.asString());
    } catch (InterruptedException | ExecutionException e) {
        throw HttpExceptionBuilder.create().withMessage("Unable to get InputStream from HttpEntity")
                    .withThrowable(e).build();
    }

    LOGGER.info("Http Response: {}", response);
    return response;
}
2
liltitus27

Il est possible d'utiliser multipart/form-data ou tout autre type de contenu - mais vous devez encoder le corps dans le bon format vous-même. Le client lui-même ne fait aucun encodage basé sur le type de contenu.

Cela signifie que votre meilleure option est d'utiliser un autre client HTTP similaire Apache HttpComponents client ou d'utiliser uniquement l'encodeur d'une autre bibliothèque comme dans l'exemple de la réponse de @ nullpointer .


Si vous codez le corps vous-même, notez que vous ne pouvez pas appeler plusieurs fois des méthodes comme POST. POST définit simplement le BodyProcessor et le rappeler ne remplacera que les processeurs précédemment définis. Vous devez implémenter un processeur qui produit le corps entier dans le format correct.

Pour multipart/form-data cela signifie:

  1. Définissez l'en-tête boundary sur une valeur appropriée
  2. Encodez chaque paramètre pour qu'il ressemble à votre exemple. Fondamentalement, quelque chose comme ça pour la saisie de texte:

    boundary + "\nContent-Disposition: form-data; name=\"" + name + "\"\n\n" + value + "\n"
    

    Ici, le nom fait référence à l'attribut name dans le formulaire HTML. Pour l'entrée de fichier dans la question, ce serait img et la valeur serait le contenu du fichier encodé.

2
kapex

J'ai récemment publié une bibliothèque fournissant des extensions utiles à Java 11's HttpClient. La bibliothèque contient un MultipartBodyPublisher avec un moyen pratique et facile à utilisation MultipartBodyPublisher.Builder. Voici un exemple d'utilisation (JDK11 ou version ultérieure est requis):

MultipartBodyPublisher multipartBody = MultipartBodyPublisher.newBuilder()
    .textPart("foo", "foo_text")
    .filePart("bar", Path.of("path/to/file.txt"))
    .formPart("baz", BodyPublishers.ofInputStream(() -> ...))
    .build();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://example.com/"))
    .POST(multipartBody)
    .build();

Notez que vous pouvez également ajouter le BodyPublisher (ou HttpHeaders) que vous souhaitez. Voir wiki pour plus de détails.

0
Moataz Abdelnasser