square / okhttp

Square’s meticulous HTTP client for the JVM, Android, and GraalVM.
https://square.github.io/okhttp/
Apache License 2.0
45.93k stars 9.17k forks source link

Cache did not return a callback when connected to network but no internet - Retrofit2 + RxJava with OkHttp #6626

Closed ArcherEmiya05 closed 3 years ago

ArcherEmiya05 commented 3 years ago

Cache data only works when the device network such as Mobile Data or Wi-Fi is off, if either of the two is on but no internet service / no data connection the response/result callback is not being trigger. Both of the said scenario throws an error Unable to resolve host www.xyz.com No address associated with hostname yet the result callback only works when a device is Mobile Data/Wi-Fi is completely off. I need to use the cache as well for the said second case where after throwing an error on resolving host due to the fact that internet service is not available yet or too slow to respond.

luis33338 commented 3 years ago

Baja

swankjesse commented 3 years ago

Executable test case? We have lots of tests that you can use as a starting point.

ArcherEmiya05 commented 3 years ago

Executable test case? We have lots of tests that you can use as a starting point.

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

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

                } else {
                    Log.wtf("INTERCEPT",
                        "REGULAR REQUEST : CACHE-CONTROL: $cacheControlStatus | 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()

                    chain.proceed(chain.request().newBuilder()
                        .removeHeader("Pragma")
                        .removeHeader("Cache-Control")
                        .header("Cache-Control", "public, $cacheControl")
                        .build())
                } 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()
                    chain.proceed(offlineRequest)
                }
            }
        }

        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>

}

Activity or Fragment class

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

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

I tried with YouTube API, cache is used when completely offline otherwise will throw the same error but no callbacks on both result and error inside subscribe.

Logs

FETCH ONLINE FALLBACK TO CACHE Unable to resolve host "xyz.com": No address associated with hostname No callbacks happened afterwards, unlike where Wifi is off

Let's continue on stackoverflow, it's really awkward to split the conversation. And this issue tracker is for reproducible bug reports.

https://stackoverflow.com/questions/67002032/retrofit2-and-okhttp3-use-cache-only-when-error-occur-such-as-network-errors-or

Oh no why prematurely closing, I already solved that one but I think this is different.

yschimke commented 3 years ago

Let's continue on stackoverflow, it's really awkward to split the conversation. And this issue tracker is for reproducible bug reports.

https://stackoverflow.com/questions/67002032/retrofit2-and-okhttp3-use-cache-only-when-error-occur-such-as-network-errors-or

yschimke commented 3 years ago

If you do want to reopen a reproducible bug report, have a read of a site like http://sscce.org to understand what would help us reproduce, fix and ensure the bug doesn't happen again. Also it's unclear if the problem is due to a server response or specific to your app and is just a usage question.

Above is not what we would consider a reproducible bug report as it requires us to spend a lot of time setting up a test and making assumptions also.

ArcherEmiya05 commented 3 years ago

If you do want to reopen a reproducible bug report, have a read of a site like http://sscce.org to understand what would help us reproduce, fix and ensure the bug doesn't happen again. Also it's unclear if the problem is due to a server response or specific to your app and is just a usage question.

Above is not what we would consider a reproducible bug report as it requires us to spend a lot of time setting up a test and making assumptions also.

Why would it be a server response issue? The said scenarios had no internet connection at all.

ArcherEmiya05 commented 3 years ago

Can someone at least explain what could be the reason that no callbacks is being made when connected to a network but having no internet service? While completely turning off all network connectivity will still give you a callback?

yschimke commented 3 years ago

Is this the same discussion as in stackoverflow? Can we continue it there, it's really awkward to have two forums for the same report.

yschimke commented 3 years ago

For context Jesse and myself are both reasonably active on stackoverflow, so having to respond to everything twice isn't a great experience for us, and we'd like to keep the issue tracker for reproducible issues.

ArcherEmiya05 commented 3 years ago

Is this the same discussion as in stackoverflow? Can we continue it there, it's really awkward to have two forums for the same report.

Not really since all I need now is the callback in a case where a device is connected to a network that do not serve with internet or data. I also received zero answers every time I asked about Square libraries like with Retrofit2 even though the are suggesting to use SO instead of reporting thus it took days before solving it.

yschimke commented 3 years ago

This is not a support forum, stackoverflow is. I'll take a another look there in next hour or so, but I want to discourage general usage questions here in github unless they are in the form of a reproducible bug report. Is there a second question on stackoverflow I'm missing?

ArcherEmiya05 commented 3 years ago

This is not a support forum, stackoverflow is. I'll take a another look there in next hour or so, but I want to discourage general usage questions here in github unless they are in the form of a reproducible bug report. Is there a second question on stackoverflow I'm missing?

Thanks, I will put the progress update on the same question.

yschimke commented 3 years ago

Thank you, I really honestly appreciate you switching the conversation back to stackoverflow. Other projects might work differently, but given a small handful of core maintainers, we can't scale our free time to support all users of OkHttp via the bug tracker. And we spend a large majority of our efforts trying to make the core library easier to use, have good documentation and debugging tools. Using stackoverflow for that helps us answer valid questions like yours once in a public forum.

ArcherEmiya05 commented 3 years ago

Thank you, I really honestly appreciate you switching the conversation back to stackoverflow. Other projects might work differently, but given a small handful of core maintainers, we can't scale our free time to support all users of OkHttp via the bug tracker. And we spend a large majority of our efforts trying to make the core library easier to use, have good documentation and debugging tools. Using stackoverflow for that helps us answer valid questions like yours once in a public forum.

Yes really appreciate the hard work behind the scene on all of these wonderful library. Thanks once again for looking on us

ArcherEmiya05 commented 3 years ago

@swankjesse @swankjesse the SO question now updated. Thank you