Repository: square/retrofit
Issue: Allow objects attached to @Header arguments

Is there any reason not to allow Header fields to have arbitrary objects, and toString them, just like FormUrlEncoded Field params are today?

The current String-only behavior encourages lax typing for things like cookies or other Strings with specific semantic meaning.


Issue comments:
`.toString()` is called for `@Header` values: https://github.com/square/retrofit/blob/816e551ebcf76d4b31eca6b6439de02b2f9ccf32/retrofit/src/main/java/retrofit/RequestBuilder.java#L241-243

Ah, but we reject non-`String` types: https://github.com/square/retrofit/blob/816e551ebcf76d4b31eca6b6439de02b2f9ccf32/retrofit/src/main/java/retrofit/RestMethodInfo.java#L386-L389

Yeah want to send a PR?

Yeah, it'd be fun to mess around with the Retrofit code a bit. I'll update
this check, the test, and the markdown doc.


Pull request review comments:
nit: two-space indent

two-space indent for this whole test

This block seems to have odd spacing.


Patch that resolved the issue:
diff --git a/retrofit/src/main/java/retrofit/RestMethodInfo.java b/retrofit/src/main/java/retrofit/RestMethodInfo.java
index cf73f9491f..c14b00ff9c 100644
--- a/retrofit/src/main/java/retrofit/RestMethodInfo.java
+++ b/retrofit/src/main/java/retrofit/RestMethodInfo.java
@@ -383,10 +383,6 @@ private void parseParameters() {
             paramUsage[i] = ParamUsage.ENCODED_QUERY_MAP;
           } else if (annotationType == Header.class) {
             String name = ((Header) parameterAnnotation).value();
-            if (parameterType != String.class) {
-              throw parameterError(i, "@Header parameter type must be String. Found: %s.",
-                  parameterType.getSimpleName());
-            }
 
             paramNames[i] = name;
             paramUsage[i] = ParamUsage.HEADER;
diff --git a/website/index.html b/website/index.html
index 36b4bbe260..5f8b52991d 100644
--- a/website/index.html
+++ b/website/index.html
@@ -114,7 +114,7 @@ <h4>Header Manipulation</h4>
 @GET("/users/{username}")
 User getUser(@Path("username") String username);</pre>
             <p>Note that headers do not overwrite each other. All headers with the same name will be included in the request.</p>
-            <p>A request Header can be updated dynamically using the <code>@Header</code> annotation. A corresponding String parameter must be provided to the <code>@Header</code>. If the value is null, the header will be omitted.</p>
+            <p>A request Header can be updated dynamically using the <code>@Header</code> annotation. A corresponding parameter must be provided to the <code>@Header</code>. If the value is null, the header will be omitted. Otherwise, <code>toString</code> will be called on the value, and the result used.</p>
             <pre class="prettyprint">@GET("/user")
 void getUser(@Header("Authorization") String authorization, Callback&lt;User> callback)</pre>
             <p>Headers that need to be added to every request can be specified using a <code>RequestInterceptor</code>. The following code creates a <code>RequestInterceptor</code> that will add a <code>User-Agent</code> header to every request.</p>


Test patch:
diff --git a/retrofit/src/test/java/retrofit/RequestBuilderTest.java b/retrofit/src/test/java/retrofit/RequestBuilderTest.java
index 5fe78daab3..1cfbd46b90 100644
--- a/retrofit/src/test/java/retrofit/RequestBuilderTest.java
+++ b/retrofit/src/test/java/retrofit/RequestBuilderTest.java
@@ -5,6 +5,7 @@
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.lang.reflect.Method;
+import java.math.BigInteger;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -719,6 +720,21 @@ public class RequestBuilderTest {
     assertThat(request.getBody()).isNull();
   }
 
+  @Test public void headerParamToString() throws Exception {
+    Object toStringHeaderParam = new BigInteger("1234");
+    Request request = new Helper() //
+        .setMethod("GET") //
+        .setUrl("http://example.com") //
+        .setPath("/foo/bar/") //
+        .addHeaderParam("kit", toStringHeaderParam) //
+        .build();
+    assertThat(request.getMethod()).isEqualTo("GET");
+    assertThat(request.getHeaders()) //
+        .containsExactly(new Header("kit", "1234"));
+    assertThat(request.getUrl()).isEqualTo("http://example.com/foo/bar/");
+    assertThat(request.getBody()).isNull();
+  }
+
   @Test public void headerParam() throws Exception {
     Request request = new Helper() //
         .setMethod("GET") //
diff --git a/retrofit/src/test/java/retrofit/RestMethodInfoTest.java b/retrofit/src/test/java/retrofit/RestMethodInfoTest.java
index fcf67096f2..1a118eba7c 100644
--- a/retrofit/src/test/java/retrofit/RestMethodInfoTest.java
+++ b/retrofit/src/test/java/retrofit/RestMethodInfoTest.java
@@ -6,6 +6,7 @@
 import java.lang.annotation.Target;
 import java.lang.reflect.Method;
 import java.lang.reflect.Type;
+import java.math.BigInteger;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
@@ -1219,23 +1220,19 @@ Response a(@Header("a") String a, @Header("b") String b) {
     assertThat(methodInfo.requestParamUsage).containsExactly(HEADER, HEADER);
   }
 
-  @Test public void headerParamMustBeString() {
+  @Test public void headerConvertedToString() {
     class Example {
-      @GET("/")
-      Response a(@Header("a") TypedOutput a, @Header("b") int b) {
+      @GET("/") Response a(@Header("first") BigInteger bi) {
         return null;
       }
     }
 
     Method method = TestingUtils.getMethod(Example.class, "a");
     RestMethodInfo methodInfo = new RestMethodInfo(method);
-    try {
-      methodInfo.init();
-      fail();
-    } catch (IllegalArgumentException e) {
-      assertThat(e).hasMessage(
-          "Example.a: @Header parameter type must be String. Found: TypedOutput. (parameter #1)");
-    }
+    methodInfo.init();
+
+    assertThat(methodInfo.requestParamNames).containsExactly("first");
+    assertThat(methodInfo.requestParamUsage).containsExactly(HEADER);
   }
 
   @Test public void onlyOneEncodingIsAllowedMultipartFirst() {