I want to cache the response for 2 hours and hence, tried adding cache to my okhttp client, every time hitting API with the network it works fine but next time, enabling airplane mode, we try fetching data in Offline mode, it gives retrofit2.HttpException: HTTP 504 Unsatisfiable Request (only-if-cached)
I have tried every solution related to this exception posted here on SO. Please help me find out what am I doing wrong...been stuck at this for really long.
interface TrendingRepoService {
#GET("/repositories?language=&since=daily")
suspend fun getTrendingRepos() : List<Repo>
}
object RetrofitClient {
private fun getClient(context: Context): Retrofit {
val cacheSize = (5 * 1024 * 1024).toLong() //cache size is 5mb
val myCache = Cache(File(context.cacheDir, "responses"), cacheSize)
val okHttpClient = OkHttpClient.Builder()
.cache(myCache)
.addInterceptor { chain ->
var request = chain.request()
if (hasNetwork(context)!!) {
request.newBuilder().removeHeader("Pragma")
.addHeader("Cache-Control", "public, max-age=" + 5)
} else {
request = request.newBuilder()
.removeHeader("Pragma")
.addHeader("Cache-Control", "public, only-if-cached, max-stale=" + 60 * 60 * 2) //cache should expire after 2 hours
.build()
}
println("network: " + chain.proceed(request).networkResponse())
println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" + myCache.directory().lastModified())
println("cache: " + chain.proceed(request).cacheResponse())
chain.proceed(request)
}
.build()
return Retrofit.Builder()
.client(okHttpClient)
.baseUrl("https://github-trending-api.now.sh")
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().create()))
.build()
}
fun getTrendingService(context: Context): TrendingRepoService {
return getClient(context).create(TrendingRepoService::class.java)
}
}
I can see the following files in my app cache folder
The contents of file stored .0.tmp ending file in cache
https://github-trending-api.now.sh/repositories?language=&since=daily
GET
0
HTTP/1.1 200
14
date: Mon, 04 Nov 2019 19:27:43 GMT
content-type: application/json; charset=utf-8
x-powered-by: Express
access-control-allow-origin: *
etag: W/"4fcb-tcvKxcVttStpWpvGjIHTyk/TMbU"
cache-control: max-age=120
x-now-trace: hel1,bru1,sfo1
x-now-id: hel1:rv96m-1572895662521-476640337170
strict-transport-security: max-age=63072000
x-now-instance: 4008292682
content-encoding: gzip
server: now
OkHttp-Sent-Millis: 1572895661313
OkHttp-Received-Millis: 1572895662894
TLS_AES_256_GCM_SHA384
2
MIIFTzCCBDegAwIBAgISA4Bu+MYBP......
0
TLSv1.3
How max-age is 120?? Every time I am getting 504 error when I want to fetch data with cache.
When I hit API with no network only journal file exists in my cache folder the other two files are not there the next time.
Related
I want to connect to the Skyscanner API, using Kotlin and Retrofit. https://rapidapi.com/skyscanner/api/skyscanner-flight-search
When attempting to POST the 'create session' call, I get a 500 error, but the logs aren't giving a specific reason. I can only assume that my post data isn't being formatted correctly, but I'm using Retrofit with GSon to handle this for me.
One clue, is that in their Java sample code, they pass the form data in the following format: "inboundDate=2019-09-10&children=0&adults=1" whereas after GSon convertion from my sessionObject class, my data is in the format {"adults":1,"country":"GB","outboundDate":"2020-01-06"} - I'm unsure how, using Retrofit, I can pass my data in that format, and whether that's the issue causing the 500.
Here are some code snippets:
// my object for posting data
class SessionBody {
#SerializedName("country")
var country: String = ""
#SerializedName("currency")
var currency: String = ""
...etc...
// my interface
#Headers("Content-Type: application/x-www-form-urlencoded")
#POST("pricing/v1.0/")
fun postUser(#Body sessionBody: SessionBody): Call<Void>
// my connector class
val interceptor = HttpLoggingInterceptor()
interceptor.level = HttpLoggingInterceptor.Level.BODY
val client = OkHttpClient.Builder()
.addInterceptor(Interceptor { chain ->
val original = chain.request()
val request = original.newBuilder()
.header("X-RapidAPI-Host", "skyscanner-skyscanner-flight-search- v1.p.rapidapi.com")
.header("X-RapidAPI-Key", "...my key here...")
.method(original.method(), original.body())
.build()
return#Interceptor chain.proceed(request)
})
.addInterceptor(interceptor)
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().create()))
.client(client)
.build()
val api = retrofit.create(TravelEzyApi::class.java)
val call = api.postUser(sessionBody)
And here is the output from the logs...
D/OkHttp: --> POST https://skyscanner-skyscanner-flight-search-v1.p.rapidapi.com/apiservices/pricing/v1.0/
D/OkHttp: Content-Type: application/x-www-form-urlencoded
Content-Length: 142
X-RapidAPI-Host: skyscanner-skyscanner-flight-search-v1.p.rapidapi.com
X-RapidAPI-Key: ... my key here ...
{"adults":1,"country":"GB","currency":"GBP","destinationPlace":"BKK-sky","locale":"en-GB","originPlace":"LHR-sky","outboundDate":"2020-01-06"}
--> END POST (142-byte body)
D/OkHttp: <-- 500 Internal Server Error
D/OkHttp: Cache-Control: private
Content-Type: application/json
Date: Tue, 22 Oct 2019 10:48:44 GMT
Server: RapidAPI-1.0.32
X-RapidAPI-Region: AWS - eu-west-1
X-RapidAPI-Version: 1.0.32
Content-Length: 2
Connection: keep-alive
{}
D/OkHttp: <-- END HTTP (2-byte body)
Any help or clues greatly appreciated.
The 500 Status code indicates that the server has encountered a situation it does not know how to handle.
This should work fine by now. You can always write to the RapidAPI support team if the error still persists at support#rapidapi.com
I'm building an Android app, which uses Retrofit for the networking.
Here's the code:
private val loggingInterceptor by lazy {
HttpLoggingInterceptor(HttpLoggingInterceptor.Logger { message -> Log.d(TAG, message) }).apply {
level = if(BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE
}
}
val client = OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.build()
Retrofit.Builder()
.baseUrl(" https://mybaseurl.com/")
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build()
I use an HttpLoggingInterceptor, however logs are too verbose because it logs all the headers..
content-language: en-GB
content-type: application/json
date: Sun, 07 Jul 2019 12:52:36 GMT
p3p: CP="NON CUR OTPi OUR NOR UNI"
transfer-encoding: chunked
cache-control: no-cache,no-store,must-reva
x-powered-by: Servlet/3.0
expires: Thu, 01 Dec 1994 16:00:00 GMT
pragma: no-cache
and more....
If I switch to LogLevel Basic though, I don't get the request and response, but just the URLs.
Btw, looking inside HttpLoggingInterceptor code:
boolean logHeaders = logBody || level == Level.HEADERS;
So it seems that there is no actual way to log body without the headers.
Is there any hacky way or workaround?
I'm trying to use Caching using Retrofit and OkHttp.
I'm adding this interceptor to OkHttp builder like.
#Provides
#Singleton
#TokenHttpClient
internal fun provideTokenOkHttpClient(
#BaseHttpClient okHttpClient: OkHttpClient,
cacheInterceptor: NetworkCacheInterceptor): OkHttpClient {
return okHttpClient.newBuilder()
.addInterceptor(cacheInterceptor)
.build()
And the interceptor looks like this:
class NetworkCacheInterceptor(val context: Context): Interceptor {
#Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
var response = chain.request()
response = if (hasNetwork(context))
response.newBuilder()
.removeHeader("Pragma")
.removeHeader("Cache-Control")
.header("Cache-Control",
"public, max-age=${CacheControl.Builder().maxAge(30, TimeUnit.DAYS).build()}")
.build()
else {
response.newBuilder()
.removeHeader("Pragma")
.removeHeader("Cache-Control")
.header("Cache-Control",
"public, only-if-cached, max-stale=${CacheControl.Builder().maxAge(30, TimeUnit.DAYS).build()}")
.cacheControl(CacheControl.FORCE_CACHE)
.build()
response
}
return chain.proceed(response)
}
}
This way of implementation it's saving responses into cache folder. But it's not working when offline. I've tired to separate interceptors and add the caching as networkInterceptor and the offline interceptor as normal interceptor but it didn't work. On this case no cache was saved.
When I check cached files I see these headers:
GET
0
HTTP/1.1 200
15
Access-Control-Expose-Headers: value
Cache-Control: no-cache
ETag: "01785cb0981d28aa96d58e491dbd6873e"
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Strict-Transport-Security: max-age=31536000 ; includeSubDomains
X-Frame-Options: SAMEORIGIN
Referrer-Policy: no-referrer
Content-Type: application/json;charset=UTF-8
Date: Tue, 21 May 2019 07:50:51 GMT
Transfer-Encoding: chunked
Content-Encoding: gzip
OkHttp-Sent-Millis: 1558425050896
OkHttp-Received-Millis: 1558425050928
Any idea? What I'm missing?
This is what I am trying to implement:
If there is connection:
Data was updated more than 1 hour ago: update data
Data was updated less than 1 hour ago: get data from cache
If there is no connection:
Data was updated more than 48 hours ago: show message "No internet
connection"
Data was updated less than 48 hours ago: get data from cache
This is my REST client module where I tried to implement before stated logic.
#Module
public class DarkSkyApiModule {
public final String BASE_URL = "https://api.darksky.net";
private static final Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = new Interceptor() {
#Override
public Response intercept(Chain chain) throws IOException {
Response originalResponse = chain.proceed(chain.request());
if (NetworkUtil.isNetworkAvailable()) {
int maxAge = 60 * 60; // read from cache for 1 hour
return originalResponse.newBuilder()
.header("Cache-Control", "public, max-age=" + maxAge)
.build();
} else {
int maxStale = 60 * 60 * 48; // tolerate 48-hours stale
return originalResponse.newBuilder()
.header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale)
.build();
}
}
};
#Provides
public OkHttpClient provideClient() {
//setup cache
File httpCacheDirectory = new File(FileManager.getCacheDir(), "responses");
int cacheSize = 10 * 1024 * 1024; // 10 MiB
Cache cache = new Cache(httpCacheDirectory, cacheSize);
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
return new OkHttpClient.Builder()
.addNetworkInterceptor(REWRITE_CACHE_CONTROL_INTERCEPTOR)
.addInterceptor(interceptor)
.cache(cache)
.build();
}
#Provides
public Retrofit provideRetrofit(String baseURL, OkHttpClient client) {
return new Retrofit.Builder()
.baseUrl(baseURL)
.client(client)
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build();
}
#Provides
public DarkSkyApiService provideApiService() {
return provideRetrofit(BASE_URL, provideClient()).create(DarkSkyApiService.class);
}
}
But result is not as expected:
If there is connection:
Data was updated more than 1 hour ago:
D/OkHttp: Cache-Control: public, max-age=3600
W/System.err: remove failed: ENOENT (No such file or directory) :
/data/user/0/com.android.example/cache/responses/2fb62bf59f85c3371d74d511a3bbeac2.0
W/System.err: remove failed: ENOENT (No such file or directory) : /data/user/0/com.android.example/cache/responses/2fb62bf59f85c3371d74d511a3bbeac2.1
D/OkHttp: [json_data]
Data was updated less than 1 hour ago:
D/OkHttp: Cache-Control: public, max-age=3600
W/System.err: remove failed: ENOENT (No such file or directory) :
/data/user/0/com.android.example/cache/responses/2fb62bf59f85c3371d74d511a3bbeac2.0
W/System.err: remove failed: ENOENT (No such file or directory) : /data/user/0/com.android.example/cache/responses/2fb62bf59f85c3371d74d511a3bbeac2.1
D/OkHttp: [json_data]
If there is no connection:
Data was updated more than 1 hour ago:
Unable to resolve host "api.darksky.net": No address associated with
hostname
Data was updated less than 48 hours ago:
W/System.err: remove failed: ENOENT (No such file or directory) : /data/user/0/com.android.example/cache/responses/journal.tmp
D/OkHttp: Cache-Control: public, max-age=3600
D/OkHttp: [json_data]
FileManager.java
public class FileManager {
static Context context;
public static void init(Context c) {
context = c;
}
public static File getCacheDir(){
return context.getCacheDir();
}
}
How to solve this problem? What I should set as max-age and max-stale?
I use retrofit and picasso library. Picasso manages caching itself. But when I view logcat, I see log below. What does it mean? Doesn't the webservice and backend send cache info correctly? How can I solve this to make Retrofit cache correctly. So, Does this data make sense?
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Content-Type: application/json
Date: Wed, 14 Dec 2016 07:15:48 GMT
Expires: Thu, 19 Nov 1981 08:52:00 GMT
OkHttp-Received-Millis: 1481597410805
OkHttp-Response-Source: NETWORK 200
OkHttp-Selected-Protocol: http/1.1
OkHttp-Sent-Millis: 1481597409021
Pragma: no-cache
Server: Apache
Transfer-Encoding: chunked
You can set cache option to OkHttp with using Interceptor.
OkHttpClient client = new OkHttpClient();
client.networkInterceptors().add(new CachingControlInterceptor());
Retrofit restAdapter = new Retrofit.Builder()
.client(client)
.baseUrl(Constants.BASE_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
and CachingControlInterceptor is:
public class CachingControlInterceptor implements Interceptor {
#Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
// Add Cache Control only for GET methods
if (request.method().equals("GET")) {
if (ConnectivityUtil.checkConnectivity(YaootaApplication.getContext())) {
// 1 day
request = request.newBuilder()
.header("Cache-Control", "only-if-cached")
.build();
} else {
// 4 weeks stale
request = request.newBuilder()
.header("Cache-Control", "public, max-stale=2419200")
.build();
}
}
Response originalResponse = chain.proceed(request);
return originalResponse.newBuilder()
.header("Cache-Control", "max-age=600")
.build();
}
}
See this:
https://stackoverflow.com/a/34401686/850347