Repository: square/retrofit
Issue: Allow to pass null for @Part


Issue comments:

Pull request review comments:

Patch that resolved the issue:
diff --git a/retrofit/src/main/java/retrofit/RequestBuilder.java b/retrofit/src/main/java/retrofit/RequestBuilder.java
index 5023b2e428..0b85c4369e 100644
--- a/retrofit/src/main/java/retrofit/RequestBuilder.java
+++ b/retrofit/src/main/java/retrofit/RequestBuilder.java
@@ -167,14 +167,12 @@ void setArguments(Object[] args) {
           }
           break;
         case PART:
-          if (value == null) {
-            throw new IllegalArgumentException(
-                "Multipart part \"" + name + "\" value must not be null.");
-          }
-          if (value instanceof TypedOutput) {
-            multipartBody.addPart(name, (TypedOutput) value);
-          } else {
-            multipartBody.addPart(name, converter.toBody(value));
+          if (value != null) { // Skip null values.
+            if (value instanceof TypedOutput) {
+              multipartBody.addPart(name, (TypedOutput) value);
+            } else {
+              multipartBody.addPart(name, converter.toBody(value));
+            }
           }
           break;
         case BODY:
@@ -209,6 +207,10 @@ Request build() throws UnsupportedEncodingException {
       url.append(queryParams);
     }
 
+    if (multipartBody != null && multipartBody.getPartCount() == 0) {
+      throw new IllegalStateException("Multipart requests must contain at least one part.");
+    }
+
     return new Request(requestMethod, url.toString(), headers, body);
   }
 }
diff --git a/retrofit/src/main/java/retrofit/mime/MultipartTypedOutput.java b/retrofit/src/main/java/retrofit/mime/MultipartTypedOutput.java
index 62fd0abd8e..90ab62396d 100644
--- a/retrofit/src/main/java/retrofit/mime/MultipartTypedOutput.java
+++ b/retrofit/src/main/java/retrofit/mime/MultipartTypedOutput.java
@@ -51,6 +51,10 @@ public void addPart(String name, TypedOutput body) {
     length += part.length;
   }
 
+  public int getPartCount() {
+    return parts.size();
+  }
+
   @Override public String fileName() {
     return null;
   }


Test patch:
diff --git a/retrofit/src/test/java/retrofit/RequestBuilderTest.java b/retrofit/src/test/java/retrofit/RequestBuilderTest.java
index 8d916123f5..0266007289 100644
--- a/retrofit/src/test/java/retrofit/RequestBuilderTest.java
+++ b/retrofit/src/test/java/retrofit/RequestBuilderTest.java
@@ -315,6 +315,30 @@ public class RequestBuilderTest {
     assertThat(two).contains("kit").contains("kat");
   }
 
+  @Test public void multipartNullRemovesPart() throws Exception {
+    Request request = new Helper() //
+        .setMethod("POST") //
+        .setHasBody() //
+        .setUrl("http://example.com") //
+        .setPath("/foo/bar/") //
+        .setMultipart() //
+        .addPart("ping", "pong") //
+        .addPart("fizz", null) //
+        .build();
+    assertThat(request.getMethod()).isEqualTo("POST");
+    assertThat(request.getHeaders()).isEmpty();
+    assertThat(request.getUrl()).isEqualTo("http://example.com/foo/bar/");
+
+    MultipartTypedOutput body = (MultipartTypedOutput) request.getBody();
+    List<byte[]> bodyParts = MimeHelper.getParts(body);
+    assertThat(bodyParts).hasSize(1);
+
+    Iterator<byte[]> iterator = bodyParts.iterator();
+
+    String one = new String(iterator.next(), "UTF-8");
+    assertThat(one).contains("ping").contains("pong");
+  }
+
   @Test public void multipartPartOptional() throws Exception {
     try {
       new Helper() //
@@ -325,9 +349,9 @@ public class RequestBuilderTest {
           .setMultipart() //
           .addPart("ping", null) //
           .build();
-      fail("Null multipart part is not allowed.");
-    } catch (IllegalArgumentException e) {
-      assertThat(e.getMessage()).isEqualTo("Multipart part \"ping\" value must not be null.");
+      fail("Empty multipart request is not allowed.");
+    } catch (IllegalStateException e) {
+      assertThat(e.getMessage()).isEqualTo("Multipart requests must contain at least one part.");
     }
   }