Host interceptor HttpUrl.parse IllegalArguementException in latest Okhttp - android

I have to intercept host at run time . As my url is dynamic. below code is working fine in old okhttp3
Working with old Okhttp
class HostSelectionInterceptor #Inject constructor(val chiPrefs: ChiPrefs): Interceptor{
override fun intercept(chain: Interceptor.Chain): Response {
var request: Request = chain.request()
var host = String.format(Locale.ENGLISH, "https://%s.cognitiveintl.com",
chiPrefs.sitePrefix())
request.url().pathSegments().forEach {
host += "/$it"
}
if(host.isNotEmpty()){
val newUrl = HttpUrl.parse(host)
request = request.newBuilder().url(newUrl!!).build()
}
return chain.proceed(request)
}
}
but after upgrading it to latest version .
val newUrl = HttpUrl.parse(host) // deprecated..
HttpUrl.parse. become deprecated..
After r & d , I update my code like
val newUrl = request.url.newBuilder()
.host(host) ///crashed at this line
.build()
request = request.newBuilder()
.url(newUrl)
.build()
It give IllegalArguementException . Suggest a solution to it.
Crash :
FATAL EXCEPTION: OkHttp Dispatcher
Process: com.chi.doctorapp.dev, PID: 2906
java.lang.IllegalArgumentException: unexpected host: https://chi-dev1.cognitiveintl.com/api/doctor_app/GetProfile
at okhttp3.HttpUrl$Builder.host(HttpUrl.kt:961)
at com.chi.doctorapp.di.interceptors.HostSelectionInterceptor.intercept(HostSelectionInterceptor.kt:28)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:100)

Replace this:
HttpUrl.parse(host)
With this:
host.toHttpUrlOrNull()
You'll need this import:
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull()
This is documented in the upgrade guide.

Using
request.url.newBuilder()
.host(host)
.build()
would be the correct way.
The reason this crashes for you, is that pass a complete url as host.
For example for the url https://subdomain.example.com/some/path
the host is subdomain.example.com
So instead try this:
class HostSelectionInterceptor #Inject constructor(val chiPrefs: ChiPrefs): Interceptor{
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
//Notice the removed "https://"
val host = String.format(Locale.ENGLISH, "%s.weburlintl.com",
chiPrefs.sitePrefix())
// This will keep the path, query parameters, etc.
val newUrl = request.url.newBuilder()
.host(host)
.build()
val newRequest = request
.newBuilder()
.url(newUrl)
.build()
return chain.proceed(newRequest)
}
}

Related

Retrofit doesn't send POST request

I can't seem to get the POST request working with Retrofit. My code:
ApiService.kt contains a function
#Headers("Content-Type: application/json")
#POST("resume")
suspend fun resumeAsync(#Body request: JSONObject): Response<String>
Then in my ListViewModel.kt I have a function
fun resume(id: String) {
coroutineScope.launch {
try {
val paramObject = JSONObject()
paramObject.put("id", id)
val response = Api.retrofitService.resumeAsync(paramObject)
if (response.isSuccessful) {
_status.value = "Success: Resumed!"
}
} catch (e: Exception) {
_status.value = "Failure: " + e.message
}
}
}
Why is this not working? I don't get any error or response back. If I put Log.i in the Api or the view model it says it's triggered
By debugging I found out this error:
2020-09-15 11:40:10.904 20622-20622/com.example.app I/NETWORK: Unable to create #Body converter for class org.json.JSONObject (parameter #1) for method ApiService.resumeAsync
I am using Moshi as well
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
private val retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl(BASE_URL)
.build()
You can try adding a logging interceptor to your okHttp client and check what you're sending and receiving in the request.
val logging = HttpLoggingInterceptor()
logging.setLevel(HttpLoggingInterceptor.Level.BODY)
Is the url/endpoint correct?
Are you missing a "/" at the end of the url?
Have you declared internet permission in the manifest?
etc.
Solution:
Wrapping request body:
#Body body: RequestBody
val body = RequestBody.create(MediaType.parse("application/json"), obj.toString())
If you need to receive raw json then use Call<*>
#Headers("Content-Type: application/json")
#POST("resume")
fun resumeAsync(#Body request: JSONObject): retrofit2.Call<String>
Inside coroutine (without suspend keyword above)
// or .awaitResponse() to get Response<*> object
val response = Api.retrofitService.resumeAsync(paramObject).await()
Can you debug on this line
if (response.isSuccessful) {
try to check the variable response;
Or you shoud check whether the server is work, check it with other tools like Postman
So I solved my problem by doing what #Paul Nitu recommended
val body = RequestBody.create(MediaType.parse("application/json"), obj.toString())

Retrofit2 authentication error to IBM's Speech to Text

I am trying to access IBM's Speech to Text service without using the library. I am using Retrofit with GSON.
The issue is in the authentication, which apparently does not occur correctly, returning code 401. From the official documentation, the HTTP request should come in this format
curl -X POST -u "apikey:{apikey}" \
--header "Content-Type: audio/flac" \
--data-binary #{path_to_file}audio-file.flac \
"{url}/v1/recognize"
When I test the curl command with my credentials, the service works fine.
This is the interface I'm using
interface SpeechToTextApi {
#Multipart
#POST("v1/recognize")
fun speechToText(
#Header("Authorization") authKey: String,
#Part("file") filename: RequestBody,
#Part voiceFile: MultipartBody.Part
): Call<List<SpeechToText>>
}
where I have the following data classes
data class SpeechToText(val results: List<SttResult>)
data class SttResult(val alternatives: List<RecognitionResult>, val final: Boolean)
data class RecognitionResult(val confidence: Float, val transcript: String)
and this is how I set up Retrofit
private val retrofit = Retrofit.Builder()
.baseUrl(STT_BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
private val service = retrofit.create(SpeechToTextApi::class.java)
while calling the actual service looks like this
val requestFile = RequestBody.create(MediaType.parse("audio/mp3"), file.name)
val body = MultipartBody.Part.createFormData("file", file.name, requestFile)
service
.speechToText(getString(R.string.stt_iam_api_key), requestFile, body)
.enqueue(object: Callback<List<SpeechToText>> {
override fun onResponse(call: Call<List<SpeechToText>>, response: Response<List<SpeechToText>>) {
val listOfStts = response.body()
Log.d(TAG, "Response code: ${response.code()}")
if (listOfStts != null) {
for (stt in listOfStts) {
for (res in stt.results) {
Log.d(TAG, "Final value: ${res.final}")
for (alt in res.alternatives) {
Log.d(TAG, "Alternative confidence: ${alt.confidence}\nTranscript: ${alt.transcript}")
Toast.makeText(this#MainActivity, alt.transcript, Toast.LENGTH_SHORT).show()
}
}
}
}
}
override fun onFailure(call: Call<List<SpeechToText>>, t: Throwable) {
Log.d(TAG, "Error: ${t.message}")
t.printStackTrace()
}
})
Recordings are MP3 files, for which I am sure they are stored correctly and accessible. I have replaced audio/flac with audio/mp3 as well.
Issue seems to be in the way authentication works. Prior to the code I have shown above, I've used
private val retrofit = Retrofit.Builder()
.baseUrl(STT_BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(OkHttpClient.Builder()
.addInterceptor { chain ->
val request = chain.request()
val headers = request
.headers()
.newBuilder()
.add("Authorization", getString(R.string.stt_iam_api_key))
.build()
val finalRequest = request.newBuilder().headers(headers).build()
chain.proceed(finalRequest)
}
.build())
.build()
but the same response code 401 persisted. Of course, the interface method lacked the #Header parameter.
Any sort of help is much appreciated.
I am kind of saddened by the fact nobody was able to solve this one sooner, but here's the solution I came across by accident when working on a different project altogether.
As you can see from the curl command, authentication comes in the form of username: password pattern, in this case, username being apikey string and password is your API key.
So the way you should tackle this is by building your Retrofit instance this way:
fun init(token: String) {
//Set logging interceptor to BODY and redact Authorization header
interceptor.level = HttpLoggingInterceptor.Level.BODY
interceptor.redactHeader("Authorization")
//Build OkHttp client with logging and token interceptors
val okhttp = OkHttpClient().newBuilder()
.addInterceptor(interceptor)
.addInterceptor(TokenInterceptor(token))
.build()
//Set field naming policy for Gson
val gsonBuilder = GsonBuilder()
gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
//Build Retrofit instance
retrofit = Retrofit.Builder()
.baseUrl(IBM_BASE_URL)
.addConverterFactory(GsonConverterFactory.create(gsonBuilder.create()))
.client(okhttp)
.build()
}
and create this custom interceptor
class TokenInterceptor constructor(private val token: String) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val original = chain.request()
val requestBuilder = original
.newBuilder()
.addHeader("Authorization", Credentials.basic("apikey", token))
.url(original.url)
return chain.proceed(requestBuilder.build())
}
}
You need to use Credentials.basic() in order to encode credentials.
I really hope somebody with a similar issue stumbles across this and saves themselves some time.

Dynamic urls with Koin ans Retrofit

Using Retrofit for network calls and Koin for dependency injection in an Android app, how to support dynamic url change?
(while using the app, users can switch to another server)
EDIT: network module is declared like this:
fun networkModule(baseUrl: String) = module {
single<Api> {
Retrofit.Builder()
.baseUrl(baseUrl)
.client(OkHttpClient.Builder().readTimeout(30, TimeUnit.SECONDS)
.connectTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build())
.build().create(Api::class.java)
}
I am starting Koin in the Aplication class onCreate like this:
startKoin {
if (BuildConfig.DEBUG) AndroidLogger() else EmptyLogger()
androidContext(this#App)
modules(listOf(networkModule(TEST_API_BASE_URL), storageModule, integrationsModule, appModule))
}
I faced the same problem recently. The most convenient way is to use a Interceptor to change the baseUrl dynamically.
class HostSelectionInterceptor(defaultHost: String? = null, defaultPort: Int? = null) : Interceptor {
#Volatile var host: String? = null
#Volatile var port: Int? = null
init {
host = defaultHost
port = defaultPort
}
#Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): okhttp3.Response {
var request = chain.request()
this.host?.let {host->
val urlBuilder = request.url().newBuilder()
urlBuilder.host(host)
this.port?.let {
urlBuilder.port(it)
}
request = request.newBuilder().url(urlBuilder.build()).build()
}
return chain.proceed(request)
}
}
Initialize it with your default url.
single { HostSelectionInterceptor(HttpUrl.parse(AppModuleProperties.baseUrl)?.host()) }
single { createOkHttpClient(interceptors = listOf(get<HostSelectionInterceptor>()))}
And add this interceptor when creating your OkHttpClient.
val builder = OkHttpClient().newBuilder()
interceptors?.forEach { builder.addInterceptor(it) }
To change the url you only have to update the interceptors member.
fun baseUrlChanged(baseUrl: String) {
val hostSelectionInterceptor = get<HostSelectionInterceptor>()
hostSelectionInterceptor.host = baseUrl
}
I've tried with Koin loading/unloading modules..and for a short period of time it worked, but later, after a minimal change I wasn't able to make it reload again.
At the end, I solved it with wrapper object:
class DynamicRetrofit(private val gson: Gson) {
private fun buildClient() = OkHttpClient.Builder()
.build()
private var baseUrl = "https://etc..." //default url
private fun buildApi() = Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create(gson))
.client(buildClient())
.build().create(MyApi::class.java)
var api: MyApi = buildApi()
private set
fun setUrl(url: String) {
if (baseUrl != url)
baseUrl = url
api = buildApi()
}}
I declare it in within Koin module like this:
single<DynamicRetrofit>()
{
DynamicRetrofit(get(), get())
}
and use it in pretty standard way:
dynamicRetrofit.api.makeSomeRequest()
It was good solution for my case since I change baseUrl very rarely. If you need to make often and parallel calls to two different servers it will probably be inefficient since you this will recreate HTTP client often.

Error Management with paging library android

I am using paging library version 2.1.0 in with java. How I can manage false status or empty array from API. I tried finding some solution but didn't get any.
If you're using Retrofit, you could use Interceptor to intercept error code.
Below is the code to handle response code error 401. Similarly, you can handle any response code.
var retrofit:Retrofit = Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(BASE_URL)
.client(getOkHttpClient())
.build()
private fun getOkHttpClient() : OkHttpClient{
val okHttpCLient = OkHttpClient.Builder()
.addInterceptor(object : Interceptor{
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
if(response.code() == 401){
Log.e(TAG,"Un-Authorized user")
}
return response
}
})
return okHttpCLient.build()
}
//OkHttp Library in Gradle file:
implementation 'com.squareup.okhttp3:okhttp:3.12.0'

How to specify Get-Request encoding (Retrofit + OkHttp)

I'm using Retrofit2 + OkHttp3 in my Android app to make a GET - Request to a REST-Server. The problem is that the server doesn't specify the encoding of the JSON it delivers. This results in an 'é' being received as '�' (the Unicode replacement character).
Is there a way to tell Retrofit or OkHttp which encoding the response has?
This is how I initialize Retrofit (Kotlin code):
val gson = GsonBuilder()
.setDateFormat("d.M.yyyy")
.create()
val client = OkHttpClient.Builder()
.build()
val retrofit = Retrofit.Builder()
.baseUrl(RestService.BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build()
val rest = retrofit.create(RestService::class.java)
PS: The server isn't mine. So I cannot fix the initial problem on the server side.
Edit: The final solution
class EncodingInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
val mediaType = MediaType.parse("application/json; charset=iso-8859-1")
val modifiedBody = ResponseBody.create(mediaType, response.body().bytes())
val modifiedResponse = response.newBuilder()
.body(modifiedBody)
.build()
return modifiedResponse
}
}
One way to do this is to build an Interceptor that takes the response and sets an appropriate Content-Type like so:
class ResponseInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
val modified = response.newBuilder()
.addHeader("Content-Type", "application/json; charset=utf-8")
.build()
return modified
}
}
You would add it to your OkHttp client like so:
val client = OkHttpClient.Builder()
.addInterceptor(ResponseInterceptor())
.build()
You should make sure you either only use this OkHttpClient for your API that has no encoding specified, or have the interceptor only add the header for the appropriate endpoints to avoid overwriting valid content type headers from other endpoints.
class FixEncodingInterceptor implements Interceptor {
#Override
public Response intercept(Chain chain) throws IOException {
Response response = chain.proceed(chain.request());
MediaType oldMediaType = MediaType.parse(response.header("Content-Type"));
// update only charset in mediatype
MediaType newMediaType = MediaType.parse(oldMediaType.type()+"/"+oldMediaType.subtype()+"; charset=windows-1250");
// update body
ResponseBody newResponseBody = ResponseBody.create(newMediaType, response.body().bytes());
return response.newBuilder()
.removeHeader("Content-Type")
.addHeader("Content-Type", newMediaType.toString())
.body(newResponseBody)
.build();
}
}
and add to OkHttp:
builder.addInterceptor(new FixEncodingInterceptor());
This post is old but I found a solution that works for me in Kotlin (the answer of #BryanHerbst didn't quite worked for me)
class EncodingInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
var encodedBody = ""
val encoding = InputStreamReader(
response.body?.byteStream(),
Charset.forName("ISO-8859-1")
).forEachLine {
encodedBody += it
}
return response.newBuilder()
.addHeader("Content-Type", "application/xml; charset=utf-8")
.body(encodedBody.toResponseBody())
.build()
}
}

Categories

Resources