square / retrofit

A type-safe HTTP client for Android and the JVM
https://square.github.io/retrofit/
Apache License 2.0
42.96k stars 7.3k forks source link

Retrofit2 + OkHttp with RxJava: onNext() callback not being triggered when connected to a network that has no internet #3542

Open ArcherEmiya05 opened 3 years ago

ArcherEmiya05 commented 3 years ago

Whenever there is an error like network related errors, we will use the last cached data from the client instead. It works but only when not connected to any network. If a client is connected to any network where internet service is not really available the callbacks is no longer working.

So far I do not know which side this error came from base on this 3 libraries.

Here we are using YouTube API as a sample.

interface EndpointServices {

    companion object {

        private fun interceptor(): Interceptor {
            return Interceptor { chain ->
                val request: Request = chain.request()
                val originalResponse: Response = chain.proceed(request)
                val cacheControlStatus: String? = originalResponse.header("Cache-Control")
                if (cacheControlStatus == null || cacheControlStatus.contains("no-store") || cacheControlStatus.contains(
                        "no-cache") ||
                    cacheControlStatus.contains("must-revalidate") || cacheControlStatus.contains("max-stale=0")
                ) {

                    Log.wtf("INTERCEPT", "ORIGINAL CACHE-CONTROL: $cacheControlStatus")

                } else {

                    Log.wtf("INTERCEPT",
                        "ORIGINAL : CACHE-CONTROL: $cacheControlStatus")

                }

                Log.wtf("INTERCEPT",
                    "OVERWRITE CACHE-CONTROL: ${request.cacheControl} | CACHEABLE? ${
                        CacheStrategy.isCacheable(originalResponse,
                            request)
                    }")

                originalResponse.newBuilder()
                    .build()

            }
        }

        private fun onlineOfflineHandling(): Interceptor {
        return Interceptor { chain ->
            try {
                Log.wtf("INTERCEPT", "FETCH ONLINE")
                val cacheControl = CacheControl.Builder()
                    .maxAge(5, TimeUnit.SECONDS)
                    .build()

                val response = chain.proceed(chain.request().newBuilder()
                    .removeHeader("Pragma")
                    .removeHeader("Cache-Control")
                    .header("Cache-Control", "public, $cacheControl")
                    .build())

                Log.wtf("INTERCEPT", "CACHE ${response.cacheResponse} NETWORK ${response.networkResponse}")

                response
            } catch (e: Exception) {
                Log.wtf("INTERCEPT", "FALLBACK TO CACHE ${e.message}")

                val cacheControl: CacheControl = CacheControl.Builder()
                    .maxStale(1, TimeUnit.DAYS)
                    .onlyIfCached() // Use Cache if available
                    .build()

                val offlineRequest: Request = chain.request().newBuilder()
                    .cacheControl(cacheControl)
                    .build()

                val response = chain.proceed(offlineRequest)

                Log.wtf("INTERCEPT", "CACHE ${response.cacheResponse} NETWORK ${response.networkResponse}")

                response
            }
        }
    }

        fun create(baseUrl: String, context: Context): EndpointServices {

            // Inexact 150 MB of maximum cache size for a total of 4000 assets where about 1MB/30 assets
            // The remaining available space will be use for other cacheable requests
            val cacheSize: Long = 150 * 1024 * 1024

            val cache = Cache(context.cacheDir, cacheSize)

            Log.wtf("CACHE DIRECTORY", cache.directory.absolutePath)

            for (cacheUrl in cache.urls())
                Log.wtf("CACHE URLS", cacheUrl)

            Log.wtf("CACHE OCCUPIED/TOTAL SIZE", "${cache.size()} ${cache.maxSize()}")

            val interceptor = HttpLoggingInterceptor()
            interceptor.level = HttpLoggingInterceptor.Level.BODY

            val httpClient = OkHttpClient.Builder()
                .cache(cache)
                .addInterceptor(interceptor)
                .callTimeout(10, TimeUnit.SECONDS)
                .connectTimeout(10, TimeUnit.SECONDS)
                .addNetworkInterceptor(interceptor())
                .addInterceptor(onlineOfflineHandling())
                .build()

            val retrofit = Retrofit.Builder()
                .addCallAdapterFactory(
                    RxJava2CallAdapterFactory.create()
                )
                .addConverterFactory(
                    MoshiConverterFactory.create()
                )
                .client(httpClient)
                .baseUrl(baseUrl)
                .build()

            return retrofit.create(EndpointServices::class.java)

        }

    }

    @GET("search")
    fun getVideoItems(
        @Query("key") key: String,
        @Query("part") part: String,
        @Query("maxResults") maxResults: String,
        @Query("order") order: String,
        @Query("type") type: String,
        @Query("channelId") channelId: String,
    ):
            Single<VideoItemModel>

}

MainActivity

EndpointServices.create(url, requireContext()).getVideoItems(
            AppUtils.videoKey,
            "id,snippet",
            "20",
            "date",
            "video",
            channelId
        )
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(
                { result ->

                    Log.wtf("RESPONSE", result.toString())
                    adapter.submitList(result.videoData)

                    swipeRefreshLayout.isRefreshing = false

                    logTxt.text = null

                },
                { error ->
                    Log.wtf("WTF", "${error.message}")
                    swipeRefreshLayout.isRefreshing = false
                    if (adapter.currentList.isEmpty() || (error is HttpException && error.code() == HttpURLConnection.HTTP_GATEWAY_TIMEOUT)){
                        adapter.submitList(mutableListOf())
                        logTxt.text = getString(R.string.swipeToRefresh)
                    }
                }
            )

FLOW BASED ON LOGS

WHEN ONLINE

A/CACHE DIRECTORY: /data/data/com.appname.app/cache
A/CACHE URLS: www.api.com
A/CACHE OCCUPIED/TOTAL SIZE: 228982 157286400
A/INTERCEPT: FETCH ONLINE
A/INTERCEPT: ORIGINAL : CACHE-CONTROL: private
A/INTERCEPT: OVERWRITE CACHE-CONTROL: public, max-age=5 | CACHEABLE? true
A/INTERCEPT: CACHE Response{protocol=http/1.1, code=200, message=, url=https://api.com} NETWORK Response{protocol=h2, code=304, message=, url=https://api.com}
A/RESPONSE: VideoItemModel(.....) WORKING!

COMPLETELY OFFLINE (Wi-Fi/Mobile Data OFF)

A/CACHE DIRECTORY: /data/data/com.appname.app/cache
A/CACHE URLS: www.api.com
A/CACHE OCCUPIED/TOTAL SIZE: 228982 157286400
A/INTERCEPT: FETCH ONLINE
A/INTERCEPT: FALLBACK TO CACHE Unable to resolve host "api.com": No address associated with hostname
A/INTERCEPT: CACHE Response{protocol=http/1.1, code=200, message=, url=https://api.com} NETWORK null
A/RESPONSE: VideoItemModel(.....) WORKING!

JUST CONNECTED TO A NETWORK BUT REALLY NO INTERNET SERVICE (Wi-Fi/Mobile Data ON)

A/CACHE DIRECTORY: /data/data/com.appname.app/cache
A/CACHE URLS: www.api.com
A/CACHE OCCUPIED/TOTAL SIZE: 228982 157286400
A/INTERCEPT: FETCH ONLINE
A/INTERCEPT: FALLBACK TO CACHE Unable to resolve host "api.com": No address associated with hostname
???WHERE IS THE CALLBACK JUST LIKE THE PREVIOUS ONE???

Also worth mentioning that neither of the line Log.wtf("INTERCEPT", "CACHE ${response.cacheResponse} NETWORK ${response.networkResponse}") is being called on this last scenario.

Dependencies

    implementation 'com.squareup.moshi:moshi-kotlin:1.11.0'
    implementation 'com.squareup.retrofit2:adapter-rxjava2:2.3.0'
    implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'

    implementation 'com.squareup.okhttp3:logging-interceptor:4.9.0'

    implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
ArcherEmiya05 commented 3 years ago

For SO link here it is. Thank you!