Repository: square/retrofit
Issue: Override Content-Type Header

``` java
@Headers({"Content-Type:application/json-patch+json"})
```

But two `Content-Type` heads set :(
The other one is `application/json; charset=UTF-8`


Issue comments:
I know retrofit uses mimeType of TypedOutput as Content-Type, but sometimes we need to override the Content-Type like PATCH method with Content-Type "application/json-patch+json".

Makes sense. I'll look into it this week.


Pull request review comments:
Suggestion: Move this block to a memoized method on RestMethodInfo, so it's cached.


Patch that resolved the issue:
diff --git a/retrofit/src/main/java/retrofit/RequestBuilder.java b/retrofit/src/main/java/retrofit/RequestBuilder.java
index 725094aa2a..dbee94a2f6 100644
--- a/retrofit/src/main/java/retrofit/RequestBuilder.java
+++ b/retrofit/src/main/java/retrofit/RequestBuilder.java
@@ -48,6 +48,7 @@ final class RequestBuilder implements RequestInterceptor.RequestFacade {
   private String relativeUrl;
   private StringBuilder queryParams;
   private List<Header> headers;
+  private boolean hasContentTypeHeader;
 
   RequestBuilder(String apiUrl, RestMethodInfo methodInfo, Converter converter) {
     this.apiUrl = apiUrl;
@@ -62,6 +63,7 @@ final class RequestBuilder implements RequestInterceptor.RequestFacade {
     if (methodInfo.headers != null) {
       headers = new ArrayList<Header>(methodInfo.headers);
     }
+    hasContentTypeHeader = methodInfo.hasContentTypeHeader;
 
     relativeUrl = methodInfo.requestUrl;
 
@@ -100,6 +102,10 @@ final class RequestBuilder implements RequestInterceptor.RequestFacade {
       this.headers = headers = new ArrayList<Header>(2);
     }
     headers.add(new Header(name, value));
+
+    if ("Content-Type".equalsIgnoreCase(name)) {
+      hasContentTypeHeader = true;
+    }
   }
 
   @Override public void addPathParam(String name, String value) {
@@ -309,6 +315,20 @@ Request build() throws UnsupportedEncodingException {
       url.append(queryParams);
     }
 
+    TypedOutput body = this.body;
+    if (body != null) {
+      // Only add Content-Type header from the body if one is not already set.
+      if (!hasContentTypeHeader) {
+        addHeader("Content-Type", body.mimeType());
+      }
+
+      // Only add Content-Length header from the body if it is known.
+      long length = body.length();
+      if (length != -1) {
+        addHeader("Content-Length", String.valueOf(length));
+      }
+    }
+
     return new Request(requestMethod, url.toString(), headers, body);
   }
 }
diff --git a/retrofit/src/main/java/retrofit/RestAdapter.java b/retrofit/src/main/java/retrofit/RestAdapter.java
index 11762f3147..be98cca8f3 100644
--- a/retrofit/src/main/java/retrofit/RestAdapter.java
+++ b/retrofit/src/main/java/retrofit/RestAdapter.java
@@ -444,14 +444,6 @@ Request logAndReplaceRequest(String name, Request request) throws IOException {
       TypedOutput body = request.getBody();
       if (body != null) {
         bodySize = body.length();
-        String bodyMime = body.mimeType();
-
-        if (bodyMime != null) {
-          log.log("Content-Type: " + bodyMime);
-        }
-        if (bodySize != -1) {
-          log.log("Content-Length: " + bodySize);
-        }
 
         if (logLevel.ordinal() >= LogLevel.FULL.ordinal()) {
           if (!request.getHeaders().isEmpty()) {
@@ -465,7 +457,7 @@ Request logAndReplaceRequest(String name, Request request) throws IOException {
 
           byte[] bodyBytes = ((TypedByteArray) body).getBytes();
           bodySize = bodyBytes.length;
-          String bodyCharset = MimeUtil.parseCharset(bodyMime);
+          String bodyCharset = MimeUtil.parseCharset(body.mimeType());
           log.log(new String(bodyBytes, bodyCharset));
         }
       }
diff --git a/retrofit/src/main/java/retrofit/RestMethodInfo.java b/retrofit/src/main/java/retrofit/RestMethodInfo.java
index 0e1bf42e04..ebd28ebfde 100644
--- a/retrofit/src/main/java/retrofit/RestMethodInfo.java
+++ b/retrofit/src/main/java/retrofit/RestMethodInfo.java
@@ -97,6 +97,7 @@ enum RequestType {
   Set<String> requestUrlParamNames;
   String requestQuery;
   List<retrofit.client.Header> headers;
+  boolean hasContentTypeHeader;
 
   // Parameter-level details
   String[] requestParamNames;
@@ -221,7 +222,7 @@ private void parsePath(String path) {
     requestQuery = query;
   }
 
-  private List<retrofit.client.Header> parseHeaders(String[] headers) {
+  List<retrofit.client.Header> parseHeaders(String[] headers) {
     List<retrofit.client.Header> headerList = new ArrayList<retrofit.client.Header>();
     for (String header : headers) {
       int colon = header.indexOf(':');
@@ -229,8 +230,12 @@ private List<retrofit.client.Header> parseHeaders(String[] headers) {
         throw methodError("@Headers value must be in the form \"Name: Value\". Found: \"%s\"",
             header);
       }
-      headerList.add(new retrofit.client.Header(header.substring(0, colon),
-          header.substring(colon + 1).trim()));
+      String headerName = header.substring(0, colon);
+      String headerValue = header.substring(colon + 1).trim();
+      if ("Content-Type".equalsIgnoreCase(headerName)) {
+        hasContentTypeHeader = true;
+      }
+      headerList.add(new retrofit.client.Header(headerName, headerValue));
     }
     return headerList;
   }
@@ -372,6 +377,10 @@ private void parseParameters() {
 
             paramNames[i] = name;
             paramUsage[i] = ParamUsage.HEADER;
+
+            if ("Content-Type".equalsIgnoreCase(name)) {
+              hasContentTypeHeader = true;
+            }
           } else if (annotationType == Field.class) {
             if (requestType != RequestType.FORM_URL_ENCODED) {
               throw parameterError(i, "@Field parameters can only be used with form encoding.");
diff --git a/retrofit/src/main/java/retrofit/appengine/UrlFetchClient.java b/retrofit/src/main/java/retrofit/appengine/UrlFetchClient.java
index fd25039c34..5bc2298615 100644
--- a/retrofit/src/main/java/retrofit/appengine/UrlFetchClient.java
+++ b/retrofit/src/main/java/retrofit/appengine/UrlFetchClient.java
@@ -71,12 +71,6 @@ static HTTPRequest createRequest(Request request) throws IOException {
 
     TypedOutput body = request.getBody();
     if (body != null) {
-      fetchRequest.setHeader(new HTTPHeader("Content-Type", body.mimeType()));
-      long length = body.length();
-      if (length != -1) {
-        fetchRequest.setHeader(new HTTPHeader("Content-Length", String.valueOf(length)));
-      }
-
       ByteArrayOutputStream baos = new ByteArrayOutputStream();
       body.writeTo(baos);
       fetchRequest.setPayload(baos.toByteArray());
diff --git a/retrofit/src/main/java/retrofit/client/ApacheClient.java b/retrofit/src/main/java/retrofit/client/ApacheClient.java
index 236e7005ec..727101c464 100644
--- a/retrofit/src/main/java/retrofit/client/ApacheClient.java
+++ b/retrofit/src/main/java/retrofit/client/ApacheClient.java
@@ -131,7 +131,6 @@ static class TypedOutputEntity extends AbstractHttpEntity {
 
     TypedOutputEntity(TypedOutput typedOutput) {
       this.typedOutput = typedOutput;
-      setContentType(typedOutput.mimeType());
     }
 
     @Override public boolean isRepeatable() {
diff --git a/retrofit/src/main/java/retrofit/client/Client.java b/retrofit/src/main/java/retrofit/client/Client.java
index 5608b53522..53a75b2ebd 100644
--- a/retrofit/src/main/java/retrofit/client/Client.java
+++ b/retrofit/src/main/java/retrofit/client/Client.java
@@ -25,6 +25,10 @@ public interface Client {
   /**
    * Synchronously execute an HTTP represented by {@code request} and encapsulate all response data
    * into a {@link Response} instance.
+   * <p>
+   * Note: If the request has a body, its length and mime type will have already been added to the
+   * header list as {@code Content-Length} and {@code Content-Type}, respectively. Do NOT alter
+   * these values as they might have been set as a result of an application-level configuration.
    */
   Response execute(Request request) throws IOException;
 
diff --git a/retrofit/src/main/java/retrofit/client/UrlConnectionClient.java b/retrofit/src/main/java/retrofit/client/UrlConnectionClient.java
index cd7872e673..a052b82086 100644
--- a/retrofit/src/main/java/retrofit/client/UrlConnectionClient.java
+++ b/retrofit/src/main/java/retrofit/client/UrlConnectionClient.java
@@ -57,11 +57,9 @@ void prepareRequest(HttpURLConnection connection, Request request) throws IOExce
     TypedOutput body = request.getBody();
     if (body != null) {
       connection.setDoOutput(true);
-      connection.addRequestProperty("Content-Type", body.mimeType());
       long length = body.length();
       if (length != -1) {
         connection.setFixedLengthStreamingMode((int) length);
-        connection.addRequestProperty("Content-Length", String.valueOf(length));
       } else {
         connection.setChunkedStreamingMode(CHUNK_SIZE);
       }


Test patch:
diff --git a/retrofit/src/test/java/retrofit/RequestBuilderTest.java b/retrofit/src/test/java/retrofit/RequestBuilderTest.java
index 1a18628110..81dfcc5f61 100644
--- a/retrofit/src/test/java/retrofit/RequestBuilderTest.java
+++ b/retrofit/src/test/java/retrofit/RequestBuilderTest.java
@@ -7,6 +7,7 @@
 import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -420,7 +421,9 @@ public class RequestBuilderTest {
         .setBody(Arrays.asList("quick", "brown", "fox")) //
         .build();
     assertThat(request.getMethod()).isEqualTo("POST");
-    assertThat(request.getHeaders()).isEmpty();
+    assertThat(request.getHeaders()).containsExactly(
+        new Header("Content-Type", "application/json; charset=UTF-8"),
+        new Header("Content-Length", "23"));
     assertThat(request.getUrl()).isEqualTo("http://example.com/foo/bar/");
     assertTypedBytes(request.getBody(), "[\"quick\",\"brown\",\"fox\"]");
   }
@@ -451,7 +454,9 @@ public class RequestBuilderTest {
         .addPathParam("kit", "kat") //
         .build();
     assertThat(request.getMethod()).isEqualTo("POST");
-    assertThat(request.getHeaders()).isEmpty();
+    assertThat(request.getHeaders()).containsExactly(
+        new Header("Content-Type", "application/json; charset=UTF-8"),
+        new Header("Content-Length", "23"));
     assertThat(request.getUrl()).isEqualTo("http://example.com/foo/bar/pong/kat/");
     assertTypedBytes(request.getBody(), "[\"quick\",\"brown\",\"fox\"]");
   }
@@ -467,7 +472,9 @@ public class RequestBuilderTest {
         .addPart("kit", new TypedString("kat")) //
         .build();
     assertThat(request.getMethod()).isEqualTo("POST");
-    assertThat(request.getHeaders()).isEmpty();
+    assertThat(request.getHeaders()).hasSize(2);
+    assertThat(request.getHeaders().get(0).getName()).isEqualTo("Content-Type");
+    assertThat(request.getHeaders().get(1)).isEqualTo(new Header("Content-Length", "414"));
     assertThat(request.getUrl()).isEqualTo("http://example.com/foo/bar/");
 
     MultipartTypedOutput body = (MultipartTypedOutput) request.getBody();
@@ -494,7 +501,9 @@ public class RequestBuilderTest {
         .addPart("fizz", null) //
         .build();
     assertThat(request.getMethod()).isEqualTo("POST");
-    assertThat(request.getHeaders()).isEmpty();
+    assertThat(request.getHeaders()).hasSize(2);
+    assertThat(request.getHeaders().get(0).getName()).isEqualTo("Content-Type");
+    assertThat(request.getHeaders().get(1)).isEqualTo(new Header("Content-Length", "228"));
     assertThat(request.getUrl()).isEqualTo("http://example.com/foo/bar/");
 
     MultipartTypedOutput body = (MultipartTypedOutput) request.getBody();
@@ -617,8 +626,7 @@ public class RequestBuilderTest {
         .setMethod("GET") //
         .setUrl("http://example.com") //
         .setPath("/foo/bar/") //
-        .addHeader("ping", "pong") //
-        .addHeader("kit", "kat") //
+        .addHeaders("ping: pong", "kit: kat") //
         .build();
     assertThat(request.getMethod()).isEqualTo("GET");
     assertThat(request.getHeaders()) //
@@ -647,7 +655,7 @@ public class RequestBuilderTest {
         .setMethod("GET") //
         .setUrl("http://example.com") //
         .setPath("/foo/bar/") //
-        .addHeader("ping", "pong") //
+        .addHeaders("ping: pong") //
         .addInterceptorHeader("kit", "kat") //
         .build();
     assertThat(request.getMethod()).isEqualTo("GET");
@@ -662,7 +670,7 @@ public class RequestBuilderTest {
         .setMethod("GET") //
         .setUrl("http://example.com") //
         .setPath("/foo/bar/") //
-        .addHeader("ping", "pong") //
+        .addHeaders("ping: pong") //
         .addInterceptorHeader("kit", "kat") //
         .addHeaderParam("fizz", "buzz") //
         .build();
@@ -678,7 +686,7 @@ public class RequestBuilderTest {
         .setMethod("GET") //
         .setUrl("http://example.com") //
         .setPath("/foo/bar/") //
-        .addHeader("ping", "pong") //
+        .addHeaders("ping: pong") //
         .addHeaderParam("kit", "kat") //
         .build();
     assertThat(request.getMethod()).isEqualTo("GET");
@@ -693,7 +701,7 @@ public class RequestBuilderTest {
         .setMethod("GET") //
         .setUrl("http://example.com") //
         .setPath("/foo/bar/") //
-        .addHeader("ping", "pong") //
+        .addHeaders("ping: pong") //
         .addHeaderParam("kit", "kat") //
         .build();
     assertThat(request.getMethod()).isEqualTo("GET");
@@ -712,6 +720,32 @@ public class RequestBuilderTest {
     assertThat(request.getUrl()).isEqualTo("http://example.com/foo/bar/");
   }
 
+  @Test public void contentTypeAnnotationHeaderOverrides() throws Exception {
+    Request request = new Helper() //
+        .setMethod("POST") //
+        .setUrl("http://example.com") //
+        .setPath("/") //
+        .addHeaders("Content-Type: text/not-plain") //
+        .setBody(new TypedString("Plain")) //
+        .build();
+    assertThat(request.getHeaders()) //
+        .containsExactly(new Header("Content-Type", "text/not-plain"),
+            new Header("Content-Length", "5"));
+  }
+
+  @Test public void contentTypeParameterHeaderOverrides() throws Exception {
+    Request request = new Helper() //
+        .setMethod("POST") //
+        .setUrl("http://example.com") //
+        .setPath("/") //
+        .addHeaderParam("Content-Type", "text/not-plain") //
+        .setBody(new TypedString("Plain")) //
+        .build();
+    assertThat(request.getHeaders()) //
+        .containsExactly(new Header("Content-Type", "text/not-plain"),
+            new Header("Content-Length", "5"));
+  }
+
   private static void assertTypedBytes(TypedOutput bytes, String expected) throws IOException {
     assertThat(bytes).isNotNull();
     ByteArrayOutputStream baos = new ByteArrayOutputStream();
@@ -730,7 +764,7 @@ private static class Helper {
     private final List<String> paramNames = new ArrayList<String>();
     private final List<ParamUsage> paramUsages = new ArrayList<ParamUsage>();
     private final List<Object> args = new ArrayList<Object>();
-    private final List<Header> headers = new ArrayList<Header>();
+    private final List<String> headers = new ArrayList<String>();
     private final List<Header> interceptorHeaders = new ArrayList<Header>();
     private final Map<String, String> interceptorPathParams = new LinkedHashMap<String, String>();
     private final Map<String, String> interceptorQueryParams = new LinkedHashMap<String, String>();
@@ -838,8 +872,8 @@ Helper addHeaderParam(String name, Object value) {
       return this;
     }
 
-    Helper addHeader(String name, String value) {
-      headers.add(new Header(name, value));
+    Helper addHeaders(String... headers) {
+      Collections.addAll(this.headers, headers);
       return this;
     }
 
@@ -887,7 +921,7 @@ Request build() throws Exception {
       methodInfo.requestQuery = query;
       methodInfo.requestParamNames = paramNames.toArray(new String[paramNames.size()]);
       methodInfo.requestParamUsage = paramUsages.toArray(new ParamUsage[paramUsages.size()]);
-      methodInfo.headers = headers;
+      methodInfo.headers = methodInfo.parseHeaders(headers.toArray(new String[headers.size()]));
       methodInfo.loaded = true;
 
       RequestBuilder requestBuilder = new RequestBuilder(url, methodInfo, GSON);
diff --git a/retrofit/src/test/java/retrofit/appengine/UrlFetchClientTest.java b/retrofit/src/test/java/retrofit/appengine/UrlFetchClientTest.java
index 2fd5010df0..35455fb651 100644
--- a/retrofit/src/test/java/retrofit/appengine/UrlFetchClientTest.java
+++ b/retrofit/src/test/java/retrofit/appengine/UrlFetchClientTest.java
@@ -48,9 +48,7 @@ public class UrlFetchClientTest {
     assertThat(fetchRequest.getMethod()).isEqualTo(POST);
     assertThat(fetchRequest.getURL().toString()).isEqualTo(HOST + "/foo/bar/");
     List<HTTPHeader> fetchHeaders = fetchRequest.getHeaders();
-    assertThat(fetchHeaders).hasSize(2);
-    assertHeader(fetchHeaders.get(0), "Content-Type", "text/plain; charset=UTF-8");
-    assertHeader(fetchHeaders.get(1), "Content-Length", "2");
+    assertThat(fetchHeaders).hasSize(0);
     assertBytes(fetchRequest.getPayload(), "hi");
   }
 
@@ -65,11 +63,7 @@ public class UrlFetchClientTest {
     assertThat(fetchRequest.getMethod()).isEqualTo(POST);
     assertThat(fetchRequest.getURL().toString()).isEqualTo(HOST + "/that/");
     List<HTTPHeader> fetchHeaders = fetchRequest.getHeaders();
-    assertThat(fetchHeaders).hasSize(2);
-    HTTPHeader headerZero = fetchHeaders.get(0);
-    assertThat(headerZero.getName()).isEqualTo("Content-Type");
-    assertThat(headerZero.getValue()).startsWith("multipart/form-data; boundary=");
-    assertHeader(fetchHeaders.get(1), "Content-Length", String.valueOf(body.length()));
+    assertThat(fetchHeaders).hasSize(0);
     assertThat(fetchRequest.getPayload()).isNotEmpty();
   }
 
diff --git a/retrofit/src/test/java/retrofit/client/ApacheClientTest.java b/retrofit/src/test/java/retrofit/client/ApacheClientTest.java
index a9e64ebbbd..4cb15bbb83 100644
--- a/retrofit/src/test/java/retrofit/client/ApacheClientTest.java
+++ b/retrofit/src/test/java/retrofit/client/ApacheClientTest.java
@@ -56,7 +56,6 @@ public class ApacheClientTest {
     HttpEntity entity = entityRequest.getEntity();
     assertThat(entity).isNotNull();
     assertBytes(ByteStreams.toByteArray(entity.getContent()), "hi");
-    assertThat(entity.getContentType().getValue()).isEqualTo("text/plain; charset=UTF-8");
   }
 
   @Test public void multipart() {
diff --git a/retrofit/src/test/java/retrofit/client/UrlConnectionClientTest.java b/retrofit/src/test/java/retrofit/client/UrlConnectionClientTest.java
index 3539ef0a3a..c07afa9476 100644
--- a/retrofit/src/test/java/retrofit/client/UrlConnectionClientTest.java
+++ b/retrofit/src/test/java/retrofit/client/UrlConnectionClientTest.java
@@ -46,10 +46,7 @@ public class UrlConnectionClientTest {
 
     assertThat(connection.getRequestMethod()).isEqualTo("POST");
     assertThat(connection.getURL().toString()).isEqualTo(HOST + "/foo/bar/");
-    assertThat(connection.getRequestProperties()).hasSize(2);
-    assertThat(connection.getRequestProperty("Content-Type")) //
-        .isEqualTo("text/plain; charset=UTF-8");
-    assertThat(connection.getRequestProperty("Content-Length")).isEqualTo("2");
+    assertThat(connection.getRequestProperties()).hasSize(0);
     assertBytes(connection.getOutputStream().toByteArray(), "hi");
   }
 
@@ -67,9 +64,7 @@ public class UrlConnectionClientTest {
 
     assertThat(connection.getRequestMethod()).isEqualTo("POST");
     assertThat(connection.getURL().toString()).isEqualTo(HOST + "/that/");
-    assertThat(connection.getRequestProperties()).hasSize(2);
-    assertThat(connection.getRequestProperty("Content-Type")).startsWith("multipart/form-data;");
-    assertThat(connection.getRequestProperty("Content-Length")).isEqualTo(String.valueOf(output.length));
+    assertThat(connection.getRequestProperties()).hasSize(0);
     assertThat(output.length).isGreaterThan(0);
   }