Skip to content

Commit

Permalink
Java HTTP Fault Injector Sample Using Storage Blobs (#1684)
Browse files Browse the repository at this point in the history
Java HTTP Fault Injector Sample Using Storage Blobs
  • Loading branch information
alzimmermsft authored Jun 10, 2021
1 parent 98a65a4 commit 54285e3
Show file tree
Hide file tree
Showing 6 changed files with 267 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ public class App {
public static void main(String[] args) throws Exception {
HttpClient httpClient = HttpClient.create();

// You must either add the .NET developer certiifcate to the Java cacerts keystore, or uncomment the following
// You must either add the .NET developer certificate to the Java cacerts keystore, or uncomment the following
// lines to disable SSL validation.
//
// io.netty.handler.ssl.SslContext sslContext = io.netty.handler.ssl.SslContextBuilder
// .forClient().trustManager(io.netty.handler.ssl.util.InsecureTrustManagerFactory.INSTANCE).build();
// httpClient = httpClient.secure(sslContextBuilder -> sslContextBuilder.sslContext(sslContext));

System.out.println("Sending request...");

HttpClientResponse response = get(httpClient, "https://www.example.org").block();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.example</groupId>
<artifactId>storage-blobs</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-storage-blob</artifactId>
<version>12.12.0</version>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import com.azure.core.http.HttpClient;
import com.azure.core.util.BinaryData;
import com.azure.core.util.Configuration;
import com.azure.storage.blob.BlobClient;
import com.azure.storage.blob.BlobClientBuilder;
import com.azure.storage.common.policy.RequestRetryOptions;
import com.azure.storage.common.policy.RetryPolicyType;

import java.time.Duration;

/**
* This is a sample application using azure-storage-blob to send requests to the HTTP fault injector. All concepts
* presented here are applicable to all Azure SDKs which use HTTP as its network transport.
*/
public class App {
public static void main(String[] args) {
HttpClient httpClient = HttpClient.createDefault();

// You must either add the .NET developer certificate to the Java cacerts keystore, or uncomment the following
// lines to disable SSL validation if using Netty/Reactor Netty as the underlying HttpClient.
//
// io.netty.handler.ssl.SslContext sslContext = io.netty.handler.ssl.SslContextBuilder
// .forClient().trustManager(io.netty.handler.ssl.util.InsecureTrustManagerFactory.INSTANCE).build();
// httpClient = httpClient.secure(sslContextBuilder -> sslContextBuilder.sslContext(sslContext));

BlobClient blobClient = new BlobClientBuilder()
.connectionString(Configuration.getGlobalConfiguration().get("STORAGE_CONNECTION_STRING"))
.containerName("sample")
.blobName("sample.txt")
.retryOptions(new RequestRetryOptions(RetryPolicyType.FIXED, 3, Duration.ofMinutes(1),
Duration.ofSeconds(1), Duration.ofSeconds(1), null))
// .httpClient(new FaultInjectorHttpClient(httpClient)) // Using an HttpClient is also a valid option.
.addPolicy(new FaultInjectorUrlRewriterPolicy())
.buildClient();

System.out.println("Sending request...");
BinaryData content = blobClient.downloadContent();
System.out.printf("Content: %s%n", content);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import com.azure.core.http.HttpClient;
import com.azure.core.http.HttpRequest;
import com.azure.core.http.HttpResponse;
import com.azure.core.util.Context;
import reactor.core.publisher.Mono;

import java.util.Objects;

/**
* General purpose {@link HttpClient} which re-writes request URLs to use the HTTP fault injector before using an
* underlying {@link HttpClient} to send the network request.
*/
public final class FaultInjectorHttpClient implements HttpClient {
private final HttpClient httpClient;
private final String host;
private final int port;

/**
* Default constructor for {@link FaultInjectorHttpClient} which expects the HTTP fault injector to use {@link
* Utils#DEFAULT_HTTP_FAULT_INJECTOR_HOST} and running on port {@link Utils#DEFAULT_HTTP_FAULT_INJECTOR_HTTPS_PORT}.
*
* @param httpClient The underlying {@link HttpClient} used to make network requests.
*/
public FaultInjectorHttpClient(HttpClient httpClient) {
this(httpClient, Utils.DEFAULT_HTTP_FAULT_INJECTOR_HOST, Utils.DEFAULT_HTTP_FAULT_INJECTOR_HTTPS_PORT);
}

/**
* Constructor for {@link FaultInjectorHttpClient} which allows for the configuration of which {@code host} and
* {@code port} the HTTP fault injector is using.
*
* @param httpClient The underlying {@link HttpClient} used to make network requests.
* @param host The host HTTP fault injector is running on.
* @param port The port HTTP fault injector is using.
* @throws NullPointerException If {@code httpClient} or {@code host} is null.
* @throws IllegalArgumentException If {@code host} is an empty string or {@code port} is an invalid port.
*/
public FaultInjectorHttpClient(HttpClient httpClient, String host, int port) {
this.httpClient = Objects.requireNonNull(httpClient, "'httpClient' cannot be null.");
Utils.validateHostAndPort(host, port);

this.host = host;
this.port = port;
}

@Override
public Mono<HttpResponse> send(HttpRequest request) {
return send(request, Context.NONE);
}

@Override
public Mono<HttpResponse> send(HttpRequest request, Context context) {
return Mono.defer(() -> Mono.fromCallable(() -> Utils.rewriteUrlToUseFaultInjector(request, host, port)))
.flatMap(rewrittenHttpRequest -> httpClient.send(rewrittenHttpRequest, context));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import com.azure.core.http.HttpPipelineCallContext;
import com.azure.core.http.HttpPipelineNextPolicy;
import com.azure.core.http.HttpPipelinePosition;
import com.azure.core.http.HttpResponse;
import com.azure.core.http.policy.HttpPipelinePolicy;
import reactor.core.publisher.Mono;

/**
* General purpose {@link HttpPipelinePolicy} which re-writes the request URL to send it to the HTTP fault injector.
*/
public final class FaultInjectorUrlRewriterPolicy implements HttpPipelinePolicy {
private final String host;
private final int port;

/**
* Default constructor for {@link FaultInjectorUrlRewriterPolicy} which expects the HTTP fault injector to use
* {@link Utils#DEFAULT_HTTP_FAULT_INJECTOR_HOST} and running on port
* {@link Utils#DEFAULT_HTTP_FAULT_INJECTOR_HTTPS_PORT}.
*/
public FaultInjectorUrlRewriterPolicy() {
this(Utils.DEFAULT_HTTP_FAULT_INJECTOR_HOST, Utils.DEFAULT_HTTP_FAULT_INJECTOR_HTTPS_PORT);
}

/**
* Constructor used to configure re-writing the request URL to the non-default HTTP fault injector host and port.
*
* @param host The host HTTP fault injector is running on.
* @param port The port HTTP fault injector is using.
* @throws NullPointerException If {@code host} is null.
* @throws IllegalArgumentException If {@code host} is an empty string or {@code port} is an invalid port.
*/
public FaultInjectorUrlRewriterPolicy(String host, int port) {
Utils.validateHostAndPort(host, port);

this.host = host;
this.port = port;
}

@Override
public Mono<HttpResponse> process(HttpPipelineCallContext context, HttpPipelineNextPolicy next) {
context.setHttpRequest(Utils.rewriteUrlToUseFaultInjector(context.getHttpRequest(), host, port));

return next.process();
}

@Override
public HttpPipelinePosition getPipelinePosition() {
// The policy should be ran per retry in case calls are made to a secondary, fail-over host.
return HttpPipelinePosition.PER_RETRY;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import com.azure.core.http.HttpRequest;

import java.net.URI;
import java.util.Objects;

/**
* Utility class containing constants and methods for re-writing {@link HttpRequest HttpRequests} to use the HTTP fault
* injector.
*/
public final class Utils {
/**
* The default host used by HTTP fault injector.
*/
public static final String DEFAULT_HTTP_FAULT_INJECTOR_HOST = "localhost";

/**
* The default HTTP port used by HTTP fault injector.
*/
public static final int DEFAULT_HTTP_FAULT_INJECTOR_HTTP_PORT = 7777;

/**
* The default HTTPS port used by HTTP fault injector.
*/
public static final int DEFAULT_HTTP_FAULT_INJECTOR_HTTPS_PORT = 7778;

/**
* The HTTP header used by HTTP fault injector to determine where it needs to forward a request.
*/
public static final String HTTP_FAULT_INJECTOR_UPSTREAM_HOST_HEADER = "X-Upstream-Host";

/**
* Utility method which re-writes the {@link HttpRequest HttpRequest's} URL to use the HTTP fault injector.
* <p>
* This will set the HTTP header {@link #HTTP_FAULT_INJECTOR_UPSTREAM_HOST_HEADER} to the request URL used by the
* HTTP request before re-writing and will update the request URL to use the HTTP fault injector.
*
* @param request The {@link HttpRequest} having its URL re-written.
* @param host The HTTP fault injector host.
* @param port The HTTP fault injector port.
* @return The updated {@link HttpRequest} with its URL re-written.
* @throws NullPointerException If {@code request} or {@code host} are null.
* @throws IllegalArgumentException If {@code host} is an empty string or {@code port} is
* an invalid port.
* @throws IllegalStateException If the request URL isn't valid or the HTTP fault injector URL isn't valid.
*/
public static HttpRequest rewriteUrlToUseFaultInjector(HttpRequest request, String host, int port) {
validateHostAndPort(host, port);

try {
URI requestUri = request.getUrl().toURI();
URI faultInjectorUri = new URI(requestUri.getScheme(), requestUri.getUserInfo(), host,
port, requestUri.getPath(), requestUri.getQuery(), requestUri.getFragment());

String xUpstreamHost = (requestUri.getPort() < 0)
? requestUri.getHost()
: requestUri.getHost() + ":" + requestUri.getPort();

return request.setHeader(HTTP_FAULT_INJECTOR_UPSTREAM_HOST_HEADER, xUpstreamHost)
.setUrl(faultInjectorUri.toURL());
} catch (Exception exception) {
throw new IllegalStateException(exception);
}
}

/*
* Helper method for validating the HTTP fault injector host and port.
*/
static void validateHostAndPort(String host, int port) {
Objects.requireNonNull(host, "'host' cannot be null.");

if (host.isEmpty()) {
throw new IllegalArgumentException("'host' must be a non-empty string.");
}

if (port < 1 || port > 65535) {
throw new IllegalArgumentException("'port' must be a valid port number.");
}
}

private Utils() { }
}

0 comments on commit 54285e3

Please sign in to comment.