Handle API exceptions in upload and download operations#38
Handle API exceptions in upload and download operations#38
Conversation
420e25c to
75f7060
Compare
There was a problem hiding this comment.
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()anddownloadExport()to throwDidwwApiExceptionon non-2xx responses. - Added
parseApiException()helper inDidwwClientto parse JSON:APIerrorsarrays 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.
| 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); | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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 ".
| return new DidwwApiException(httpStatus, body.isEmpty() | |
| return new DidwwApiException(httpStatus, (body == null || body.trim().isEmpty()) |
| 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) { | ||
| } |
There was a problem hiding this comment.
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.
| 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); | |
| } |
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
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.
| throw new DidwwApiException(response.code(), "HTTP " + response.code()); | |
| String errorBody = null; | |
| if (response.body() != null) { | |
| errorBody = response.body().string(); | |
| } | |
| throw parseApiException(response.code(), errorBody); |
| private DidwwApiException parseApiException(int httpStatus, String body) { | ||
| try { | ||
| JsonNode root = objectMapper.readTree(body); | ||
| JsonNode errorsNode = root.get("errors"); | ||
| if (errorsNode != null && errorsNode.isArray()) { |
There was a problem hiding this comment.
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.
| if (title != null && !title.isBlank()) { | ||
| return title; | ||
| } | ||
| return "HTTP " + httpStatus; |
There was a problem hiding this comment.
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.
| 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"; |
2797fc1 to
3b46af0
Compare
There was a problem hiding this comment.
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.
| 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; | ||
| } |
There was a problem hiding this comment.
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.
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.
…body in parseApiException
4a22cb1 to
a3cb1b4
Compare
|
There was a problem hiding this comment.
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 toDidwwClient#parseApiException) whenbodyis 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.
| 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; |
There was a problem hiding this comment.
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.
| 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"; | ||
| } |
There was a problem hiding this comment.
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.
|
Closing this PR after testing actual DIDWW API error responses:
JSON:API error parsing in upload/download adds no practical value since the API doesn't return JSON:API errors for these endpoints. |



Summary
Test plan