Skip to content

Commit

Permalink
feat: additional callback methods for Interceptor interface
Browse files Browse the repository at this point in the history
Added two more methods to the Interceptor interface.
The main intention is to provide enough functionality to
implement an HttpLoggingInterceptor compatible with
every HttpClient implementation.

Signed-off-by: Marc Nuri <marc@marcnuri.com>
  • Loading branch information
manusa committed Apr 4, 2023
1 parent 3237246 commit 7102d7f
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@

package io.fabric8.kubernetes.client.http;

import java.nio.ByteBuffer;
import java.util.List;
import java.util.concurrent.CompletableFuture;

public interface Interceptor {

public interface RequestTags {
interface RequestTags {

<T> T getTag(Class<T> type);

Expand All @@ -35,6 +37,31 @@ public interface RequestTags {
default void before(BasicBuilder builder, HttpRequest request, RequestTags tags) {
}

/**
* Called after a non-WebSocket HTTP response is received. The body might or might not be already consumed.
* <p>
* Should be used to analyze response codes and headers, original response shouldn't be altered.
*
* @param response the response received from the server.
*/
default void after(HttpResponse<?> response) {

}

// In case we want to encapsulate to spy the responses from the server

/**
* Called before a request to allow the encapsulation of the provided consumer.
* <p>
*
* @param consumer the original consumer.
* @param request the HTTP request.
* @return the consumer to use.
*/
default AsyncBody.Consumer<List<ByteBuffer>> consumer(AsyncBody.Consumer<List<ByteBuffer>> consumer, HttpRequest request) {
return consumer;
}

/**
* Called after a websocket failure or by default from a normal request
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,19 +88,22 @@ private CompletableFuture<HttpResponse<AsyncBody>> consumeBytesOnce(HttpRequest
for (Interceptor interceptor : builder.getInterceptors().values()) {
interceptor.before(copy, standardHttpRequest, this);
standardHttpRequest = copy.build();
consumer = interceptor.consumer(consumer, standardHttpRequest);
}
final Consumer<List<ByteBuffer>> effectiveConsumer = consumer;

CompletableFuture<HttpResponse<AsyncBody>> cf = consumeBytesDirect(standardHttpRequest, consumer);
CompletableFuture<HttpResponse<AsyncBody>> cf = consumeBytesDirect(standardHttpRequest, effectiveConsumer);

for (Interceptor interceptor : builder.getInterceptors().values()) {
cf = cf.thenCompose(response -> {
interceptor.after(response);
if (!HttpResponse.isSuccessful(response.code())) {
return interceptor.afterFailure(copy, response, this)
.thenCompose(b -> {
if (Boolean.TRUE.equals(b)) {
// before starting another request, make sure the old one is cancelled / closed
response.body().cancel();
return consumeBytesDirect(copy.build(), consumer);
return consumeBytesDirect(copy.build(), effectiveConsumer);
}
return CompletableFuture.completedFuture(response);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@
import org.junit.jupiter.api.Test;

import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
Expand Down Expand Up @@ -261,6 +263,89 @@ public CompletableFuture<Boolean> afterFailure(BasicBuilder builder, HttpRespons
assertThat(server.getLastRequest().getPath()).isEqualTo("/valid-url");
}

@Test
@DisplayName("after, called after HTTP successful response")
public void afterHttpSuccess() throws Exception {
// Given
server.expect().withPath("/success").andReturn(200, "This works").once();
final CompletableFuture<HttpResponse<?>> responseFuture = new CompletableFuture<>();
final HttpClient.Builder builder = getHttpClientFactory().newBuilder()
.addOrReplaceInterceptor("after", new Interceptor() {
@Override
public void after(HttpResponse<?> response) {
responseFuture.complete(response);
}
});
// When
try (HttpClient client = builder.build()) {
client.consumeBytes(
client.newHttpRequestBuilder().uri(server.url("/success")).build(),
(s, ab) -> ab.consume())
.get(10, TimeUnit.SECONDS);
}
// Then
assertThat(responseFuture)
.succeedsWithin(1, TimeUnit.SECONDS)
.extracting(HttpResponse::code)
.isEqualTo(200);
}

@Test
@DisplayName("after, called after HTTP error response")
public void afterHttpError() throws Exception {
// Given
server.expect().withPath("/client-error").andReturn(400, "Client problems").once();
final CompletableFuture<HttpResponse<?>> responseFuture = new CompletableFuture<>();
final HttpClient.Builder builder = getHttpClientFactory().newBuilder()
.addOrReplaceInterceptor("after", new Interceptor() {
@Override
public void after(HttpResponse<?> response) {
responseFuture.complete(response);
}
});
// When
try (HttpClient client = builder.build()) {
client.consumeBytes(
client.newHttpRequestBuilder().uri(server.url("/client-error")).build(),
(s, ab) -> ab.consume())
.get(10, TimeUnit.SECONDS);
}
// Then
assertThat(responseFuture)
.succeedsWithin(1, TimeUnit.SECONDS)
.extracting(HttpResponse::code)
.isEqualTo(400);
}

@Test
@DisplayName("consumer, can encapsulate the original consumer to log response body")
public void consumerForLogging() throws Exception {
// Given
server.expect().withPath("/look-at-my-body").andReturn(200, "I\nHave\r\nNice Body\r").once();
final StringBuilder sb = new StringBuilder();
final HttpClient.Builder builder = getHttpClientFactory().newBuilder()
.addOrReplaceInterceptor("consumer", new Interceptor() {
@Override
public AsyncBody.Consumer<List<ByteBuffer>> consumer(AsyncBody.Consumer<List<ByteBuffer>> consumer,
HttpRequest request) {
return (value, asyncBody) -> {
value.stream().map(BufferUtil::copy).forEach(bb -> sb.append(StandardCharsets.UTF_8.decode(bb)));
consumer.consume(value, asyncBody);
};
}
});
String receivedResponse;
// When
try (HttpClient client = builder.build()) {
receivedResponse = client.sendAsync(
client.newHttpRequestBuilder().uri(server.url("/look-at-my-body")).build(),
String.class)
.get(10, TimeUnit.SECONDS).body();
}
// Then
assertThat(sb).hasToString("I\nHave\r\nNice Body\r").hasToString(receivedResponse);
}

@Test
@DisplayName("interceptors (HTTP) should be applied in the order they were added")
public void interceptorsHttpAreAppliedInOrder() throws Exception {
Expand Down

0 comments on commit 7102d7f

Please sign in to comment.