В этой статье (хотя на статью она не особо тянет, просто заметка, скорее всего) мы научимся отправлять HTTP POST запрос с ContentType: multipart/form-data с помощью встроенного в Java 11 HttpClient.
Тут надо сразу признать, что в самом HttpClient, который вошёл в Java 11, нет особой поддержки multipart/form-data (или я не нашёл?), поэтому большую часть тела запроса приходится формировать вручную.
Для начала нужно понять, что такое multipart/form-data? Этот тип Content-Type используется в первую очередь для отправки файлов на сервер. Тело запроса разделяется на части с помощью последовательности символов-ограничителей (boundary), которые генерируются случайным образом с двумя предваряющими минусами --. Каждая часть содержит один элемент управления формы, который содержит данные файла или другого элемента ввода.
Сама последовательность символов boundary указывается в Content-Type:
1 |
Content-Type: multipart/form-data; boundary=---------------------------257907702532414757652973581333 |
Пример запроса multipart/form-data, генерируемого Mozilla Firefox:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
POST /upload HTTP/1.1 Host: example.com User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:79.0) Gecko/20100101 Firefox/79.0 Accept: */* Accept-Language: ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate, br Content-Type: multipart/form-data; boundary=---------------------------257907702532414757652973581333 Content-Length: 16737 Origin: http://example.com DNT: 1 Connection: keep-alive Referer: http://example.com Pragma: no-cache Cache-Control: no-cache -----------------------------257907702532414757652973581333 Content-Disposition: form-data; name="field1" value1 -----------------------------257907702532414757652973581333 Content-Disposition: form-data; name="field2" value2 -----------------------------257907702532414757652973581333 Content-Disposition: form-data; name="file"; filename="small-jpg-image.webp" Content-Type: application/octet-stream <ЗДЕСЬ ИДУТ БАЙТЫ СОДЕРЖИМОГО ФАЙЛА> |
А теперь код на Java,который может генерировать аналогичные запросы. Самая сложная часть здесь в ofMultipartData, всё остальное просто обычная инициализация HttpClient:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpRequest.BodyPublisher; import java.net.http.HttpRequest.BodyPublishers; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; public class HttpClientMultipartExample { public static void main(String[] args) throws IOException, InterruptedException, URISyntaxException, NoSuchAlgorithmException { String url = "http://127.0.0.1:8080"; String filePath = "/home/user/Изображения/044c0e33a8f98fc898de8.png"; String boundary = "-------------oiawn4tp89n4e9p5"; Map<Object, Object> data = new HashMap<>(); // some form fields data.put("field1", "value1"); data.put("field2", "value2"); // file upload data.put("attachment1", Paths.get(filePath)); HttpRequest request = HttpRequest.newBuilder().uri(new URI(url)) .headers("Content-Type", "multipart/form-data; boundary=" + boundary) .POST(oMultipartData(data, boundary)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(java.time.Duration.ofSeconds(10L)) .build(); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("response statusCode = " + response.statusCode()); System.out.println("response body = " + response.body()); } public static BodyPublisher oMultipartData(Map<Object, Object> data, String boundary) throws IOException { var byteArrays = new ArrayList<byte[]>(); byte[] separator = ("--" + boundary + "\r\nContent-Disposition: form-data; name=") .getBytes(StandardCharsets.UTF_8); for (Map.Entry<Object, Object> entry : data.entrySet()) { byteArrays.add(separator); if (entry.getValue() instanceof Path) { var path = (Path) entry.getValue(); String mimeType = Files.probeContentType(path); byteArrays.add(("\"" + entry.getKey() + "\"; filename=\"" + path.getFileName() + "\"\r\nContent-Type: " + mimeType + "\r\n\r\n").getBytes(StandardCharsets.UTF_8)); byteArrays.add(Files.readAllBytes(path)); byteArrays.add("\r\n".getBytes(StandardCharsets.UTF_8)); } else { byteArrays.add( ("\"" + entry.getKey() + "\"\r\n\r\n" + entry.getValue() + "\r\n").getBytes(StandardCharsets.UTF_8)); } } byteArrays .add(("--" + boundary + "--").getBytes(StandardCharsets.UTF_8)); return BodyPublishers.ofByteArrays(byteArrays); } } |
Странно, что я не смог найти метода, который бы сам делал то, что мы здесь делаем в ofMultipartData. Такого в Java 11 действительно нет, или я что-то пропустил?
В данном примере мы устанавливаем максимальное время ожидания окончания запроса в 10 секунд. По умолчанию эти таймауты устанавливаются в бесконечность. Поэтому важно устанавливать их вручную либо для каждого запроса, либо для всего экземпляра HttpClient.
В учебнике по Java можно найти много другой полезной информации.
Muchas gracias, funcionó muy bien tu solución.
Thx for sharing!
Федя, привет от Ромы из справочников:)! Спасибо, помогло! Пробовал через спринговый RestTemplate, потом штатным HttpClient’ом без этих ухищрений, потом нагуглил тебя.
Один вопрос — откуда значение boundary? Просто магическое число (строка)?
Там просто разделитель. Любой набор символов. В данном примере просто захардкожен как магическое число. Но вообще его можно каждый раз просто генерировать заново случайным образом.
Фёдор, добрый день. Не подскажете, чем может быть вызвана ошибка закрытия соединения HttpClient’ом в данном случае?
upstream prematurely closed connection while reading response header from upstream, client: , server: , request: «POST HTTP/1.1», upstream: «», host: «»
Если честно, здесь очень сложно сказать. Всякое могло быть.
А в каких направлениях можно покопать, кроме увеличения тайм-аута?
Если честно, я вообще без понятия. Это может быть что угодно. Вплоть до какого-нибудь файрвола.