Comment on page
Fuel
The core package for
Fuel
. The documentation outlined here touches most subjects and functions but is not exhaustive.You can download and install
Fuel
with Maven
and Gradle
. The core package has the following dependencies:implementation 'com.github.kittinunf.fuel:fuel:<latest-version>'
You can make requests using functions on
Fuel
, a FuelManager
instance or the string extensions.Fuel.get("https://httpbin.org/get")
.response { request, response, result ->
println(request)
println(response)
val (bytes, error) = result
if (bytes != null) {
println("[response bytes] ${String(bytes)}")
}
}
/*
* --> GET https://httpbin.org/get
* "Body : (empty)"
* "Headers : (0)"
*
* <-- 200 (https://httpbin.org/get)
* Response : OK
* Length : 268
* Body : ({
* "args": {},
* "headers": {
* "Accept": "text/html, image/gif, image/jpeg, *; q=.2, *\/*; q=.2",
* "Connection": "close",
* "Host": "httpbin.org",
* "User-Agent": "Java/1.8.0_172"
* },
* "origin": "123.456.789.123",
* "url": "https://httpbin.org/get"
* })
* Headers : (8)
* Connection : keep-alive
* Date : Thu, 15 Nov 2018 00:47:50 GMT
* Access-Control-Allow-Origin : *
* Server : gunicorn/19.9.0
* Content-Type : application/json
* Content-Length : 268
* Access-Control-Allow-Credentials : true
* Via : 1.1 vegur
* [response bytes] {
* "args": {},
* "headers": {
* "Accept": "text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2",
* "Connection": "close",
* "Host": "httpbin.org",
* "User-Agent": "Java/1.8.0_172"
* },
* "origin": "123.456.789.123",
* "url": "https://httpbin.org/get"
* }
*/
The extensions and functions made available by the core package are listed here:
Fuel Method | String extension | Fuel /FuelManager method |
Method.GET | "https://httpbin.org/get".httpGet() | Fuel.get("https://httpbin.org/get") |
Method.POST | "https://httpbin.org/post".httpPost() | Fuel.post("https://httpbin.org/post") |
Method.PUT | "https://httpbin.org/put".httpPut() | Fuel.put("https://httpbin.org/put") |
Method.PATCH | "https://httpbin.org/patch".httpPatch() | Fuel.patch("https://httpbin.org/patch") |
Method.HEAD | "https://httpbin.org/get".httpHead() | Fuel.head("https://httpbin.org/get") |
Method.OPTIONS | not supported | Fuel.request(Method.OPTIONS, "https://httpbin.org/anything") |
Method.TRACE | not supported | Fuel.request(Method.TRACE, "https://httpbin.org/anything") |
Method.CONNECT | not supported | not supported |
The default
client
is HttpClient
which is a thin wrapper over java.net.HttpUrlConnection
. java.net.HttpUrlConnection
does not support a PATCH
method. HttpClient
converts PATCH
requests to a POST
request and adds a X-HTTP-Method-Override: PATCH
header. While this is a semi-standard industry practice not all APIs are configured to accept this header by default.Fuel.patch("https://httpbin.org/patch")
.also { println(it) }
.response { result -> }
/* --> PATCH https://httpbin.org/post
* "Body : (empty)"
* "Headers : (1)"
* Content-Type : application/x-www-form-urlencoded
*/
// What is actually sent to the server
/* --> POST (https://httpbin.org/post)
* "Body" : (empty)
* "Headers : (3)"
* Accept-Encoding : compress;q=0.5, gzip;q=1.0
* Content-Type : application/x-www-form-urlencoded
* X-HTTP-Method-Override : PATCH
*/
Experimental
As of version
1.16.x
you can opt-in to forcing a HTTP Method on the java.net.HttpUrlConnnection
instance using reflection.FuelManager.instance.forceMethods = true
Fuel.patch("https://httpbin.org/patch")
.also { println(it) }
.response { result -> }
/* --> PATCH (https://httpbin.org/patch)
* "Body" : (empty)
* "Headers : (3)"
* Accept-Encoding : compress;q=0.5, gzip;q=1.0
* Content-Type : application/x-www-form-urlencoded
*/
Connect is not supported by the Java JVM via the regular HTTP clients, and is therefore not supported.
All the
String
extensions listed above, as well as the Fuel
and FuelManager
calls accept a parameter parameters: Parameters
.- URL encoded style for
GET
andDELETE
requestFuel.get("https://httpbin.org/get", listOf("foo" to "foo", "bar" to "bar")).url// https://httpbin.org/get?foo=foo&bar=barFuel.delete("https://httpbin.org/delete", listOf("foo" to "foo", "bar" to "bar")).url// https://httpbin.org/delete?foo=foo&bar=bar - Support
x-www-form-urlencoded
forPUT
,POST
andPATCH
Fuel.post("https://httpbin.org/post", listOf("foo" to "foo", "bar" to "bar")).also { println(it.url) }.also { println(String(it.body().toByteArray())) }// https://httpbin.org/post// "foo=foo&bar=bar"Fuel.put("https://httpbin.org/put", listOf("foo" to "foo", "bar" to "bar")).also { println(it.url) }.also { println(String(it.body().toByteArray())) }// https://httpbin.org/put// "foo=foo&bar=bar"
If a request already has a body, the parameters are url-encoded instead. You can remove the handling of parameter encoding by removing the default
ParameterEncoder
request interceptor from your FuelManager
.The
UploadRequest
handles encoding parameters in the body. Therefore by default, parameter encoding is ignored by ParameterEncoder
if the content type is multipart/form-data
.All requests can have parameters, regardless of the method.
- a list is encoded as
key[]=value1&key[]=value2&...
- an array is encoded as
key[]=value1&key[]=value2&...
- an empty string value is encoded as
key
- a null value is removed
Bodies are formed from generic streams, but there are helpers to set it from values that can be turned into streams. It is important to know that, by default, the streams are NOT read into memory until the
Request
is sent. However, if you pass in an in-memory value such as a ByteArray
or String
, Fuel
uses RepeatableBody
, which are kept into memory until the Request
is dereferenced.When you're using the default
Client
, bodies are supported for:POST
PUT
PATCH
(actually aPOST
, as noted above)DELETE
There are several functions to set a
Body
for the request. If you are looking for a multipart/form-data
upload request, checkout the UploadRequest
feature.Fuel.post("https://httpbin.org/post")
.body("My Post Body")
.also { println(it) }
.response { result -> }
/* --> POST https://httpbin.org/post
* "Body : My Post Body"
* "Headers : (1)"
* Content-Type : application/x-www-form-urlencoded
*/
If you don't want to set the
application/json
header, you can use .jsonBody(value: String)
extension to automatically do this for you.Fuel.post("https://httpbin.org/post")
.jsonBody("{ \"foo\" : \"bar\" }")
.also { println(it) }
.response { result -> }
/* --> POST https://httpbin.org/post
* "Body : { "foo" : "bar" }"
* "Headers : (1)"
* Content-Type : application/json
*/
Fuel.post("https://httpbin.org/post")
.header(Headers.CONTENT_TYPE, "text/plain")
.body("my body is plain")
.also { println(it) }
.response { result -> }
/* --> POST https://httpbin.org/post
* "Body : my body is plain"
* "Headers : (1)"
* Content-Type : text/plain
*/
Fuel.post("https://httpbin.org/post")
.header(Headers.CONTENT_TYPE, "text/plain")
.body(File("lipsum.txt"))
.also { println(it) }
.response { result -> }
/* --> POST https://httpbin.org/post
* "Body : Lorem ipsum dolor sit amet, consectetur adipiscing elit."
* "Headers : (1)"
* Content-Type : text/plain
*/
val stream = ByteArrayInputStream("source-string-from-string".toByteArray())
Fuel.post("https://httpbin.org/post")
.header(Headers.CONTENT_TYPE, "text/plain")
.body(stream)
.also { println(it) }
.response { result -> }
/* --> POST https://httpbin.org/post
* "Body : source-string-from-string"
* "Headers : (1)"
* Content-Type : text/plain
*/
Fuel always reads the body lazily, which means you can also provide a callback that will return a stream. This is also known as a
BodyCallback
:val produceStream = { ByteArrayInputStream("source-string-from-string".toByteArray()) }
Fuel.post("https://httpbin.org/post")
.header(Headers.CONTENT_TYPE, "text/plain")
.body(produceStream)
.also { println(it) }
.response { result -> }
/* --> POST https://httpbin.org/post
* "Body : source-string-from-string"
* "Headers : (1)"
* Content-Type : text/plain
*/
The default redirection interceptor only forwards
RepeatableBody
, and only if the status code is 307 or 308, as per the RFCs. In order to use a RepeatableBody
, pass in a String
or ByteArray
as body, or explicitely set repeatable = true
for the fun body(...)
call.NOTE this loads the entire body into memory, and therefore is not suited for large bodies.
There are many ways to set, overwrite, remove and append headers. For your convenience, internally used and common header names are attached to the
Headers
companion and can be accessed (e.g. Headers.CONTENT_TYPE
, Headers.ACCEPT
, ...).The most common ones are mentioned here:
call | arguments | action |
---|---|---|
request[header] | header: String | Get the current values of the header, after normalisation of the header |
request.header(header) | header: String | Get the current values |
call | arguments | action |
---|---|---|
request[header] = values | header: String , values: Collection<*> | Set the values of the header, overriding what's there, after normalisation of the header |
request[header] = value | header: String , value: Any | Set the value of the header, overriding what's there, after normalisation of the header |
request.header(map) | map: Map<String, Any> | Replace the headers with the map provided |
request.header(pair, pair, ...) | vararg pairs: Pair<String, Any> | Replace the headers with the pairs provided |
request.header(header, values) | header: String, values: Collection<*> | Replace the header with the provided values |
request.header(header, value) | header: String, value: Any | Replace the header with the provided value |
request.header(header, value, value, ...) | header: String, vararg values: Any | Replace the header with the provided values |
call | arguments | action |
---|---|---|
request.appendHeader(pair, pair, ...) | vararg pairs: Pair<String, Any> | Append each pair, using the key as header name and value as header content |
request.appendHeader(header, value) | header: String, value: Any | Appends the value to the header or sets it if there was none yet |
request.appendHeader(header, value, value, ...) | header: String, vararg values: Any | Appends the value to the header or sets it if there was none yet |
Note that headers which by the RFC may only have one value are always overwritten, such as
Content-Type
.The
baseHeaders
set through a FuelManager
are only applied to a Request
if that request does not have that specific header set yet. There is no appending logic. If you set a header it will overwrite the base value.Any
Client
can add, remove or transform HeaderValues
before it sends the Request
or after it receives the Response
. The default Client
for example sets TE
values.Even though some headers can only be set once (and will overwrite even when you try to append), the internal structure is always a list. Before a
Request
is made, the default Client
collapses the multiple values, if allowed by the RFCs, into a single header value delimited by a separator for that header. Headers that can only be set once will use the last value by default and ignore earlier set values.Authentication can be added to a
Request
using the .authentication()
feature. By default, authentication
is passed on when using the default redirectResponseInterceptor
(which is enabled by default), unless it is redirecting to a different host. You can remove this behaviour by implementing your own redirection logic.When you call.authentication()
, a few extra functions are available. If you call a regular function (e.g..header()
) the extra functions are no longer available, but you can safely call.authentication()
again without losing any previous calls.
- Basic authentication
val username = "username"
val password = "abcd1234"
Fuel.get("https://httpbin.org/basic-auth/$user/$password")
.authentication()
.basic(username, password)
.response { result -> }
- Bearer authentication
val token = "mytoken"
Fuel.get("https://httpbin.org/bearer")
.authentication()
.bearer(token)
.response { result -> }
- Any authentication using a header
Fuel.get("https://httpbin.org/anything")
.header(Headers.AUTHORIZATION, "Custom secret")
.response { result -> }
Any request supports
Progress
callbacks when uploading or downloading a body; the Connection
header does not support progress (which is the only thing that is sent if there are no bodies). You can have as many progress handlers of each type as you like.Fuel.post("/post")
.body(/*...*/)
.requestProgress { readBytes, totalBytes ->
val progress = readBytes.toFloat() / totalBytes.toFloat() * 100
println("Bytes uploaded $readBytes / $totalBytes ($progress %)")
}
.response { result -> }
Fuel.get("/get")
.responseProgress { readBytes, totalBytes ->
val progress = readBytes.toFloat() / totalBytes.toFloat() * 100
println("Bytes downloaded $readBytes / $totalBytes ($progress %)")
}
.response { result -> }
Often the progress of a download will be shown as notification. Since Android Nougat (API 24), only 10 notification updates per second per app are allowed. Anything more than that will result in a log error like
E/NotificationService: Package enqueue rate is 10.062265. Shedding events. package=...
.It is the responsibility of the library user – not of fuel – to remedy this limit. A simple solution could look like this:
var lastUpdate = 0L
Fuel.get("/get")
.progress { readBytes, totalBytes ->
// allow 2 updates/second max - more than 10/second will be blocked
if (System.currentTimeMillis() - lastUpdate > 500) {
lastUpdate = System.currentTimeMillis()
val progress = readBytes.toFloat() / totalBytes.toFloat() * 100
myNotificationHelper.notifyDownloadProgress(progress)
}
}
.response { result -> }
Not all source
Body
or Response
Body
report their total size. If the size is not known, the current size will be reported. This means that you will constantly get an increasing amount of totalBytes that equals readBytes.Fuel supports multipart uploads using the
.upload()
feature. You can turn any Request
into a upload request by calling .upload()
or call .upload(method = Method.POST)
directly onto Fuel
/ FuelManager
.When you call.upload()
, a few extra functions are available. If you call a regular function (e.g..header()
) the extra functions are no longer available, but you can safely call.upload()
again without losing any previous calls.
method | arguments | action |
---|---|---|
request.add { } | varargs dataparts: (Request) -> DataPart | Add one or multiple DataParts lazily |
request.add() | varargs dataparts: DataPart | Add one or multiple DataParts |
request.progress(handler) | hander: ProgressCallback | Add a requestProgress handler |
Fuel.upload("/post")
.add { FileDataPart(File("myfile.json"), name = "fieldname", filename="contents.json") }
.response { result -> }
In order to add
DataPart
s that are sources from a File
, you can use FileDataPart
, which takes a file: File
. There are some sane defaults for the field name name: String
, and remote file name filename: String
, as well as the Content-Type
and Content-Disposition
fields, but you can override them.In order to receive a list of files, for example in the field
files
, use the array notation:Fuel.upload("/post")
.add(
FileDataPart(File("myfile.json"), name = "files[]", filename="contents.json"),
FileDataPart(File("myfile2.json"), name = "files[]", filename="contents2.json"),
FileDataPart(File("myfile3.json"), name = "files[]", filename="contents3.json")
)
.response { result -> }
Sending multiple files in a single datapart is not supported as it's deprecated by the multipart/form-data RFCs, but to simulate this behaviour, give the same
name
to multiple parts.You can use the convenience constructors
FileDataPart.from(directory: , filename: , ...args)
to create a FileDataPart
from String
arguments.Sometimes you have some content inline that you want to turn into a
DataPart
. You can do this with InlineDataPart
:Fuel.upload("/post")
.add(
FileDataPart(File("myfile.json"), name = "file", filename="contents.json"),
InlineDataPart(myInlineContent, name = "metadata", filename="metadata.json", contentType = "application/json")
)
.response { result -> }
A
filename
is not mandatory and is empty by default; the contentType
is text/plain
by default.You can also add dataparts from arbitrary
InputStream
s, which you can do using BlobDataPart
:Fuel.upload("/post")
.add(
FileDataPart(File("myfile.json"), name = "file", filename="contents.json"),
BlobDataPart(someInputStream, name = "metadata", filename="metadata.json", contentType = "application/json", contentLength = 555)
)
.response { result -> }
If you don't set the
contentLength
to a positive integer, your entire Request
Content-Length
will be undeterminable and the default HttpClient
will switch to chunked streaming mode with an arbitrary stream buffer size.Simply don't call
add
. The parameters are encoded as parts!val formData = listOf("Email" to "[email protected]", "Name" to "Joe Smith" )
Fuel.upload("/post", param = formData)
.response { result -> }
As mentioned before, you can use
Fuel
both synchronously and a-synchronously, with support for coroutines.By default, there are three response functions to get a request synchronously:
function | arguments | result |
---|---|---|
response() | none | ResponseResultOf<ByteArray> |
responseString(charset) | charset: Charset | ResponseResultOf<String> |
responseObject(deserializer) | deserializer: Deserializer<U> | ResponseResultOf<U> |
The default charset is
UTF-8
. If you want to implement your own deserializers, scroll down to advanced usage.Add a handler to a blocking function, to make it asynchronous:
function | arguments | result |
---|---|---|
response() { handler } | handler: Handler | CancellableRequest |
responseString(charset) { handler } | charset: Charset, handler: Handler | CancellableRequest |
responseObject(deserializer) { handler } | deserializer: Deserializer, handler: Handler | CancellableRequest |
The default charset is
UTF-8
. If you want to implement your own deserializers, scroll down to advanced usage.The core package has limited support for coroutines:
function | arguments | result |
---|---|---|
await(deserializer) | deserializer: Deserializer<U> | U |
awaitResult(deserializer) | deserializer: Deserializer<U> | Result<U, FuelError> |
awaitResponse(deserializer) | deserializer: Deserializer<U> | ResponseOf<U> |
awaitResponseResult(deserializer) | deserializer: Deserializer<U> | ResponseResultOf<U> |
When using other packages such as
fuel-coroutines
, more response/await functions are available.- The
ResponseResultOf<U>
type is aTriple
of theRequest
,Response
and aResult<U, FuelError>
- The
ResponseOf<U>
type is aTriple
of theRequest
,Response
and aU
; errors are thrown - The
Result<U, FuelError>
type is a non-throwing wrapper aroundU
- The
U
type doesn't wrap anything; errors are thrown
When defining a handler, you can use one of the following for all
responseXXX
functions that accept a Handler
:type | handler fns | arguments | description |
---|---|---|---|
Handler<T> | 2 | 1 | calls success with an instance of T or failure on errors |
ResponseHandler<T> | 2 | 3 | calls success with Request , Response and an instance of T , or failure or errors |
ResultHandler<T> | 1 | 1 | invokes the function with Result<T, FuelError> |
ResponseResultHandler<T> | 1 | 3 | invokes the function with Request Response and Result<T, FuelError> |
This means that you can either choose to unwrap the
Result
yourself using a ResultHandler
or ResponseResultHandler
, or define dedicated callbacks in case of success or failure.Result is a functional style data structure that represents data that contains result of Success or Failure but not both. It represents the result of an action that can be success (with result) or error.
Working with result is easy:
- You can call [
fold
] and define a tranformation function for both cases that results in the same return type, - use
when
checking whether it isResult.Success
orResult.Failure
Fuel supports downloading the request
Body
to a file using the .download()
feature. You can turn any Request
into a download request by calling .download()
or call .download(method = Method.GET)
directly onto Fuel
/ FuelManager
.When you call.download()
, a few extra functions are available. If you call a regular function (e.g..header()
) the extra functions are no longer available, but you can safely call.download()
again without losing any previous calls.
method | arguments | action |
---|---|---|
request.fileDestination { } | (Response, Request) -> File | Set the destination file callback where to store the data |
request.streamDestination { } | (Response, Request) -> Pair<OutputStream, () -> InputStream> | Set the destination file callback where to store the data |
request.progress(handler) | hander: ProgressCallback | Add a responseProgress handler |
Fuel.download("https://httpbin.org/bytes/32768")
.fileDestination { response, url -> File.createTempFile("temp", ".tmp") }
.progress { readBytes, totalBytes ->
val progress = readBytes.toFloat() / totalBytes.toFloat() * 100
println("Bytes downloaded $readBytes / $totalBytes ($progress %)")
}
.response { result -> }
The
stream
variant expects your callback to provide a Pair
with both the OutputStream
to write too, as well as a callback that gives an InputStream
, or raises an error.- The
OutputStream
is always closed after the body has been written. Make sure you wrap whatever functionality you need on top of the stream and don't rely on the stream to remain open. - The
() -> InputStream
replaces the body after the current body has been written to theOutputStream
. It is used to make sure you can also retrieve the body via theresponse
/await
method results. If you don't want the body to be readable after downloading it, you have to do two things:- use an
EmptyDeserializer
withawait(deserializer)
or one of theresponse(deserializer)
variants - provide an
InputStream
callback thatthrows
or returns an emptyInputStream
.
For downloading larger files it is good to use
request.streamDestination { }
instead of request.fileDestination { }
val outputStream = FileOutputStream(File(filePath))
Fuel.download("https://httpbin.org/bytes/32768")
.streamDestination { response, request -> Pair(outputStream, { request.body.toStream() }) }
.progress { readBytes, totalBytes ->
val progress = readBytes.toFloat() / totalBytes.toFloat() * 100
println("Bytes downloaded $readBytes / $totalBytes ($progress %)")
}
.response { result ->
result.fold(
success = {
},
failure = {
}
)
}
The
response
functions called with a handler
are async and return a CancellableRequest
. These requests expose a few extra functions that can be used to control the Future
that should resolve a response:val request = Fuel.get("https://httpbin.org/get")
.interrupt { request -> println("${request.url} was interrupted and cancelled") }
.response { result ->
// if request is cancelled successfully, response callback will not be called.
// Interrupt callback (if provided) will be called instead
}
request.cancel() // this will cancel on-going request
If you can't get hold of the
CancellableRequest
because, for example, you are adding this logic in an Interceptor
, a generic Queue
, or a ProgressCallback
, you can call tryCancel()
which returns true if it was cancelled and false otherwise. At this moment blocking
requests can not be cancelled.baseHeaders
is to manage common HTTP header pairs in format ofMap<String, String>
.- The base headers are only applied if the request does not have those headers set.
FuelManager.instance.baseHeaders = mapOf("Device" to "Android")
Headers
can be added to a request via various methods including
fun header(name: String, value: Any): Request = request.header("foo", "a")
fun header(pairs: Map<String, Any>): Request = request.header(mapOf("foo" to "a"))
fun header(vararg pairs: Pair<String, Any>): Request = request.header("foo" to "a")
operator fun set(header: String, value: Collection<Any>): Request = request["foo"] = listOf("a", "b")
operator fun set(header: String, value: Any): Request = request["foo"] = "a"
- By default, all subsequent calls overwrite earlier calls, but you may use the
appendHeader
variant to append values to existing values.- In earlier versions (1.x.y), a
mapOf
overwrote, andvarargs pair
did not, but this was confusing. In 2.0, this issue has been fixed and improved so it works as expected.
fun appendHeader(header: String, value: Any): Request
fun appendHeader(header: String, vararg values: Any): Request
fun appendHeader(vararg pairs