MultiPartEntityBuilder单元测试抛出ContentTooLongException问题
问题描述
基于javax.servlet开发的应用中实现了一个代理类,负责为POST请求构造multipart form data格式的请求体。实际调用servlet接口开展回归测试时,应用运行完全符合预期,但对应的单元测试始终执行失败。
- 异常抛出代码行:
String httpPostBody = IOUtils.toString(httpPost.getEntity().getContent()); - 具体异常触发点:
httpPost.getEntity().getContent()调用 - 完整异常信息:
org.apache.http.ContentTooLongException: Content length is unknown at org.apache.http.entity.mime.MultipartFormEntity.getContent(MultipartFormEntity.java:101) at gov.va.cds.test.CDSRequestBuilderTest.testCreatePostRequest(CDSRequestBuilderTest.java:117)
关联代码
单元测试代码
@Test public void testCreatePostRequest() throws IOException, ServletException, URISyntaxException, NullPointerException { URI newTargetUri = new URI(targetUriPost); cdsRequestBuilder = new CDSRequestBuilder(); HttpPost httpPost = cdsRequestBuilder.createPostRequest(mockClientRequest, newTargetUri); // Parse the entity String httpPostBody = IOUtils.toString(httpPost.getEntity().getContent()); // Perform evaluation Assert.assertNotNull(httpPost); Assert.assertEquals(httpPost.getHeaders(HttpHeaders.HOST)[0].toString(), ("Host: " + newTargetUri.getHost())); Assert.assertTrue(httpPost.getHeaders(HttpHeaders.CONTENT_LENGTH).length == 0); Assert.assertTrue(httpPostBody.contains("payload")); Assert.assertTrue(httpPostBody.contains("file")); Assert.assertTrue(httpPostBody.contains(testFileBinaryData)); }
被测方法核心逻辑
private HttpEntity convertInputStreamToMultiPart(Collection<Part> clientParts, String boundary) throws IOException { Part payload = null; Part file = null; for (Part part : clientParts) { if (part.getName().equalsIgnoreCase("payload")) { payload = part; } else if (part.getName().equalsIgnoreCase("file")) { file = part; } } if(payload == null) { logIt.error("Failed to extract payload - no payload found"); throw new IllegalStateException("Payload is missing from client request."); } if(file == null || file.getSubmittedFileName().length() == 0) { logIt.error("Failed to extract file - no file found"); throw new IllegalStateException("File is missing from client request."); } logIt.info("Payload is parsed from client: {}", IOUtils.toString(payload.getInputStream())); return MultipartEntityBuilder.create() .addTextBody("payload", IOUtils.toString(payload.getInputStream())) .addBinaryBody("file", file.getInputStream(), ContentType.parse("application/pdf"), file.getSubmittedFileName()) .setContentType(ContentType.parse(MediaType.APPLICATION_FORM_URLENCODED)) .setBoundary(boundary) .build(); }
单元测试Mock逻辑
Part mockFilePart = mock(Part.class); when(mockFilePart.getName()).thenReturn("file"); when(mockFilePart.getSubmittedFileName()).thenReturn(testFileName); when(mockFilePart.getInputStream()).thenReturn(IOUtils.toInputStream(testFileBinaryData));
补充背景:此前将文件输入流临时保存为File对象,再传入addBinaryBody构造二进制请求体时,单元测试可正常通过;基于性能测试结果优化代码后,改为直接传入文件输入流对象构造请求体,随即出现上述测试失败问题。
问题根因
- 流式实体的接口限制:直接传入
InputStream给MultipartEntityBuilder.addBinaryBody()时,构造出的MultipartFormEntity属于非缓冲流式实体,这类实体不会预先加载流内容计算总长度,其getContent()方法的源码逻辑明确要求必须已知内容长度,否则直接抛出ContentTooLongException。之前传入File对象时,构造的是可计算长度的非流式实体,因此不会触发该异常。 - 测试逻辑与实际运行路径不一致:实际业务运行发送HTTP请求时,HttpClient不会调用
getContent()方法读取实体内容,而是通过writeTo(OutputStream)方法直接将流内容写出到网络通道,因此回归测试完全正常;只有单元测试中主动调用getContent()读取内容时才会触发异常。 - 隐藏的业务代码bug:当前代码给multipart实体设置的Content-Type为
application/x-www-form-urlencoded,不符合multipart请求的格式规范,正确值应为multipart/form-data,该问题会导致下游服务无法正常解析请求体。
修复方案
- 修正单元测试的内容读取逻辑:不要调用
getContent()方法读取实体,改用实体的writeTo()方法将内容输出到内存流后再解析,该路径和实际发请求的逻辑完全一致,不会触发长度校验。示例代码:// 替换原IOUtils.toString(httpPost.getEntity().getContent())逻辑 ByteArrayOutputStream baos = new ByteArrayOutputStream(); httpPost.getEntity().writeTo(baos); String httpPostBody = baos.toString(StandardCharsets.UTF_8); - 修正业务代码的Content-Type配置错误:将
.setContentType(ContentType.parse(MediaType.APPLICATION_FORM_URLENCODED))修改为.setContentType(ContentType.MULTIPART_FORM_DATA),避免请求格式不符合规范。 - 非必要不推荐的可选方案:如果确实需要调用
getContent()读取实体,可以在构造MultipartEntity时调用setBufferContent(true)开启内容缓冲,构造阶段会将流内容全部读入内存计算长度,但会增加内存开销,抵消直接传流的性能优化收益。
内容的提问来源于stack exchange,提问作者E_R
相关产品推荐
相关产品推荐

