Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
5 changes: 5 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Contributing

## Generating Javadoc

Build the aggregated API reference locally with `just javadoc` (or `./gradlew aggregateJavadoc`). The HTML output is placed under `build/docs/javadoc/index.html`.
23 changes: 20 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,25 @@ readerIdFuture
.join();
```

## Handling errors

Catch `ApiStatusException` to inspect an unsuccessful API response:

```java
import com.sumup.sdk.core.ApiStatusException;

try {
client.checkouts().get("your-checkout-id");
} catch (ApiStatusException error) {
error.getProblem().ifPresent(problem ->
System.out.println(problem.type() + ": " + problem.detail()));
}
```

`getStatusCode()`, `getResponseBody()`, and `getResponseHeaders()` provide the HTTP
status, original response text, and headers. Connection failures raise
`ApiConnectionException`; timeouts raise `ApiTimeoutException`. Both keep the original cause.

## Handling events

```java
Expand Down Expand Up @@ -302,8 +321,6 @@ export SUMUP_MERCHANT_CODE="your_merchant_code"
./gradlew :examples:card-reader-checkout:run
```

## Generating Javadoc

Build the aggregated API reference locally with `just javadoc` (or `./gradlew aggregateJavadoc`). The HTML output is placed under `build/docs/javadoc/index.html`.
For development instructions, see [Contributing](CONTRIBUTING.md).

[docs-badge]: https://img.shields.io/badge/SumUp-documentation-white.svg?logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgY29sb3I9IndoaXRlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogICAgPHBhdGggZD0iTTIyLjI5IDBIMS43Qy43NyAwIDAgLjc3IDAgMS43MVYyMi4zYzAgLjkzLjc3IDEuNyAxLjcxIDEuN0gyMi4zYy45NCAwIDEuNzEtLjc3IDEuNzEtMS43MVYxLjdDMjQgLjc3IDIzLjIzIDAgMjIuMjkgMFptLTcuMjIgMTguMDdhNS42MiA1LjYyIDAgMCAxLTcuNjguMjQuMzYuMzYgMCAwIDEtLjAxLS40OWw3LjQ0LTcuNDRhLjM1LjM1IDAgMCAxIC40OSAwIDUuNiA1LjYgMCAwIDEtLjI0IDcuNjlabTEuNTUtMTEuOS03LjQ0IDcuNDVhLjM1LjM1IDAgMCAxLS41IDAgNS42MSA1LjYxIDAgMCAxIDcuOS03Ljk2bC4wMy4wM2MuMTMuMTMuMTQuMzUuMDEuNDlaIiBmaWxsPSJjdXJyZW50Q29sb3IiLz4KPC9zdmc+
2 changes: 1 addition & 1 deletion codegen/internal/generator/templates/model.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public record {{ .ClassName }}(
public {{ .ClassName }} {
additionalProperties = additionalProperties == null
? java.util.Map.of()
: java.util.Map.copyOf(additionalProperties);
: java.util.Collections.unmodifiableMap(new java.util.LinkedHashMap<>(additionalProperties));
}

@JsonAnyGetter
Expand Down
62 changes: 53 additions & 9 deletions src/main/java/com/sumup/sdk/core/ApiClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.sumup.sdk.models.Problem;
import java.io.IOException;
import java.lang.reflect.Array;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpTimeoutException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.OffsetDateTime;
Expand All @@ -23,7 +26,9 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;

/** Minimal HTTP client wrapper used by the generated SDK clients. */
public final class ApiClient {
Expand Down Expand Up @@ -75,15 +80,19 @@ public <T> T send(
RequestOptions requestOptions) {
HttpRequest request =
buildRequest(method, path, queryParams, headerParams, body, requestOptions);
HttpResponse<String> response;
try {
HttpResponse<String> response =
httpClient.send(request, HttpResponse.BodyHandlers.ofString());
return readResponse(response, responseType);
response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
} catch (IOException | InterruptedException e) {
if (e instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
throw new ApiException("Failed to execute request", e);
throw transportException(e);
}
try {
return readResponse(response, responseType);
} catch (IOException e) {
throw new ApiException("Failed to decode response", e);
}
}

Expand All @@ -109,12 +118,22 @@ public <T> CompletableFuture<T> sendAsync(
buildRequest(method, path, queryParams, headerParams, body, requestOptions);
return httpClient
.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(
response -> {
.handle(
(response, failure) -> {
if (failure != null) {
Throwable cause =
failure instanceof CompletionException && failure.getCause() != null
? failure.getCause()
: failure;
if (cause instanceof CancellationException cancelled) {
throw cancelled;
}
throw transportException(cause);
}
try {
return readResponse(response, responseType);
} catch (IOException e) {
throw new ApiException("Failed to execute request", e);
throw new ApiException("Failed to decode response", e);
}
});
}
Expand Down Expand Up @@ -169,7 +188,32 @@ private <T> T readResponse(HttpResponse<String> response, TypeReference<T> respo
}
return objectMapper.readValue(responseBody, responseType);
}
throw new ApiException("Request failed", status, response.body());
Problem problem = null;
try {
JsonNode json = objectMapper.readTree(response.body());
if (json != null && json.hasNonNull("type") && json.get("type").isTextual()) {
problem = objectMapper.treeToValue(json, Problem.class);
}
} catch (IOException | IllegalArgumentException ignored) {
// Preserve unrecognized or malformed bodies in the HTTP exception.
}
String message = "Request failed";
if (problem != null) {
if (problem.detail() != null && !problem.detail().isBlank()) {
message = problem.detail();
} else if (problem.title() != null && !problem.title().isBlank()) {
message = problem.title();
}
}
throw new ApiStatusException(
message, status, response.body(), problem, response.headers(), response.uri());
}

private static ApiConnectionException transportException(Throwable cause) {
if (cause instanceof HttpTimeoutException) {
return new ApiTimeoutException("Request timed out", cause);
}
return new ApiConnectionException("Failed to execute request", cause);
}

private Duration effectiveTimeout(RequestOptions requestOptions) {
Expand Down Expand Up @@ -319,7 +363,7 @@ private static ObjectMapper defaultObjectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
mapper.setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL);
return mapper;
}

Expand Down
8 changes: 8 additions & 0 deletions src/main/java/com/sumup/sdk/core/ApiConnectionException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.sumup.sdk.core;

/** The request could not be completed. */
public class ApiConnectionException extends ApiException {
public ApiConnectionException(String message, Throwable cause) {
super(message, cause);
}
}
41 changes: 40 additions & 1 deletion src/main/java/com/sumup/sdk/core/ApiException.java
Original file line number Diff line number Diff line change
@@ -1,22 +1,48 @@
package com.sumup.sdk.core;

import com.sumup.sdk.models.Problem;
import java.net.URI;
import java.net.http.HttpHeaders;
import java.util.List;
import java.util.Map;
import java.util.Optional;

/**
* Exception thrown when the SumUp API responds with a non-success status or the HTTP request fails.
*/
public final class ApiException extends RuntimeException {
public class ApiException extends RuntimeException {
private final int statusCode;
private final String responseBody;
private final Problem problem;
private final Map<String, List<String>> responseHeaders;
private final URI requestUri;

public ApiException(String message, int statusCode, String responseBody) {
this(message, statusCode, responseBody, null, null, null);
}

public ApiException(
String message,
int statusCode,
String responseBody,
Problem problem,
HttpHeaders headers,
URI requestUri) {
super(message + " (" + statusCode + ")");
this.statusCode = statusCode;
this.responseBody = responseBody;
this.problem = problem;
this.responseHeaders = headers == null ? Map.of() : headers.map();
this.requestUri = requestUri;
}

public ApiException(String message, Throwable cause) {
super(message, cause);
this.statusCode = -1;
this.responseBody = null;
this.problem = null;
this.responseHeaders = Map.of();
this.requestUri = null;
}

public int getStatusCode() {
Expand All @@ -26,4 +52,17 @@ public int getStatusCode() {
public String getResponseBody() {
return responseBody;
}

/** Typed Problem details, when the API returned a Problem response. */
public Optional<Problem> getProblem() {
return Optional.ofNullable(problem);
}

public Map<String, List<String>> getResponseHeaders() {
return responseHeaders;
}

public Optional<URI> getRequestUri() {
return Optional.ofNullable(requestUri);
}
}
18 changes: 18 additions & 0 deletions src/main/java/com/sumup/sdk/core/ApiStatusException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.sumup.sdk.core;

import com.sumup.sdk.models.Problem;
import java.net.URI;
import java.net.http.HttpHeaders;

/** The API returned an unsuccessful HTTP response. */
public final class ApiStatusException extends ApiException {
public ApiStatusException(
String message,
int status,
String body,
Problem problem,
HttpHeaders headers,
URI requestUri) {
super(message, status, body, problem, headers, requestUri);
}
}
8 changes: 8 additions & 0 deletions src/main/java/com/sumup/sdk/core/ApiTimeoutException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.sumup.sdk.core;

/** The request timed out. */
public class ApiTimeoutException extends ApiConnectionException {
public ApiTimeoutException(String message, Throwable cause) {
super(message, cause);
}
}
3 changes: 2 additions & 1 deletion src/main/java/com/sumup/sdk/models/Problem.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ public record Problem(
additionalProperties =
additionalProperties == null
? java.util.Map.of()
: java.util.Map.copyOf(additionalProperties);
: java.util.Collections.unmodifiableMap(
new java.util.LinkedHashMap<>(additionalProperties));
}

@JsonAnyGetter
Expand Down
Loading
Loading