Multipart Uploads with OkHttp... and a Uri
As changes like scoped storage increase the need to be able to work with
content
Uri
values, I have been increasingly pointing developers
to this OkHttp issue comment.
If you have a need to do a multipart form upload, and your content is identified
by a Uri
instead of a File
, the InputStreamRequestBody
that Jared Burrows
created (with help from Jake Wharton) is what you need.
However, I never created a sample app for this, simply because I did not have a
Web service needing that sort of upload. I realized a week ago that RequestBin
exists and can handle this scenario. So, this evening I tossed together
this sample app to demonstrate
a Kotlin port of InputStreamRequestBody
and its use.
The OkHttp recipes page
shows how to use MultipartBody.Builder
to create the body of a POST
request. That
MultipartBody.Builder
has addFormDataPart()
methods that let you fill in the elements
of the “form”. For including a “file” (from the standpoint of the form), addFormDataPart()
can accept a RequestBody
. The specific recipe in that OkHttp documentation shows
creating one from a File
, such as by using the asRequestBody()
extension function
in Kotlin.
If you have a content
Uri
, though, OkHttp alone will not be sufficient. OkHttp
supports non-Android environments. As a result, OkHttp knows nothing
about Uri
or ContentResolver
or other Android SDK classes. But RequestBody
is an abstract class
, so we can create our own implementation that can use
Android SDK classes for our own Android projects. That’s what Jared did in that OkHttp
issue.
In Kotlin, an equivalent implementation looks like this:
class InputStreamRequestBody(
private val contentType: MediaType,
private val contentResolver: ContentResolver,
private val uri: Uri
) : RequestBody() {
override fun contentType() = contentType
override fun contentLength(): Long = -1
@Throws(IOException::class)
override fun writeTo(sink: BufferedSink) {
val input = contentResolver.openInputStream(uri)
input?.use { sink.writeAll(it.source()) }
?: throw IOException("Could not open $uri")
}
}
Now, I spent perhaps an hour on this sample. As the saying goes, “your mileage may vary”, and your standard concerns about using code from blog posts are warranted. That said, it’s a fairly straight-up port of Jared’s original Java sample, and it works in light testing.
You can create an instance of InputStreamRequestBody
and include it in an
addFormDataPart()
call where the OkHttp recipe shows the one based on the File
.
So, you wind up with something like this:
val contentPart = InputStreamRequestBody(type, resolver, content)
val requestBody = MultipartBody.Builder()
.setType(MultipartBody.FORM)
// TODO add other form elements here
.addFormDataPart("something", name, contentPart)
.build()
val request = Request.Builder()
.url(serverUrl)
.post(requestBody)
.build()
ok.newCall(request).execute().use { response ->
// TODO something with the response
}
Here:
-
type
is an OkHttpMediaType
identifying the MIME type of the content -
resolver
is aContentResolver
, obtained from a handyContext
-
content
is theUri
to the content to upload -
name
is some “filename” for the content -
serverUrl
is the URL you want toPOST
to
In the sample app, I have a version of that code that uses DocumentFile
to
obtain the MIME type and name to use. My serverUrl
comes from BuildConfig
;
see the sample app’s README
for information about where that URL will need
to come from if you clone and try running the sample. The content
Uri
comes from ACTION_OPEN_DOCUMENT
: the user clicks a big “Upload Something!”
button, chooses a document, and that document gets uploaded to the server.
If you are using a different HTTP client library, it needs some similar solution
if it is going to behave well on modern versions of Android, where filesystem
access is constrained and content
Uri
values are increasingly common.
Many thanks to Jared and Jake for the original implementation!
UPDATE: 2020-07-06: See also this related blog post
from cketti. It shows a similar ContentUriRequestBody
and also shows the
download-to-a-Uri
scenario.