Skip to content

Commit

Permalink
Merge pull request #575 from microsoft/feature/header-observe
Browse files Browse the repository at this point in the history
- adds headers inspection middleware
  • Loading branch information
baywet authored Aug 14, 2023
2 parents 4da7ad9 + f12e357 commit 1a1fc24
Show file tree
Hide file tree
Showing 11 changed files with 275 additions and 13 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

## [0.7.0] - 2023-08-18

### Added

- Added headers inspection option and handler.

## [0.6.0] - 2023-08-08

### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,18 @@
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;

class CaseInsensitiveMap implements Map<String, Set<String>>{
/**
* A map that is case insensitive on the keys
*/
public class CaseInsensitiveMap implements Map<String, Set<String>>{
private final HashMap<String, HashSet<String>> internalMap = new HashMap<>();

/**
* Formats the string to lower case
* @param key string to normalize to lower case
* @return The normalized string
*/
@Nonnull
protected String normalizeKey(@Nonnull final String key) {
Objects.requireNonNull(key);
return key.toLowerCase(Locale.ROOT);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,19 @@

import jakarta.annotation.Nonnull;

class Headers extends CaseInsensitiveMap {
/**
* A class representing the headers of a request or a response.
*/
public abstract class Headers extends CaseInsensitiveMap {
/** Default constructor */
public Headers() {
protected Headers() {
super();
}

/** Copy constructor */
public Headers(@Nonnull Headers headers) {
/** Copy constructor
* @param headers The headers to initialize with.
*/
protected Headers(@Nonnull Headers headers) {
super();
Objects.requireNonNull(headers);
putAll(headers);
Expand Down
5 changes: 5 additions & 0 deletions components/http/okHttp/spotBugsExcludeFilter.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@ xsi:schemaLocation="https://github.com/spotbugs/filter/3.0.0 https://raw.githubu
<Bug pattern="EI_EXPOSE_REP2" />
<Or>
<Class name="com.microsoft.kiota.http.middleware.UserAgentHandler" />
<Class name="com.microsoft.kiota.http.middleware.HeadersInspectionHandler" />
<Class name="com.microsoft.kiota.http.OkHttpRequestAdapter" />
</Or>
</Match>
<Match>
<Bug pattern="EI_EXPOSE_REP" />
<Class name="com.microsoft.kiota.http.middleware.options.HeadersInspectionOption" />
</Match>
<Match>
<Bug pattern="ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD" />
<Class name="com.microsoft.kiota.http.OkHttpRequestAdapter" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.microsoft.kiota.http.middleware.RedirectHandler;
import com.microsoft.kiota.http.middleware.RetryHandler;
import com.microsoft.kiota.http.middleware.UserAgentHandler;
import com.microsoft.kiota.http.middleware.HeadersInspectionHandler;
import com.microsoft.kiota.http.middleware.ParametersNameDecodingHandler;

import okhttp3.Interceptor;
Expand Down Expand Up @@ -50,7 +51,8 @@ public static Interceptor[] createDefaultInterceptors() {
new RedirectHandler(),
new RetryHandler(),
new ParametersNameDecodingHandler(),
new UserAgentHandler()
new UserAgentHandler(),
new HeadersInspectionHandler()
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.microsoft.kiota.http.middleware;

import jakarta.annotation.Nonnull;
import kotlin.Pair;

import java.io.IOException;
import java.util.Objects;
import java.util.Set;

import com.microsoft.kiota.http.middleware.options.HeadersInspectionOption;
import com.microsoft.kiota.http.middleware.options.RetryHandlerOption;

import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Scope;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;

/**
* The middleware responsible for inspecting the request and response headers
*/
public class HeadersInspectionHandler implements Interceptor {
/**
* Create a new instance of the HeadersInspectionHandler class with the default options
*/
public HeadersInspectionHandler() {
this(new HeadersInspectionOption());
}
/**
* Create a new instance of the HeadersInspectionHandler class with the provided options
* @param options The options to use for the handler
*/
public HeadersInspectionHandler(@Nonnull final HeadersInspectionOption options) {
this.options = Objects.requireNonNull(options);
}
private final HeadersInspectionOption options;

/** {@inheritDoc} */
@Nonnull
@Override
@SuppressWarnings("UnknownNullness")
public Response intercept(final Chain chain) throws IOException {
Objects.requireNonNull(chain, "parameter chain cannot be null");
Request request = chain.request();
HeadersInspectionOption inspectionOption = request.tag(HeadersInspectionOption.class);
if(inspectionOption == null) { inspectionOption = options; }
final Span span = ObservabilityHelper.getSpanForRequest(request, "HeadersInspectionHandler_Intercept");
Scope scope = null;
if (span != null) {
scope = span.makeCurrent();
span.setAttribute("com.microsoft.kiota.handler.headersInspection.enable", true);
}
try {
if (span != null) {
request = request.newBuilder().tag(Span.class, span).build();
}
if (inspectionOption.getInspectRequestHeaders()) {
for(final Pair<? extends String, ? extends String> header : request.headers()) {
inspectionOption.getRequestHeaders().put(header.getFirst(), Set.of(header.getSecond()));
}
}
final Response response = chain.proceed(request);
if (inspectionOption.getInspectResponseHeaders()) {
for(final Pair<? extends String, ? extends String> header : response.headers()) {
inspectionOption.getResponseHeaders().put(header.getFirst(), Set.of(header.getSecond()));
}
}
return response;
} finally {
if (scope != null) {
scope.close();
}
if (span != null) {
span.end();
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.microsoft.kiota.http.middleware.options;

import jakarta.annotation.Nonnull;

import com.microsoft.kiota.RequestHeaders;
import com.microsoft.kiota.RequestOption;
import com.microsoft.kiota.ResponseHeaders;

/**
* The options to be passed to the headers inspection middleware.
*/
public class HeadersInspectionOption implements RequestOption {
private boolean inspectRequestHeaders;
/**
* Gets whether to inspect request headers
* @return Whether to inspect request headers
*/
public boolean getInspectRequestHeaders() {
return inspectRequestHeaders;
}
/**
* Sets whether to inspect request headers
* @param inspectRequestHeaders Whether to inspect request headers
*/
public void setInspectRequestHeaders(boolean inspectRequestHeaders) {
this.inspectRequestHeaders = inspectRequestHeaders;
}

private boolean inspectResponseHeaders;
/**
* Gets whether to inspect response headers
* @return Whether to inspect response headers
*/
public boolean getInspectResponseHeaders() {
return inspectResponseHeaders;
}
/**
* Sets whether to inspect response headers
* @param inspectResponseHeaders Whether to inspect response headers
*/
public void setInspectResponseHeaders(boolean inspectResponseHeaders) {
this.inspectResponseHeaders = inspectResponseHeaders;
}

private final RequestHeaders requestHeaders = new RequestHeaders();
private final ResponseHeaders responseHeaders = new ResponseHeaders();
/**
* Create default instance of headers inspection options, with default values of inspectRequestHeaders and inspectResponseHeaders.
*/
public HeadersInspectionOption() {
this(false, false);
}
/**
* Create an instance with provided values
* @param shouldInspectResponseHeaders Whether to inspect response headers
* @param shouldInspectRequestHeaders Whether to inspect request headers
*/
public HeadersInspectionOption(final boolean shouldInspectRequestHeaders, final boolean shouldInspectResponseHeaders) {
this.inspectResponseHeaders = shouldInspectResponseHeaders;
this.inspectRequestHeaders = shouldInspectRequestHeaders;
}
/**
* Get the request headers
* @return The request headers
*/
@Nonnull
public RequestHeaders getRequestHeaders() {
return this.requestHeaders;
}
/**
* Get the response headers
* @return The response headers
*/
@Nonnull
public ResponseHeaders getResponseHeaders() {
return this.responseHeaders;
}

/** {@inheritDoc} */
@SuppressWarnings("unchecked")
@Override
@Nonnull
public <T extends RequestOption> Class<T> getType() {
return (Class<T>) HeadersInspectionOption.class;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public UserAgentHandlerOption() { }
@Nonnull
private String productName = "kiota-java";
@Nonnull
private String productVersion = "0.6.0";
private String productVersion = "0.7.0";
/**
* Gets the product name to be used in the user agent header
* @return the product name
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.microsoft.kiota.http;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.io.IOException;

import org.junit.jupiter.api.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

import com.microsoft.kiota.http.middleware.HeadersInspectionHandler;
import com.microsoft.kiota.http.middleware.options.HeadersInspectionOption;

import okhttp3.Headers;
import okhttp3.Interceptor.Chain;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;

class HeadersInspectionHandlerTest {
private final Chain mockChain;
private final Response mockResponse;
public HeadersInspectionHandlerTest() throws IOException {
mockResponse = mock(Response.class);
when(mockResponse.code()).thenReturn(200);
when(mockResponse.message()).thenReturn("OK");
when(mockResponse.body()).thenReturn(mock(ResponseBody.class));
when(mockResponse.headers()).thenReturn(new Headers.Builder().add("test", "test").build());
mockChain = mock(Chain.class);
when(mockChain.proceed(any(Request.class))).thenAnswer(new Answer<Response>() {
public Response answer(InvocationOnMock invocation) {
Object[] args = invocation.getArguments();
Request request = (Request) args[0];
when(mockResponse.request()).thenReturn(request);
return mockResponse;
}
});
}
@Test
void instantiatesWithDefaults() {
final HeadersInspectionHandler handler = new HeadersInspectionHandler();
assertNotNull(handler);
}
@Test
void getsRequestHeaders() throws IOException {
final HeadersInspectionOption option = new HeadersInspectionOption(true, false);
final HeadersInspectionHandler handler = new HeadersInspectionHandler(option);
final Request request = new Request.Builder().url("http://localhost").addHeader("test", "test").build();
when(mockChain.request()).thenReturn(request);
handler.intercept(mockChain);
assertNotNull(option.getRequestHeaders());
assertNotNull(option.getResponseHeaders());
assertEquals(1, option.getRequestHeaders().size());
assertEquals(0, option.getResponseHeaders().size());
assertEquals("test", option.getRequestHeaders().get("test").toArray()[0]);
}
@Test
void getsResponseHeaders() throws IOException {
final HeadersInspectionOption option = new HeadersInspectionOption(false, true);
final HeadersInspectionHandler handler = new HeadersInspectionHandler(option);
final Request request = new Request.Builder().url("http://localhost").addHeader("test", "test").build();
when(mockChain.request()).thenReturn(request);
handler.intercept(mockChain);
assertNotNull(option.getRequestHeaders());
assertNotNull(option.getResponseHeaders());
assertEquals(0, option.getRequestHeaders().size());
assertEquals(1, option.getResponseHeaders().size());
assertEquals("test", option.getResponseHeaders().get("test").toArray()[0]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import com.microsoft.kiota.http.middleware.UserAgentHandler;
import com.microsoft.kiota.http.middleware.options.UserAgentHandlerOption;

public class UserAgentHandlerTest {
class UserAgentHandlerTest {
private final Chain mockChain;
private final Response mockResponse;
public UserAgentHandlerTest() throws IOException {
Expand All @@ -39,10 +39,10 @@ public Object answer(InvocationOnMock invocation) {
when(mockResponse.request()).thenReturn(request);
return mockResponse;
}
});
});
}
@Test
public void addsTheProduct() throws IOException {
void addsTheProduct() throws IOException {
final UserAgentHandler handler = new UserAgentHandler();
final Request request = new Request.Builder().url("http://localhost").build();
when(mockChain.request()).thenReturn(request);
Expand All @@ -53,7 +53,7 @@ public void addsTheProduct() throws IOException {
assertEquals("kiota-java", result.header("User-Agent").split("/")[0]);
}
@Test
public void addsTheProductOnce() throws IOException {
void addsTheProductOnce() throws IOException {
final UserAgentHandler handler = new UserAgentHandler();
final Request request = new Request.Builder().url("http://localhost").build();
when(mockChain.request()).thenReturn(request);
Expand All @@ -65,7 +65,7 @@ public void addsTheProductOnce() throws IOException {
assertEquals(1, result.header("User-Agent").split("kiota-java").length - 1);
}
@Test
public void doesNotAddTheProductWhenDisabled() throws IOException {
void doesNotAddTheProductWhenDisabled() throws IOException {
final UserAgentHandler handler = new UserAgentHandler(new UserAgentHandlerOption() {{ setEnabled(false); }});
final Request request = new Request.Builder().url("http://localhost").build();
when(mockChain.request()).thenReturn(request);
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ org.gradle.caching=true

mavenGroupId = com.microsoft.kiota
mavenMajorVersion = 0
mavenMinorVersion = 6
mavenMinorVersion = 7
mavenPatchVersion = 0
mavenArtifactSuffix =

Expand Down

0 comments on commit 1a1fc24

Please sign in to comment.