I have a singleton class for network connections. I want the timeout increased and have written the following code.
BizAnalystApiv2 provideBizAnalystApiV2(final ObjectMapper mapper) {
final HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
if (BuildConfig.DEBUG) {
logging.setLevel(HttpLoggingInterceptor.Level.BODY);
} else {
logging.setLevel(HttpLoggingInterceptor.Level.NONE);
}
final OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(logging)
.addInterceptor(new Interceptor() {
#Override
public Response intercept(Chain chain) throws IOException {
Request original = chain.request();
Request request = original.newBuilder()
.header("instanceId", FirebaseInstanceId.getInstance().getId())
.build();
return chain.proceed(request);
}
})
.readTimeout(70, TimeUnit.SECONDS)
.build();
return new Retrofit.Builder()
.addConverterFactory(JacksonConverterFactory.create(mapper))
.client(client)
.baseUrl(BizAnalystServicev2.getServerUrl())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build()
.create(BizAnalystApiv2.class);
}
The timeout remains at 15 seconds which is the default timeout. Can you suggest what is the issue.
I am using okhttp(v 3.4.1) and retrofit (v 2.1.0)
Related
I use HttpClient object for PostAsync. I need to add BackgroundSessionConfiguration for iOS while I am creating HttpClient object. So I changed my code like this:
var configuration = NSUrlSessionConfiguration.CreateBackgroundSessionConfiguration ("my.app.identifier");
_client = new HttpClient (new NSUrlSessionHandler (configuration));
This works when I send first request with PostAsync. But when I send request second time, it doesn't work.
I did it for Login Operation like this: (It works first time but if I logout and login again, it doesn't work.)
public class LoginService
{
private HttpClient _client;
public LoginService()
{
if (_client == null)
{
_client = Helper.CreateHttpClientLogin(_client);
}
}
public async Task<LoginResponse<LoginDataResponse>> Login(LoginRequest request)
{
LoginResponse<LoginDataResponse> responseModel = new LoginResponse<LoginDataResponse>();
try
{
string json = JsonConvert.SerializeObject(request);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var jsonBody = await _client.PostAsync(App.ServiceURL.Login_Url, content);
string jsonstr = await jsonBody.Content.ReadAsStringAsync();
if (jsonstr == null || jsonstr == "")
{
responseModel.Success = false;
responseModel.Status = 0;
responseModel.Message = AppResources.UnknownHostException;
}
else
responseModel = (LoginResponse<LoginDataResponse>)JsonConvert.DeserializeObject(jsonstr, typeof(LoginResponse<LoginDataResponse>));
}
catch (Exception ex)
{
string text = ex.ToString();
responseModel.Status = 0;
AppResources.Culture = CrossMultilingual.Current.CurrentCultureInfo;
responseModel.Message = AppResources.UnknownHostException;
}
return responseModel;
}
}
public class Helper
{
public static HttpClient CreateHttpClientLogin(HttpClient _client)
{
if (Device.RuntimePlatform == Device.iOS)
{
var configuration = NSUrlSessionConfiguration.CreateBackgroundSessionConfiguration("my.app.identifier");
_client = new HttpClient(new NSUrlSessionHandler(configuration));
}
else
{
//_client = new HttpClient(new System.Net.Http.HttpClientHandler());
HttpClientHandler handler = new HttpClientHandler();
handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true;
_client = new HttpClient(handler);
}
return _client;
}
}
And I have this code on AppDelegate: (I don't know but maybe it causes the bug)
public static Action BackgroundSessionCompletionHandler;
public override void HandleEventsForBackgroundUrl(UIApplication application, string sessionIdentifier, Action completionHandler)
{
// We get a completion handler which we are supposed to call if our transfer is done.
BackgroundSessionCompletionHandler = completionHandler;
}
What must I do for this?
Edit:
I solved the problem I mentioned above by creating the Login Service object once the application was first opened. (After logout previously, I was rebuilding every time I login)
But now I have other error. When I run my app on "iPhone 7 plus - iOS 13.6" device I got this error:
System.Net.Http.HttpRequestException: unknown error ---> Foundation.NSErrorException: Error Domain=NSURLErrorDomain Code=-1 "unknown error" UserInfo={NSErrorFailingURLStringKey=https://mydomain/Api/Login, NSErrorFailingURLKey=https://mydomain/Api/Login, _NSURLErrorRelatedURLSessionTaskErrorKey=(
"BackgroundDataTask <E69F3EAF-0AE9-4FAE-A01B-988167B7F6BC>.<3>"
), _NSURLErrorFailingURLSessionTaskErrorKey=BackgroundDataTask <E69F3EAF-0AE9-4FAE-A01B-988167B7F6BC>.<3>, NSLocalizedDescription=unknown error}
--- End of inner exception stack trace ---
at System.Net.Http.NSUrlSessionHandler.SendAsync (System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) [0x001d4] in /Library/Frameworks/Xamarin.iOS.framework/Versions/13.20.2.2/src/Xamarin.iOS/Foundation/NSUrlSessionHandler.cs:527
at System.Net.Http.HttpClient.FinishSendAsyncBuffered (System.Threading.Tasks.Task`1[TResult] sendTask, System.Net.Http.HttpRequestMessage request, System.Threading.CancellationTokenSource cts, System.Boolean disposeCts) [0x0017e] in /Library/Frameworks/Xamarin.iOS.framework/Versions/Current/src/Xamarin.iOS/external/corefx/src/System.Net.Http/src/System/Net/Http/HttpClient.cs:506
at App.Services.LoginService.Login (FileOrbis.Models.RequestModels.LoginRequest request) [0x00084] in C:\Users\PcName\Desktop\App\App\Services\LoginService.cs:40
And simulator log file is:
Startup:
arguments: --device=06098E5B-1853-4A83-8434-8071D8973A14 --launchsim=//Users/deytek/Library/Caches/Xamarin/mtbs/builds/App.iOS/b2c75f2acbd4ff91c305dba10ca791b7/bin/iPhoneSimulator/Debug/App.iOS.app -argument=-monodevelop-port -argument=51890 -setenv=__XAMARIN_DEBUG_PORT__=51890 --sdkroot=/Applications/Xcode.app -h=192.168.1.7 -ssh=deytek --launched-by=devenv-16.0
version: 16.7.0.0 (54a29526ef6f853bdd37adbcc3791ce90ca82735)
Connecting to existing client
Exit:
Exit Code: 0
I encounter with this error when I use Background Session Configuration. If I use normal HttpClient object (without Background Session Configuration), it works
NOTE: I also tried iPhone 5s iOS 12.4.8 and iPad Pro (3rd Generation) iOS 13.6.1 It works these devices. But it doesn't work on iPhone 7 Plus 13.6
I want to make use of persistent http connections using Spring RestTemplate when accessing a REST api over https. I cannot make it work; a new connection is created for each request and SSL handshake takes place each time.
Is it possible to have reusable connections over https with RestTemplate and if so, how to configure it?
I set up a RestTemplate to make requests over https. That works correctly.
However I notice in the logs that a new SSL handshake takes place with every request.
I set up a RestTemplate in a test as follows:
#Before
public void setupPersistentHttpConnectionBackedRestTemplate() {
final SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(
sslContext,
new String[] { "TLSv1.2" },
null,
SSLConnectionSocketFactory.getDefaultHostnameVerifier());
final Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
.register("https", sslSocketFactory)
.build();
final PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(registry);
final CloseableHttpClient httpClient = HttpClients.custom()
.setSSLSocketFactory(sslSocketFactory)
.setConnectionManager(connectionManager)
.build();
final HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
requestFactory.setHttpClient(httpClient);
restTemplate.getRestTemplate().setRequestFactory(requestFactory);
}
Then I make several calls using this RestTemplate like this:
ResponseEntity<String> response = restTemplate.exchange("/tomcat/sleep?millis={millis}", HttpMethod.GET, HttpEntity.EMPTY, String.class, SLEEP_DURATION);
I investigated the code of spring-mvc and apache and notice the following.
In Spring RestTemplate execute method, a new request is created and then the request gets executed and the result returned.
ClientHttpRequest request = createRequest(url, method);
if (requestCallback != null) {
requestCallback.doWithRequest(request);
}
response = request.execute();
handleResponse(url, method, response);
return (responseExtractor != null ? responseExtractor.extractData(response) : null);
That in turn ends in calling HttpComponentsClientHttpRequestFactory where a new http context is created every time:
#Override
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
HttpClient client = getHttpClient();
HttpUriRequest httpRequest = createHttpUriRequest(httpMethod, uri);
postProcessHttpRequest(httpRequest);
HttpContext context = createHttpContext(httpMethod, uri);
if (context == null) {
context = HttpClientContext.create();
}
...
When following the chain of calls during the request execute call, I end up in apache MainClientExec. There it tries to reuse a connection based on the route and the context user token. After the request is executed, the user token is retrieved from the context and stored for further lookup.
#Override
public CloseableHttpResponse execute(
final HttpRoute route,
final HttpRequestWrapper request,
final HttpClientContext context,
final HttpExecutionAware execAware) throws IOException, HttpException {
...
Object userToken = context.getUserToken();
final ConnectionRequest connRequest = connManager.requestConnection(route, userToken);
...
if (userToken == null) {
userToken = userTokenHandler.getUserToken(context);
context.setAttribute(HttpClientContext.USER_TOKEN, userToken);
}
if (userToken != null) {
connHolder.setState(userToken);
}
...
In the case of a https connection, the user token gets retrieved from the SSL principal, that in turn gets it from the SSL certificate:
#Override
public Object getUserToken(final HttpContext context) {
...
if (userPrincipal == null) {
final HttpConnection conn = clientContext.getConnection();
if (conn.isOpen() && conn instanceof ManagedHttpClientConnection) {
final SSLSession sslsession = ((ManagedHttpClientConnection) conn).getSSLSession();
if (sslsession != null) {
userPrincipal = sslsession.getLocalPrincipal();
}
}
}
public Principal getLocalPrincipal() {
if (this.cipherSuite.keyExchange != KeyExchange.K_KRB5 && this.cipherSuite.keyExchange != KeyExchange.K_KRB5_EXPORT) {
return this.localCerts == null ? null : this.localCerts[0].getSubjectX500Principal();
} else {
return this.localPrincipal == null ? null : this.localPrincipal;
}
}
The PoolingHttpClientConnectionManager tries to return reusable connections based on the route and the state (in which the user token has been stored).
But since the RestTemplate starts with a new request with a new context each time, the uset token is lost and the PoolingHttpClientConnectionManager can not find a reusable connection and thus creates a new one every time.
I would expect that the RestRemplate could create a request that re-uses that connection instead of creating a new one every time.
I was trying to achieve the same and the only way I see to do this is by extending HttpComponentsClientHttpRequestFactory to set a UserToken, in this case a Principal cert.getSubjectDN() then override createHttpContext(HttpMethod httpMethod, URI uri)
#Override
protected HttpContext createHttpContext(HttpMethod httpMethod, URI uri) {
HttpClientContext context = HttpClientContext.create();
context.setUserToken(userToken);
return context;
}
A simple Spring Boot REST Controller
#PostMapping(path = "check-and-submit", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<MyOutput> checkAndSave(#RequestBody #Valid MyInput input, Errors errors){
ResponseEntity<MyOutput> result = null;
if (errors.hasErrors()) {
result = new ResponseEntity<>(MyOutput.buildErrorResponse(errors), HttpStatus.INTERNAL_SERVER_ERROR);
} else {
myDao.save(input.buildEntity());
result = new ResponseEntity<>(MyOutput.buildSuccessResponse(), HttpStatus.OK);
}
return result;
}
And the test class for it
public static void main(String[] args) {
MyInput dto = new MyInput();
// set properties
RestTemplate restTemplate = new RestTemplate();
MultiValueMap<String, String> headers = new LinkedMultiValueMap<String, String>();
headers.add("Content-Type", "application/json");
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
HttpEntity<MyInput> request = new HttpEntity<MyInput>(dto, headers);
try {
ResponseEntity<MyOutput> result = restTemplate.postForEntity(URL, request, MyOutput.class);
System.out.println(result);
} catch(Exception e) {
e.printStackTrace();
}
}
For success scenario this works fine. But, for exception scenrio, i.e. HTTP 500 this fails
org.springframework.web.client.HttpServerErrorException: 500 null
at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:97)
As suggested in one of the posts, I created a error-handler that can successfully read the response
public class TestHandler extends DefaultResponseErrorHandler {
#Override
public void handleError(ClientHttpResponse response) throws IOException {
Scanner scanner = new Scanner(response.getBody());
String data = "";
while (scanner.hasNext())
data += scanner.next();
System.out.println(data);
scanner.close();
}
}
But how can I let RestTemplate read and deserialize the response JSON even in case of HTTP 500.
Before any other human-question-flagging-bot marks this as duplicate, here's a humble explanation on how this is different from the others.
All other questions address how to handle HTTP 500, at max read the response-body. This questions is directed at if it is possible to deserialize the response as JSON as well. Such functionality is well established in frameworks such as JBoss RESTEasy. Checking how same can be achieved in Spring.
This should work.
try {
ResponseEntity<MyOutput> result = restTemplate.postForEntity(URL, request, MyOutput.class);
} catch(HttpServerErrorException errorException) {
String responseBody = errorException.getResponseBodyAsString();
// You can use this string to create MyOutput pojo using ObjectMapper.
}
From the answer given for this question Can Retrofit with OKHttp use cache data when offline i was able to come up with this, but the code seems not to cache. What could i be doing wrong?
This my okhttp client
long SIZE_OF_CACHE = 10 * 1024 * 1024; // 10 MB
Cache cache = new Cache(getDirectory(), SIZE_OF_CACHE);
if (cache == null) {
Toast.makeText(AppController.getInstance().getApplicationContext(), "could n0t set cache", Toast.LENGTH_SHORT).show();
}
client = new OkHttpClient
.Builder()
.addNetworkInterceptor(REWRITE_CACHE_CONTROL_INTERCEPTOR)
.cache(cache)
.build();
Add my network interceptor is as below:
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 (isConnected()) {
int maxAge = 60; // read from cache for 1 minute
return originalResponse.newBuilder()
.header("Cache-Control", "public, max-age=" + maxAge)
.build();
} else {
int maxStale = 60 * 60 * 24; // tolerate 1-day stale
return originalResponse.newBuilder()
.header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale)
.build();
}
}
};
Am adding to retrofit like this:
public static Retrofit getClient() {
createCacheForOkHTTP();
if (retrofit == null) {
retrofit = new Retrofit.Builder()
.client(client)
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
}
return retrofit;
}
//Add in my activity:
ApiInterface apiService =
ApiClient.getClient().create(ApiInterface.class);
Call<MovieResponse> call = apiService.getPopularMoviesDetails(ApiKey, page);
call.enqueue(new Callback<MovieResponse>() {
#Override
public void onResponse(Call<MovieResponse> call, Response<MovieResponse> response) {
progressBar.setVisibility(View.GONE);
mErrorView.setVisibility(View.GONE);
if (response.isSuccessful()) {
movies = response.body().getResults();
movieAdapter.setMovieList(movies);
mRecyclerView.setAdapter(movieAdapter);
} else {
Toast.makeText(getActivity(), "header" + response.headers() + "code" + response.code() + "errorbody" + response.errorBody() + "errorbody" + response.message(), Toast.LENGTH_SHORT).show();
}
}
#Override
public void onFailure(Call<MovieResponse> call, Throwable t) {
progressBar.setVisibility(View.GONE);
// Log error here since request failed
Log.e(TAG, t.toString());
mErrorView.setVisibility(View.VISIBLE);
}
});
//interface
#GET("movie/popular")
Call<MovieResponse> getPopularMoviesDetails(#Query("api_key") String apiKey, #Query("page") int page);
You shouldn’t rewrite responses from the server to facilitate caching. It’s better to ask your server’s administrators to include cache headers when they serve responses. That way the cache works for all clients – not just OkHttp.
That said, you’re an adult and you’re entitled to take technical shortcuts to avoid talking to your server team.
First you need to change your code to use both a network interceptor and an application interceptor. Why two? Well, when you’re making requests into the cache the network interceptors haven’t run yet. And when you’re writing responses into the cache the application interceptors haven’t run yet.
Your application interceptor will rewrite your request headers to include this when you prefer the cache, and to permit the cache to serve stale responses:
Cache-Control: only-if-cached, max-stale=86400
Your network interceptor will rewrite your server’s response headers to include this so that all responses are cached for 24 hours:
Cache-Control: max-age=86400
Not sure if you already fix this. I found a great/short solution hope it helps.
I create a CacheControlInterceptor. This has 24 hours of cache, if there you are offline and your response is less than 24 hours old you will get the cache response.
public class CacheControlInterceptor implements Interceptor {
private static final String CACHE_CONTROL_HEADER = "Cache-Control";
private static final String MAX_AGE_HEADER_VALUE = "public, max-age=";
private static final String MAX_STALE_HEADER_VALUE = "public, only-if-cached, max-stale=";
// Specifies the maximum amount of time a resource will be considered fresh.
private static final int MAX_AGE = 60;
// Indicates that the client is willing to accept a response that has exceeded its expiration time.
private static final int MAX_STALE = 60 * 60 * 24; // 24 hours cache.
#Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
if (WatchItApplication.hasNetwork()) {
request = request.newBuilder()
.header(CACHE_CONTROL_HEADER, MAX_AGE_HEADER_VALUE + MAX_AGE).build();
} else {
request = request.newBuilder()
.header(CACHE_CONTROL_HEADER, MAX_STALE_HEADER_VALUE + MAX_STALE).build();
}
return chain.proceed(request);
}
}
On this method I create the OKHttpCliente . As you notice I am adding the CacheControlInterceptor and cache.
private OkHttpClient createDefaultOkHttpClient() {
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
interceptor.setLevel(HttpLoggingInterceptor.Level.BASIC);
return new OkHttpClient.Builder()
.retryOnConnectionFailure(true)
.connectTimeout(TIMEOUT_MILLIS, TIMEOUT_UNIT)
.readTimeout(TIMEOUT_MILLIS, TIMEOUT_UNIT)
.writeTimeout(TIMEOUT_MILLIS, TIMEOUT_UNIT)
.addNetworkInterceptor(new HeaderInterceptor())
.cache(new Cache(WatchItApplication.getInstance().getCacheDir(), 10 * 1024 * 1024))
.addInterceptor(new CacheControlInterceptor())
.addInterceptor(interceptor)
.build();
}
Finally retrofit:
retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.client(createDefaultOkHttpClient())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
Let me know if that works.
I was able to resolve the issue.This is how i did it.
private static OkHttpClient getCacheClient(final Context context) {
Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = new Interceptor() {
#Override
public okhttp3.Response intercept(Chain chain) throws IOException {
okhttp3.Response originalResponse = chain.proceed(chain.request());
if (isConnected()) {
// Internet available; read from cache for 0 day
// Why? Reduce server load, better UX
int maxAge = 0;
return originalResponse.newBuilder()
.header("Cache-Control", "public, max-age=" + maxAge)
.build();
} else {
// No internet; tolerate cached data for 1 week
int maxStale = 60 * 60 * 24 * 7;
return originalResponse.newBuilder()
.header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale)
.build();
}
}
};
File httpCacheDirectory = new File(context.getCacheDir(), "cachedir");
int size = 5 * 1024 * 1024;
Cache cache = new Cache(httpCacheDirectory, size);
return new OkHttpClient.Builder()
.addNetworkInterceptor(REWRITE_CACHE_CONTROL_INTERCEPTOR)
.cache(cache)
.build();
}
And usage:
public static Retrofit getClient(final Context context) {
if (retrofit == null) {
retrofit = new Retrofit.Builder()
.client(getCacheClient(context))
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
}
return retrofit;
}
when i use retrofit .I got the exception java.lang.NoClassDefFoundError: Failed resolution of: Lokio/Buffer;i use okhttpclient in order to set header for retrofit.get userList is a post method,and i need to send body in request.
private void getUserList(int startIndex){
final JSONObject audienceObj = ProtocolHelper.getProtocolUtils(mContext).getUserlistJsonObj(mRoomData.mUid, mRoomData.mRoomId, startIndex);
OkHttpClient okClient = new OkHttpClient.Builder()
.addInterceptor(
new Interceptor() {
#Override
public okhttp3.Response intercept(Interceptor.Chain chain) throws IOException {
Request original = chain.request();
// Request customization: add request headers
Request.Builder requestBuilder = original.newBuilder()
.header("sessionId", CommonData.getUserInfo(mContext).sessionId);
Request request = requestBuilder.build();
return chain.proceed(request);
}
})
.build();
String baseUrl = ProtocolUtils.BASE_URL+"/";
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.client(okClient)
.build();
String audienceUrl = ProtocolHelper.getProtocolUtils(mContext).getProtocolUrl(ProtocolUtils.PROTOCOL_MSG_ID_MEMBER_LIST);
AudienceInterface audienceInterface = retrofit.create(AudienceInterface.class);
Call<String> call = audienceInterface.getAudienceList(audienceUrl,audienceObj);
call.enqueue(new Callback<String>() {
#Override
public void onResponse(Call<String> call, Response<String> response) {
Log.d(TAG, "onResponse");
}
#Override
public void onFailure(Call<String> call, Throwable t) {
Log.d(TAG, "onFailure"+t.getMessage());
}
});
}
public interface AudienceInterface {
#POST("{url}")
Call<String>getAudienceList(#Path("url") String url,#Body JSONObject boder);
}
the log t.getMessage is :java.lang.NoClassDefFoundError: Failed resolution of: Lokio/Buffer;
I solved it by adding:
implementation 'com.squareup.okio:okio:2.1.0'
in dependencies under build.gradle(Module: app).
Alright.~I found this error last time too.
By this:
NoClassDefFoundError: Failed resolution of: Lokio/Buffer
You might lost another jar lib--Okio.
You can download the jar file from github:
https://github.com/square/okio
And add this lib to your project.