My project use spring-cloud-starter-openfeign.(version 2.1.0)
open feign is using feign-okhttp as http client and I know it uses ok-http3 internally.
I want the OkHttp3 connection to be reset or delete from connection-pool when http request gets a 500 error response.
To do that I need to detect a 500 error, so I figured I could use a network interceptor.
However, the RealConnection object I get with chain.getConnection() doesn't seem to have any methods to reset or close the connection.
How can I implement what I have in mind?
#Bean
public OkHttpClient client() {
return new OkHttpClient(
new okhttp3.OkHttpClient.Builder()
.addNetworkInterceptor(new ResetConnection())
.build());
}
public static class ResetConnection implements Interceptor {
#Override
public Response intercept(Chain chain) throws IOException {
okhttp3.Request request = chain.request();
Response response = chain.proceed(request);
if (response.code() >= 500 && response.code() < 600) {
// ???????????
}
return response;
}
}
Related
I would like to use a global header for all my requests. Therefore I have implemented the following class:
public class HeaderInterceptor {
public Response intercept(Chain chain) throws IOException {
Request request = chain.request()
.newBuilder()
.method("GET", null)
.addHeader("Accept", "application/json")
.addHeader("Basic ", "abcdefghi123456789")
.build();
Response response = chain.proceed(request);
return response;
}
}
Now I would like to do the following in the main()-method:
public static void main(String[] args) throws Exception {
OkHttpClient httpClient = new OkHttpClient.Builder().addInterceptor(MyInterceptor).build();
Request reqAllProjects = new Request.Builder()
.url("https://example.com/projects")
.build();
Response resAllProjects = httpClient.newCall(reqAllProjects).execute();
String responseData = resAllProjects.body().string();
System.out.println(responseData);
}
I'm not sure now how to use my HeaderInterceptor. I guess I'll have to enter it here, right?
OkHttpClient httpClient = new OkHttpClient.Builder().addInterceptor(??MyInterceptor??).build();
I tried something like this: addInterceptor(HeaderInterceptor.intercept()) but this is not working...
Can someone help me please? And does the rest of it look fine? Many thanks in advance!
The interceptor class that you have created doesn't seem to be implementing the Interceptor interface. You need to implement as below
public class HeaderInterceptor implements Interceptor {
#Override
public Response intercept(Interceptor.Chain chain) throws IOException {
Request request = chain.request()
.newBuilder()
.addHeader("Accept", "application/json")
.addHeader("Basic ", "abcdefghi123456789")
.build();
Response response = chain.proceed(request);
return response;
}
}
Do note that you should not be modifying the method and body of the request as .method("GET", null) unless you actually need so, as it can result in all the HTTP requests made by the client to make GET requests with null body.
Then add the interceptor while building the client as below
OkHttpClient httpClient = new OkHttpClient.Builder()
.addInterceptor(new HeaderInterceptor()).build();
Have a look at the OkHttp documentation for more info.
Have you checked this question : Okhttp3: Add global header to all requests error
It should be something like
.addInterceptor(new Interceptor())
I am using feign client to connect to downstream service.
I got a requirement that when one of the downstream service endpoint returns 400 ( it's partial success scenario ) our service need this to be converted to 200 success with the response value.
I am looking for a best way of doing this.
We are using error decoder to handle the errors and the above conversion is applicable for only one endpoint not for all the downstream endpoints and noticed that decode() method should returns exception back.
You will need to create a customized Client to intercept the Response early enough to change the response status and not invoke the ErrorDecoder. The simplest approach is to create a wrapper on an existing client and create a new Response with a 200 status. Here is an example when using Feign's ApacheHttpClient:
public class ClientWrapper extends ApacheHttpClient {
private ApacheHttpClient delegate;
public ClientWrapper(ApacheHttpClient client) {
this.client = client;
}
#Override
public Response execute(Request request, Request.Options options) throws IOException {
/* execute the request on the delegate */
Response response = this.client.execute(request, options);
/* check the response code and change */
if (response.status() == 400) {
response = Response.builder(response).status(200).build();
}
return response;
}
}
This customized client can be used on any Feign client you need.
Another way of doing is by throwing custom exception at error decoder and convert this custom exception to success at spring global exception handler (using #RestControllerAdvice )
public class CustomErrorDecoder implements ErrorDecoder {
#Override
public Exception decode(String methodKey, Response response) {
if (response.status() == 400 && response.request().url().contains("/wanttocovert400to200/clientendpoints") {
ResponseData responseData;
ObjectMapper mapper = new ObjectMapper();
try {
responseData = mapper.readValue(response.body().asInputStream(), ResponseData.class);
} catch (Exception e) {
responseData = new ResponseData();
}
return new PartialSuccessException(responseData);
}
return FeignException.errorStatus(methodKey, response);
}}
And the Exception handler as below
#RestControllerAdvice
public class GlobalControllerExceptionHandler {
#ResponseStatus(HttpStatus.OK)
#ExceptionHandler(PartialSuccessException.class)
public ResponseData handlePartialSuccessException(
PartialSuccessException ex) {
return ex.getResponseData();
}
}
Change the microservice response:
public class CustomFeignClient extends Client.Default {
public CustomFeignClient(
final SSLSocketFactory sslContextFactory, final HostnameVerifier
hostnameVerifier) {
super(sslContextFactory, hostnameVerifier);
}
#Override
public Response execute(final Request request, final Request.Options
options) throws IOException {
Response response = super.execute(request, options);
if (HttpStatus.SC_OK != response.status()) {
response =
Response.builder()
.status(HttpStatus.SC_OK)
.body(InputStream.nullInputStream(), 0)
.headers(response.headers())
.request(response.request())
.build();
}
return response;
}
}
Add a Feign Client Config:
#Configuration
public class FeignClientConfig {
#Bean
public Client client() {
return new CustomFeignClient(null, null);
}
}
I've been struggling with the following issue:
I have a spring boot application which allows a user to post JSON content to an API endpoint. To use this endpoint, the user has to authenticate himself via basic authentication. Moreover, I use OkHttp (3.6.0) as an HTTP client.
Now if I post a large payload (> 4 MB) while being unauthorized, the OkHttp client fails with the following error:
java.net.SocketException: Connection reset by peer: socket write error
To reproduce the issue, I created a minimal client and server:
Server (Spring Boot Web App)
#SpringBootApplication
#RestController
public class App {
#PostMapping
public String create(#RequestBody Object obj) {
System.out.println(obj);
return "success";
}
public static void main(String[] args) {
SpringApplication.run(App.class);
}
}
Client (OkHttp 3.6.0)
public class Main {
public static void main(String[] args) {
OkHttpClient client = new OkHttpClient
.Builder()
.build();
Request request = new Request.Builder()
.url("http://localhost:8080")
.header("Content-Type", "application/json")
.post(RequestBody.create(MediaType.parse("application/json"), new File("src/main/java/content.json")))
// .post(RequestBody.create(MediaType.parse("application/json"), new File("src/main/java/content-small.json")))
.build();
try {
Response response = client.newCall(request).execute();
System.out.println(response);
} catch (IOException e) {
e.printStackTrace();
}
}
}
Instead of the previously mentioned exception ("java.net.SocketException: Connection reset by peer: socket write error"), I would expect the response to be a default error message with HTTP status code 401, e.g. {"timestamp":1508767498068,"status":401,"error":"Unauthorized","message":"Full authentication is required to access this resource","path":"/"}. This is the result I get when using cURL and Postman as clients.
When I'm using less payload (content-small.json; approx. 1KB) instead of the large payload (content.json; approx. 4881KB), I receive the expected response, i.e. Response{protocol=http/1.1, code=401, message=, url=http://localhost:8080/}.
The issue is actually embedded in a larger project with Eureka and Feign clients. Threfore, I would like to continue using OkHttp client and I need the expected behavior.
My problem analysis
Of course, I tried to solve this problem myself for quite some time now. The IOException occurs when the request body is written to the HTTP stream:
if (permitsRequestBody(request) && request.body() != null) {
Sink requestBodyOut = httpStream.createRequestBody(request, request.body().contentLength());
BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
request.body().writeTo(bufferedRequestBody);
bufferedRequestBody.close();
}
My assumption is that the server closes the connection as soon as it receives the headers (as the request is unauthorized), but the client continues trying to write to the stream although it is already closed.
Update
I've also implemented a simple client with Unirest which shows the same behavior. Implementation:
public class UnirestMain {
public static void main(String[] args)
throws IOException, UnirestException {
HttpResponse response = Unirest
.post("http://localhost:8080")
.header("Content-Type", "aplication/json")
.body(Files.readAllBytes(new File("src/main/java/content.json").toPath()))
// .body(Files.readAllBytes(new File("src/main/java/content-small.json").toPath()))
.asJson();
System.out.println(response.getStatus());
System.out.println(response.getStatusText());
System.out.println(response.getBody());
}
}
Expected output: {"path":"/","error":"Unauthorized","message":"Full authentication is required to access this resource","timestamp":1508769862951,"status":401}
Actual output: java.net.SocketException: Connection reset by peer: socket write error
Im trying to authenticate to Cloudinary API service using the below code but i get 401 unauthorized error, it expects credentials in this format https://API_KEY:API_SECRET#..., when i substitute with actual values it works great with browser/postman but fails with retrofit2, below is my code.
// create and initialize retrofit2 client
public static OkHttpClient getClient(){
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
interceptor.setLevel(Level.BASIC);
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new Interceptor() {
#Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request().newBuilder()
.addHeader("API_KEY","API_SECRET")
.addHeader("Accept","Application/JSON").build();
return chain.proceed(request);
}
})
.addInterceptor(interceptor)
.build();
return client;
}
private static Retrofit retrofit = null;
public static Retrofit getClient(String baseUrl){
if (retrofit == null){
retrofit = new Retrofit.Builder()
.client(getClient())
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.build();
}
return retrofit;
}
// Interface with get methods to access image resources
public interface CloudinaryService {
#GET("resources/image")
Call<imageresponse> getImageResource();
}
// Util class to make requests
public class ApiUtils {
private static final String BASE_URL = "http://api.cloudinary.com/v...";
public static CloudinaryService getImageService(){
return RetrofitClient.getClient(BASE_URL)
.create(CloudinaryService.class);
}
}
Any help fixing the error will be highly appreciated, not sure if need custom converter. thanks
***** Edit******
public static String credentials = Credentials.basic(API_KEY,API_SECRET);
OkHttpClient client = new OkHttpClient.Builder()
// .authenticator(new Authenticator() {
// #Override
// public Request authenticate(Route route, Response response) throws IOException {
//
// return response.request().newBuilder().header("Authorization", credentials).build();
// }
// })
.addInterceptor(new Interceptor() {
#Override
public Response intercept(Chain chain) throws IOException {
Request request = (chain.request().newBuilder()
.header("Accept","Application/JSON")
.header("Cache-Control", "public, max-age=" + 60)
.header("Authorization",credentials).build());
return chain.proceed(request);
}
})
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.addInterceptor(loggingInterceptor)
.addInterceptor(provideOfflineCacheInterceptor())
.addNetworkInterceptor(provideCacheInterceptor())
.cache(getCache())
.build();
return client;
}
I was able to fix the issue with adding authenticator to the builder.
.authenticator(new Authenticator() {
#Override
public Request authenticate(Route route, Response response) throws IOException {
return response.request().newBuilder().header("Authorization", credentials).build();
}
})
thanks for all your help.
request = chain.request();
builder = request.newBuilder();
if (TextUtils.isEmpty(request.header(AUTH)) && UserPreference.getInstance().isSignin())
builder.addHeader(AUTH, UserPreference.getInstance().getAccessToken());
if (NetUtil.hasNetwork(GridInnApplication.getInstance()))
builder.header(USER_AGENT, userAgent);
else
builder.cacheControl(CacheControl.FORCE_CACHE);
request = builder.build();
Response response = chain.proceed(request);
if (NetUtil.hasNetwork(GridInnApplication.getInstance())) {
String cacheControl = request.cacheControl().toString();
return response.newBuilder()
.header(CACHE_CONTROL, cacheControl)
.removeHeader(PRAGMA)
.build();
} else {
return response.newBuilder()
.addHeader(CACHE_CONTROL, CACHE_CONTROL_ONLY_CACHED)
.removeHeader(PRAGMA)
.build();
}
//you can results before returing intercept
The answer provided by leafNext will work but will cause every request to be sent twice - The authenticator only kicks in if the server responds with 401. You send the request, get 401 and then send it again with proper credentials.
The correct solution is to provide the credentials from the get go, using the interceptor. It's similar to what you tried to do originally, but you got the syntax wrong. The expected format is basic authentication.
.addInterceptor(new Interceptor() {
#Override
public Response intercept(Interceptor.Chain chain) throws IOException {
// Request customization: add request headers
return chain.proceed(chain.request().newBuilder()
.header("Authorization", credentials).build());
}
});
Where credentials should follow the basic authentication protocol: Assuming the Api key is key and the secret is secret, you base64-encode the expression key:secret and prefix it with Basic. In this example the value of credentials should end up like so:
Basic a2V5OnNlY3JldA==
Edit - Added a fully working independent code bit to verify basic auth is working for okhttp (and thus with retrofit when using okhttp):
public int testBasicAuth() throws IOException {
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new Interceptor() {
#Override
public Response intercept(Chain chain) throws IOException {
Request request = (chain.request().newBuilder()
.header("Authorization",okhttp3.Credentials.basic(KEY, SECRET)).build());
return chain.proceed(request);
}
}).build();
Request request = new Request.Builder()
.url("https://api.cloudinary.com/v1_1/[cloud_name]/resources/image")
.build();
int code = client.newCall(request).execute().code();
return code; // 200
}
I'm trying to create a websocket and dynamically recalculate its header in every message sent. Is it possible?
I was trying to use an interceptor but is only called once.
public void run() {
// only open a websocket if there aren't websockets already open
if (this.webSocket == null || !this.openingWS) {
this.openingWS = true;
wsBuilder = new OkHttpClient.Builder();
OkHttpClient client = wsBuilder.addInterceptor(this)
.readTimeout(0, TimeUnit.MILLISECONDS)
.build();
Request request = new Request.Builder()
.url("wss://...")
.build();
client.newWebSocket(request, this);
// Trigger shutdown of the dispatcher's executor so this process can exit cleanly.
client.dispatcher().executorService().shutdown();
}
}
#Override public void onOpen(WebSocket webSocket, Response response) {
this.openingWS = false; // already open
this.webSocket = webSocket; // storing websocket for future usages
if (listener != null) listener.onWSOpen();
}
public void sendCommand(String cmd) {
System.out.println("SEND " + cmd);
if (webSocket != null) webSocket.send(cmd);
}
This same class is implementing the interceptor
public Response intercept(Chain chain) throws IOException {
Request originalRequest = chain.request();
if (!isSpecial()) return chain.proceed(originalRequest);
okhttp3.Request.Builder builder = originalRequest.newBuilder()
.addHeader("text", "...")
.addHeader("dfds", "...");
Request compressedRequest = builder.build();
return chain.proceed(compressedRequest);
}
The authentication code sent in the header will change every X seconds/minutes.
If it's not possible to change dynamically the header, what is the best way to approach this kind of connection?
Thank you for your help.
I think the headers are send only first time when you request the connection, later is depends on frames between the client and the server.
So if you want to inform the server that you had changed the header then send message with your new header. Or you can close the connection and start a new one with the new header.