Skip to content

Commit d63af00

Browse files
committed
refactor: simplify error surface
1 parent 84546f3 commit d63af00

5 files changed

Lines changed: 64 additions & 180 deletions

File tree

README.md

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,21 +78,17 @@ try {
7878
facturapi.customers().retrieve("cus_123");
7979
} catch (FacturapiException e) {
8080
System.out.println(e.getMessage());
81-
82-
var apiError = e.getApiError();
83-
if (apiError != null) {
84-
System.out.println(apiError.getStatus());
85-
System.out.println(apiError.getCode());
86-
System.out.println(apiError.getPath());
87-
}
81+
System.out.println(e.getStatusCode());
82+
System.out.println(e.getErrorCode());
83+
System.out.println(e.getErrorPath());
8884
}
8985
```
9086

9187
## Design
9288

9389
- Inputs use flexible JSON dictionaries (`Map<String, Object>`).
9490
- Outputs are typed Java models (`Invoice`, `Customer`, `SearchResult<T>`, etc.).
95-
- Errors expose the API error payload when the response body is JSON.
91+
- Errors expose the useful API error fields directly on `FacturapiException`.
9692
- Auth uses `Authorization: Bearer <apiKey>`.
9793

9894
## Configuration
Lines changed: 12 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,37 @@
11
package io.facturapi;
22

3-
import io.facturapi.models.ApiError;
4-
53
public class FacturapiException extends RuntimeException {
6-
private final ApiError apiError;
74
private final int statusCode;
8-
private final String responseBody;
5+
private final Object errorCode;
6+
private final String errorPath;
97

108
public FacturapiException(String message) {
11-
super(message);
12-
this.apiError = null;
13-
this.statusCode = -1;
14-
this.responseBody = null;
9+
this(message, -1, null, null);
1510
}
1611

1712
public FacturapiException(String message, Throwable cause) {
1813
super(message, cause);
19-
this.apiError = null;
2014
this.statusCode = -1;
21-
this.responseBody = null;
15+
this.errorCode = null;
16+
this.errorPath = null;
2217
}
2318

24-
public FacturapiException(String message, int statusCode, String responseBody) {
19+
public FacturapiException(String message, int statusCode, Object errorCode, String errorPath) {
2520
super(message);
26-
this.apiError = null;
2721
this.statusCode = statusCode;
28-
this.responseBody = responseBody;
29-
}
30-
31-
public FacturapiException(ApiError apiError, String responseBody) {
32-
super(apiError != null && apiError.getMessage() != null && !apiError.getMessage().isBlank()
33-
? apiError.getMessage()
34-
: "An error occurred");
35-
this.apiError = apiError;
36-
this.statusCode = apiError != null && apiError.getStatus() != null ? apiError.getStatus() : -1;
37-
this.responseBody = responseBody;
22+
this.errorCode = errorCode;
23+
this.errorPath = errorPath;
3824
}
3925

4026
public int getStatusCode() {
4127
return statusCode;
4228
}
4329

44-
public String getResponseBody() {
45-
return responseBody;
46-
}
47-
48-
public ApiError getApiError() {
49-
return apiError;
30+
public Object getErrorCode() {
31+
return errorCode;
5032
}
5133

52-
public ApiError getError() {
53-
return apiError;
34+
public String getErrorPath() {
35+
return errorPath;
5436
}
5537
}

src/main/java/io/facturapi/http/FacturapiHttpClient.java

Lines changed: 45 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import com.fasterxml.jackson.databind.JsonNode;
55
import com.fasterxml.jackson.databind.ObjectMapper;
66
import io.facturapi.FacturapiException;
7-
import io.facturapi.models.ApiError;
87
import java.io.IOException;
98
import java.io.InputStream;
109
import java.net.URI;
@@ -147,7 +146,7 @@ private InputStream requestStream(String method, String path, Object body) {
147146
responseBytes = errorStream == null ? new byte[0] : errorStream.readAllBytes();
148147
}
149148
String bodyText = responseBytes.length == 0 ? "" : new String(responseBytes, StandardCharsets.UTF_8);
150-
throw new FacturapiException(parseApiError(bodyText, statusCode), bodyText);
149+
throw buildApiException(bodyText, statusCode);
151150
}
152151
return response.body();
153152
} catch (IOException e) {
@@ -202,64 +201,67 @@ private static JsonNode firstDefined(JsonNode node, String... keys) {
202201
return null;
203202
}
204203

205-
private ApiError parseApiError(String bodyText, int statusCode) {
206-
ApiError apiError = null;
204+
private FacturapiException buildApiException(String bodyText, int statusCode) {
205+
int resolvedStatus = statusCode;
206+
Object errorCode = null;
207+
String errorPath = null;
208+
String message = "Request failed with status " + statusCode;
209+
207210
if (bodyText != null && !bodyText.isBlank()) {
208211
try {
209-
apiError = objectMapper.readValue(bodyText, ApiError.class);
210-
} catch (Exception ignored) {
211-
try {
212-
JsonNode error = objectMapper.readTree(bodyText);
213-
apiError = new ApiError();
214-
215-
JsonNode messageNode = firstDefined(error, "message", "error", "detail");
212+
JsonNode root = objectMapper.readTree(bodyText);
213+
if (root != null) {
214+
JsonNode messageNode = firstDefined(root, "message", "error", "detail");
216215
if (messageNode != null && messageNode.isTextual()) {
217-
apiError.setMessage(messageNode.asText());
216+
message = messageNode.asText();
217+
} else if (root.isTextual()) {
218+
message = root.asText();
218219
} else {
219-
apiError.setMessage(bodyText);
220+
message = bodyText;
220221
}
221222

222-
JsonNode statusNode = firstDefined(error, "status");
223+
JsonNode statusNode = firstDefined(root, "status");
223224
if (statusNode != null) {
224-
apiError.setStatus(statusNode);
225+
if (statusNode.isIntegralNumber()) {
226+
resolvedStatus = statusNode.intValue();
227+
} else if (statusNode.isNumber()) {
228+
resolvedStatus = (int) Math.round(statusNode.asDouble());
229+
} else if (statusNode.isTextual()) {
230+
try {
231+
resolvedStatus = Integer.parseInt(statusNode.asText());
232+
} catch (NumberFormatException ignored) {
233+
resolvedStatus = statusCode;
234+
}
235+
}
225236
}
226237

227-
JsonNode codeNode = firstDefined(error, "code");
228-
if (codeNode != null) {
229-
apiError.setCode(codeNode);
238+
JsonNode codeNode = firstDefined(root, "code");
239+
if (codeNode != null && !codeNode.isNull()) {
240+
if (codeNode.isTextual()) {
241+
errorCode = codeNode.asText();
242+
} else if (codeNode.isIntegralNumber()) {
243+
errorCode = codeNode.intValue();
244+
} else if (codeNode.isNumber()) {
245+
errorCode = codeNode.numberValue();
246+
} else if (codeNode.isBoolean()) {
247+
errorCode = codeNode.asBoolean();
248+
} else {
249+
errorCode = codeNode.toString();
250+
}
230251
}
231252

232-
JsonNode pathNode = firstDefined(error, "path");
253+
JsonNode pathNode = firstDefined(root, "path");
233254
if (pathNode != null && pathNode.isTextual()) {
234-
apiError.setPath(pathNode.asText());
255+
errorPath = pathNode.asText();
235256
}
236257

237-
JsonNode okNode = firstDefined(error, "ok");
238-
if (okNode != null && okNode.isBoolean()) {
239-
apiError.setOk(okNode.asBoolean());
240-
}
241-
} catch (Exception secondaryIgnored) {
242-
apiError = new ApiError();
243-
apiError.setMessage(bodyText.isBlank() ? "Request failed with status " + statusCode : bodyText);
244258
}
259+
} catch (Exception ignored) {
260+
message = bodyText.isBlank() ? message : bodyText;
245261
}
246262
}
247263

248-
if (apiError == null) {
249-
apiError = new ApiError();
250-
apiError.setMessage("Request failed with status " + statusCode);
251-
}
252-
253-
if (apiError.getMessage() == null || apiError.getMessage().isBlank()) {
254-
apiError.setMessage("Request failed with status " + statusCode);
255-
}
256-
if (apiError.getStatus() == null) {
257-
apiError.setStatus(statusCode);
258-
}
259-
if (!apiError.isOk()) {
260-
apiError.setOk(false);
261-
}
262-
return apiError;
264+
return new FacturapiException(message, resolvedStatus, errorCode, errorPath);
263265
}
264266

265267
private HttpRequest buildRequest(
@@ -295,7 +297,7 @@ private void validateResponse(HttpResponse<byte[]> response) {
295297
int statusCode = response.statusCode();
296298
if (statusCode < 200 || statusCode >= 300) {
297299
String bodyText = response.body() == null ? "" : new String(response.body(), StandardCharsets.UTF_8);
298-
throw new FacturapiException(parseApiError(bodyText, statusCode), bodyText);
300+
throw buildApiException(bodyText, statusCode);
299301
}
300302
}
301303

src/main/java/io/facturapi/models/ApiError.java

Lines changed: 0 additions & 90 deletions
This file was deleted.

src/test/java/io/facturapi/FacturapiHttpClientTest.java

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
44
import static org.junit.jupiter.api.Assertions.assertEquals;
5-
import static org.junit.jupiter.api.Assertions.assertFalse;
6-
import static org.junit.jupiter.api.Assertions.assertNotNull;
75
import static org.junit.jupiter.api.Assertions.assertThrows;
86
import static org.junit.jupiter.api.Assertions.assertTrue;
97

@@ -52,7 +50,7 @@ void returnsBinaryBytesForPdf() {
5250
@Test
5351
void throwsFacturapiExceptionWithApiMessage() {
5452
StubHttpClient httpClient = new StubHttpClient();
55-
httpClient.enqueueJson(400, "{\"message\":\"Invalid customer\",\"status\":\"400\",\"ok\":false,\"code\":\"validation_error\",\"path\":\"customer.tax_id\"}");
53+
httpClient.enqueueJson(400, "{\"message\":\"Invalid customer\",\"status\":\"400\",\"code\":\"validation_error\",\"path\":\"customer.tax_id\"}");
5654

5755
FacturapiHttpClient client = new FacturapiHttpClient(
5856
FacturapiConfig.builder("sk_test_123")
@@ -67,11 +65,7 @@ void throwsFacturapiExceptionWithApiMessage() {
6765

6866
assertEquals(400, ex.getStatusCode());
6967
assertTrue(ex.getMessage().contains("Invalid customer"));
70-
assertNotNull(ex.getApiError());
71-
assertEquals("Invalid customer", ex.getApiError().getMessage());
72-
assertEquals(400, ex.getApiError().getStatus());
73-
assertFalse(ex.getApiError().isOk());
74-
assertEquals("validation_error", ex.getApiError().getCode());
75-
assertEquals("customer.tax_id", ex.getApiError().getPath());
68+
assertEquals("validation_error", ex.getErrorCode());
69+
assertEquals("customer.tax_id", ex.getErrorPath());
7670
}
7771
}

0 commit comments

Comments
 (0)