Skip to content

Commit

Permalink
Allow adding diagnostics data to exceptions
Browse files Browse the repository at this point in the history
  • Loading branch information
bannmann committed Nov 19, 2023
1 parent 2607d49 commit 88cae44
Show file tree
Hide file tree
Showing 11 changed files with 246 additions and 227 deletions.
118 changes: 118 additions & 0 deletions src/main/java/com/github/bannmann/restflow/AbstractRequester.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package com.github.bannmann.restflow;

import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import lombok.RequiredArgsConstructor;

import dev.failsafe.Failsafe;
import dev.failsafe.Policy;

@RequiredArgsConstructor
abstract class AbstractRequester<B, R> implements Requester<R>
{
protected final HttpRequest request;
protected final ClientConfig clientConfig;
protected final ConcurrentMap<String, Object> diagnosticsData = new ConcurrentHashMap<>();

@Override
public final CompletableFuture<R> start()
{
var diagnosticsDataSupplier = clientConfig.getDiagnosticsDataSupplier();
if (diagnosticsDataSupplier != null)
{
Map<String, Object> values = diagnosticsDataSupplier.get();
diagnosticsData.putAll(values);
}

return send().thenApply(this::extractValue);
}

private CompletableFuture<HttpResponse<B>> send()
{
List<Policy<HttpResponse<?>>> policies = clientConfig.getPolicies();
if (!policies.isEmpty())
{
return Failsafe.with(policies)
.getStageAsync(context -> sendOnce());
}

return sendOnce();
}

private CompletableFuture<HttpResponse<B>> sendOnce()
{
return clientConfig.getHttpClient()
.sendAsync(request, getBodyHandler())
.handle(this::addDetailsForLowLevelExceptions)
.thenApply(this::failOrPassThrough);
}

protected abstract HttpResponse.BodyHandler<B> getBodyHandler();

private <T> T addDetailsForLowLevelExceptions(T result, Throwable throwable)
{
if (throwable != null)
{
String message = String.format("Request to URL %s failed", request.uri());
throw new RequestFailureException(request, message, throwable, diagnosticsData);
}
return result;
}

private HttpResponse<B> failOrPassThrough(HttpResponse<B> response)
{
verifyNoErrors(response);
return response;
}

protected abstract void verifyNoErrors(HttpResponse<B> response);

protected abstract R extractValue(HttpResponse<B> response);

protected boolean isFailure(int responseStatus)
{
return !isSuccess(responseStatus);
}

protected boolean isSuccess(int responseStatus)
{
return responseStatus >= 200 && responseStatus < 300;
}

protected <T> RequestStatusException createException(HttpResponse<T> response)
{
int status = response.statusCode();
String body = getStringBody(response);
String message = String.format("Got status %d with message '%s' for %s %s",
status,
body,
request.method(),
request.uri());

return RequestStatusException.builder()
.message(message)
.request(request)
.response(response)
.status(status)
.body(body)
.diagnosticsData(diagnosticsData)
.build();
}

private <T> String getStringBody(HttpResponse<T> response)
{
T body = response.body();
if (body != null)
{
return body.toString()
.trim();
}
return null;
}
}
4 changes: 4 additions & 0 deletions src/main/java/com/github/bannmann/restflow/ClientConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import java.net.http.HttpClient;
import java.net.http.HttpResponse;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;

import javax.json.bind.Jsonb;

Expand All @@ -20,6 +22,8 @@
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public final class ClientConfig
{
private final Supplier<Map<String, Object>> diagnosticsDataSupplier;

private final @NonNull HttpClient httpClient;

@Singular
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public ExecuteHandle customizingRequest(RequestCustomizer requestCustomizer)

public CompletableFuture<Void> execute()
{
return Requesters.createRegular(requestSpecification)
return RegularRequester.forSpec(requestSpecification)
.start();
}
}
4 changes: 2 additions & 2 deletions src/main/java/com/github/bannmann/restflow/FetchHandle.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ public FetchHandle<R> customizingRequest(RequestCustomizer requestCustomizer)

public CompletableFuture<R> fetch()
{
return Requesters.createRegular(requestSpecification)
return RegularRequester.forSpec(requestSpecification)
.start();
}

public CompletableFuture<Optional<R>> tryFetch()
{
return Requesters.createOptional(requestSpecification)
return OptionalRequester.forSpec(requestSpecification)
.start();
}
}
54 changes: 54 additions & 0 deletions src/main/java/com/github/bannmann/restflow/OptionalRequester.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.github.bannmann.restflow;

import java.net.http.HttpResponse;
import java.util.Optional;

import com.github.mizool.core.rest.errorhandling.HttpStatus;

final class OptionalRequester<B, R> extends AbstractRequester<B, Optional<R>>
{
public static <B, R> Requester<Optional<R>> forSpec(RequestSpecification<B, R> spec)
{
return new OptionalRequester<>(spec);
}

private final RequestSpecification<B, R> spec;

private OptionalRequester(RequestSpecification<B, R> spec)
{
super(spec.createFinalRequest(), spec.getClientConfig());
this.spec = spec;
}

@Override
protected HttpResponse.BodyHandler<B> getBodyHandler()
{
return spec.getResponseBodyConfig()
.getBodyHandler();
}

@Override
protected void verifyNoErrors(HttpResponse<B> response)
{
int responseStatus = response.statusCode();
if (isFailure(responseStatus) && responseStatus != HttpStatus.NOT_FOUND)
{
throw createException(response);
}
}

@Override
protected Optional<R> extractValue(HttpResponse<B> response)
{
if (response.statusCode() == HttpStatus.NOT_FOUND)
{
return Optional.empty();
}

B body = response.body();
R result = spec.getResponseBodyConfig()
.getResponseConverter()
.apply(body);
return Optional.of(result);
}
}
44 changes: 44 additions & 0 deletions src/main/java/com/github/bannmann/restflow/RegularRequester.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.github.bannmann.restflow;

import java.net.http.HttpResponse;

final class RegularRequester<B, R> extends AbstractRequester<B, R>
{
public static <B, R> Requester<R> forSpec(RequestSpecification<B, R> spec)
{
return new RegularRequester<>(spec);
}

private final RequestSpecification<B, R> spec;

private RegularRequester(RequestSpecification<B, R> spec)
{
super(spec.createFinalRequest(), spec.getClientConfig());
this.spec = spec;
}

@Override
protected HttpResponse.BodyHandler<B> getBodyHandler()
{
return spec.getResponseBodyConfig()
.getBodyHandler();
}

@Override
protected void verifyNoErrors(HttpResponse<B> response)
{
int responseStatus = response.statusCode();
if (isFailure(responseStatus))
{
throw createException(response);
}
}

@Override
protected R extractValue(HttpResponse<B> response)
{
return spec.getResponseBodyConfig()
.getResponseConverter()
.apply(response.body());
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.github.bannmann.restflow;

import java.net.http.HttpRequest;
import java.util.Map;

import lombok.Getter;

Expand All @@ -12,10 +13,13 @@
public abstract class RequestException extends RuntimeException
{
private final transient HttpRequest request;
private final transient Map<String, Object> diagnosticsData;

protected RequestException(HttpRequest request, String message, Throwable cause)
protected RequestException(
HttpRequest request, String message, Throwable cause, Map<String, Object> diagnosticsData)
{
super(message, cause);
this.request = request;
this.diagnosticsData = diagnosticsData;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.github.bannmann.restflow;

import java.net.http.HttpRequest;
import java.util.Map;

/**
* Thrown when the request failed without a response.
Expand All @@ -9,8 +10,9 @@
*/
public class RequestFailureException extends RequestException
{
public RequestFailureException(HttpRequest request, String message, Throwable cause)
public RequestFailureException(
HttpRequest request, String message, Throwable cause, Map<String, Object> diagnosticsData)
{
super(request, message, cause);
super(request, message, cause, diagnosticsData);
}
}
10 changes: 5 additions & 5 deletions src/main/java/com/github/bannmann/restflow/RequestHandle.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public <T> FetchHandle<T> returning(Class<T> responseClass)
var responseBodyConfig = new ResponseBodyConfig<>(HttpResponse.BodyHandlers.ofString(),
s -> clientConfig.getJsonb()
.fromJson(s, responseClass));
var spec = new RequestSpecification<String, T>(request, responseBodyConfig, clientConfig);
var spec = new RequestSpecification<>(request, responseBodyConfig, clientConfig);
return new FetchHandle<>(spec);
}

Expand All @@ -34,7 +34,7 @@ public <T> FetchHandle<T> returning(Type runtimeType)
var responseBodyConfig = new ResponseBodyConfig<String, T>(HttpResponse.BodyHandlers.ofString(),
s -> clientConfig.getJsonb()
.fromJson(s, runtimeType));
var spec = new RequestSpecification<String, T>(request, responseBodyConfig, clientConfig);
var spec = new RequestSpecification<>(request, responseBodyConfig, clientConfig);
return new FetchHandle<>(spec);
}

Expand All @@ -56,23 +56,23 @@ public FetchHandle<JsonArray> returningJsonArray()
public FetchHandle<String> returningString()
{
var responseBodyConfig = new ResponseBodyConfig<>(HttpResponse.BodyHandlers.ofString(), string -> string);
var spec = new RequestSpecification<String, String>(request, responseBodyConfig, clientConfig);
var spec = new RequestSpecification<>(request, responseBodyConfig, clientConfig);
return new FetchHandle<>(spec);
}

public FetchHandle<InputStream> returningInputStream()
{
var responseBodyConfig = new ResponseBodyConfig<>(HttpResponse.BodyHandlers.ofInputStream(),
inputStream -> inputStream);
var spec = new RequestSpecification<InputStream, InputStream>(request, responseBodyConfig, clientConfig);
var spec = new RequestSpecification<>(request, responseBodyConfig, clientConfig);
return new FetchHandle<>(spec);
}

public ExecuteHandle returningNothing()
{
// Note: we use ofString() handler because discarding() would also discard the body of an error response.
var responseBodyConfig = new ResponseBodyConfig<String, Void>(HttpResponse.BodyHandlers.ofString(), v -> null);
var spec = new RequestSpecification<String, Void>(request, responseBodyConfig, clientConfig);
var spec = new RequestSpecification<>(request, responseBodyConfig, clientConfig);
return new ExecuteHandle(spec);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Map;

import lombok.Builder;
import lombok.Getter;
Expand All @@ -20,9 +21,15 @@ public class RequestStatusException extends RequestException

@Builder
public RequestStatusException(
String message, Throwable cause, HttpRequest request, HttpResponse<?> response, int status, String body)
String message,
Throwable cause,
HttpRequest request,
HttpResponse<?> response,
int status,
String body,
Map<String, Object> diagnosticsData)
{
super(request, message, cause);
super(request, message, cause, diagnosticsData);

this.response = response;
this.status = status;
Expand Down
Loading

0 comments on commit 88cae44

Please sign in to comment.