Java客户端发送Multipart POST请求遇HttpMediaTypeNotSupportedException排查
项目详情
- 主项目
- Spring Boot版本:3.1.0
- Java版本:17
- QA测试项目
- Spring Boot版本:3.1.0
- Java版本:1.8
Java客户端代码示例
public boolean makePostRequest(final String endpoint) { final HttpClient client = HttpClientBuilder.create().setDefaultCookieStore(getRest().getRequestCookies()).build(); final HttpPost conn = getRestUtil().setUpPostConnection(endpoint); // File Upload if (Objects.nonNull(getFilePathForPost())) { final MultipartEntityBuilder builder = MultipartEntityBuilder.create(); builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE); final File file = new File(getFilePathForPost()); final FileBody fb = new FileBody(file); builder.addPart(getFileParamName(), fb); try { final String[] lines = getRequestPayload().split("\n"); for (final String line : lines) { builder.addTextBody(line.split("=")[0], line.split("=")[1], ContentType.parse(multipartRequestPayloadType)); } } catch (final NullPointerException ignored) { // A Post request with no payload will be done } final String boundary = "---------------" + UUID.randomUUID().toString(); builder.setBoundary(boundary); conn.setHeader(CONTENT_TYPE_HEADER, ContentType.MULTIPART_FORM_DATA.getMimeType() + ";boundary=" + boundary); builder.setContentType(ContentType.MULTIPART_FORM_DATA); conn.setEntity(builder.build()); conn.getRequestLine(); setFilePathForPost(null); } else if (Objects.nonNull(getRequestPayload())) { conn.setHeader(CHARSET_TYPE_HEADER, CHARSET_TYPE_UTF_8); conn.setHeader(CONTENT_TYPE_HEADER, getRequestPayloadType()); conn.setEntity(new StringEntity(getRequestPayload(), StandardCharsets.UTF_8)); } }
Spring Boot控制器代码示例
@PostMapping(path = "/partners/{partnerSid}/invoices/{invoiceSid}/partner-actions", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE}) public InvoiceApprovalResponse approveInvoice(@PathVariable final UUID partnerSid, @PathVariable final UUID invoiceSid, @Valid @RequestPart("data") final InvoiceApprovalRequest request, @RequestPart("file") final MultipartFile partnerInvoice) { // ... controller logic ... }
错误信息
org.springframework.web.HttpMediaTypeNotSupportedException: Content-Type 'application/octet-stream' is not supported
观察结果
- 已确认Java客户端中Content-Type头设置为multipart/form-data
- 使用了正确的请求部分名称(data和file)
- 服务器端控制器方法已正确标注
consumes = {MediaType.MULTIPART_FORM_DATA_VALUE} - Postman发送请求正常,推测两种请求存在细微差异
已采取步骤
- 在客户端添加调试器,确认构造的请求无误
- 通过curl和Postman发送相同请求均获得200响应
疑问
- 为何Java客户端发送请求会触发HttpMediaTypeNotSupportedException,而Postman却正常?
- Apache HttpClient构造请求的方式与Postman有何差异?
- 使用Apache HttpClient发送Multipart请求时,需注意哪些额外头或配置?
- 如何进一步检查Java客户端发送的原始请求,以找出与Postman请求的差异?
排查与解决方案建议
问题根源
错误提示指向application/octet-stream不被支持,核心原因是请求中某个multipart部分的Content-Type不符合控制器要求:
- 控制器的
@RequestPart("data")需要JSON格式的请求体,若客户端未显式设置该部分的Content-Type为application/json,Spring无法正确解析为InvoiceApprovalRequest对象 - FileBody默认可能因文件类型推断失败,使用
application/octet-stream,虽MultipartFile理论支持,但结合其他配置冲突会触发报错
具体修复步骤
显式设置
data部分的Content-Type为application/json
控制器要求data是JSON对象,需确保添加该部分时指定正确的Content-Type,避免依赖不确定的multipartRequestPayloadType变量:// 替换原循环逻辑,假设getRequestPayload()返回完整的JSON字符串对应data字段 if (Objects.nonNull(getRequestPayload())) { builder.addTextBody("data", getRequestPayload(), ContentType.APPLICATION_JSON); }若必须按行分割,需避免JSON中的
=被错误拆分:final String[] lines = getRequestPayload().split("\n"); for (final String line : lines) { String[] parts = line.split("=", 2); // 只分割第一个= if (parts.length == 2) { // 对data字段强制设置application/json ContentType contentType = "data".equals(parts[0]) ? ContentType.APPLICATION_JSON : ContentType.parse(multipartRequestPayloadType); builder.addTextBody(parts[0], parts[1], contentType); } }移除手动设置的全局Content-Type头
MultipartEntityBuilder.build()会自动生成包含正确boundary的Content-Type头,手动设置会导致与实体内部的配置冲突:// 删除该行代码 // conn.setHeader(CONTENT_TYPE_HEADER, ContentType.MULTIPART_FORM_DATA.getMimeType() + ";boundary=" + boundary);显式指定FileBody的Content-Type(可选)
若文件类型无法被自动推断,可手动设置符合实际文件类型的Content-Type,避免默认的application/octet-stream:// 示例:PDF文件 FileBody fb = new FileBody(file, ContentType.APPLICATION_PDF); // 通用二进制文件 FileBody fb = new FileBody(file, ContentType.DEFAULT_BINARY);
疑问解答
Java客户端与Postman的差异
Postman会自动为每个multipart部分设置标准Content-Type(如JSON部分设为application/json,文件按类型设置),且自动处理boundary和请求头;而Java客户端需显式配置每个细节,手动设置错误或冲突会导致解析失败。Apache HttpClient构造请求的差异
Apache HttpClient是底层HTTP客户端,需开发者手动控制每个请求部分的Content-Type、边界、编码等;Postman模拟浏览器行为,自动遵循HTTP规范填充这些细节,适配Spring的解析逻辑。Apache HttpClient发送Multipart请求的注意事项
- 不要手动设置全局Content-Type头,由
MultipartEntityBuilder自动生成 - 为每个请求部分显式设置对应Content-Type(尤其是JSON、文件等特殊类型)
- 避免对复杂Payload(如JSON)进行字符串分割操作,直接传递完整内容
- 使用
HttpMultipartMode.BROWSER_COMPATIBLE确保请求格式符合浏览器标准,适配Spring的Multipart解析
- 不要手动设置全局Content-Type头,由
检查原始请求差异的方法
- Spring Boot日志排查:开启DEBUG日志记录完整请求内容,在
application.yml中添加:logging: level: org.springframework.web: DEBUG org.apache.coyote.http11: DEBUG - 抓包对比:用Wireshark或Fiddler分别捕获Java客户端和Postman的请求,对比请求头、每个multipart部分的Content-Type、边界值及内容格式。
- Spring Boot日志排查:开启DEBUG日志记录完整请求内容,在
内容的提问来源于stack exchange,提问作者Mari Mbiru

