This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch coheigea/CXF-8922 in repository https://gitbox.apache.org/repos/asf/cxf.git
commit c0c0da3d9d275eb776ac6c69a5414b434389093f Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Fri Oct 9 18:02:42 2026 +0100 CXF-8922: Default form-data parts without Content-Type to text/plain --- .../org/apache/cxf/attachment/AttachmentUtil.java | 24 ++++++++-- .../apache/cxf/attachment/AttachmentUtilTest.java | 44 +++++++++++++++++++ .../cxf/jaxrs/provider/MultipartProviderTest.java | 51 ++++++++++++++++++++++ 3 files changed, 115 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/org/apache/cxf/attachment/AttachmentUtil.java b/core/src/main/java/org/apache/cxf/attachment/AttachmentUtil.java index 0917d1d72a4..2ab2bddc990 100644 --- a/core/src/main/java/org/apache/cxf/attachment/AttachmentUtil.java +++ b/core/src/main/java/org/apache/cxf/attachment/AttachmentUtil.java @@ -412,14 +412,15 @@ public final class AttachmentUtil { AttachmentImpl att = new AttachmentImpl(id); + String cd = getHeader(headers, "Content-Disposition"); + String fileName = getContentDispositionFileName(cd); + String ct = getHeader(headers, "Content-Type"); if (StringUtils.isEmpty(ct)) { - ct = MessageUtils.getContextualString(message, ATTACHMENT_CONTENT_TYPE, "application/octet-stream"); + ct = MessageUtils.getContextualString(message, ATTACHMENT_CONTENT_TYPE, + getDefaultContentType(cd)); } - String cd = getHeader(headers, "Content-Disposition"); - String fileName = getContentDispositionFileName(cd); - String encoding = null; for (Map.Entry<String, List<String>> e : headers.entrySet()) { @@ -447,6 +448,21 @@ public final class AttachmentUtil { return att; } + /** + * RFC 7578, section 4.4: a multipart/form-data part without a Content-Type header + * defaults to "text/plain". File parts (those carrying a "filename" parameter) and + * all other attachments keep defaulting to "application/octet-stream". + */ + private static String getDefaultContentType(String cd) { + if (!StringUtils.isEmpty(cd)) { + ContentDisposition c = new ContentDisposition(cd); + if ("form-data".equalsIgnoreCase(c.getType()) && c.getParameter("filename") == null) { + return "text/plain"; + } + } + return "application/octet-stream"; + } + static String getContentDispositionFileName(String cd) { if (StringUtils.isEmpty(cd)) { return null; diff --git a/core/src/test/java/org/apache/cxf/attachment/AttachmentUtilTest.java b/core/src/test/java/org/apache/cxf/attachment/AttachmentUtilTest.java index bcab2aabb03..41c76ba274d 100644 --- a/core/src/test/java/org/apache/cxf/attachment/AttachmentUtilTest.java +++ b/core/src/test/java/org/apache/cxf/attachment/AttachmentUtilTest.java @@ -18,14 +18,20 @@ */ package org.apache.cxf.attachment; +import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.math.BigInteger; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; import org.apache.cxf.io.CachedOutputStream; +import org.apache.cxf.message.Attachment; import org.apache.cxf.message.Message; import org.apache.cxf.message.MessageImpl; @@ -364,4 +370,42 @@ public class AttachmentUtilTest { // Will throw exception } } + + @Test + public void testCreateAttachmentFormDataFieldDefaultsToTextPlain() throws IOException { + // RFC 7578, section 4.4 + assertEquals("text/plain", + createAttachmentContentType("form-data; name=\"dzchunkindex\"", null)); + } + + @Test + public void testCreateAttachmentFormDataFileDefaultsToOctetStream() throws IOException { + assertEquals("application/octet-stream", + createAttachmentContentType("form-data; name=\"file\"; filename=\"a.bin\"", null)); + } + + @Test + public void testCreateAttachmentNoContentDispositionDefaultsToOctetStream() throws IOException { + assertEquals("application/octet-stream", createAttachmentContentType(null, null)); + } + + @Test + public void testCreateAttachmentFormDataFieldHonoursDefaultContentTypeProperty() throws IOException { + assertEquals("application/json", + createAttachmentContentType("form-data; name=\"field\"", "application/json")); + } + + private static String createAttachmentContentType(String cd, String defaultCt) throws IOException { + Map<String, List<String>> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + if (cd != null) { + headers.put("Content-Disposition", Collections.singletonList(cd)); + } + Message message = new MessageImpl(); + if (defaultCt != null) { + message.put(AttachmentUtil.ATTACHMENT_CONTENT_TYPE, defaultCt); + } + Attachment att = AttachmentUtil.createAttachment( + new ByteArrayInputStream("3".getBytes(StandardCharsets.UTF_8)), headers, message); + return att.getDataHandler().getContentType(); + } } diff --git a/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/provider/MultipartProviderTest.java b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/provider/MultipartProviderTest.java index e3712fb6b31..35499c4ea5a 100644 --- a/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/provider/MultipartProviderTest.java +++ b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/provider/MultipartProviderTest.java @@ -28,7 +28,9 @@ import java.util.stream.IntStream; import jakarta.ws.rs.core.MediaType; import org.apache.cxf.attachment.AttachmentDeserializer; +import org.apache.cxf.endpoint.Endpoint; import org.apache.cxf.jaxrs.ext.MessageContextImpl; +import org.apache.cxf.jaxrs.ext.multipart.Multipart; import org.apache.cxf.jaxrs.impl.MetadataMap; import org.apache.cxf.message.Exchange; import org.apache.cxf.message.ExchangeImpl; @@ -39,6 +41,8 @@ import org.junit.Test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class MultipartProviderTest { @Test @@ -130,4 +134,51 @@ public class MultipartProviderTest { new MetadataMap<String, String>(), msg.getContent(InputStream.class))); } + + @Test + public void testFormDataFieldWithoutContentTypeAsInteger() throws Exception { + assertEquals(Integer.valueOf(3), readFormDataInteger(null)); + } + + @Test + public void testFormDataFieldWithTextPlainContentTypeAsInteger() throws Exception { + assertEquals(Integer.valueOf(3), readFormDataInteger("text/plain")); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private Object readFormDataInteger(String partContentType) throws Exception { + StringBuilder sb = new StringBuilder() + .append("--bound\r\n") + .append("Content-Disposition: form-data; name=\"dzchunkindex\"\r\n"); + if (partContentType != null) { + sb.append("Content-Type: ").append(partContentType).append("\r\n"); + } + sb.append("\r\n") + .append("3\r\n") + .append("--bound--\r\n"); + + final Exchange exchange = new ExchangeImpl(); + final Endpoint endpoint = mock(Endpoint.class); + when(endpoint.get(ServerProviderFactory.class.getName())).thenReturn(ServerProviderFactory.getInstance()); + exchange.put(Endpoint.class, endpoint); + final Message msg = new MessageImpl(); + msg.setExchange(exchange); + exchange.setInMessage(msg); + msg.put(Message.CONTENT_TYPE, "multipart/form-data; boundary=bound"); + InputStream is = new ByteArrayInputStream(sb.toString().getBytes(StandardCharsets.UTF_8)); + msg.setContent(InputStream.class, is); + + final MultipartProvider p = new MultipartProvider(); + p.setMessageContext(new MessageContextImpl(msg)); + Annotation[] anns = FormDataResource.class.getMethod("upload", Integer.class) + .getParameterAnnotations()[0]; + return p.readFrom((Class)Integer.class, Integer.class, anns, + MediaType.valueOf("multipart/form-data; boundary=bound"), new MetadataMap<String, String>(), is); + } + + public static class FormDataResource { + public void upload(@Multipart(value = "dzchunkindex", required = false) Integer index) { + // complete + } + } }
