Skip to content

Handle API exceptions in upload and download operations#38

Closed
Fivell wants to merge 5 commits intomainfrom
fix/upload-download-api-exception
Closed

Handle API exceptions in upload and download operations#38
Fivell wants to merge 5 commits intomainfrom
fix/upload-download-api-exception

Conversation

@Fivell
Copy link
Member

@Fivell Fivell commented Mar 10, 2026

Summary

  • Upload and download now throw DidwwApiException with parsed JSON:API errors instead of generic DidwwClientException
  • Added parseApiException() helper for consistent error handling

Test plan

  • Tests for upload API error with JSON and non-JSON bodies
  • Tests for download API error propagation
  • All tests pass

@Fivell Fivell requested a review from Copilot March 10, 2026 22:37
@Fivell Fivell force-pushed the fix/upload-download-api-exception branch from 420e25c to 75f7060 Compare March 10, 2026 22:39
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the SDK’s upload/download operations to raise DidwwApiException with parsed JSON:API errors details (when available), enabling callers to handle API failures consistently instead of receiving generic DidwwClientExceptions.

Changes:

  • Updated uploadEncryptedFile() and downloadExport() to throw DidwwApiException on non-2xx responses.
  • Added parseApiException() helper in DidwwClient to parse JSON:API errors arrays and fall back to raw body text.
  • Added tests covering API error propagation for export download/decompress and encrypted file upload (JSON and non-JSON error bodies).

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
src/main/java/com/didww/sdk/DidwwClient.java Introduces parseApiException() and switches upload/download error handling to DidwwApiException.
src/test/java/com/didww/sdk/EncryptedFileUploadTest.java Adds coverage for upload API failures returning JSON:API errors and plain-text bodies.
src/test/java/com/didww/sdk/resource/ExportTest.java Adds coverage for download and download+decompress API error propagation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +316 to +336
private DidwwApiException parseApiException(int httpStatus, String body) {
try {
JsonNode root = objectMapper.readTree(body);
JsonNode errorsNode = root.get("errors");
if (errorsNode != null && errorsNode.isArray()) {
List<DidwwApiException.ApiError> errors = new ArrayList<>();
for (JsonNode errorNode : errorsNode) {
DidwwApiException.ApiError error = objectMapper.treeToValue(
errorNode, DidwwApiException.ApiError.class);
errors.add(error);
}
if (!errors.isEmpty()) {
return new DidwwApiException(httpStatus, errors);
}
}
} catch (Exception ignored) {
}
return new DidwwApiException(httpStatus, body.isEmpty()
? "HTTP " + httpStatus
: body);
}
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parseApiException() may produce an unhelpful exception message of ...: null when the JSON:API error object omits detail (e.g., the added test uses only title). DidwwApiException(List<ApiError>) currently builds its message from getDetail(), so consider normalizing parsed errors (e.g., if detail is blank, set it from title/status) or otherwise ensure the resulting exception message is meaningful.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

}
} catch (Exception ignored) {
}
return new DidwwApiException(httpStatus, body.isEmpty()
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In parseApiException(), the fallback message uses body.isEmpty(). If the response body is whitespace-only (e.g., "\n"), this will create a DidwwApiException with a blank message. Consider treating blank bodies as empty (e.g., trim/isBlank) so the fallback message becomes something like "HTTP ".

Suggested change
return new DidwwApiException(httpStatus, body.isEmpty()
return new DidwwApiException(httpStatus, (body == null || body.trim().isEmpty())

Copilot uses AI. Check for mistakes.
Comment on lines +316 to +332
private DidwwApiException parseApiException(int httpStatus, String body) {
try {
JsonNode root = objectMapper.readTree(body);
JsonNode errorsNode = root.get("errors");
if (errorsNode != null && errorsNode.isArray()) {
List<DidwwApiException.ApiError> errors = new ArrayList<>();
for (JsonNode errorNode : errorsNode) {
DidwwApiException.ApiError error = objectMapper.treeToValue(
errorNode, DidwwApiException.ApiError.class);
errors.add(error);
}
if (!errors.isEmpty()) {
return new DidwwApiException(httpStatus, errors);
}
}
} catch (Exception ignored) {
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parseApiException() duplicates the JSON:API error parsing logic already present in ReadOnlyRepository.handleErrorResponse(). To keep error parsing consistent across the SDK, consider extracting this into a shared helper (e.g., a small utility or a static method on DidwwApiException) and reusing it in both places.

Suggested change
private DidwwApiException parseApiException(int httpStatus, String body) {
try {
JsonNode root = objectMapper.readTree(body);
JsonNode errorsNode = root.get("errors");
if (errorsNode != null && errorsNode.isArray()) {
List<DidwwApiException.ApiError> errors = new ArrayList<>();
for (JsonNode errorNode : errorsNode) {
DidwwApiException.ApiError error = objectMapper.treeToValue(
errorNode, DidwwApiException.ApiError.class);
errors.add(error);
}
if (!errors.isEmpty()) {
return new DidwwApiException(httpStatus, errors);
}
}
} catch (Exception ignored) {
}
private static List<DidwwApiException.ApiError> extractApiErrors(String body, ObjectMapper objectMapper) {
List<DidwwApiException.ApiError> errors = new ArrayList<>();
try {
JsonNode root = objectMapper.readTree(body);
JsonNode errorsNode = root.get("errors");
if (errorsNode != null && errorsNode.isArray()) {
for (JsonNode errorNode : errorsNode) {
DidwwApiException.ApiError error =
objectMapper.treeToValue(errorNode, DidwwApiException.ApiError.class);
if (error != null) {
errors.add(error);
}
}
}
} catch (Exception ignored) {
// If parsing fails, fall back to using the raw body as the exception message.
}
return errors;
}
private DidwwApiException parseApiException(int httpStatus, String body) {
List<DidwwApiException.ApiError> errors = extractApiErrors(body, objectMapper);
if (!errors.isEmpty()) {
return new DidwwApiException(httpStatus, errors);
}

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new DidwwClientException("Failed to download export: HTTP " + response.code());
throw new DidwwApiException(response.code(), "HTTP " + response.code());
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In downloadExport(), unsuccessful responses currently throw a DidwwApiException with only "HTTP " and do not read/parse the response body. This loses JSON:API error details (and is inconsistent with uploadEncryptedFile()/parseApiException and repository error handling). Consider reading the error response body when !response.isSuccessful() and throwing parseApiException(response.code(), body) so callers get parsed errors / a more informative message.

Suggested change
throw new DidwwApiException(response.code(), "HTTP " + response.code());
String errorBody = null;
if (response.body() != null) {
errorBody = response.body().string();
}
throw parseApiException(response.code(), errorBody);

Copilot uses AI. Check for mistakes.
Comment on lines +315 to +319
private DidwwApiException parseApiException(int httpStatus, String body) {
try {
JsonNode root = objectMapper.readTree(body);
JsonNode errorsNode = root.get("errors");
if (errorsNode != null && errorsNode.isArray()) {
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parseApiException() duplicates the JSON:API error parsing logic that already exists in ReadOnlyRepository.handleErrorResponse() (src/main/java/com/didww/sdk/repository/ReadOnlyRepository.java:91-110). This duplication risks the two implementations drifting over time. Consider extracting the parsing into a shared utility (e.g., a static helper on DidwwApiException or a small internal util class) and reusing it from both places.

Copilot uses AI. Check for mistakes.
if (title != null && !title.isBlank()) {
return title;
}
return "HTTP " + httpStatus;
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DidwwApiException.buildMessage() already includes the HTTP status in the prefix ("DIDWW API error (HTTP )"). When an ApiError has neither detail nor title, errorMessage() falls back to "HTTP ", producing redundant messages like "... (HTTP 404): HTTP 404". Consider using a non-duplicative fallback (e.g., error.getStatus()/error.getCode(), or a generic "Unknown error") instead.

Suggested change
return "HTTP " + httpStatus;
String status = error.getStatus();
if (status != null && !status.isBlank()) {
return status;
}
String code = error.getCode();
if (code != null && !code.isBlank()) {
return code;
}
return "Unknown error";

Copilot uses AI. Check for mistakes.
@Fivell Fivell force-pushed the fix/upload-download-api-exception branch from 2797fc1 to 3b46af0 Compare March 11, 2026 16:21
@Fivell Fivell requested a review from Copilot March 11, 2026 16:31
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +47 to +57
private static String errorMessage(ApiError error, int httpStatus) {
String detail = error.getDetail();
if (detail != null && !detail.isBlank()) {
return detail;
}
String title = error.getTitle();
if (title != null && !title.isBlank()) {
return title;
}
return "HTTP " + httpStatus;
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

errorMessage falls back to returning "HTTP <status>" when both detail and title are blank. Since buildMessage already includes the HTTP status prefix, this can produce redundant messages like "DIDWW API error (HTTP 404): HTTP 404". Consider returning a non-redundant placeholder (or an empty string and skipping it) so the message stays informative without repeating the status code.

Copilot uses AI. Check for mistakes.
Fivell added 5 commits March 11, 2026 22:24
When a JSON:API error has title but no detail, the exception message
showed "null". Now falls back to title when detail is null/blank,
and to HTTP status code when both are absent.
Download export errors return empty body with text/html Content-Type,
not JSON:API error responses. Only upload errors return proper JSON:API
errors, so keep parseApiException for upload only.
@Fivell Fivell force-pushed the fix/upload-download-api-exception branch from 4a22cb1 to a3cb1b4 Compare March 11, 2026 21:24
@sonarqubecloud
Copy link

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

src/main/java/com/didww/sdk/repository/ReadOnlyRepository.java:96

  • When the response is unsuccessful and the body is empty (e.g., no body or 0-length body), this throws new DidwwApiException(response.code(), body) with an empty message. Consider defaulting to a helpful message like "HTTP <status>" (similar to DidwwClient#parseApiException) when body is blank so callers don’t get a blank exception message.
            String body = response.body() != null ? response.body().string() : "";
            List<DidwwApiException.ApiError> errors = DidwwApiException.extractApiErrors(body, objectMapper);
            if (errors.isEmpty()) {
                throw new DidwwApiException(response.code(), body);
            }
            throw new DidwwApiException(response.code(), errors);

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +63 to +78
public static List<ApiError> extractApiErrors(String body, ObjectMapper objectMapper) {
List<ApiError> errors = new ArrayList<>();
try {
JsonNode root = objectMapper.readTree(body);
JsonNode errorsNode = root.get("errors");
if (errorsNode != null && errorsNode.isArray()) {
for (JsonNode errorNode : errorsNode) {
ApiError error = objectMapper.treeToValue(errorNode, ApiError.class);
if (error != null) {
errors.add(error);
}
}
}
} catch (Exception ignored) {
}
return errors;
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extractApiErrors relies on objectMapper.readTree(body) throwing for null/blank/non-JSON bodies and then swallowing the exception. Adding an early return when body is null/blank (and ideally narrowing the catch to Jackson/IO parsing exceptions) would avoid using exceptions for control flow and reduce the risk of hiding unexpected failures.

Copilot uses AI. Check for mistakes.
Comment on lines +47 to +57
private static String errorMessage(ApiError error, int httpStatus) {
String detail = error.getDetail();
if (detail != null && !detail.isBlank()) {
return detail;
}
String title = error.getTitle();
if (title != null && !title.isBlank()) {
return title;
}
return "Unknown error";
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

errorMessage(ApiError error, int httpStatus) doesn’t use the httpStatus parameter. Dropping the unused parameter (and updating the call site) will avoid compiler/IDE warnings and keep the helper’s contract minimal.

Copilot uses AI. Check for mistakes.
@Fivell
Copy link
Member Author

Fivell commented Mar 11, 2026

Closing this PR after testing actual DIDWW API error responses:

  • Upload 422: returns Rails-style validation hash ({"errors":{"base":["outdated fingerprint"]}}) — not JSON:API format
  • Upload 401: returns JSON:API but is already handled by the auth interceptor before reaching upload code
  • Download 404/401: returns empty HTML body

JSON:API error parsing in upload/download adds no practical value since the API doesn't return JSON:API errors for these endpoints.

@Fivell Fivell closed this Mar 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants