Skip to content

Allow setting a custom EventListener in ApiClientBuilder #1621

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,16 @@
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;

import com.linecorp.bot.client.base.http.EventListenerAdapter;
import com.linecorp.bot.client.base.http.HttpAuthenticator;
import com.linecorp.bot.client.base.http.HttpChain;
import com.linecorp.bot.client.base.http.HttpEventListener;
import com.linecorp.bot.client.base.http.HttpInterceptor;
import com.linecorp.bot.client.base.http.HttpResponse;
import com.linecorp.bot.jackson.ModelObjectMapper;

import okhttp3.Dispatcher;
import okhttp3.EventListener;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
Expand Down Expand Up @@ -89,6 +92,13 @@ public ApiClientBuilder(URI apiEndPoint, Class<T> clientClass, ExceptionBuilder
*/
private List<Interceptor> additionalInterceptors = new ArrayList<>();

/**
* Custom EventListener
*
* <p>You can add your own EventListener.
*/
private EventListener eventListener;

private Proxy proxy;

private HttpAuthenticator proxyAuthenticator;
Expand Down Expand Up @@ -141,6 +151,11 @@ public ApiClientBuilder<T> addInterceptor(HttpInterceptor interceptor) {
return this;
}

public ApiClientBuilder<T> setEventListener(HttpEventListener httpEventListener) {
this.eventListener = new EventListenerAdapter(httpEventListener);
return this;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe you implemented HttpEventListener by wrapping okHttp's EventListener with reference to the Interceptor implementation, considering consistency with existing code.

However, with this approach, sdk users need to implement a class that inherits from the wrapped HttpEventListener class each time, which is cumbersome. Therefore, how about directly acceptting okHttp's EventListener as follows...? 🙏

Suggested change
public ApiClientBuilder<T> setEventListener(HttpEventListener httpEventListener) {
this.eventListener = new EventListenerAdapter(httpEventListener);
return this;
}
public ApiClientBuilder<T> setEventListener(EventListener eventListener) {
this.eventListener = eventListener;
return this;
}

Alternatively, I think it would also be good to return a builder so that users can set EventListener and other components as they prefer.

Copy link
Contributor Author

@kabigon-sung kabigon-sung Jun 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your suggestion.

If directly accepting an HttpEventListener is not a concern from your perspective, I think it’s a simple and convenient approach.

I also noticed that you mentioned another approach — returning a builder so that users can set the EventListener and other components. Could you explain this approach in more detail? (It seems that the OkHttpClientBuilder is created in build method and pass to the Retrofit.Builder )

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your confirmation!

I was imagining a structure that wouldn't require additional implementation when adding not just EventListener but other options as well. Specifically, I was thinking about making the OkHttpClientBuilder directly accessible to users so they could handle the client themselves. However, as you mentioned, it would need to be passed to the Retrofit.Builder afterward, so it probably wouldn't result in a clean implementation.

It seems better to simply allow setting the EventListener directly. 🙏

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your detail explanation. I've modified the implementation to directly allow setting a EventListener. Kindly review the PR and let me know if there are any issues.


/**
* The maximum number of requests to execute concurrently.
* Default: 64
Expand Down Expand Up @@ -230,6 +245,10 @@ public T build() {
}
});

if (this.eventListener != null) {
okHttpClientBuilder.eventListener(this.eventListener);
}

if (this.proxy != null) {
okHttpClientBuilder.proxy(this.proxy);
}
Expand Down Expand Up @@ -262,6 +281,7 @@ public String toString() {
+ ", readTimeout=" + readTimeout
+ ", writeTimeout=" + writeTimeout
+ ", additionalInterceptors=" + additionalInterceptors
+ ", eventListener=" + eventListener
+ ", maxRequests=" + maxRequests
+ ", maxRequestsPerHost=" + maxRequestsPerHost
+ '}';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/*
* Copyright 2025 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

package com.linecorp.bot.client.base.http;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.util.List;

import okhttp3.Call;
import okhttp3.Connection;
import okhttp3.EventListener;
import okhttp3.Handshake;
import okhttp3.HttpUrl;
import okhttp3.Protocol;
import okhttp3.Request;
import okhttp3.Response;

public class EventListenerAdapter extends EventListener {
private final HttpEventListener httpEventListener;

public EventListenerAdapter(HttpEventListener httpEventListener) {
this.httpEventListener = httpEventListener;
}

@Override
public void callStart(Call call) {
httpEventListener.callStart(new HttpCall(call));
}

@Override
public void proxySelectStart(Call call, HttpUrl url) {
httpEventListener.proxySelectStart(new HttpCall(call), new HttpEndpoint(url));
}

@Override
public void proxySelectEnd(Call call, HttpUrl url, List<Proxy> proxies) {
httpEventListener.proxySelectEnd(new HttpCall(call), new HttpEndpoint(url), proxies);
}

@Override
public void dnsStart(Call call, String domainName) {
httpEventListener.dnsStart(new HttpCall(call), domainName);
}

@Override
public void dnsEnd(Call call, String domainName, List<InetAddress> inetAddressList) {
httpEventListener.dnsEnd(new HttpCall(call), domainName, inetAddressList);
}

@Override
public void connectStart(Call call, InetSocketAddress inetSocketAddress, Proxy proxy) {
httpEventListener.connectStart(new HttpCall(call), inetSocketAddress, proxy);
}

@Override
public void secureConnectStart(Call call) {
httpEventListener.secureConnectStart(new HttpCall(call));
}

@Override
public void secureConnectEnd(Call call, Handshake handshake) {
httpEventListener.secureConnectEnd(new HttpCall(call), handshake != null ? new TlsHandshake(handshake) : null);
}

@Override
public void connectEnd(Call call, InetSocketAddress inetSocketAddress, Proxy proxy, Protocol protocol) {
httpEventListener.connectEnd(new HttpCall(call), inetSocketAddress, proxy, protocol != null ? new HttpProtocol(protocol) : null);
}

@Override
public void connectFailed(Call call, InetSocketAddress inetSocketAddress, Proxy proxy, Protocol protocol, IOException ioe) {
httpEventListener.connectFailed(new HttpCall(call), inetSocketAddress, proxy, protocol != null ? new HttpProtocol(protocol) : null, ioe);
}

@Override
public void connectionAcquired(Call call, Connection connection) {
httpEventListener.connectionAcquired(new HttpCall(call), new HttpConnection(connection));
}

@Override
public void connectionReleased(Call call, Connection connection) {
httpEventListener.connectionReleased(new HttpCall(call), new HttpConnection(connection));
}

@Override
public void requestHeadersStart(Call call) {
httpEventListener.requestHeadersStart(new HttpCall(call));
}

@Override
public void requestHeadersEnd(Call call, Request request) {
httpEventListener.requestHeadersEnd(new HttpCall(call), new HttpRequest(request));
}

@Override
public void requestBodyStart(Call call) {
httpEventListener.requestBodyStart(new HttpCall(call));
}

@Override
public void requestBodyEnd(Call call, long byteCount) {
httpEventListener.requestBodyEnd(new HttpCall(call), byteCount);
}

@Override
public void requestFailed(Call call, IOException ioe) {
httpEventListener.requestFailed(new HttpCall(call), ioe);
}

@Override
public void responseHeadersStart(Call call) {
httpEventListener.responseHeadersStart(new HttpCall(call));
}

@Override
public void responseHeadersEnd(Call call, Response response) {
httpEventListener.responseHeadersEnd(new HttpCall(call), new HttpResponse(response));
}

@Override
public void responseBodyStart(Call call) {
httpEventListener.responseBodyStart(new HttpCall(call));
}

@Override
public void responseBodyEnd(Call call, long byteCount) {
httpEventListener.responseBodyEnd(new HttpCall(call), byteCount);
}

@Override
public void responseFailed(Call call, IOException ioe) {
httpEventListener.responseFailed(new HttpCall(call), ioe);
}

@Override
public void callEnd(Call call) {
httpEventListener.callEnd(new HttpCall(call));
}

@Override
public void callFailed(Call call, IOException ioe) {
httpEventListener.callFailed(new HttpCall(call), ioe);
}

@Override
public void canceled(Call call) {
httpEventListener.canceled(new HttpCall(call));
}

@Override
public void satisfactionFailure(Call call, Response response) {
httpEventListener.satisfactionFailure(new HttpCall(call), new HttpResponse(response));
}

@Override
public void cacheHit(Call call, Response response) {
httpEventListener.cacheHit(new HttpCall(call), new HttpResponse(response));
}

@Override
public void cacheMiss(Call call) {
httpEventListener.cacheMiss(new HttpCall(call));
}

@Override
public void cacheConditionalHit(Call call, Response cachedResponse) {
httpEventListener.cacheConditionalHit(new HttpCall(call), new HttpResponse(cachedResponse));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2025 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

package com.linecorp.bot.client.base.http;

import okhttp3.Call;

public class HttpCall {
private final Call call;

public HttpCall(Call call) {
this.call = call;
}

public HttpRequest request() {
return new HttpRequest(call.request());
}

public Call toOkHttpCall() {
return call;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright 2025 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

package com.linecorp.bot.client.base.http;

import okhttp3.Connection;

public class HttpConnection {
private final Connection connection;

public HttpConnection(Connection connection) {
this.connection = connection;
}

public Connection toOkHttpConnection() {
return connection;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright 2025 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

package com.linecorp.bot.client.base.http;

import okhttp3.HttpUrl;

public class HttpEndpoint {
private final HttpUrl httpUrl;

public HttpEndpoint(HttpUrl httpUrl) {
this.httpUrl = httpUrl;
}

public HttpUrl toOkHttpUrl() {
return httpUrl;
}
}
Loading