21

Consider the following code:

    OkHttpClient client = new OkHttpClient();

    MediaType mediaType = MediaType.parse("text/plain; charset=utf-8"); // [A]
    RequestBody body = RequestBody.create(mediaType, media);
    String[] aclHeader = "x-goog-acl:public-read".split(":");

    Request request = new Request.Builder()
            .addHeader("Content-Type", "text/plain") // [B]
            .addHeader(aclHeader[0], aclHeader[1])
            .url(url)
            .put(body)
            .build();

    Response response = client.newCall(request).execute();

I am accessing GCS from a client, with a previously signed URL.

Problem: It seems okhttp adds the charset declared for the body [A] to the URL as well (at least for text/plain), even though it is not declared in [B]. This messes up my signed URL and GCS returns 403 Forbidden.

  • If I remove the charset from [A], it is still added.
  • If I add the charset to the signed URL before I sign it, it works and GCS returns 200 OK.

But this is not as it should be. At least when working with signed URLs, these must be sent to the server exactly as declared.

I tried using the Apache http client (which I don't want to use in production as okhttpclient is already part of my installation) and that client does not expose this behavior:

        String[] aclHeader = "x-goog-acl:public-read".split(":");

        StatusLine statusLine = Request

                .Put(url)
                .addHeader("Content-Type", "text/plain")
                .addHeader(aclHeader[0], aclHeader[1])
                .bodyByteArray(media)

                .execute().returnResponse().getStatusLine();

Is there a way to suppress the behavior in okhttp, that it adds to the Content-Type or transfers the Content-Type within the body redundantly?

Oliver Hausler
  • 4,900
  • 4
  • 35
  • 70

3 Answers3

28

I found the solution:

The following line is the culprit:

RequestBody body = RequestBody.create(mediaType, media);

create has 3 signatures for media:

  • String
  • byte[]
  • File

When I pass a String, it disregards the supplied mediaType and adds the charset to it. Even for image/jpeg it would send

image/jpeg; charset=utf-8

to the server.

Using byte[] or File suppresses that behavior.

I hope this helps you!

[Stupid me - for simplicity I gave it a String during testing, as I didn't care about the body ;-( ]

Oliver Hausler
  • 4,900
  • 4
  • 35
  • 70
  • 1
    Yup. OkHttp needs to decide how to convert your string to bytes, and when it does so it notes that decision in the charset. By the way, why aren't you specifying a charset? The server may use heuristics to guess, and it can guess wrong! – Jesse Wilson Aug 29 '14 at 10:42
  • I understand, yes. I had not specified the charset because I thought an image is binary and I only used the String to feed in something. I didn't care about the content so I didn't care about the charset either. But I admit it makes sense. – Oliver Hausler Aug 30 '14 at 04:05
  • 1
    Could you please give me some examples?? I have no idea how to use it. – c-an Feb 13 '19 at 17:43
3

When you create requestbody, just set the content typy as "null", and add header manual, like this:

    OkHttpClient client = new OkHttpClient();

    RequestBody body = RequestBody.create('your media string', null);
    String[] aclHeader = "x-goog-acl:public-read".split(":");

    Request request = new Request.Builder()
            .addHeader("Content-Type", "text/plain") // [B]
            .addHeader(aclHeader[0], aclHeader[1])
            .url(url)
            .put(body)
            .build();

    Response response = client.newCall(request).execute();

Beacuse when I read okhttp source code, in RequestBody.kt, I find following code:

   /**
     * Returns a new request body that transmits this string. If [contentType] is non-null and lacks
     * a charset, this will use UTF-8.
     */
    @JvmStatic
    @JvmName("create")
    fun String.toRequestBody(contentType: MediaType? = null): RequestBody {
      var charset: Charset = UTF_8
      var finalContentType: MediaType? = contentType
      if (contentType != null) {
        val resolvedCharset = contentType.charset()
        if (resolvedCharset == null) {
          charset = UTF_8
          finalContentType = "$contentType; charset=utf-8".toMediaTypeOrNull()
        } else {
          charset = resolvedCharset
        }
      }
      val bytes = toByteArray(charset)
      return bytes.toRequestBody(finalContentType, 0, bytes.size)
    }
jhzhang_09
  • 31
  • 2
1

You can register a networkInterceptor that overrides the Content-Type header, for example: FixContentTypeInterceptor.java:

import okhttp3.*;
import java.io.IOException;

public final class FixContentTypeInterceptor implements Interceptor {
    @Override public Response intercept(Interceptor.Chain chain) throws IOException {
        Request originalRequest = chain.request();

        Request fixedRequest = originalRequest.newBuilder()
                .header("Content-Type", "application/json")
                .build();
        return chain.proceed(fixedRequest);
    }

}

Main.java:

[...]
MediaType JSON = MediaType.get("application/json; charset=utf-8");
String json = "{}";
OkHttpClient client = new OkHttpClient.Builder()
                .addNetworkInterceptor(new FixContentTypeInterceptor())
                .build();
RequestBody body = RequestBody.create(json, JSON);
Request request = new Request.Builder()
                .url(url)
                .post(body)
                .build();
[...]

There is a discussion about this in okhttp's Github. https://github.com/square/okhttp/issues/3081

Miguel Santos
  • 344
  • 3
  • 9