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
+        }
+    }
 }

Reply via email to