So I'm having an issue that looks that it came straight out from the Twilight Zone.
Problem
I have to hit a REST API endpoint from a backend, the thing is that in order to hit that endpoint I need to go through a VPN. Otherwise the host is not reachable. On desktop everything works fine, I open Postman, hit the GET endpoint and get the response. However when I try to hit the same endpoint through my Android device Retrofit throws an UnknownHostException.
Context
The endpoint url is something like https://api.something.something.net/
. I'm using dependency injection with Dagger, so I've a NetworkModule
that looks like:
...
NetworkModule("https://api.something.something.net/")
...
@Module
class NetworkModule(
private val baseHost: String
) {
...
@Provides
@Named("authInterceptor")
fun providesAuthInterceptor(
@Named("authToken") authToken: String
): Interceptor {
return Interceptor { chain ->
var request = chain.request()
request = request.newBuilder()
.addHeader("Content-Type", "application/json")
.addHeader("Authorization", "Bearer $authToken")
.build()
val response = chain.proceed(request)
}
}
...
@Provides
@Singleton
fun provideOkHttpClient(
curlInterceptor: CurlInterceptor,
@Named("authInterceptor") authInterceptor: Interceptor
): OkHttpClient {
val builder = OkHttpClient.Builder()
builder.addInterceptor(authInterceptor)
builder.addInterceptor(curlInterceptor)
return builder.build()
}
...
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient, gson: Gson): Retrofit {
return Retrofit.Builder()
.baseUrl(baseHost)
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.client(okHttpClient)
.build()
}
}
Then I've a bunch of Repositories which are the ones doing the request through Retrofit:
class MyApiRepositoryImpl(
val myRetrofitApi: MyRetrofitApi,
val uiScheduler: Scheduler,
val backgroundScheduler: Scheduler
) : MyApiRepository {
override fun getSomethingFromTheApi(): Observable<DataResource<List<ApiSomethingResponse>>> {
return myRetrofitApi.getResponseFromEndpoint()
.map {
if (it.isSuccessful) {
DataResource(it.body()?.list!!, ResourceType.NETWORK_SUCCESS)
} else {
throw RuntimeException("Network request failed code ${it.code()}")
}
}
.subscribeOn(backgroundScheduler)
.observeOn(uiScheduler)
.toObservable()
}
}
And this is the Retrofit's API interface:
interface MyRetrofitApi {
@GET("/v1/something/")
fun getResponseFromEndpoint(): Single<Response<ApiSomethingResponse>>
}
So, when I call this Repository method from my Interactor/UseCases it jumps straight through the onError and shows the UnknownHostException.
What I tried so far
- I switched Retrofit by Volley and later by Ion, just to be sure that wasn't something related to the rest client. I got the same exception in all cases:
java.net.UnknownHostException: Unable to resolve host "api.something.something.net": No address associated with hostname
com.android.volley.NoConnectionError: java.net.UnknownHostException: Unable to resolve host "api.something.something.net": No address associated with hostname
I tried every configuration possible with Retrofit and the OkHttpClient:
On OkHttpClient I tried setting the
followSslRedirects
to true and false.followRedirects
to true and false. SethostnameVerifier
to allow any hostname to pass through. Set a SSLSocketFactory to allow any unsigned certificates to pass through.On my Manifest I set my
android:networkSecurityConfig
to:
https://github.com/commonsguy/cwac-netsecurity/issues/5
I tested the App on my Android Device (Android Nougat), on Emulators with Nougat, Marshmellow and Oreo, and a Genymotion emulator with Nougat.
I tried hitting a random public endpoint (
https://jsonplaceholder.typicode.com/todos/1
) and It worked perfectly. So this isn't an issue with the internet connection.I've these three permissions set on my Manifest:
android.permission.INTERNET android.permission.ACCESS_NETWORK_STATE android.permission.ACCESS_WIFI_STATE
It's super weird because I've an Interceptor set to convert all the requests into cURL requests, I copied and pasted the same request that is failing into Postman and works perfectly.
- On my laptop I'm using Cisco AnyConnect, on my Android device I'm using the Cisco AnyConnect App and AFAIK on the emulators and Genymotion it should use the same network than the hosting machine.
There's a couple of websites that are only visible through the VPN and I can see them on the devices and on the emulators. But the endpoint URL is still unreachable from the App.
Couple of weird things
Yes, this gets weirder. If I hit the endpoint through the Chrome browser in my device or in the emulator I got redirected into a login page, that's because I'm not sending the authorization token. Now If I check the network responses through chrome://inspect I can get the IP of the host. Now if I change the base url of the NetworkModule by the IP and I add this line to the authorization Interceptor:
@Provides
@Named("authInterceptor")
fun providesAuthInterceptor(
@Named("authToken") authToken: String
): Interceptor {
return Interceptor { chain ->
var request = chain.request()
request = request.newBuilder()
.addHeader("Content-Type", "application/json")
.addHeader("Authorization", "Bearer $authToken")
.build()
val response = chain.proceed(request)
response.newBuilder().code(HttpURLConnection.HTTP_SEE_OTHER).build() // <--- This one
}
}
Then I start getting:
11-13 13:56:01.084 4867-4867/com.something.myapp D/GetSomethingUseCase: javax.net.ssl.SSLPeerUnverifiedException: Hostname <BASE IP> not verified:
certificate: sha256/someShaString
DN: CN=something.server.net,OU=Title,O=Company Something\, LLC,L=Some City,ST=SomeState,C=SomeCountryCode
subjectAltNames: [someServer1.com, someServer2.com, someServer3.com, someServer4.com, andSoOn.com]
I'm not sure if this is unrelated or if it's actually one step forward to fix it.
Any tip or advice is appreciated.
Thanks,