Multiple Base Url handling in one Ktor Client Instance - android

Hello i want to use Multiple Base urls one ktor client instance . i was able to acheive it in Retrofit 2 . but stuck in Ktor 2.1.0
IN retrofit here is my interceptor :
#Singleton
#Provides
#Named("baseurl")
fun provideChangeBaseUrlInterceptor(controller: CLibController) = Interceptor { chain ->
var host = controller.getENTBaseUrlOnline().toHttpUrl()
var request: Request = chain.request()
when {
request.getAnnotation(HomeApi::class.java) == HomeApi() -> {
host = controller.getENTBaseUrlOnline().toHttpUrl()
}
request.getAnnotation(OffersApi::class.java) == OffersApi() -> {
host = controller.getOutletBaseUrlOnline().toHttpUrl()
}
request.getAnnotation(FavApi::class.java) == FavApi() -> {
host = controller.getOutletBaseUrlOnline().toHttpUrl()
}
request.getAnnotation(ProfileApi::class.java) == ProfileApi() -> {
host = controller.getAuthBaseUrlOnline().toHttpUrl()
}
}
var newUrl: HttpUrl? = null
try {
newUrl = request.url.newBuilder().scheme(host.scheme).host(host.toUrl().toURI().host)
.build()
} catch (e: URISyntaxException) {
e.printStackTrace()
}
assert(newUrl != null)
request = request.newBuilder().url(newUrl!!).build()
chain.proceed(request)
}
i have 7 base urls (Microservices backend) and each base url is accosicate with specific endpoints,SO i will acheive it with one instance of retrofit by categorize them with a custom annotations On my Api Class.
and get the Annotations in interceptor like the code above.Once i get the annotations of a request in interceptor i will changes it url to my desire url
Can any one help me achive this type of custom logic with ktor cleint (Single Client for multiple base urls )

Okay i was finally able to make multiple base urls handled with one client instance . just like custom annotations ktor client gives us attributes to send custom data in any request .
here is the ktor client code :
#Provides
#Singleton
fun provideKtor(jwtToken: String, cLibController: CLibController):
HttpClient =
HttpClient(Android) {
defaultRequest {
url {
contentType(ContentType.Application.Json)
accept(ContentType.Application.Json)
protocol = URLProtocol.HTTPS
host = cLibController.getENTBaseUrlOnline()
}
}
install(Logging) { level = LogLevel.ALL }
install(ContentNegotiation) { gson { } }
install(Auth) {
bearer {
loadTokens {
BearerTokens(jwtToken, jwtToken)
}
}
}
}.apply {
plugin(HttpSend).intercept { request ->
when (**request.attributes[MyAttributeKey]**) { // this is the key
"homeApi" -> request.url.host = cLibController.getENTBaseUrlOnline()
"profileApi" -> request.url.host = cLibController.getAuthBaseUrlOnline()
}
execute(request)
}
}
and setting my attribute key in each api class like :
client.post {
attributes.put(MyAttributeKey, "profileApi")
url {...}
}

Related

How to regenerate token in Android GraphQL?

What is the better approach to regenerate the JWT token using refresh token in Apollo Graphql in Andriod Kotlin?.
Now we are using an Auth interceptor that extends ApolloInterceptor to attach the JWT token in the request header.
I want to check whether the Toke expires from here and it should call another mutation to generate a new token. Then it should proceed with the previous call with the new token.
Please refer to the code below
class AuthInterceptor(private val jwtToken: String) : ApolloInterceptor {
override fun interceptAsync(
request: ApolloInterceptor.InterceptorRequest,
chain: ApolloInterceptorChain,
dispatcher: Executor,
callBack: ApolloInterceptor.CallBack
) {
val header = request.requestHeaders.toBuilder().addHeader(
HEADER_AUTHORIZATION,
"$HEADER_AUTHORIZATION_BEARER $jwtToken"
).build()
chain.proceedAsync(request.toBuilder().requestHeaders(header).build(), dispatcher, callBack)
}
override fun dispose() {}
companion object {
private const val HEADER_AUTHORIZATION = "Authorization"
private const val HEADER_AUTHORIZATION_BEARER = "Bearer"
}
}
If you are using Apollo Android Client V3 and Using Kotlin Coroutine then use Apollo Runtime Dependency and Try HttpInterceptor instead of ApolloInterceptor. I think this is the better/best approach. For Reference Click Here
In your app-level build.gradle file
plugins {
......
id("com.apollographql.apollo3").version("3.5.0")
.....
}
dependencies {
implementation("com.apollographql.apollo3:apollo-runtime")
}
Now write your interceptor for the Apollo client.
FYI: If you've added the Authorization header using Interceptor or using addHttpHeader in client already then remove it or don't add header here val response = chain.proceed(request.newBuilder().addHeader("Authorization", "Bearer $token").build()), just build the request. Otherwise Authorization header will add multiple times in the request. So, be careful.
class AuthorizationInterceptor #Inject constructor(
val tokenRepo: YourTokenRepo
) : HttpInterceptor {
private val mutex = Mutex()
override suspend fun intercept(request: HttpRequest, chain: HttpInterceptorChain): HttpResponse {
var token = mutex.withLock {
// get current token
}
val response = chain.proceed(request.newBuilder().addHeader("Authorization", "Bearer $token").build())
return if (response.statusCode == 401) {
token = mutex.withLock {
// get new token from your refresh token API
}
chain.proceed(request.newBuilder().addHeader("Authorization", "Bearer $token").build())
} else {
response
}
}
}
Configure your Apollo client like below.
ApolloClient.Builder()
.httpServerUrl(BASE_GRAPHQL_URL)
.webSocketServerUrl(BASE_GRAPHQL_WEBSOCKET_ENDPOINT) // if needed
.addHttpHeader("Accept", "application/json")
.addHttpHeader("Content-Type", "application/json")
.addHttpHeader("User-Agent", userAgent)
.addHttpInterceptor(AuthorizationInterceptor(YourTokenRepo))
.httpExposeErrorBody(true)
.build()

How to dynamically set list of headers in ktor?

#Dao
interface TokenDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertToken(token: TokenEntity): Long
#Query("SELECT * FROM token WHERE id = :id")
suspend fun getTokenInfo(id: String): TokenEntity
}
#Module
#InstallIn(SingletonComponent::class)
object NetworkModule {
private val json = Json {
encodeDefaults = true
ignoreUnknownKeys = true
}
#OptIn(InternalAPI::class)
#Singleton
#Provides
fun provideKtorHttpClient(
tokenRepository: TokenRepository,
tokenDao: TokenDao
): HttpClient {
return HttpClient(OkHttp) {
//Json Setting
install(JsonFeature) {
serializer = KotlinxSerializer(json = json)
}
//Logging Setting
install(Logging) {
logger = object : Logger {
override fun log(message: String) {
Timber.d("api log: $message")
}
}
logger = Logger.DEFAULT
level = LogLevel.ALL
}
//Timeout Setting
install(HttpTimeout) {
requestTimeoutMillis = 15_000
connectTimeoutMillis = 15_000
socketTimeoutMillis = 15_000
}
//Default Request Setting
defaultRequest {
contentType(ContentType.Application.Json)
accept(ContentType.Application.Json)
headers {
append("Accept-Version", "v1")
CoroutineScope(Main).launch {
val authToken = tokenDao.getTokenInfo("auth_token")
authToken?.let {
append(HttpHeaders.Authorization, authToken.token)
}
}
}
url {
protocol = URLProtocol.HTTP
host = Constants.BASE_URL
}
}
}
}
When sending a request to the backend server,
if there is a value stored in the room, put it in the authorization of the header and
try not to send it if there is no value.
Therefore, if I call the value from the room with CorutineScope, and if value is empty,
just skip
But although there is a value, the header does not include the value.
When using Ktor, how should I implement the part where I put autorization depending on the presence or absence of token in the value of the header?
Ktor is not an opinionated web client.
Dynamically setting the header on the client builder would work as well as having two instances of the client, one with authentication and the other without it.
From the code you've pasted I believe the less complex thing may be to always provide an unauthenticated client and set the header as soon as the request needs to go off but it's really up to you

OkHttpClient injected with Koin loses Authorization header

I'm adding DI to the existing project, in process I faced problem that header Authorization disappears from request. There is no any exceptions or logs from Retrofit/OkHttp. My dependencies are:
implementation 'com.squareup.retrofit2:retrofit:2.6.0'
implementation 'com.squareup.okhttp:okhttp:2.7.5'
implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0'
implementation 'org.koin:koin-android:2.1.3'
I create http client using provideClient:
class OkHttpProvider private constructor() {
companion object {
fun provideClient(credentials: UsernamePasswordCredentials? = null, context: Context): OkHttpClient {
val client = OkHttpClient.Builder()
// logs
if (BuildConfig.DEBUG) {
client.addInterceptor(
HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)
)
}
if (credentials != null) {
val creds = Credentials.basic(credentials.userName, credentials.password)
val headerInterceptor = Interceptor { chain ->
var request = chain.request()
val headers = request
.headers()
.newBuilder()
.add("Authorization", creds)
.build()
request = request.newBuilder().headers(headers).build()
chain.proceed(request)
}
//client.addInterceptor(AccessTokenInterceptor(credentials))
client.addInterceptor(headerInterceptor)
}
client
.callTimeout(60L, TimeUnit.SECONDS)
.connectTimeout(10L, TimeUnit.SECONDS)
.readTimeout(60L, TimeUnit.SECONDS)
.writeTimeout(60L, TimeUnit.SECONDS)
.sslSocketFactory(getSslContext().socketFactory).hostnameVerifier { _, _ -> true }
client.addInterceptor(ChuckInterceptor(context))
return client.build()
}
private fun getSslContext(): SSLContext {
...implementation...
}
}
}
My modules for http client and Retrofit are below:
object HttpClientModule {
val module = module {
single(named(COMMON)) {
OkHttpProvider.provideClient(
get<SharedPreferenceManager>().getUserCredentials(),
androidContext()
)
}
...other versions...
}
const val COMMON = "common"
}
object ApiModule {
val module = module {
single {
RetrofitFactory.getServiceInstance(
ApiService::class.java,
get<SharedPreferenceManager>().getString(LocalDataSource.BUILD_OPTION_API, ""),
get(named(HttpClientModule.COMMON))
)
}
...other apis...
}
}
object RetrofitFactory {
const val GEO_URL = "http://maps.googleapis.com/maps/api/"
fun <T> getServiceInstance(
clazz: Class<T>,
url: String = GEO_URL,
client: OkHttpClient
): T = getRetrofitInstance(url, client).create(clazz)
private fun getRetrofitInstance(
url: String,
client: OkHttpClient
) = Retrofit.Builder()
.baseUrl(url)
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.build()
}
App starts to work with "admin" user and has some credentials saved in shared preferences, when user starts login with phone and sms and requests are sent with "admin" Authorization header, when user inputs code from sms and his new user credentials are saved in shared preferences. After that app sends two requests and Authorization header isn't presented in them. I saw it in Chuck, I even rechecked it using Charles.
To fix this problem I tried few solutions. Firstly, I changed inject for http client from single to factory, that didn't work. Secondly, I googled the problem, but I didn't mentions of this phenomenon. Thirdly, I wrote AccessTokenInterceptor according to this article and also cover everything with logs. I noticed that interceptor works fine in normal cases, but when Authorization header is missing method intercept is not called. This might be reason why default headerInterceptor also not working. Fourthly, I upgraded versions of Retrofit and OkHttp, this also didn't helped.
I noticed interesting thing about that bug: if I restart app after Retrofit lost Authorization header, app works fine test user is properly logged with correct token. Any attempts to relog without restarting the app fails. Maybe someone had similar problem or knows what is happening here, any ideas are welcomed.
I finally find solution to this problem. The problem was user credentials was passed to provideClient only once, when it's created. At that moment user was logged as admin, and standard user credentials was empty, so http client for ApiService was created without Authorization header.
To solve this I changed AccessTokenInterceptor form article (HttpClientType is a enum to select which credentials need to use):
class AccessTokenInterceptor(
private val sharedPreferenceManager: SharedPreferenceManager,
private val clientType: OkHttpProvider.HttpClientType
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val credentials = getUserCredentials(clientType)
if (credentials != null) {
val accessToken = Credentials.basic(credentials.userName, credentials.password)
val request = newRequestWithAccessToken(chain.request(), accessToken)
return chain.proceed(request)
} else {
return chain.proceed(chain.request())
}
}
private fun getUserCredentials(clientType: OkHttpProvider.HttpClientType): UsernamePasswordCredentials? {
return when (clientType) {
OkHttpProvider.HttpClientType.COMMON -> sharedPreferenceManager.getUserCredentials()
OkHttpProvider.HttpClientType.ADMIN -> ServiceCredentialsUtils.getCredentials(sharedPreferenceManager)
}
}
private fun newRequestWithAccessToken(#NonNull request: Request, #NonNull accessToken: String): Request {
return if (request.header("Authorization") == null) {
request.newBuilder()
.header("Authorization", accessToken)
.build()
} else {
request
}
}
}
Now each time request is sending, Interceptor gets user's credentials and adds header to request.

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.

Singleton that will hold an instance of Retrofit

So I have created a login that will take an username and password input from the user, encode it with Base64 in order to create a token in the format: ("Authorization", AUTH) where AUTH = "Basic " + Base64 encoding of user and password. This is sent via Headers.
So, in the end, it looks like this: Authorization: Basic XXXXXX, where XXXXXX is the user token.
And then it will check whether or not that user exists in the database via an API request.
I am using Retrofit and OkHttp3 in the same class as RetrofitClient and this class is responsible for using the API and adding those Headers.
Later, I use the RetrofitClient class on the Login Activity.
What I need to do now, is make this "token" available to all the other activities by creating a Singleton that will store the data of the Retrofit after a successful login. But I do not know how to do this.
I started learning Kotlin and Android 3 weeks ago.
Here is my code:
GET_LOGIN.kt
interface GET_LOGIN {
#GET("login")
fun getAccessToken() : Call<String>
}
RetrofitClient.kt
class RetrofitClient {
fun login(username:String, password:String){
val credentials = username + ":" + password
val AUTH = "Basic " + Base64.encodeToString(credentials.toByteArray(Charsets.UTF_8), Base64.DEFAULT).trim()
retrofit = init(AUTH)
}
// Initializing Retrofit
fun init(AUTH: String) : Retrofit{
// Creating the instance of an Interceptor
val logging = HttpLoggingInterceptor()
logging.level = HttpLoggingInterceptor.Level.BODY
// Creating the OkHttp Builder
val client = OkHttpClient().newBuilder()
// Creating the custom Interceptor with Headers
val interceptor = Interceptor { chain ->
val request = chain?.request()?.newBuilder()?.addHeader("Authorization", AUTH)?.build()
chain?.proceed(request)
}
client.addInterceptor(interceptor) // Attaching the Interceptor
//client.addInterceptor(logging) // Attaching the Interceptor
// Creating the instance of a Builder
val retrofit = Retrofit.Builder()
.baseUrl("https://srodki.herokuapp.com/") // The API server
.client(client.build()) // Adding Http Client
.addConverterFactory(GsonConverterFactory.create()) // Object Converter
.build()
return retrofit
}
lateinit var retrofit : Retrofit
fun providesGetLogin(): GET_LOGIN = retrofit.create(GET_LOGIN::class.java)
}
LoginActivity.kt
var RetrofitClient : RetrofitClient = RetrofitClient()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
loginBtn.setOnClickListener {
val user = userTxt.text.toString()
val pass = passTxt.text.toString()
if (validateLogin(user, pass)){
login(user, pass)
}
}
}
fun validateLogin(user: String, pass: String): Boolean {
if (user == null || user.trim().isEmpty()){
Toast.makeText(this, "Missing Username or Password", Toast.LENGTH_SHORT).show()
return false
}
if (pass == null || pass.trim().isEmpty()){
Toast.makeText(this, "Missing Username or Password", Toast.LENGTH_SHORT).show()
return false
}
return true
}
fun login(user: String, pass: String) {
RetrofitClient.login(user, pass)
val apiLogin = RetrofitClient.providesGetLogin().getAccessToken()
apiLogin.enqueue(object : Callback<LoginResponse> {
override fun onResponse(call: Call<LoginResponse>, response: Response<LoginResponse>) {
if(response.isSuccessful){
if(response.body()?.code == 0){
Toast.makeText(this#LoginActivity, "Login Successful!", Toast.LENGTH_SHORT).show()
val intent = Intent(this#LoginActivity, List_usersActivity::class.java)
startActivity(intent)
} else {
Toast.makeText(this#LoginActivity, "Login Failed.", Toast.LENGTH_SHORT).show()
}
}
}
override fun onFailure(call: Call<LoginResponse>, t: Throwable) {
Toast.makeText(this#LoginActivity, "Login Failed.", Toast.LENGTH_SHORT).show()
}
})
}
}
first and foremost, please use camel case on java and kotlin. We have standards in java and kotlin on programming. And i can see that you are trying to do DI, but, thats not how you do it in Android.
Anyways, you could do this a couple of ways without even using a singleton but by saving it on a storage. Options are Shared Preferences, Local Storage and SQLite. But, if you insist on using a singleton. You can do it like this:
object MySingleton { // This is how you declare singletons in kotlin
lateinit var token: String;
}
EDIT
So, from your comment, it looked like you need to store the token. You could start by using sharedpreferences(database would be better) and store the token there. I assume you don't know how to so here is an example:
val sp = SharedPreferences("sp", 0);
sp.edit().putString("token", theTokenVariable); // not sure of this function
sp.edit().apply(); // you could use commit if you dont mind sharedpreferences to lag your screen(if it ever will)
Now how do you get the token from retrofit? The only way i could help you right now is that you could retrieve the response body from the response variable you receive from onResponse of the retrofit call. From there it is your problem mate. I don't know how your response is formatted, how it should be retrieved etc. A recommendation would be to format it as JSON.

Categories

Resources