24

I need to upload a binary file bundled in an apk to a server using okhttp. Using urlconnection, you can simply get an inputstream to an asset and then put that into your request. However, okhttp only gives you the option of uploading byte arrays, strings, or files. Since you can't get a file path for an asset bundled in the apk, is the only option to copy the file to the local file directory (I'd rather not do that) and then give the file to okhttp? Is there no way to simply make a request using the assetinputstream directly to the web server?

EDIT: I used the accepted answer but instead of making a static utility class I simply subclassed RequestBody

 public class InputStreamRequestBody extends RequestBody {

private InputStream inputStream;
private MediaType mediaType;

public static RequestBody create(final MediaType mediaType, final InputStream inputStream) {


    return new InputStreamRequestBody(inputStream, mediaType);
}

private InputStreamRequestBody(InputStream inputStream, MediaType mediaType) {
    this.inputStream = inputStream;
    this.mediaType = mediaType;
}

@Override
public MediaType contentType() {
    return mediaType;
}

@Override
public long contentLength() {
    try {
        return inputStream.available();
    } catch (IOException e) {
        return 0;
    }
}

@Override
public void writeTo(BufferedSink sink) throws IOException {
    Source source = null;
    try {
        source = Okio.source(inputStream);
        sink.writeAll(source);
    } finally {
        Util.closeQuietly(source);
    }
 }
}

My only concern with this approach is the unreliability of inputstream.available() for content-length. The static constructor is to match okhttp's internal implementation

dabluck
  • 1,641
  • 2
  • 13
  • 20

3 Answers3

45

You might not be able to do it directly using the library but you could create a little utility class which would do it for you. You could then simply re-use it everywhere you need it.

public class RequestBodyUtil {

    public static RequestBody create(final MediaType mediaType, final InputStream inputStream) {
        return new RequestBody() {
            @Override
            public MediaType contentType() {
                return mediaType;
            }

            @Override
            public long contentLength() {
                try {
                    return inputStream.available();
                } catch (IOException e) {
                    return 0;
                }
            }

            @Override
            public void writeTo(BufferedSink sink) throws IOException {
                Source source = null;
                try {
                    source = Okio.source(inputStream);
                    sink.writeAll(source);
                } finally {
                    Util.closeQuietly(source);
                }
            }
        };
    }
}

Then simply use it like so

OkHttpClient client = new OkHttpClient();

MediaType MEDIA_TYPE_MARKDOWN
        = MediaType.parse("text/x-markdown; charset=utf-8");

InputStream inputStream = getAssets().open("README.md");

RequestBody requestBody = RequestBodyUtil.create(MEDIA_TYPE_MARKDOWN, inputStream);
Request request = new Request.Builder()
        .url("https://api.github.com/markdown/raw")
        .post(requestBody)
        .build();

Response response = client.newCall(request).execute();
if (!response.isSuccessful())
    throw new IOException("Unexpected code " + response);

Log.d("POST", response.body().string());    

This example code was based on this code. Replace the Assets file name and the MediaType with your own.

Vadim Kotov
  • 8,084
  • 8
  • 48
  • 62
Miguel
  • 19,793
  • 8
  • 56
  • 46
  • this is actually quite clever thanks. not sure why i didn't think of it. i will update with my final solution when i've tested it but it will be a version of this. – dabluck Aug 20 '14 at 04:07
  • 2
    have you ever actually had any connection issue with this implementation of RequestBody ? According to how `Call` handles the retry in case of connection issue, `writeTo` may be called more than once, which would crash because the stream is closed. – njzk2 Sep 17 '14 at 19:21
  • @njzk2 I haven't had the need to use this code. Looking at the `Call` source code I see what you mean but I haven't tested it. To solve this instead you could read the whole `InputStream` into a byte[] and then simply use call to `Requestbody.create(mediaType, content)` method. This would allow for multiple calls to `writeTo` without any issues. You could still wrap all of this into a nice util class of some sort for easy re-use. – Miguel Sep 18 '14 at 13:57
  • 1
    @MiguelLavigne: this is actually what I ended up doing. Also, this feature is apparently being worked on https://github.com/square/okhttp/pull/1038 and I have seen a commit that uses a `inputStream.reset()` to allow retry, but that still does not account for non-resetable inputStreams, which contentProviders can provide (I ended up putting the content in a byte[]) – njzk2 Sep 18 '14 at 14:10
  • @njzk2 have you tried turning a non resettable stream into a resettable stream? You could potentially do a check `if (!is.markSupported()) is = new BufferedInputStream(is);` Then you could simply use the reset alternative. I'm not sure if this solution is sound, I can't test it but perhaps you can. – Miguel Sep 18 '14 at 14:38
  • I did check `isMarkSupported()`. `getContentResolver().getInputStream(...).isMarkSupported` on a media Uri returns false. I did not though of the BufferedInputStream, good point. – njzk2 Sep 18 '14 at 14:39
  • per comment on linked github issue above, this solution can fail when using content providers, and there is no easy workaround besides keeping the byte array. works fine when not using content providers though – dabluck Jan 07 '15 at 00:20
  • 1
    java.lang.OutOfMemoryError: OutOfMemoryError thrown while trying to throw OutOfMemoryError; no stack trace available. How can I solve this? – Sudhir Singh Khanger Aug 29 '16 at 18:13
  • 1
    It's not working together with body level logging. Stream breaks if logging set to Level.BODY. – Daniel Hári Jan 27 '17 at 12:30
  • @MiguelLavigne How to pass other parameter ? – Hardik Parmar May 14 '20 at 14:34
0

I know its late, but better than never.

The trick here if you don't know the size of your data, you can skip content-length header and replace it by Http1.1 transfer-encoding: Chuncked

For more info please read https://www.oracle.com/technical-resources/articles/javame/chunking.html

-2

Its way too easy using snoopy api: one line of code if you exclude identifiers definition :)

URI uri = ...;
Path fileToUpload = ...;
Snoopy.builder()
      .config(SnoopyConfig.defaults())
      .build()
      .post(uri)
      .followRedirects(true)
      .failIfNotSuccessfulResponse(true)
      .body(fileToUpload)
      .consumeAsString();

https://bitbucket.org/abuwandi/snoopy

Still no android release but its coming soon...

  • Maybe I'm missing something but as far as I can tell the Snoopy source implies the `.body(fileToUpload)` method of the builder doesn't support an `InputStream`? – Jasper Siepkes Jun 24 '20 at 13:59
  • That's correct, the example above is showing a path as the body, version 0.8.7 supports input streams, and still the same amount of code as above, one small change would be in line 2 InputStream fileToUpload = ...; – TheReALDeAL Jun 26 '20 at 05:28