Repository: square/okhttp
Issue: Can't execute DELETE without body.

I'm trying to create Request just as:

```
Request.Builder builder = new Request.Builder().url("URL");
builder.method("DELETE", null);
```

(because there are no `.delete()` method)

When I'm trying to execute such request it crashes, with error message:
`java.lang.IllegalStateException: Cannot stream a request body without chunked encoding or a known content length!`

As I get it, it happens because DELETE can "has request body" (`hasRequestBody` returns `true` for DELETE) and when we are trying to create request body it blows up, because neither `contentLength` info provided nor `Transfer-Encoding` is set to `chunked` because no body provided.

So, there is workaround: just add `Content-Length: 0` header.

Looks like it is somekind related to #605.


Changed files:
okhttp-tests/src/test/java/com/squareup/okhttp/internal/huc/JavaApiConverterTest.java
okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java
okhttp/src/main/java/com/squareup/okhttp/Call.java
okhttp/src/main/java/com/squareup/okhttp/Request.java
okhttp/src/main/java/com/squareup/okhttp/internal/Util.java
samples/guide/src/main/java/com/squareup/okhttp/guide/PostExample.java
okhttp-tests/src/test/java/com/squareup/okhttp/RequestTest.java
okhttp-tests/src/test/java/com/squareup/okhttp/CallTest.java

diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/CallTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/CallTest.java
index d5851488f..c44f386b8 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/CallTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/CallTest.java
@@ -65,6 +65,148 @@ public final class CallTest {
     cache.delete();
   }
 
+  @Test public void get() throws Exception {
+    server.enqueue(new MockResponse().setBody("abc").addHeader("Content-Type: text/plain"));
+    server.play();
+
+    Request request = new Request.Builder()
+        .url(server.getUrl("/"))
+        .header("User-Agent", "SyncApiTest")
+        .build();
+
+    executeSynchronously(request)
+        .assertCode(200)
+        .assertContainsHeaders("Content-Type: text/plain")
+        .assertBody("abc");
+
+    RecordedRequest recordedRequest = server.takeRequest();
+    assertEquals("GET", recordedRequest.getMethod());
+    assertEquals("SyncApiTest", recordedRequest.getHeader("User-Agent"));
+    assertEquals(0, recordedRequest.getBody().length);
+    assertNull(recordedRequest.getHeader("Content-Length"));
+  }
+
+  @Test public void head() throws Exception {
+    server.enqueue(new MockResponse().addHeader("Content-Type: text/plain"));
+    server.play();
+
+    Request request = new Request.Builder()
+        .url(server.getUrl("/"))
+        .head()
+        .header("User-Agent", "SyncApiTest")
+        .build();
+
+    executeSynchronously(request)
+        .assertCode(200)
+        .assertContainsHeaders("Content-Type: text/plain");
+
+    RecordedRequest recordedRequest = server.takeRequest();
+    assertEquals("HEAD", recordedRequest.getMethod());
+    assertEquals("SyncApiTest", recordedRequest.getHeader("User-Agent"));
+    assertEquals(0, recordedRequest.getBody().length);
+    assertNull(recordedRequest.getHeader("Content-Length"));
+  }
+
+  @Test public void post() throws Exception {
+    server.enqueue(new MockResponse().setBody("abc"));
+    server.play();
+
+    Request request = new Request.Builder()
+        .url(server.getUrl("/"))
+        .post(Request.Body.create(MediaType.parse("text/plain"), "def"))
+        .build();
+
+    executeSynchronously(request)
+        .assertCode(200)
+        .assertBody("abc");
+
+    RecordedRequest recordedRequest = server.takeRequest();
+    assertEquals("POST", recordedRequest.getMethod());
+    assertEquals("def", recordedRequest.getUtf8Body());
+    assertEquals("3", recordedRequest.getHeader("Content-Length"));
+    assertEquals("text/plain; charset=utf-8", recordedRequest.getHeader("Content-Type"));
+  }
+
+  @Test public void postZeroLength() throws Exception {
+    server.enqueue(new MockResponse().setBody("abc"));
+    server.play();
+
+    Request request = new Request.Builder()
+        .url(server.getUrl("/"))
+        .method("POST", null)
+        .build();
+
+    executeSynchronously(request)
+        .assertCode(200)
+        .assertBody("abc");
+
+    RecordedRequest recordedRequest = server.takeRequest();
+    assertEquals("POST", recordedRequest.getMethod());
+    assertEquals(0, recordedRequest.getBody().length);
+    assertEquals("0", recordedRequest.getHeader("Content-Length"));
+    assertEquals(null, recordedRequest.getHeader("Content-Type"));
+  }
+
+  @Test public void delete() throws Exception {
+    server.enqueue(new MockResponse().setBody("abc"));
+    server.play();
+
+    Request request = new Request.Builder()
+        .url(server.getUrl("/"))
+        .delete()
+        .build();
+
+    executeSynchronously(request)
+        .assertCode(200)
+        .assertBody("abc");
+
+    RecordedRequest recordedRequest = server.takeRequest();
+    assertEquals("DELETE", recordedRequest.getMethod());
+    assertEquals(0, recordedRequest.getBody().length);
+    assertEquals("0", recordedRequest.getHeader("Content-Length"));
+    assertEquals(null, recordedRequest.getHeader("Content-Type"));
+  }
+
+  @Test public void put() throws Exception {
+    server.enqueue(new MockResponse().setBody("abc"));
+    server.play();
+
+    Request request = new Request.Builder()
+        .url(server.getUrl("/"))
+        .put(Request.Body.create(MediaType.parse("text/plain"), "def"))
+        .build();
+
+    executeSynchronously(request)
+        .assertCode(200)
+        .assertBody("abc");
+
+    RecordedRequest recordedRequest = server.takeRequest();
+    assertEquals("PUT", recordedRequest.getMethod());
+    assertEquals("def", recordedRequest.getUtf8Body());
+    assertEquals("3", recordedRequest.getHeader("Content-Length"));
+    assertEquals("text/plain; charset=utf-8", recordedRequest.getHeader("Content-Type"));
+  }
+
+  @Test public void patch() throws Exception {
+    server.enqueue(new MockResponse().setBody("abc"));
+    server.play();
+
+    Request request = new Request.Builder()
+        .url(server.getUrl("/"))
+        .patch(Request.Body.create(MediaType.parse("text/plain"), "def"))
+        .build();
+
+    executeSynchronously(request)
+        .assertCode(200)
+        .assertBody("abc");
+
+    RecordedRequest recordedRequest = server.takeRequest();
+    assertEquals("PATCH", recordedRequest.getMethod());
+    assertEquals("def", recordedRequest.getUtf8Body());
+    assertEquals("3", recordedRequest.getHeader("Content-Length"));
+    assertEquals("text/plain; charset=utf-8", recordedRequest.getHeader("Content-Type"));
+  }
+
   @Test public void illegalToExecuteTwice() throws Exception {
     server.enqueue(new MockResponse()
         .setBody("abc")
@@ -127,25 +269,6 @@ public final class CallTest {
     assertTrue(server.takeRequest().getHeaders().contains("User-Agent: SyncApiTest"));
   }
 
-  @Test public void get() throws Exception {
-    server.enqueue(new MockResponse()
-        .setBody("abc")
-        .addHeader("Content-Type: text/plain"));
-    server.play();
-
-    Request request = new Request.Builder()
-        .url(server.getUrl("/"))
-        .header("User-Agent", "SyncApiTest")
-        .build();
-
-    executeSynchronously(request)
-        .assertCode(200)
-        .assertContainsHeaders("Content-Type: text/plain")
-        .assertBody("abc");
-
-    assertTrue(server.takeRequest().getHeaders().contains("User-Agent: SyncApiTest"));
-  }
-
   @Test public void get_Async() throws Exception {
     server.enqueue(new MockResponse()
         .setBody("abc")
@@ -340,25 +463,6 @@ public final class CallTest {
     assertEquals(301, response.code());
   }
 
-  @Test public void post() throws Exception {
-    server.enqueue(new MockResponse().setBody("abc"));
-    server.play();
-
-    Request request = new Request.Builder()
-        .url(server.getUrl("/"))
-        .post(Request.Body.create(MediaType.parse("text/plain"), "def"))
-        .build();
-
-    executeSynchronously(request)
-        .assertCode(200)
-        .assertBody("abc");
-
-    RecordedRequest recordedRequest = server.takeRequest();
-    assertEquals("def", recordedRequest.getUtf8Body());
-    assertEquals("3", recordedRequest.getHeader("Content-Length"));
-    assertEquals("text/plain; charset=utf-8", recordedRequest.getHeader("Content-Type"));
-  }
-
   @Test public void post_Async() throws Exception {
     server.enqueue(new MockResponse().setBody("abc"));
     server.play();
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/RequestTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/RequestTest.java
index fcb26b1fa..1878da5cf 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/RequestTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/RequestTest.java
@@ -23,6 +23,7 @@ import okio.Buffer;
 import org.junit.Test;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
 
 public final class RequestTest {
   @Test public void string() throws Exception {
@@ -73,6 +74,36 @@ public final class RequestTest {
     assertEquals("Retransmit body", "616263", bodyToHex(body));
   }
 
+  /** Common verbs used for apis such as GitHub, AWS, and Google Cloud. */
+  @Test public void crudVerbs() {
+    MediaType contentType = MediaType.parse("application/json");
+    Request.Body body = Request.Body.create(contentType, "{}");
+
+    Request get = new Request.Builder().url("http://localhost/api").get().build();
+    assertEquals("GET", get.method());
+    assertNull(get.body());
+
+    Request head = new Request.Builder().url("http://localhost/api").head().build();
+    assertEquals("HEAD", head.method());
+    assertNull(head.body());
+
+    Request delete = new Request.Builder().url("http://localhost/api").delete().build();
+    assertEquals("DELETE", delete.method());
+    assertNull(delete.body());
+
+    Request post = new Request.Builder().url("http://localhost/api").post(body).build();
+    assertEquals("POST", post.method());
+    assertEquals(body, post.body());
+
+    Request put = new Request.Builder().url("http://localhost/api").put(body).build();
+    assertEquals("PUT", put.method());
+    assertEquals(body, put.body());
+
+    Request patch = new Request.Builder().url("http://localhost/api").patch(body).build();
+    assertEquals("PATCH", patch.method());
+    assertEquals(body, patch.body());
+  }
+
   private String bodyToHex(Request.Body body) throws IOException {
     Buffer buffer = new Buffer();
     body.writeTo(buffer);
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/huc/JavaApiConverterTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/huc/JavaApiConverterTest.java
index ac9b48fa8..060b11a95 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/huc/JavaApiConverterTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/huc/JavaApiConverterTest.java
@@ -209,7 +209,7 @@ public class JavaApiConverterTest {
   @Test public void createOkResponse_fromCacheResponse() throws Exception {
     final String statusLine = "HTTP/1.1 200 Fantastic";
     URI uri = new URI("http://foo/bar");
-    Request request = new Request.Builder().url(uri.toURL()).method("GET", null).build();
+    Request request = new Request.Builder().url(uri.toURL()).build();
     CacheResponse cacheResponse = new CacheResponse() {
       @Override
       public Map<String, List<String>> getHeaders() throws IOException {
@@ -244,7 +244,7 @@ public class JavaApiConverterTest {
     final Principal serverPrincipal = SERVER_CERT.getSubjectX500Principal();
     final List<Certificate> serverCertificates = Arrays.<Certificate>asList(SERVER_CERT);
     URI uri = new URI("https://foo/bar");
-    Request request = new Request.Builder().url(uri.toURL()).method("GET", null).build();
+    Request request = new Request.Builder().url(uri.toURL()).build();
     SecureCacheResponse cacheResponse = new SecureCacheResponse() {
       @Override
       public Map<String, List<String>> getHeaders() throws IOException {
@@ -517,7 +517,7 @@ public class JavaApiConverterTest {
 
   @Test public void createJavaUrlConnection_accessibleRequestInfo_GET() throws Exception {
     Request okRequest = createArbitraryOkRequest().newBuilder()
-        .method("GET", null)
+        .get()
         .build();
     Response okResponse = createArbitraryOkResponse(okRequest);
     HttpURLConnection httpUrlConnection = JavaApiConverter.createJavaUrlConnection(okResponse);
@@ -529,7 +529,7 @@ public class JavaApiConverterTest {
 
   @Test public void createJavaUrlConnection_accessibleRequestInfo_POST() throws Exception {
     Request okRequest = createArbitraryOkRequest().newBuilder()
-        .method("POST", createRequestBody("PostBody"))
+        .post(createRequestBody("PostBody"))
         .build();
     Response okResponse = createArbitraryOkResponse(okRequest);
     HttpURLConnection httpUrlConnection = JavaApiConverter.createJavaUrlConnection(okResponse);
@@ -541,7 +541,7 @@ public class JavaApiConverterTest {
 
   @Test public void createJavaUrlConnection_https_extraHttpsMethods() throws Exception {
     Request okRequest = createArbitraryOkRequest().newBuilder()
-        .method("GET", null)
+        .get()
         .url("https://secure/request")
         .build();
     Handshake handshake = Handshake.get("SecureCipher", Arrays.<Certificate>asList(SERVER_CERT),
@@ -584,7 +584,7 @@ public class JavaApiConverterTest {
     Request okRequest =
         createArbitraryOkRequest().newBuilder()
             .url("http://insecure/request")
-            .method("GET", null)
+            .get()
             .build();
     Response okResponse = createArbitraryOkResponse(okRequest).newBuilder()
         .protocol(Protocol.HTTP_1_1)
@@ -607,7 +607,7 @@ public class JavaApiConverterTest {
     Request okRequest =
         createArbitraryOkRequest().newBuilder()
             .url("http://insecure/request")
-            .method("POST", createRequestBody("RequestBody"))
+            .post(createRequestBody("RequestBody"))
             .build();
     Response.Body responseBody = createResponseBody("ResponseBody");
     Response okResponse = createArbitraryOkResponse(okRequest).newBuilder()
@@ -631,7 +631,7 @@ public class JavaApiConverterTest {
     Request okRequest =
         createArbitraryOkRequest().newBuilder()
             .url("https://secure/request")
-            .method("POST", createRequestBody("RequestBody") )
+            .post(createRequestBody("RequestBody") )
             .build();
     Response.Body responseBody = createResponseBody("ResponseBody");
     Handshake handshake = Handshake.get("SecureCipher", Arrays.<Certificate>asList(SERVER_CERT),
@@ -754,10 +754,7 @@ public class JavaApiConverterTest {
   }
 
   private static Request createArbitraryOkRequest() {
-    return new Request.Builder()
-        .url("http://arbitrary/url")
-        .method("GET", null)
-        .build();
+    return new Request.Builder().url("http://arbitrary/url").build();
   }
 
   private static Response createArbitraryOkResponse(Request request) {
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Call.java b/okhttp/src/main/java/com/squareup/okhttp/Call.java
index 069a04440..3eba08ff1 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Call.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Call.java
@@ -16,8 +16,11 @@
 package com.squareup.okhttp;
 
 import com.squareup.okhttp.internal.NamedRunnable;
+import com.squareup.okhttp.internal.Util;
 import com.squareup.okhttp.internal.http.HttpEngine;
+import com.squareup.okhttp.internal.http.HttpMethod;
 import com.squareup.okhttp.internal.http.OkHeaders;
+import com.squareup.okhttp.internal.http.RetryableSink;
 import java.io.IOException;
 import java.net.ProtocolException;
 import okio.BufferedSink;
@@ -170,6 +173,7 @@ public final class Call {
 
     // Copy body metadata to the appropriate request headers.
     Request.Body body = request.body();
+    RetryableSink requestBodyOut = null;
     if (body != null) {
       MediaType contentType = body.contentType();
       if (contentType == null) throw new IllegalStateException("contentType == null");
@@ -187,10 +191,12 @@ public final class Call {
       }
 
       request = requestBuilder.build();
+    } else if (HttpMethod.hasRequestBody(request.method())) {
+      requestBodyOut = Util.emptySink();
     }
 
     // Create the initial HTTP engine. Retries and redirects need new engine for each attempt.
-    engine = new HttpEngine(client, request, false, null, null, null);
+    engine = new HttpEngine(client, request, false, null, null, requestBodyOut);
 
     while (true) {
       if (canceled) return null;
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Request.java b/okhttp/src/main/java/com/squareup/okhttp/Request.java
index 937f3e39b..a6108e18f 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Request.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Request.java
@@ -276,10 +276,18 @@ public final class Request {
       return method("POST", body);
     }
 
+    public Builder delete() {
+      return method("DELETE", null);
+    }
+
     public Builder put(Body body) {
       return method("PUT", body);
     }
 
+    public Builder patch(Body body) {
+      return method("PATCH", body);
+    }
+
     public Builder method(String method, Body body) {
       if (method == null || method.length() == 0) {
         throw new IllegalArgumentException("method == null || method.length() == 0");
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/Util.java b/okhttp/src/main/java/com/squareup/okhttp/internal/Util.java
index c481ee274..1b86147d1 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/Util.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/Util.java
@@ -16,6 +16,7 @@
 
 package com.squareup.okhttp.internal;
 
+import com.squareup.okhttp.internal.http.RetryableSink;
 import com.squareup.okhttp.internal.spdy.Header;
 import java.io.Closeable;
 import java.io.File;
@@ -222,4 +223,10 @@ public final class Util {
     }
     return result;
   }
+
+  public static RetryableSink emptySink() {
+    return EMPTY_SINK;
+  }
+
+  private static final RetryableSink EMPTY_SINK = new RetryableSink(0);
 }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java
index 5647f0b73..f5579b3dc 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java
@@ -28,6 +28,7 @@ import com.squareup.okhttp.Response;
 import com.squareup.okhttp.ResponseSource;
 import com.squareup.okhttp.Route;
 import com.squareup.okhttp.internal.Dns;
+import com.squareup.okhttp.internal.Util;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.CacheRequest;
@@ -283,7 +284,7 @@ public final class HttpEngine {
   }
 
   boolean hasRequestBody() {
-    return HttpMethod.hasRequestBody(request.method());
+    return HttpMethod.hasRequestBody(request.method()) && !Util.emptySink().equals(requestBodyOut);
   }
 
   /** Returns the request body or null if this request doesn't have a body. */
diff --git a/samples/guide/src/main/java/com/squareup/okhttp/guide/PostExample.java b/samples/guide/src/main/java/com/squareup/okhttp/guide/PostExample.java
index 68ac4e043..313e82744 100644
--- a/samples/guide/src/main/java/com/squareup/okhttp/guide/PostExample.java
+++ b/samples/guide/src/main/java/com/squareup/okhttp/guide/PostExample.java
@@ -12,10 +12,7 @@ public class PostExample {
   void run() throws IOException {
     String json = bowlingJson("Jesse", "Jake");
     Request.Body body = Request.Body.create(MediaType.parse("application/json"), json);
-    Request request = new Request.Builder()
-        .url("http://www.roundsapp.com/post")
-        .method("POST", body)
-        .build();
+    Request request = new Request.Builder().url("http://www.roundsapp.com/post").post(body).build();
 
     Response response = client.newCall(request).execute();
     System.out.println(response.body().string());