Retrofit2 authentication error to IBM's Speech to Text - android

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.

Related

Send JWE, as request body, with Retrofit

I am using Retrofit to send request as encrypted JWT (JWE) to an API.
My service interface is:
interface APICallService {
#Headers("Content-Type: application/jwt")
#POST("/v1/api/dp_checkkyc")
fun getKycCompliantStatus(#Header("Authorization") accessToken:String, kycStatusRequest: KycStatusRequest): Call<KycCompliantBaseResponse>
}
My KycStatusRequest class is:
data class KycStatusRequest(var encryptedJWT : String)
I am hitting the API with:
fun getEKycCompliantStatus(accessToken:String, pan:String) {
var jwe = EncryptedJWTGenerator(pan).jweString //This JWE works fine with Postman
val kycStatusRequest = KycStatusRequest(jwe)
val call = getServiceInstance().getKycCompliantStatus("Bearer ${accessToken.trim()}", kycStatusRequest)
call.enqueue(object : Callback<KycCompliantBaseResponse> {
override fun onResponse(call: Call<KycCompliantBaseResponse>, response: Response<KycCompliantBaseResponse>) {
if (response.code() == 200) {
val kycResponse = response.body()!!
if (kycResponse.Response.F_PAN_STATUS.equals("ok", true))
isKycCompliant = true
else if (kycResponse.Response.F_PAN_STATUS.equals("invalid", true))
isKycCompliant = false
}
else
Toast.makeText(context,"Check kyc API failure!", Toast.LENGTH_LONG).show()
}
override fun onFailure(call: Call<KycCompliantBaseResponse>, t: Throwable) {
Toast.makeText(context,"Check kyc API failure!", Toast.LENGTH_LONG).show()
}
})
}
On using the above code I get 'Internal Server Error'.
But on using the same jwe I used above with postman, API works fine.
I am suspecting that I am getting this error as I am wrapping my JWE in KycStatusRequest class before sending, which I think will convert it into a JSON with key-value pair.
How do I send my JWE as a raw text without any key-value pair?
Solved by wrapping up my JWE with RequestBody class as:
val requestBody: RequestBody = RequestBody.create(MediaType.parse("text/plain"), jwe)
I think you'll need to add a converter Factory to your retrofit builder, like this one:
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create()) //Converters can be added to support other types in body
.build()
You can learn more about converterfactory and retrofit on its website: https://square.github.io/retrofit/

Twitter oauth/request_token 200 code with empty response body

I'm implementing Twitter OAuth flows as per:
https://developer.twitter.com/en/docs/authentication/guides/log-in-with-twitter
I am getting a response back for the first step (oauth/request_token) which has a 200 code, but the response body is completely empty.
I'm using Retrofit to call the API, and have hooked up an interceptor OkHttpClient to debug the response like so:
val client = OkHttpClient.Builder().also { builder ->
builder.addInterceptor { chain ->
val request = chain.request()
val response = chain.proceed(request)
response
}
}.build()
Then setting up Retrofit like so:
Retrofit.Builder()
.baseUrl(TWITTER_AUTH_BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build()
.create(TwitterAuthRetrofit::class.java)
.getRequestToken(
authorizationHeaders
).enqueue(object : Callback<TwitterRequestToken> {
override fun onResponse(call: Call<TwitterRequestToken>, response: Response<TwitterRequestToken>) {
onSuccess(response.body())
}
override fun onFailure(call: Call<TwitterRequestToken>, t: Throwable) {
onFailure()
}
})
When I debug in the interceptor, I can see the response is successful (200) but the response body is empty, which I think is causing my Gson deserialization to fail.
The result of calling response.body.contentLength() in the interceptor is -1.
The result of calling response.code in the interceptor is 200.
Here is the model I am attempting to deserialize the response body to:
data class TwitterRequestToken(
#SerializedName(value = "oauth_token")
val token: String,
#SerializedName(value = "oauth_token_secret")
val tokenSecret: String,
#SerializedName(value = "oauth_callback_confirmed")
val callbackConfirmed: Boolean
)
Note I am using #SerializedName to provide the keys for the response body, whilst the names of my properties are arbitrary to our app (we use camel case). I add a GsonConverterFactory to the Retrofit instance using the builder and have done this in the same way for many other requests before with no issues.
Here is the response I am getting from the API, which I am looking at via debugging in the interceptor above:
Response{protocol=h2, code=200, message=, url=https://api.twitter.com/oauth/request_token}
And here is the cause message from the Throwable I am getting in the onFailure callback from Retrofit:
com.google.gson.stream.MalformedJsonException:
Use JsonReader.setLenient(true) to accept malformed JSON at line 1 column 1 path $
Has anyone got any idea what might cause this?
Finally figured it out, hope this helps someone in future...
The response body from the Twitter API for oauth/request_token isn't encoded as JSON; you will need to read it from the response buffer. Specifically, when implementing the API with Retrofit, you will want your Retrofit interface to return ResponseBody (rather than your custom class), remove GSON from the Retrofit builder and, in the onResponseCallback from Retrofit, write the following code to read the buffer to a string, then split the string on & to get each key val pair, then you can split each of these on = and make sure you have all 3 values before constructing your model:
override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) {
response.body()?.also { body ->
body.source().readString(Charsets.UTF_8).split('&').map { param ->
param.split('=').let { keyVal ->
keyVal[0] to keyVal[1]
}
}.toMap().let { paramMap ->
val oauthToken = paramMap["oauth_token"]
val oauthTokenSecret = paramMap["oauth_token_secret"]
val oauthCallbackConfirmed = paramMap["oauth_callback_confirmed"]?.toBoolean()
if (oauthToken == null || oauthTokenSecret == null || oauthCallbackConfirmed == null) {
onFailure()
} else {
onSuccess(
TwitterRequestToken(
oauthToken,
oauthTokenSecret,
oauthCallbackConfirmed
)
)
}
}
} ?: onFailure()
}

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())

Retrofit upload is storing form data inside the upload file, corrupting it

I am using Retrofit 2 to upload an audio file to an Azure blob storage service via Azure's REST APIs.
The upload appears to work, but the file stored in the Azure blob container is corrupt because as well as the audio data it contains what appears to be HTTP headers. For example, these are the contents of one uploaded file:
--3c88cdb1-5946-432d-a129-cc8e930d014c
Content-Disposition: form-data; name="tape";
filename="/data/user/0/blahblah.mp4"
Content-Type: audio/mp4
Content-Length: 8365
...expected binary data blah blah blah ....
--3c88cdb1-5946-432d-a129-cc8e930d014c--
What am I doing wrong?
My upload function looks like this:
val tapeFile = File(fileName)
val tapePart = tapeFile.asRequestBody("audio/mp4".toMediaType())
val tapeBodyPart = MultipartBody.Part.createFormData("tape",tapeFile.absolutePath, tapePart)
tapeAzureWebService.uploadTape(url, tapeBodyPart).enqueue(object : Callback<ResponseBody> {
override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) {
if (response.isSuccessful) {
etc etc
My Retrofit interface interface is like this:
#Multipart
#PUT
fun uploadTape(#Url url: String,
#Part tape: MultipartBody.Part): Call<ResponseBody>
(It's using #URL because I'm using Azure SAS, with dynamic URLs with authentication embedded in the URL as a series of query strings, and that works very well and is a neat hint for anyone who stumbles on this, by the way, since it prevents Retrofit from encoding the URL and query.)
And my OKHttp Client looks like this, adding some headers that Azure demands:
class TapeAzureWebServiceAPI {
fun service() : TapeAzureWebService {
val headerInterceptor = object: Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val original = chain.request()
val requestBuilder = original.newBuilder()
.header("x-ms-version", "2015-12-11")
.header("x-ms-blob-type","BlockBlob")
val request = requestBuilder.build()
return chain.proceed(request)
}
}
val loggingInterceptor = HttpLoggingInterceptor(object : HttpLoggingInterceptor.Logger {
override fun log(message: String) {
logI("retrofit: $message")
}
}).setLevel(HttpLoggingInterceptor.Level.BODY)
val client : OkHttpClient = OkHttpClient.Builder().apply {
this.addInterceptor(headerInterceptor)
this.addInterceptor(loggingInterceptor)
}.build()
val retrofit = Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(AZURE_URL)
.client(client)
.build()
return retrofit.create(TapeAzureWebService::class.java)
}
}
If I use a simple RequestBody rather than a multipart form, I still get the same corruption to the audio file, though there are fewer headers in the audio file.
I've looked at this a long time, and I can't tell if it's something I'm doing wrong in Retrofit, whether Azure wants different headers, or whether Azure simply doesn't like multipart form data.
thanks
John
remove #Multipart just add,
#Headers( "x-ms-blob-type: BlockBlob", "x-ms-blob-content-type: image/png")
#PUT
suspend fun uploadDocument(#Url url: String, #Body request: RequestBody)
and pass request body as,
val mediaType = "image/png".toMediaTypeOrNull()
val body = yourImageFile.asRequestBody(mediaType)

Why Retrofit doesn't send POST request to the server?

In API declaration I have this code:
#FormUrlEncoded
#POST("loginByPass/")
fun loginByPassword(#Field("login") login: String,
#Field("password") password: String,
#Field("phone") phone: String) : Observable<AuthResponse>
Retrofit object I create this way:
class API {
companion object {
fun getRetrofitAPI() : IAPI {
val interceptor = HttpLoggingInterceptor()
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
val client = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.addInterceptor(interceptor)
.addNetworkInterceptor(interceptor)
.build()
val retrofit = Retrofit.Builder()
.baseUrl(SERVER_ADDRESS)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build()
return retrofit.create(IAPI::class.java)
}
}
}
And request:
api.loginByPassword(login, password, "")
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe({
Log.w("USER_DB", "CODE: " + it.code)
}, {
this.sayError(it.localizedMessage)
}).dispose()
And I have no response at all! In logs it seems like I haven't sent any request. Also I have seen sent packages - from emulator was sent nothing. I'm calling to server IP address by HTTP.
Could you give me piece of advice, what's the problem?
According to #DrawnRaccoon 's answer, .dispose() destroys the request, so I should use CompositeDisposable to collect all requests and dispose them in onDestroy() method.

Categories

Resources