From 1bd7c4813f09aab2dcda17ca1b2de5e48756e3bb Mon Sep 17 00:00:00 2001 From: Hardik Kaurani Date: Sun, 25 Oct 2026 10:20:07 +0530 Subject: [PATCH 1/4] fix(multipart): omit nil header string values and support additionalValuesToMerge --- .../MultipartSerializationWriter.java | 57 +++++++++--- .../MultipartSerializationWriterTests.java | 89 +++++++++++++++++++ 2 files changed, 133 insertions(+), 13 deletions(-) diff --git a/components/serialization/multipart/src/main/java/com/microsoft/kiota/serialization/MultipartSerializationWriter.java b/components/serialization/multipart/src/main/java/com/microsoft/kiota/serialization/MultipartSerializationWriter.java index 634c2ab5e..939485e27 100644 --- a/components/serialization/multipart/src/main/java/com/microsoft/kiota/serialization/MultipartSerializationWriter.java +++ b/components/serialization/multipart/src/main/java/com/microsoft/kiota/serialization/MultipartSerializationWriter.java @@ -17,11 +17,14 @@ import java.time.LocalTime; import java.time.OffsetDateTime; import java.util.EnumSet; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.UUID; import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; /** Serialization writer implementation for Multipart encoded payloads */ public class MultipartSerializationWriter implements SerializationWriter { @@ -39,6 +42,9 @@ public MultipartSerializationWriter() { public void writeStringValue(@Nullable final String key, @Nullable final String value) { try { + if (key != null && !key.isEmpty() && value == null) { + return; + } if (key != null && !key.isEmpty()) writer.write(key); if (value != null && !value.isEmpty()) { if (key != null && !key.isEmpty()) writer.write(": "); @@ -125,21 +131,46 @@ public void writeObjectValue( @Nullable final T value, @Nonnull final Parsable... additionalValuesToMerge) { Objects.requireNonNull(additionalValuesToMerge); - if (value != null) { - if (onBeforeObjectSerialization != null) { - onBeforeObjectSerialization.accept(value); - } - if (value instanceof MultipartBody) { - if (onStartObjectSerialization != null) { - onStartObjectSerialization.accept(value, this); + final List nonNullAdditionalValuesToMerge = + Stream.of(additionalValuesToMerge) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + if (value != null || !nonNullAdditionalValuesToMerge.isEmpty()) { + if (value != null) { + if (onBeforeObjectSerialization != null) { + onBeforeObjectSerialization.accept(value); + } + if (value instanceof MultipartBody) { + if (onStartObjectSerialization != null) { + onStartObjectSerialization.accept(value, this); + } + value.serialize(this); + } else { + throw new RuntimeException( + "expected MultipartBody instance but got " + + value.getClass().getName()); + } + if (onAfterObjectSerialization != null) { + onAfterObjectSerialization.accept(value); } - value.serialize(this); - } else { - throw new RuntimeException( - "expected MultipartBody instance but got " + value.getClass().getName()); } - if (onAfterObjectSerialization != null) { - onAfterObjectSerialization.accept(value); + for (final Parsable additionalValueToMerge : nonNullAdditionalValuesToMerge) { + if (onBeforeObjectSerialization != null) { + onBeforeObjectSerialization.accept(additionalValueToMerge); + } + if (additionalValueToMerge instanceof MultipartBody) { + if (onStartObjectSerialization != null) { + onStartObjectSerialization.accept(additionalValueToMerge, this); + } + additionalValueToMerge.serialize(this); + } else { + throw new RuntimeException( + "expected MultipartBody instance but got " + + additionalValueToMerge.getClass().getName()); + } + if (onAfterObjectSerialization != null) { + onAfterObjectSerialization.accept(additionalValueToMerge); + } } } } diff --git a/components/serialization/multipart/src/test/java/com/microsoft/kiota/serialization/MultipartSerializationWriterTests.java b/components/serialization/multipart/src/test/java/com/microsoft/kiota/serialization/MultipartSerializationWriterTests.java index 3b198794f..aa306cf53 100644 --- a/components/serialization/multipart/src/test/java/com/microsoft/kiota/serialization/MultipartSerializationWriterTests.java +++ b/components/serialization/multipart/src/test/java/com/microsoft/kiota/serialization/MultipartSerializationWriterTests.java @@ -1,7 +1,9 @@ package com.microsoft.kiota.serialization; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -110,4 +112,91 @@ void writesAStructuredObject() throws IOException { } } } + + @Test + void writesStringValueWithKeyValue() throws IOException { + try (final var serializationWriter = new MultipartSerializationWriter()) { + serializationWriter.writeStringValue("Content-Type", "text/plain"); + try (final var result = serializationWriter.getSerializedContent()) { + try (final var reader = + new BufferedReader(new InputStreamReader(result, "UTF-8"))) { + final String strResult = reader.lines().collect(Collectors.joining("\r\n")); + assertEquals("Content-Type: text/plain", strResult); + } + } + } + } + + @Test + void writeStringValueWithNilValueOmitsHeader() throws IOException { + try (final var serializationWriter = new MultipartSerializationWriter()) { + serializationWriter.writeStringValue("Content-Type", null); + try (final var result = serializationWriter.getSerializedContent()) { + assertEquals(0, result.available()); + } + } + } + + @Test + void writeStringValueWithEmptyKeyAndNilValueWritesDelimiter() throws IOException { + try (final var serializationWriter = new MultipartSerializationWriter()) { + serializationWriter.writeStringValue("", null); + try (final var result = serializationWriter.getSerializedContent()) { + final byte[] bytes = result.readAllBytes(); + assertEquals("\r\n", new String(bytes, "UTF-8")); + } + } + } + + @Test + void writeStringValueWithEmptyKeyAndValueWritesRawLine() throws IOException { + try (final var serializationWriter = new MultipartSerializationWriter()) { + serializationWriter.writeStringValue("", "raw line"); + try (final var result = serializationWriter.getSerializedContent()) { + final byte[] bytes = result.readAllBytes(); + assertEquals("raw line\r\n", new String(bytes, "UTF-8")); + } + } + } + + @Test + void writesAStructuredObjectWithAdditionalValuesToMerge() throws IOException { + final RequestAdapter requestAdapter = mock(RequestAdapter.class); + when(requestAdapter.getSerializationWriterFactory()) + .thenReturn(new JsonSerializationWriterFactory()); + + final MultipartBody part1 = new MultipartBody(); + part1.requestAdapter = requestAdapter; + part1.addOrReplacePart("part1", "application/octet-stream", byteForTest); + + final MultipartBody part2 = new MultipartBody(); + part2.requestAdapter = requestAdapter; + part2.addOrReplacePart("part2", "application/octet-stream", new byte[] {0x04, 0x05}); + + try (final var serializationWriter = new MultipartSerializationWriter()) { + serializationWriter.writeObjectValue(null, part1, part2); + + try (final var result = serializationWriter.getSerializedContent()) { + try (final var reader = + new BufferedReader(new InputStreamReader(result, "UTF-8"))) { + final String strResult = reader.lines().collect(Collectors.joining("\r\n")); + assertNotNull(strResult); + assertTrue(strResult.contains("name=\"part1\"")); + assertTrue(strResult.contains("name=\"part2\"")); + } + } + } + } + + @Test + void throwsOnInvalidAdditionalValuesToMerge() throws IOException { + final MultipartBody part1 = new MultipartBody(); + final TestEntity nonMultipart = new TestEntity(); + + try (final var serializationWriter = new MultipartSerializationWriter()) { + assertThrows( + RuntimeException.class, + () -> serializationWriter.writeObjectValue(null, part1, nonMultipart)); + } + } } From 29b7efa0768aaf95595f33211d160cf4122f8156 Mon Sep 17 00:00:00 2001 From: Hardik Kaurani Date: Sun, 25 Oct 2026 20:19:02 +0530 Subject: [PATCH 2/4] fix(multipart): serialize merged bodies under shared boundary and address review feedback --- .../MultipartSerializationWriter.java | 112 ++++++++++-------- .../MultipartSerializationWriterTests.java | 40 +++++-- 2 files changed, 93 insertions(+), 59 deletions(-) diff --git a/components/serialization/multipart/src/main/java/com/microsoft/kiota/serialization/MultipartSerializationWriter.java b/components/serialization/multipart/src/main/java/com/microsoft/kiota/serialization/MultipartSerializationWriter.java index 939485e27..6905a1762 100644 --- a/components/serialization/multipart/src/main/java/com/microsoft/kiota/serialization/MultipartSerializationWriter.java +++ b/components/serialization/multipart/src/main/java/com/microsoft/kiota/serialization/MultipartSerializationWriter.java @@ -11,11 +11,12 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStreamWriter; -import java.io.UnsupportedEncodingException; import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; import java.time.LocalDate; import java.time.LocalTime; import java.time.OffsetDateTime; +import java.util.ArrayList; import java.util.EnumSet; import java.util.List; import java.util.Map; @@ -23,32 +24,39 @@ import java.util.UUID; import java.util.function.BiConsumer; import java.util.function.Consumer; -import java.util.stream.Collectors; -import java.util.stream.Stream; -/** Serialization writer implementation for Multipart encoded payloads */ public class MultipartSerializationWriter implements SerializationWriter { private final ByteArrayOutputStream stream = new ByteArrayOutputStream(); - private final OutputStreamWriter writer; + private final OutputStreamWriter writer = + new OutputStreamWriter(stream, StandardCharsets.UTF_8); - /** Instantiates a new MultipartSerializationWriter. */ - public MultipartSerializationWriter() { - try { - this.writer = new OutputStreamWriter(this.stream, "UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException("could not create writer", e); - } - } + private String currentTargetBoundary; + private String currentReplacementBoundary; + private boolean isLastBody; public void writeStringValue(@Nullable final String key, @Nullable final String value) { try { if (key != null && !key.isEmpty() && value == null) { return; } + String valueToWrite = value; + if (currentTargetBoundary != null + && currentReplacementBoundary != null + && value != null) { + if (value.equals("--" + currentTargetBoundary)) { + valueToWrite = "--" + currentReplacementBoundary; + } else if (value.equals("--" + currentTargetBoundary + "--")) { + if (isLastBody) { + valueToWrite = "--" + currentReplacementBoundary + "--"; + } else { + return; + } + } + } if (key != null && !key.isEmpty()) writer.write(key); - if (value != null && !value.isEmpty()) { + if (valueToWrite != null && !valueToWrite.isEmpty()) { if (key != null && !key.isEmpty()) writer.write(": "); - writer.write(value); + writer.write(valueToWrite); } writer.write("\r\n"); writer.flush(); @@ -131,46 +139,54 @@ public void writeObjectValue( @Nullable final T value, @Nonnull final Parsable... additionalValuesToMerge) { Objects.requireNonNull(additionalValuesToMerge); - final List nonNullAdditionalValuesToMerge = - Stream.of(additionalValuesToMerge) - .filter(Objects::nonNull) - .collect(Collectors.toList()); - if (value != null || !nonNullAdditionalValuesToMerge.isEmpty()) { - if (value != null) { - if (onBeforeObjectSerialization != null) { - onBeforeObjectSerialization.accept(value); - } - if (value instanceof MultipartBody) { - if (onStartObjectSerialization != null) { - onStartObjectSerialization.accept(value, this); - } - value.serialize(this); - } else { - throw new RuntimeException( - "expected MultipartBody instance but got " - + value.getClass().getName()); - } - if (onAfterObjectSerialization != null) { - onAfterObjectSerialization.accept(value); - } + final List bodies = new ArrayList<>(); + if (value != null) { + if (value instanceof MultipartBody) { + bodies.add((MultipartBody) value); + } else { + throw new RuntimeException( + "expected MultipartBody instance but got " + value.getClass().getName()); } - for (final Parsable additionalValueToMerge : nonNullAdditionalValuesToMerge) { - if (onBeforeObjectSerialization != null) { - onBeforeObjectSerialization.accept(additionalValueToMerge); - } + } + for (final Parsable additionalValueToMerge : additionalValuesToMerge) { + if (additionalValueToMerge != null) { if (additionalValueToMerge instanceof MultipartBody) { - if (onStartObjectSerialization != null) { - onStartObjectSerialization.accept(additionalValueToMerge, this); - } - additionalValueToMerge.serialize(this); + bodies.add((MultipartBody) additionalValueToMerge); } else { throw new RuntimeException( "expected MultipartBody instance but got " + additionalValueToMerge.getClass().getName()); } - if (onAfterObjectSerialization != null) { - onAfterObjectSerialization.accept(additionalValueToMerge); - } + } + } + + if (bodies.isEmpty()) { + return; + } + + final String sharedBoundary = bodies.get(0).getBoundary(); + for (int i = 0; i < bodies.size(); i++) { + final MultipartBody body = bodies.get(i); + if (onBeforeObjectSerialization != null) { + onBeforeObjectSerialization.accept(body); + } + if (onStartObjectSerialization != null) { + onStartObjectSerialization.accept(body, this); + } + + currentTargetBoundary = body.getBoundary(); + currentReplacementBoundary = sharedBoundary; + isLastBody = (i == bodies.size() - 1); + try { + body.serialize(this); + } finally { + currentTargetBoundary = null; + currentReplacementBoundary = null; + isLastBody = false; + } + + if (onAfterObjectSerialization != null) { + onAfterObjectSerialization.accept(body); } } } diff --git a/components/serialization/multipart/src/test/java/com/microsoft/kiota/serialization/MultipartSerializationWriterTests.java b/components/serialization/multipart/src/test/java/com/microsoft/kiota/serialization/MultipartSerializationWriterTests.java index aa306cf53..00eba6fa5 100644 --- a/components/serialization/multipart/src/test/java/com/microsoft/kiota/serialization/MultipartSerializationWriterTests.java +++ b/components/serialization/multipart/src/test/java/com/microsoft/kiota/serialization/MultipartSerializationWriterTests.java @@ -1,12 +1,12 @@ package com.microsoft.kiota.serialization; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import com.microsoft.kiota.Compatibility; import com.microsoft.kiota.MultipartBody; import com.microsoft.kiota.PeriodAndDuration; import com.microsoft.kiota.RequestAdapter; @@ -142,7 +142,7 @@ void writeStringValueWithEmptyKeyAndNilValueWritesDelimiter() throws IOException try (final var serializationWriter = new MultipartSerializationWriter()) { serializationWriter.writeStringValue("", null); try (final var result = serializationWriter.getSerializedContent()) { - final byte[] bytes = result.readAllBytes(); + final byte[] bytes = Compatibility.readAllBytes(result); assertEquals("\r\n", new String(bytes, "UTF-8")); } } @@ -153,7 +153,7 @@ void writeStringValueWithEmptyKeyAndValueWritesRawLine() throws IOException { try (final var serializationWriter = new MultipartSerializationWriter()) { serializationWriter.writeStringValue("", "raw line"); try (final var result = serializationWriter.getSerializedContent()) { - final byte[] bytes = result.readAllBytes(); + final byte[] bytes = Compatibility.readAllBytes(result); assertEquals("raw line\r\n", new String(bytes, "UTF-8")); } } @@ -180,9 +180,24 @@ void writesAStructuredObjectWithAdditionalValuesToMerge() throws IOException { try (final var reader = new BufferedReader(new InputStreamReader(result, "UTF-8"))) { final String strResult = reader.lines().collect(Collectors.joining("\r\n")); - assertNotNull(strResult); - assertTrue(strResult.contains("name=\"part1\"")); - assertTrue(strResult.contains("name=\"part2\"")); + final String expected = + "--" + + part1.getBoundary() + + "\r\n" + + "Content-Type: application/octet-stream\r\n" + + "Content-Disposition: form-data; name=\"part1\"\r\n\r\n" + + new String(byteForTest, "UTF-8") + + "\r\n--" + + part1.getBoundary() + + "\r\n" + + "Content-Type: application/octet-stream\r\n" + + "Content-Disposition: form-data; name=\"part2\"\r\n\r\n" + + new String(new byte[] {0x04, 0x05}, "UTF-8") + + "\r\n--" + + part1.getBoundary() + + "--"; + assertEquals(expected, strResult); + assertFalse(strResult.contains(part2.getBoundary())); } } } @@ -190,13 +205,16 @@ void writesAStructuredObjectWithAdditionalValuesToMerge() throws IOException { @Test void throwsOnInvalidAdditionalValuesToMerge() throws IOException { - final MultipartBody part1 = new MultipartBody(); final TestEntity nonMultipart = new TestEntity(); try (final var serializationWriter = new MultipartSerializationWriter()) { - assertThrows( - RuntimeException.class, - () -> serializationWriter.writeObjectValue(null, part1, nonMultipart)); + final var exception = + assertThrows( + RuntimeException.class, + () -> serializationWriter.writeObjectValue(null, null, nonMultipart)); + assertEquals( + "expected MultipartBody instance but got " + nonMultipart.getClass().getName(), + exception.getMessage()); } } } From 720ecebb588ed9a7373cbf7eba1117d38478f533 Mon Sep 17 00:00:00 2001 From: Hardik Kaurani Date: Tue, 27 Oct 2026 03:08:32 +0530 Subject: [PATCH 3/4] fix(multipart): restrict boundary replacement and delimiter suppression to framing --- .../MultipartSerializationWriter.java | 66 ++++++++++++++++--- .../MultipartSerializationWriterTests.java | 45 +++++++++++++ 2 files changed, 102 insertions(+), 9 deletions(-) diff --git a/components/serialization/multipart/src/main/java/com/microsoft/kiota/serialization/MultipartSerializationWriter.java b/components/serialization/multipart/src/main/java/com/microsoft/kiota/serialization/MultipartSerializationWriter.java index 6905a1762..9478d7502 100644 --- a/components/serialization/multipart/src/main/java/com/microsoft/kiota/serialization/MultipartSerializationWriter.java +++ b/components/serialization/multipart/src/main/java/com/microsoft/kiota/serialization/MultipartSerializationWriter.java @@ -25,14 +25,25 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; +/** Serialization writer implementation for Multipart encoded payloads */ public class MultipartSerializationWriter implements SerializationWriter { private final ByteArrayOutputStream stream = new ByteArrayOutputStream(); private final OutputStreamWriter writer = new OutputStreamWriter(stream, StandardCharsets.UTF_8); + private enum FramingState { + EXPECTING_FIRST_BOUNDARY, + IN_HEADERS, + EXPECTING_PAYLOAD, + EXPECTING_DELIMITER_AFTER_PAYLOAD, + EXPECTING_BOUNDARY_OR_CLOSING, + FINISHED + } + private String currentTargetBoundary; private String currentReplacementBoundary; private boolean isLastBody; + private FramingState framingState; public void writeStringValue(@Nullable final String key, @Nullable final String value) { try { @@ -42,15 +53,47 @@ public void writeStringValue(@Nullable final String key, @Nullable final String String valueToWrite = value; if (currentTargetBoundary != null && currentReplacementBoundary != null - && value != null) { - if (value.equals("--" + currentTargetBoundary)) { - valueToWrite = "--" + currentReplacementBoundary; - } else if (value.equals("--" + currentTargetBoundary + "--")) { - if (isLastBody) { - valueToWrite = "--" + currentReplacementBoundary + "--"; - } else { - return; - } + && framingState != null) { + switch (framingState) { + case EXPECTING_FIRST_BOUNDARY: + if ((key == null || key.isEmpty()) + && value != null + && value.equals("--" + currentTargetBoundary)) { + valueToWrite = "--" + currentReplacementBoundary; + framingState = FramingState.IN_HEADERS; + } + break; + case IN_HEADERS: + if ((key == null || key.isEmpty()) && (value == null || value.isEmpty())) { + framingState = FramingState.EXPECTING_PAYLOAD; + } + break; + case EXPECTING_PAYLOAD: + framingState = FramingState.EXPECTING_DELIMITER_AFTER_PAYLOAD; + break; + case EXPECTING_DELIMITER_AFTER_PAYLOAD: + if ((key == null || key.isEmpty()) && (value == null || value.isEmpty())) { + framingState = FramingState.EXPECTING_BOUNDARY_OR_CLOSING; + } + break; + case EXPECTING_BOUNDARY_OR_CLOSING: + if ((key == null || key.isEmpty()) && value != null) { + if (value.equals("--" + currentTargetBoundary)) { + valueToWrite = "--" + currentReplacementBoundary; + framingState = FramingState.IN_HEADERS; + } else if (value.equals("--" + currentTargetBoundary + "--")) { + if (isLastBody) { + valueToWrite = "--" + currentReplacementBoundary + "--"; + framingState = FramingState.FINISHED; + } else { + framingState = FramingState.FINISHED; + return; + } + } + } + break; + case FINISHED: + break; } } if (key != null && !key.isEmpty()) writer.write(key); @@ -177,12 +220,14 @@ public void writeObjectValue( currentTargetBoundary = body.getBoundary(); currentReplacementBoundary = sharedBoundary; isLastBody = (i == bodies.size() - 1); + framingState = FramingState.EXPECTING_FIRST_BOUNDARY; try { body.serialize(this); } finally { currentTargetBoundary = null; currentReplacementBoundary = null; isLastBody = false; + framingState = null; } if (onAfterObjectSerialization != null) { @@ -251,6 +296,9 @@ public void setOnStartObjectSerialization( public void writeByteArrayValue( @Nullable final String key, @Nullable @Nonnull final byte[] value) { + if (framingState == FramingState.EXPECTING_PAYLOAD) { + framingState = FramingState.EXPECTING_DELIMITER_AFTER_PAYLOAD; + } if (value != null) try { this.stream.write(value); diff --git a/components/serialization/multipart/src/test/java/com/microsoft/kiota/serialization/MultipartSerializationWriterTests.java b/components/serialization/multipart/src/test/java/com/microsoft/kiota/serialization/MultipartSerializationWriterTests.java index 00eba6fa5..ee4c548b5 100644 --- a/components/serialization/multipart/src/test/java/com/microsoft/kiota/serialization/MultipartSerializationWriterTests.java +++ b/components/serialization/multipart/src/test/java/com/microsoft/kiota/serialization/MultipartSerializationWriterTests.java @@ -217,4 +217,49 @@ void throwsOnInvalidAdditionalValuesToMerge() throws IOException { exception.getMessage()); } } + + @Test + void preservesStringPayloadMatchingBoundaryOrClosingDelimiterWhenMerging() throws IOException { + final RequestAdapter requestAdapter = mock(RequestAdapter.class); + when(requestAdapter.getSerializationWriterFactory()) + .thenReturn(new JsonSerializationWriterFactory()); + + final MultipartBody part1 = new MultipartBody(); + part1.requestAdapter = requestAdapter; + final String payload1 = "--" + part1.getBoundary() + "--"; + part1.addOrReplacePart("part1", "text/plain", payload1); + + final MultipartBody part2 = new MultipartBody(); + part2.requestAdapter = requestAdapter; + final String payload2 = "--" + part2.getBoundary() + "--"; + part2.addOrReplacePart("part2", "text/plain", payload2); + + try (final var serializationWriter = new MultipartSerializationWriter()) { + serializationWriter.writeObjectValue(null, part1, part2); + + try (final var result = serializationWriter.getSerializedContent()) { + try (final var reader = + new BufferedReader(new InputStreamReader(result, "UTF-8"))) { + final String strResult = reader.lines().collect(Collectors.joining("\r\n")); + final String expected = + "--" + + part1.getBoundary() + + "\r\n" + + "Content-Type: text/plain\r\n" + + "Content-Disposition: form-data; name=\"part1\"\r\n\r\n" + + payload1 + + "\r\n\r\n--" + + part1.getBoundary() + + "\r\n" + + "Content-Type: text/plain\r\n" + + "Content-Disposition: form-data; name=\"part2\"\r\n\r\n" + + payload2 + + "\r\n\r\n--" + + part1.getBoundary() + + "--"; + assertEquals(expected, strResult); + } + } + } + } } From fd7c48e2b9d13a3ea35f57a37e3797b91e3ea5ce Mon Sep 17 00:00:00 2001 From: Hardik Kaurani Date: Tue, 27 Oct 2026 23:20:23 +0530 Subject: [PATCH 4/4] fix(multipart): omit null header string values and revert additionalValuesToMerge --- .../MultipartSerializationWriter.java | 129 +++--------------- .../MultipartSerializationWriterTests.java | 105 -------------- 2 files changed, 16 insertions(+), 218 deletions(-) diff --git a/components/serialization/multipart/src/main/java/com/microsoft/kiota/serialization/MultipartSerializationWriter.java b/components/serialization/multipart/src/main/java/com/microsoft/kiota/serialization/MultipartSerializationWriter.java index 9478d7502..2946dc240 100644 --- a/components/serialization/multipart/src/main/java/com/microsoft/kiota/serialization/MultipartSerializationWriter.java +++ b/components/serialization/multipart/src/main/java/com/microsoft/kiota/serialization/MultipartSerializationWriter.java @@ -16,9 +16,7 @@ import java.time.LocalDate; import java.time.LocalTime; import java.time.OffsetDateTime; -import java.util.ArrayList; import java.util.EnumSet; -import java.util.List; import java.util.Map; import java.util.Objects; import java.util.UUID; @@ -29,77 +27,20 @@ public class MultipartSerializationWriter implements SerializationWriter { private final ByteArrayOutputStream stream = new ByteArrayOutputStream(); private final OutputStreamWriter writer = - new OutputStreamWriter(stream, StandardCharsets.UTF_8); - - private enum FramingState { - EXPECTING_FIRST_BOUNDARY, - IN_HEADERS, - EXPECTING_PAYLOAD, - EXPECTING_DELIMITER_AFTER_PAYLOAD, - EXPECTING_BOUNDARY_OR_CLOSING, - FINISHED - } + new OutputStreamWriter(this.stream, StandardCharsets.UTF_8); - private String currentTargetBoundary; - private String currentReplacementBoundary; - private boolean isLastBody; - private FramingState framingState; + /** Instantiates a new MultipartSerializationWriter. */ + public MultipartSerializationWriter() {} public void writeStringValue(@Nullable final String key, @Nullable final String value) { + if (key != null && !key.isEmpty() && value == null) { + return; + } try { - if (key != null && !key.isEmpty() && value == null) { - return; - } - String valueToWrite = value; - if (currentTargetBoundary != null - && currentReplacementBoundary != null - && framingState != null) { - switch (framingState) { - case EXPECTING_FIRST_BOUNDARY: - if ((key == null || key.isEmpty()) - && value != null - && value.equals("--" + currentTargetBoundary)) { - valueToWrite = "--" + currentReplacementBoundary; - framingState = FramingState.IN_HEADERS; - } - break; - case IN_HEADERS: - if ((key == null || key.isEmpty()) && (value == null || value.isEmpty())) { - framingState = FramingState.EXPECTING_PAYLOAD; - } - break; - case EXPECTING_PAYLOAD: - framingState = FramingState.EXPECTING_DELIMITER_AFTER_PAYLOAD; - break; - case EXPECTING_DELIMITER_AFTER_PAYLOAD: - if ((key == null || key.isEmpty()) && (value == null || value.isEmpty())) { - framingState = FramingState.EXPECTING_BOUNDARY_OR_CLOSING; - } - break; - case EXPECTING_BOUNDARY_OR_CLOSING: - if ((key == null || key.isEmpty()) && value != null) { - if (value.equals("--" + currentTargetBoundary)) { - valueToWrite = "--" + currentReplacementBoundary; - framingState = FramingState.IN_HEADERS; - } else if (value.equals("--" + currentTargetBoundary + "--")) { - if (isLastBody) { - valueToWrite = "--" + currentReplacementBoundary + "--"; - framingState = FramingState.FINISHED; - } else { - framingState = FramingState.FINISHED; - return; - } - } - } - break; - case FINISHED: - break; - } - } if (key != null && !key.isEmpty()) writer.write(key); - if (valueToWrite != null && !valueToWrite.isEmpty()) { + if (value != null && !value.isEmpty()) { if (key != null && !key.isEmpty()) writer.write(": "); - writer.write(valueToWrite); + writer.write(value); } writer.write("\r\n"); writer.flush(); @@ -182,56 +123,21 @@ public void writeObjectValue( @Nullable final T value, @Nonnull final Parsable... additionalValuesToMerge) { Objects.requireNonNull(additionalValuesToMerge); - final List bodies = new ArrayList<>(); if (value != null) { + if (onBeforeObjectSerialization != null) { + onBeforeObjectSerialization.accept(value); + } if (value instanceof MultipartBody) { - bodies.add((MultipartBody) value); + if (onStartObjectSerialization != null) { + onStartObjectSerialization.accept(value, this); + } + value.serialize(this); } else { throw new RuntimeException( "expected MultipartBody instance but got " + value.getClass().getName()); } - } - for (final Parsable additionalValueToMerge : additionalValuesToMerge) { - if (additionalValueToMerge != null) { - if (additionalValueToMerge instanceof MultipartBody) { - bodies.add((MultipartBody) additionalValueToMerge); - } else { - throw new RuntimeException( - "expected MultipartBody instance but got " - + additionalValueToMerge.getClass().getName()); - } - } - } - - if (bodies.isEmpty()) { - return; - } - - final String sharedBoundary = bodies.get(0).getBoundary(); - for (int i = 0; i < bodies.size(); i++) { - final MultipartBody body = bodies.get(i); - if (onBeforeObjectSerialization != null) { - onBeforeObjectSerialization.accept(body); - } - if (onStartObjectSerialization != null) { - onStartObjectSerialization.accept(body, this); - } - - currentTargetBoundary = body.getBoundary(); - currentReplacementBoundary = sharedBoundary; - isLastBody = (i == bodies.size() - 1); - framingState = FramingState.EXPECTING_FIRST_BOUNDARY; - try { - body.serialize(this); - } finally { - currentTargetBoundary = null; - currentReplacementBoundary = null; - isLastBody = false; - framingState = null; - } - if (onAfterObjectSerialization != null) { - onAfterObjectSerialization.accept(body); + onAfterObjectSerialization.accept(value); } } } @@ -296,9 +202,6 @@ public void setOnStartObjectSerialization( public void writeByteArrayValue( @Nullable final String key, @Nullable @Nonnull final byte[] value) { - if (framingState == FramingState.EXPECTING_PAYLOAD) { - framingState = FramingState.EXPECTING_DELIMITER_AFTER_PAYLOAD; - } if (value != null) try { this.stream.write(value); diff --git a/components/serialization/multipart/src/test/java/com/microsoft/kiota/serialization/MultipartSerializationWriterTests.java b/components/serialization/multipart/src/test/java/com/microsoft/kiota/serialization/MultipartSerializationWriterTests.java index ee4c548b5..1c162dfd0 100644 --- a/components/serialization/multipart/src/test/java/com/microsoft/kiota/serialization/MultipartSerializationWriterTests.java +++ b/components/serialization/multipart/src/test/java/com/microsoft/kiota/serialization/MultipartSerializationWriterTests.java @@ -1,7 +1,6 @@ package com.microsoft.kiota.serialization; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -158,108 +157,4 @@ void writeStringValueWithEmptyKeyAndValueWritesRawLine() throws IOException { } } } - - @Test - void writesAStructuredObjectWithAdditionalValuesToMerge() throws IOException { - final RequestAdapter requestAdapter = mock(RequestAdapter.class); - when(requestAdapter.getSerializationWriterFactory()) - .thenReturn(new JsonSerializationWriterFactory()); - - final MultipartBody part1 = new MultipartBody(); - part1.requestAdapter = requestAdapter; - part1.addOrReplacePart("part1", "application/octet-stream", byteForTest); - - final MultipartBody part2 = new MultipartBody(); - part2.requestAdapter = requestAdapter; - part2.addOrReplacePart("part2", "application/octet-stream", new byte[] {0x04, 0x05}); - - try (final var serializationWriter = new MultipartSerializationWriter()) { - serializationWriter.writeObjectValue(null, part1, part2); - - try (final var result = serializationWriter.getSerializedContent()) { - try (final var reader = - new BufferedReader(new InputStreamReader(result, "UTF-8"))) { - final String strResult = reader.lines().collect(Collectors.joining("\r\n")); - final String expected = - "--" - + part1.getBoundary() - + "\r\n" - + "Content-Type: application/octet-stream\r\n" - + "Content-Disposition: form-data; name=\"part1\"\r\n\r\n" - + new String(byteForTest, "UTF-8") - + "\r\n--" - + part1.getBoundary() - + "\r\n" - + "Content-Type: application/octet-stream\r\n" - + "Content-Disposition: form-data; name=\"part2\"\r\n\r\n" - + new String(new byte[] {0x04, 0x05}, "UTF-8") - + "\r\n--" - + part1.getBoundary() - + "--"; - assertEquals(expected, strResult); - assertFalse(strResult.contains(part2.getBoundary())); - } - } - } - } - - @Test - void throwsOnInvalidAdditionalValuesToMerge() throws IOException { - final TestEntity nonMultipart = new TestEntity(); - - try (final var serializationWriter = new MultipartSerializationWriter()) { - final var exception = - assertThrows( - RuntimeException.class, - () -> serializationWriter.writeObjectValue(null, null, nonMultipart)); - assertEquals( - "expected MultipartBody instance but got " + nonMultipart.getClass().getName(), - exception.getMessage()); - } - } - - @Test - void preservesStringPayloadMatchingBoundaryOrClosingDelimiterWhenMerging() throws IOException { - final RequestAdapter requestAdapter = mock(RequestAdapter.class); - when(requestAdapter.getSerializationWriterFactory()) - .thenReturn(new JsonSerializationWriterFactory()); - - final MultipartBody part1 = new MultipartBody(); - part1.requestAdapter = requestAdapter; - final String payload1 = "--" + part1.getBoundary() + "--"; - part1.addOrReplacePart("part1", "text/plain", payload1); - - final MultipartBody part2 = new MultipartBody(); - part2.requestAdapter = requestAdapter; - final String payload2 = "--" + part2.getBoundary() + "--"; - part2.addOrReplacePart("part2", "text/plain", payload2); - - try (final var serializationWriter = new MultipartSerializationWriter()) { - serializationWriter.writeObjectValue(null, part1, part2); - - try (final var result = serializationWriter.getSerializedContent()) { - try (final var reader = - new BufferedReader(new InputStreamReader(result, "UTF-8"))) { - final String strResult = reader.lines().collect(Collectors.joining("\r\n")); - final String expected = - "--" - + part1.getBoundary() - + "\r\n" - + "Content-Type: text/plain\r\n" - + "Content-Disposition: form-data; name=\"part1\"\r\n\r\n" - + payload1 - + "\r\n\r\n--" - + part1.getBoundary() - + "\r\n" - + "Content-Type: text/plain\r\n" - + "Content-Disposition: form-data; name=\"part2\"\r\n\r\n" - + payload2 - + "\r\n\r\n--" - + part1.getBoundary() - + "--"; - assertEquals(expected, strResult); - } - } - } - } }