I would like to create app with https requests and i use retrofit. But when i get response in headers not contains content-type. Because of this, retrofit considers that the response is a string, but there is json. Can I ignore the lack of content-type and always parse the response as json? Unfortunately I can't change the API.
this is my code
abstract class Api{
factory Api(Dio dio, {String baseUrl}) = _Api;
#JSONMessageCodec()
#POST("secret =)")
Future<List<String>> getRooms(#Header("Authorization") String basicAuth, #Header("Content-Type") String type);
}```
Related
I use Retrofit (v2.9.0) and Moshi (v1.11.0) in my app. I try to call an endpoint this way:
#FormUrlEncoded
#PATCH("anime/{anime_id}/my_list_status")
fun updateListStatus(
#Path("anime_id") animeId: Long,
#Field("num_watched_episodes") nbWatchedEpisodes: Int,
#Field("score") score: Double,
#Field("status") watchStatus: WatchStatus,
): Single<MyListStatus>
But the WatchStatus->Json conversion is not working as expect. WatchStatus is a simple enum class:
enum class WatchStatus {
COMPLETED,
DROPPED,
ON_HOLD,
PLAN_TO_WATCH,
WATCHING,
}
and I created a custom adapter because my app uses uppercase enum names while the back-end uses lowercase names:
class AnimeMoshiAdapters {
/* Others adapters */
#ToJson
fun watchStatusToJson(watchStatus: WatchStatus): String =
watchStatus.toString().toLowerCase(Locale.getDefault())
#FromJson
fun watchStatusFromJson(watchStatus: String): WatchStatus =
WatchStatus.valueOf(watchStatus.toUpperCase(Locale.getDefault()))
}
I create my Moshi instance this way:
Moshi.Builder()
.addLast(KotlinJsonAdapterFactory())
.add(AnimeMoshiAdapters())
.build()
and my Retrofit instance uses it (with Koin injection):
Retrofit.Builder()
.baseUrl(get<String>(named("baseUrl")))
.client(get(named("default")))
.addConverterFactory(MoshiConverterFactory.create(get()))
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build()
When parsing a Json to create a WatchStatus enum the adapter is used. It is noticeable because the call fails with an error "com.squareup.moshi.JsonDataException: Expected one of [COMPLETED, DROPPED, ON_HOLD, PLAN_TO_WATCH, WATCHING]" if I remove my custom adapter.
When I try to call the endpoint specified above the transformation of a WatchStatus in Json is wrong and the enum name stay in Uppercase, meaning my custom adapter is not used. If I check the Retrofit logs I can see that it send "num_watched_episodes=12&score=6.0&status=ON_HOLD", so the status is not converted in lowercase.
If I try to manually convert a WatchStatus in Json using the same Moshi instance it works as expected, so I believe my custom adapter implementation is correct.
How can I make Retrofit uses my custom Moshi adapter in this call?
Moshi adapters' toJson apply to the retrofit requests' body (== params annotated with #BODY), not query parameters (== params annotated with #FIELD). And it is correct and expected behavior, as query parameters are by standards not expected to be JSON formatted strings (eventhough in your case it's just a String). If your server expects the status field as query parameter, then there is no other way than to provide it lowerCased by yourself:
#FormUrlEncoded
#PATCH("anime/{anime_id}/my_list_status")
fun updateListStatus(
...
#Field("status") watchStatus: String
): Single<MyListStatus>
and feed your updateListStatus with already lowerCased value:
updateListStatus(..., COMPLETED.name.toLowerCase())
If you have no influence on the server's implementation, then skip the rest of this post.
If you want to utilize your custom adapter's toJson function, your server needs to change the request to accept JSON body instead of query params, say like this:
PUT: anime/{anime_id}/my_list_status
BODY:
{
"anime_id" : Long,
"num_watched_episodes" : Int,
"score" : Double,
"status" : WatchStatus
}
Then you would create a data class for the body, say named RequestBody, then you could change your request to:
#PUT("anime/{anime_id}/my_list_status")
fun updateListStatus(
...
#BODY body: RequestBody
): Single<MyListStatus>
in which case your custom adapter will take effect and transform the WatchStatus inside the RequestBody by its defined toJson logic.
The idea: There's an endpoint I'm trying to consume on Android. After calling it, the endpoint will return HTTP 202 (empty body, there's no payload at all) for a couple of times and when the data is ready it will return HTTP 200 with the data.
What I want to do is be able to parse the 202 response, but since it does not have a payload I'll just check the status and poll again if needed and once I get the 200 OK, I'll parse and use the data.
The part where I parse and use the 200 OK response is working, but parsing the 202 response does not work, I get the Exception (see below).
I know there are several approaches to this:
using Void as the return type,
using ResponseBody as the return type,
using a Converter.Factory.
I've created a converter class for this:
internal val nullOnEmptyConverterFactory = object : Converter.Factory() {
fun converterFactory() = this
override fun responseBodyConverter(
type: Type,
annotations: Array<out Annotation>,
retrofit: Retrofit
) = object : Converter<ResponseBody, Any?> {
val nextResponseBodyConverter =
retrofit.nextResponseBodyConverter<Any?>(converterFactory(), type, annotations)
override fun convert(value: ResponseBody) =
if (value.contentLength() != 0L) nextResponseBodyConverter.convert(value) else null
}
}
This is how I use it:
return Retrofit.Builder()
.client(client)
.addConverterFactory(nullOnEmptyConverterFactory)
.addConverterFactory(ScalarsConverterFactory.create()) // used for jsonp
.addConverterFactory(GsonConverterFactory.create(factory))
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.baseUrl(ApiService.getBaseUrl())
.build()
And this is the error I get:
error: java.io.EOFException
at okio.RealBufferedSource.require(RealBufferedSource.kt:55)
at okio.GzipSource.consumeHeader(GzipSource.kt:104)
at okio.GzipSource.read(GzipSource.kt:62)
at okio.RealBufferedSource.read(RealBufferedSource.kt:41)
at okio.ForwardingSource.read(ForwardingSource.kt:29)
at retrofit2.OkHttpCall$ExceptionCatchingResponseBody$1.read(OkHttpCall.java:288)
at okio.RealBufferedSource.select(RealBufferedSource.kt:93)
at okhttp3.internal.Util.readBomAsCharset(Util.kt:256)
at okhttp3.ResponseBody$BomAwareReader.read(ResponseBody.kt:208)
at com.google.gson.stream.JsonReader.fillBuffer(JsonReader.java:1295)
at com.google.gson.stream.JsonReader.nextNonWhitespace(JsonReader.java:1333)
at com.google.gson.stream.JsonReader.consumeNonExecutePrefix(JsonReader.java:1576)
at com.google.gson.stream.JsonReader.doPeek(JsonReader.java:534)
at com.google.gson.stream.JsonReader.peek(JsonReader.java:425)
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:207)
at retrofit2.converter.gson.GsonResponseBodyConverter.convert(GsonResponseBodyConverter.java:39)
at retrofit2.converter.gson.GsonResponseBodyConverter.convert(GsonResponseBodyConverter.java:27)
at data.network.util.NullOnEmptyConverterFactoryKt$nullOnEmptyConverterFactory$1$responseBodyConverter$1.convert(NullOnEmptyConverterFactory.kt:22)
at data.network.util.NullOnEmptyConverterFactoryKt$nullOnEmptyConverterFactory$1$responseBodyConverter$1.convert(NullOnEmptyConverterFactory.kt:17)
at retrofit2.OkHttpCall.parseResponse(OkHttpCall.java:225)
at retrofit2.OkHttpCall$1.onResponse(OkHttpCall.java:121)
at okhttp3.RealCall$AsyncCall.run(RealCall.kt:138)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1162)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:636)
at java.lang.Thread.run(Thread.java:764)
Found out what caused this: none of the libraries mentioned above, Retrofit, GSon are at fault. If you follow the stack trace you'll see that it points to a problem, that the response does not conform to the gzip file format.
The OkHttp library expects the response to have a 10 byte header, if you're using gzip file format. If the response does not have it, the lib throws an exception.
The API is at fault in this case.
The ticket that helped me figure out the isse.
I am working on an user app for a local charitable organization, and need to access their API. The API is from wild apricot, and this is the documentation for making a token request:
Authentication tokens are obtained from Wild Apricot's authentication service, which is located at https://oauth.wildapricot.org. This service adheres to oAuth 2.0.
This is the access option I need to implement:
-----------------In order to obtain access token with API key, you have to make the following request:
POST /auth/token HTTP/1.1
Host: oauth.wildapricot.org
Authorization: Basic BASE64_ENCODED("APIKEY:YOUR_API_KEY")
Content-type: application/x-www-form-urlencoded
grant_type=client_credentials&scope=auto
-------------------------------So. finally your request will look like:
POST /auth/token HTTP/1.1
Host: oauth.wildapricot.org
Authorization: Basic QVBJS0VZOm85c2U4N3Jnb2l5c29lcjk4MDcwOS0=
Content-type: application/x-www-form-urlencoded
grant_type=client_credentials&scope=auto
I am attempting to make this call with retrofit2, and an okhttp3 interceptor, and getting a bad request response (I am very much new and learning, and have not been able to get anything other response than a 400 bad request (when I use "/auth/token" as the endpoint), or a 404 not found (when I use "/auth/token HTTP/1.1" as the endpoint). If someone could tell me where exactly I am messing this up It would be greatly appreciated, the code I have tried is below.
Interface:
interface WAApiCall {
#POST("auth/token")
fun callPost(#Body body:String ): Call<AuthToken>
}
Call Service:
object WAApiCallService {
private const val API_KEY = "xxxxxxxxIxHavexAxValidxKeyxxxx"
private const val BASE_URL = "https://oauth.wildapricot.org/"
private val AUTH = "Basic" + Base64.encodeToString(API_KEY.toByteArray(), Base64.NO_WRAP)
private const val CONTENT_TYPE = "application/x-www-form-urlencoded"
private var api:WAApiCall? = null
private fun getWAApi(context: Context) : WAApiCall {
if(api==null){
val OkHttpClient = OkHttpClient.Builder()
val logging = HttpLoggingInterceptor()
logging.level = HttpLoggingInterceptor.Level.BASIC
OkHttpClient.addInterceptor{chain ->
val request = chain.request()
Log.d("CALL", request.body.toString())
val newRequest = request.newBuilder()
.addHeader("Host", "oauth.wildapricot.org")
.addHeader("Authorization", AUTH )
.addHeader("Content-type", CONTENT_TYPE)
.method(request.method, request.body)
.build()
chain.proceed(newRequest)
}
api = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(OkHttpClient.build())
.build()
.create(WAApiCall::class.java)
}
return api!!
}
fun call(context: Context) =
getWAApi(context)
}
Function in Main Activity to make the call:
fun testRequest(){
val call = WAApiCallService.call(this)
call.callPost("grant_type=client_credentials&scope=auto")
.enqueue(object: Callback<AuthToken>{
override fun onFailure(call: Call<AuthToken>, t: Throwable) {
Log.i("FAILURE", t.localizedMessage)
}
override fun onResponse(call: Call<AuthToken>, response: Response<AuthToken>) {
Log.i("SUCCESS", "TOKEN = ${response.body().toString()}")
Log.i("SUCCESS", "${response}")
val token = response.body()?.accessToken
Log.i("SUCCESS", "TOKEN = $token")
}
})
}
Error message:
I/SUCCESS: TOKEN = null
I/SUCCESS: Response{protocol=http/1.1, code=400, message=Bad Request, url=https://oauth.wildapricot.org/auth/token}
I think that I am just not understanding how to implement this type of request in some basic way, I could not get it to work in Postman either. I understand that I need to send the credentials to the authentication server, and receive an access token, that will expire and need to be refreshed, and that It will be included in each actual API endpoint call, I guess I'm just missing something crucial in the most important step of that process (getting the actual token, I am imagining it is a simple, forehead slapping kind of misunderstanding on my part?). The wild apricot API is on swagger hub, and I am able to gain access through that UI, with my API key, and see the responses, so I know that it is valid.
Your client credentials request looks mostly all good. The only thing I can see that looks wrong is no space character in the AUTH header between 'Basic' and the encoded credential.
If that doesn't work, could you trace the HTTP request and verify that you are sending the message you think you are.
Thank you for that observation, it led me to figuring out what ultimately was wrong in my initial attempt. After adding that space, I traced the request and found that It was actually sending two headers for content type.
The fix for that was to set the header in the retrofit call from the interface:
interface WAApiCall {
#POST("auth/token")
fun callPost(#Body Body: okhttp3.RequestBody, #Header("Content-type") type: String): Call<AuthToken>
}
As you can see the body is also slightly different, the call was getting through but was returning:
"unsupported_grant_type".
I was passing a raw string as the body parameter, which was including the quotation marks in the request. The solution there was to pass the okhttp3.Request body type rather than a raw string, in the function that makes the actual call it looks like this:
val body: "grant_type=client_credentials&scope=auto&obtain_refresh_token=true"
val requestBody = RequestBody.create("text/plain".toMediaTypeOrNull(),body)
val call = WAApiCallService.call(this)
call.callPost(requestBody,"application/x-www-form-urlencoded")
.enqueue(object: Callback<AuthToken>{
With those changes the call succeeds and my long running headache is over.
Api Interface
#FormUrlEncoded
#POST("register.php")
Observable<String> registerUser(#Field("email") String email, #Field("password") String password);
In my MVP presenter
onCreate{
Observable<String> registerUserObservable=
apiInterface.registerUser("test#gmail.com", "1234");
registerUserObservable.subscribeOn("schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::handleResult, this::handleError);
}
//methods
private void handleResult(String response){
Log.d(TAG, response);
}
private void handleError(Throwable throwable){
Log.d(TAG, throwable.getMessage());
}
These are my code for retrofit and rxjava and I am suppose to post an email and password to register a user. The server should return a string on success and a string on failure too.
Gson gson = new GsonBuilder()
.setLenient()
.create();
retrofit = new retrofit2.Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build();
I added the gson setLenient code portion because it gives me
Use JsonReader.setLenient(true) to accept malformed JSON at line 2 column 1 path $ error if I don't have it. After adding this, I am getting JSON document was not fully consumed. error which I do not know how to solve it. Is this because of the return response from the server is a string?
Thanks in advance.
If your server sends normal string response not JSON string, you need to use another converter that reads string into String. Happily there is Scalars converter officially.
A Converter which supports converting strings and both primitives and their boxed types to text/plain bodies.
On Android, I initially implemented a Retrofit interface like this:
#DELETE(USER_API_BASE_URL + "/{id}")
public void deleteUser(#Path("id") String id, Callback<User> callback);
The server returns 204 NO CONTENT upon a successful deletion. This was causing the callback to trigger failure, with retrofit.RetrofitError: End of input at character 0 of, as it was expecting a User object back with the response.
I then rewrote it like this, using Void instead of User:
#DELETE(USER_API_BASE_URL + "/{id}")
public void deleteUser(#Path("id") String id, Callback<Void> callback); <-- VOID
But I am getting the same error from the callback.
What is the proper way to fix this? Thank you.
Retrofit 2.x no longer has a ResponseCallback as mentioned in the other answer. You want to use a Response<Void> type.
The RxJava declaration:
#PUT Observable<Response<Void>> foo();
The standard declaration:
#PUT Call<Response<Void>> bar();
The solution was pointed out by Jake Wharton in the comments. Use ResponseCallback.
EDIT: this response is no longer valid for Retrofit < 2.
This is the kotlin way for the implementation to deal with HTTP 204 and no content.
#DELETE(USER_API_BASE_URL + "/{id}")
suspend fun deleteuser(#HeaderMap headers: Map<String, String>,
#Path("id") id: String)
: Response<Void>