Skip to content

Commit ed2dce7

Browse files
edridudidudiedriclaude
authored
Mark client-generated results as synthetic (#211)
Step 3 of #6. The client returns a failed Result in three cases where no HTTP error occurred: processResponseGetOne 200 with an empty body -> code 404 processResponseGetOne 200 with a null body -> code 500 badRequestResult rejected before sending -> code 400 Those codes were indistinguishable from the same codes reported by Koios, so a caller branching on getCode() could not tell a real 404 from "the list came back empty", nor a real 400 from a request that never left the client. Adds Result.synthetic, true for exactly those three cases and false for every result reflecting an actual HTTP response. The codes themselves are unchanged, so existing callers keep working; the flag is there for callers that need the distinction. Co-authored-by: dudiedri <dudi.edri@ox.security> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent a29d132 commit ed2dce7

3 files changed

Lines changed: 39 additions & 3 deletions

File tree

‎src/main/java/rest/koios/client/backend/api/base/BaseService.java‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,9 +139,9 @@ protected <T> Result<T> processResponseGetOne(Call<List<T>> call) throws ApiExce
139139
if (response.body() != null && !response.body().isEmpty()) {
140140
return (Result<T>) Result.builder().successful(true).response(response.toString()).value(response.body().get(0)).code(response.code()).build();
141141
} else if (response.body() != null) {
142-
return (Result<T>) Result.builder().successful(false).response("Response Body is Empty").code(404).build();
142+
return (Result<T>) Result.builder().successful(false).response("Response Body is Empty").code(404).synthetic(true).build();
143143
} else {
144-
return (Result<T>) Result.builder().successful(false).response("Response Body is Invalid").code(500).build();
144+
return (Result<T>) Result.builder().successful(false).response("Response Body is Invalid").code(500).synthetic(true).build();
145145
}
146146
} else {
147147
String errorBody = Objects.requireNonNull(response.errorBody()).string();
@@ -174,7 +174,7 @@ private static KoiosError parseError(String body) {
174174
}
175175

176176
protected <T> Result<T> badRequestResult(String responseText) {
177-
return (Result<T>) Result.builder().successful(false).response(responseText).code(400).build();
177+
return (Result<T>) Result.builder().successful(false).response(responseText).code(400).synthetic(true).build();
178178
}
179179

180180
/**

‎src/main/java/rest/koios/client/backend/api/base/Result.java‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,19 @@ public class Result<T> {
2323
* Null on success, and null when the body was not JSON of that shape.
2424
*/
2525
KoiosError error;
26+
27+
/**
28+
* Whether this result was constructed by the client rather than reported by the server.
29+
* <p>
30+
* The client returns a failed result in a few cases where no HTTP error occurred at all - a
31+
* request rejected by local validation before it was sent, or a 200 response whose body was
32+
* empty or missing where a single object was expected. Those results still carry a
33+
* {@code code} so that callers branching on it keep working, but the code was chosen by the
34+
* client and never came from Koios.
35+
* <p>
36+
* False for every result that reflects an actual HTTP response, successful or not. Check this
37+
* before treating {@code code} as a server status.
38+
*/
39+
boolean synthetic;
2640
}
2741

‎src/test/java/rest/koios/client/backend/api/base/BaseServiceErrorHandlingTest.java‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@
77
import org.junit.jupiter.api.BeforeEach;
88
import org.junit.jupiter.api.Test;
99
import rest.koios.client.backend.api.base.exception.ApiException;
10+
import rest.koios.client.backend.api.asset.model.AssetInformation;
1011
import rest.koios.client.backend.api.network.model.Tip;
12+
import rest.koios.client.backend.factory.options.Options;
1113
import rest.koios.client.backend.factory.BackendFactory;
1214
import rest.koios.client.backend.factory.BackendService;
1315

1416
import java.io.IOException;
17+
import java.util.List;
1518

1619
/**
1720
* Error handling tests that run against a local {@link MockWebServer} instead of a live Koios
@@ -49,6 +52,19 @@ void successfulResponseIsReturnedTest() throws ApiException {
4952
Assertions.assertEquals("Conway", result.getValue().getEra());
5053
}
5154

55+
@Test
56+
void locallyRejectedRequestIsSyntheticTest() throws ApiException {
57+
// rejected by client-side validation, so no request is ever sent
58+
Result<List<AssetInformation>> result =
59+
backendService.getAssetService().getAssetInformationBulk(null, Options.EMPTY);
60+
61+
Assertions.assertFalse(result.isSuccessful());
62+
Assertions.assertEquals(400, result.getCode());
63+
Assertions.assertTrue(result.isSynthetic(), "no request was sent, so the 400 is the client's");
64+
Assertions.assertNull(result.getError());
65+
Assertions.assertEquals(0, server.getRequestCount(), "nothing should have been sent");
66+
}
67+
5268
@Test
5369
void successCarriesNoErrorTest() throws ApiException {
5470
server.enqueue(new MockResponse()
@@ -60,6 +76,7 @@ void successCarriesNoErrorTest() throws ApiException {
6076

6177
Assertions.assertTrue(result.isSuccessful());
6278
Assertions.assertNull(result.getError());
79+
Assertions.assertFalse(result.isSynthetic());
6380
}
6481

6582
@Test
@@ -91,6 +108,7 @@ void serverErrorBodyIsSurfacedTest() throws ApiException {
91108

92109
Assertions.assertFalse(result.isSuccessful());
93110
Assertions.assertEquals(400, result.getCode());
111+
Assertions.assertFalse(result.isSynthetic(), "a real server 400 is not synthetic");
94112
Assertions.assertEquals(postgrestError, result.getResponse());
95113
Assertions.assertNull(result.getValue());
96114
// the same body, parsed
@@ -110,6 +128,7 @@ void internalServerErrorIsSurfacedTest() throws ApiException {
110128

111129
Assertions.assertFalse(result.isSuccessful());
112130
Assertions.assertEquals(500, result.getCode());
131+
Assertions.assertFalse(result.isSynthetic(), "a real server 500 is not synthetic");
113132
Assertions.assertEquals("upstream failure", result.getResponse());
114133
// a plain-text body is not a structured error; the raw text is still available
115134
Assertions.assertNull(result.getError());
@@ -135,7 +154,10 @@ void emptyResponseBodyReportsNotFoundTest() throws ApiException {
135154
Result<Tip> result = backendService.getNetworkService().getChainTip();
136155

137156
Assertions.assertFalse(result.isSuccessful());
157+
// the server returned 200; this 404 is the client's, not Koios's
138158
Assertions.assertEquals(404, result.getCode());
159+
Assertions.assertTrue(result.isSynthetic(), "an empty body yields a client-generated code");
160+
Assertions.assertNull(result.getError());
139161
Assertions.assertEquals("Response Body is Empty", result.getResponse());
140162
}
141163

0 commit comments

Comments
 (0)