Skip to content

Commit c4caac3

Browse files
SK-3131 update the response strcuture
1 parent c74d242 commit c4caac3

3 files changed

Lines changed: 238 additions & 0 deletions

File tree

flowvault/src/main/java/com/skyflow/utils/Utils.java

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,131 @@ public static ErrorRecord createErrorRecord(Map<String, Object> recordMap, int i
539539
return err;
540540
}
541541

542+
// ── Unary "records"-shaped exception fallback ─────────────────────────────
543+
//
544+
// A unary call's own (only) record can fail outright — e.g. an invalid column on the sole
545+
// record in an update/insert/get/delete request — and the vault reflects that as the overall
546+
// HTTP status, so the generated client throws ApiClientApiException instead of returning a
547+
// normal response body. When the exception body still has the familiar per-record shape
548+
// ({"records": [...]} for insert/update/get/delete, {"response": [...]} for detokenize), the
549+
// failure belongs on the response the same way a 200 partial-success does — not as a thrown
550+
// exception. Each handler below returns null when the body doesn't match that shape, so the
551+
// caller falls back to throwing a SkyflowException as before.
552+
553+
/** Record maps under {@code key} in an exception body, or null if the shape doesn't match. */
554+
private static List<Map<String, Object>> extractExceptionRecords(ApiClientApiException apiException, String key) {
555+
Object rawBody = apiException.body();
556+
if (!(rawBody instanceof Map)) {
557+
return null;
558+
}
559+
Object recordsField = ((Map<?, ?>) rawBody).get(key);
560+
if (!(recordsField instanceof List)) {
561+
return null;
562+
}
563+
List<Map<String, Object>> records = new ArrayList<>();
564+
for (Object recordObj : (List<?>) recordsField) {
565+
if (recordObj instanceof Map) {
566+
//noinspection unchecked
567+
records.add((Map<String, Object>) recordObj);
568+
}
569+
}
570+
return records.isEmpty() ? null : records;
571+
}
572+
573+
public static InsertResponse handleInsertRequestException(ApiClientApiException apiException) {
574+
List<Map<String, Object>> recordMaps = extractExceptionRecords(apiException, "records");
575+
if (recordMaps == null) {
576+
return null;
577+
}
578+
String requestId = extractRequestId(apiException.headers());
579+
List<InsertResponseRecord> records = new ArrayList<>();
580+
for (Map<String, Object> recordMap : recordMaps) {
581+
records.add(new InsertResponseRecord(
582+
readString(recordMap, "tableName"),
583+
readString(recordMap, "skyflowID"),
584+
null, null, null,
585+
readHttpCode(recordMap, apiException.statusCode()),
586+
readErrorMessage(recordMap),
587+
requestId));
588+
}
589+
return new InsertResponse(records);
590+
}
591+
592+
public static UpdateResponse handleUpdateRequestException(ApiClientApiException apiException) {
593+
List<Map<String, Object>> recordMaps = extractExceptionRecords(apiException, "records");
594+
if (recordMaps == null) {
595+
return null;
596+
}
597+
String requestId = extractRequestId(apiException.headers());
598+
List<UpdateResponseRecord> records = new ArrayList<>();
599+
for (Map<String, Object> recordMap : recordMaps) {
600+
records.add(new UpdateResponseRecord(
601+
readString(recordMap, "tableName"),
602+
readString(recordMap, "skyflowID"),
603+
null, null, null,
604+
readHttpCode(recordMap, apiException.statusCode()),
605+
readErrorMessage(recordMap),
606+
requestId));
607+
}
608+
return new UpdateResponse(records);
609+
}
610+
611+
public static GetResponse handleGetRequestException(ApiClientApiException apiException) {
612+
List<Map<String, Object>> recordMaps = extractExceptionRecords(apiException, "records");
613+
if (recordMaps == null) {
614+
return null;
615+
}
616+
String requestId = extractRequestId(apiException.headers());
617+
List<GetResponseRecord> records = new ArrayList<>();
618+
for (Map<String, Object> recordMap : recordMaps) {
619+
records.add(new GetResponseRecord(
620+
readString(recordMap, "tableName"),
621+
readString(recordMap, "skyflowID"),
622+
null, null, null,
623+
readHttpCode(recordMap, apiException.statusCode()),
624+
readErrorMessage(recordMap),
625+
requestId));
626+
}
627+
return new GetResponse(records);
628+
}
629+
630+
public static DeleteResponse handleDeleteRequestException(ApiClientApiException apiException) {
631+
List<Map<String, Object>> recordMaps = extractExceptionRecords(apiException, "records");
632+
if (recordMaps == null) {
633+
return null;
634+
}
635+
String requestId = extractRequestId(apiException.headers());
636+
List<DeleteResponseRecord> records = new ArrayList<>();
637+
for (Map<String, Object> recordMap : recordMaps) {
638+
records.add(new DeleteResponseRecord(
639+
readString(recordMap, "skyflowID"),
640+
readHttpCode(recordMap, apiException.statusCode()),
641+
readErrorMessage(recordMap),
642+
requestId));
643+
}
644+
return new DeleteResponse(records);
645+
}
646+
647+
public static DetokenizeResponse handleDetokenizeRequestException(ApiClientApiException apiException) {
648+
List<Map<String, Object>> recordMaps = extractExceptionRecords(apiException, "response");
649+
if (recordMaps == null) {
650+
return null;
651+
}
652+
String requestId = extractRequestId(apiException.headers());
653+
List<DetokenizeResponseRecord> records = new ArrayList<>();
654+
for (Map<String, Object> recordMap : recordMaps) {
655+
records.add(new DetokenizeResponseRecord(
656+
readString(recordMap, "token"),
657+
null,
658+
readString(recordMap, "tokenGroupName"),
659+
null,
660+
readHttpCode(recordMap, apiException.statusCode()),
661+
readErrorMessage(recordMap),
662+
requestId));
663+
}
664+
return new DetokenizeResponse(records);
665+
}
666+
542667
// Errors are parsed into ErrorRecord (shared with the other bulk ops), then projected onto
543668
// the unified BulkInsertResponseRecord shape that bulk insert now returns.
544669
public static List<BulkInsertResponseRecord> handleBulkInsertBatchException(

flowvault/src/main/java/com/skyflow/vault/controller/VaultController.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,13 @@ public InsertResponse insert(InsertRequest insertRequest, InsertOptions options)
142142
LogUtil.printInfoLog(InfoLogs.INSERT_REQUEST_RESOLVED.getLog());
143143
return formattedResponse;
144144
} catch (ApiClientApiException e) {
145+
// The lone record in a unary request can fail outright, which the vault reflects as
146+
// the overall HTTP status. If the body still carries the usual per-record shape,
147+
// surface it on the response like a 200 partial success would, not as an exception.
148+
InsertResponse fallback = Utils.handleInsertRequestException(e);
149+
if (fallback != null) {
150+
return fallback;
151+
}
145152
String bodyString = gson.toJson(e.body());
146153
LogUtil.printErrorLog(ErrorLogs.INSERT_RECORDS_REJECTED.getLog());
147154
throw new SkyflowException(e.statusCode(), e, e.headers(), bodyString);
@@ -256,6 +263,13 @@ public DetokenizeResponse detokenize(DetokenizeRequest detokenizeRequest, Detoke
256263
LogUtil.printInfoLog(InfoLogs.DETOKENIZE_REQUEST_RESOLVED.getLog());
257264
return formattedResponse;
258265
} catch (ApiClientApiException e) {
266+
// The lone record in a unary request can fail outright, which the vault reflects as
267+
// the overall HTTP status. If the body still carries the usual per-record shape,
268+
// surface it on the response like a 200 partial success would, not as an exception.
269+
DetokenizeResponse fallback = Utils.handleDetokenizeRequestException(e);
270+
if (fallback != null) {
271+
return fallback;
272+
}
259273
String bodyString = gson.toJson(e.body());
260274
LogUtil.printErrorLog(ErrorLogs.DETOKENIZE_REQUEST_REJECTED.getLog());
261275
throw new SkyflowException(e.statusCode(), e, e.headers(), bodyString);
@@ -375,6 +389,13 @@ public DeleteResponse delete(DeleteRequest deleteRequest, DeleteOptions options)
375389
LogUtil.printInfoLog(InfoLogs.DELETE_REQUEST_RESOLVED.getLog());
376390
return formattedResponse;
377391
} catch (ApiClientApiException e) {
392+
// The lone record in a unary request can fail outright, which the vault reflects as
393+
// the overall HTTP status. If the body still carries the usual per-record shape,
394+
// surface it on the response like a 200 partial success would, not as an exception.
395+
DeleteResponse fallback = Utils.handleDeleteRequestException(e);
396+
if (fallback != null) {
397+
return fallback;
398+
}
378399
String bodyString = gson.toJson(e.body());
379400
LogUtil.printErrorLog(ErrorLogs.DELETE_REQUEST_REJECTED.getLog());
380401
throw new SkyflowException(e.statusCode(), e, e.headers(), bodyString);
@@ -610,6 +631,13 @@ public UpdateResponse update(UpdateRequest updateRequest, UpdateOptions options)
610631
LogUtil.printInfoLog(InfoLogs.UPDATE_REQUEST_RESOLVED.getLog());
611632
return formattedResponse;
612633
} catch (ApiClientApiException e) {
634+
// The lone record in a unary request can fail outright, which the vault reflects as
635+
// the overall HTTP status. If the body still carries the usual per-record shape,
636+
// surface it on the response like a 200 partial success would, not as an exception.
637+
UpdateResponse fallback = Utils.handleUpdateRequestException(e);
638+
if (fallback != null) {
639+
return fallback;
640+
}
613641
String bodyString = gson.toJson(e.body());
614642
LogUtil.printErrorLog(ErrorLogs.UPDATE_REQUEST_REJECTED.getLog());
615643
throw new SkyflowException(e.statusCode(), e, e.headers(), bodyString);
@@ -645,6 +673,13 @@ public GetResponse get(GetRequest getRequest, GetOptions options) throws Skyflow
645673
LogUtil.printInfoLog(InfoLogs.GET_REQUEST_RESOLVED.getLog());
646674
return formattedResponse;
647675
} catch (ApiClientApiException e) {
676+
// The lone record in a unary request can fail outright, which the vault reflects as
677+
// the overall HTTP status. If the body still carries the usual per-record shape,
678+
// surface it on the response like a 200 partial success would, not as an exception.
679+
GetResponse fallback = Utils.handleGetRequestException(e);
680+
if (fallback != null) {
681+
return fallback;
682+
}
648683
String bodyString = gson.toJson(e.body());
649684
LogUtil.printErrorLog(ErrorLogs.GET_REQUEST_REJECTED.getLog());
650685
throw new SkyflowException(e.statusCode(), e, e.headers(), bodyString);

flowvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,84 @@ public void testUpdate_success() throws Exception {
312312
Assert.assertEquals("sky-1", response.getRecords().get(0).getSkyflowId());
313313
}
314314

315+
// Regression: a single failing record in a unary call can make the vault reflect the failure
316+
// as the overall HTTP status (here 400), so the generated client throws ApiClientApiException
317+
// instead of returning a normal body. When that exception body still has the familiar
318+
// per-record "records" shape, it must land on the UpdateResponse like a 200 partial success
319+
// would - not surface as a thrown SkyflowException.
320+
@Test
321+
public void testUpdate_recordLevelFailureReflectedAsHttpErrorStillReturnsResponse() throws Exception {
322+
ApiClient mockApi = Mockito.mock(ApiClient.class);
323+
RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi);
324+
325+
Map<String, Object> failedRecord = new HashMap<>();
326+
failedRecord.put("skyflowID", null);
327+
failedRecord.put("tokens", null);
328+
failedRecord.put("data", null);
329+
failedRecord.put("hashedData", null);
330+
failedRecord.put("error", "UPDATE failed. Column card_numbe is invalid. Specify a valid column.");
331+
failedRecord.put("httpCode", 400);
332+
failedRecord.put("tableName", "");
333+
Map<String, Object> responseBody = new HashMap<>();
334+
responseBody.put("records", Collections.singletonList(failedRecord));
335+
336+
when(mockRaw.update(any(), any()))
337+
.thenThrow(new ApiClientApiException("Error with status code 400", 400, responseBody));
338+
339+
VaultController controller = createControllerWithMock(mockApi);
340+
341+
UpdateRequestRecord updateRecord = UpdateRequestRecord.builder().skyflowId("sky-1").build();
342+
UpdateRequest request = UpdateRequest.builder()
343+
.tableName("table1")
344+
.records(Collections.singletonList(updateRecord))
345+
.build();
346+
347+
UpdateResponse response = controller.update(request);
348+
Assert.assertEquals(1, response.getRecords().size());
349+
Assert.assertEquals("UPDATE failed. Column card_numbe is invalid. Specify a valid column.",
350+
response.getRecords().get(0).getError());
351+
Assert.assertEquals(400, response.getRecords().get(0).getHttpCode());
352+
Assert.assertNull(response.getRecords().get(0).getSkyflowId());
353+
}
354+
355+
// Regression: a genuine whole-request API error (e.g. vault not found) has no "records" key
356+
// at all, so the fallback added above must not swallow it - it still has to throw.
357+
@Test
358+
public void testUpdate_wholeRequestApiErrorStillThrows() throws Exception {
359+
ApiClient mockApi = Mockito.mock(ApiClient.class);
360+
RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi);
361+
362+
Map<String, Object> errorBody = new HashMap<>();
363+
errorBody.put("grpc_code", 5);
364+
errorBody.put("http_code", 404);
365+
errorBody.put("message", "Invalid request. Vault not found for vaultID: vault123. Specify a valid vaultID.");
366+
errorBody.put("http_status", "Not Found");
367+
errorBody.put("details", new ArrayList<>());
368+
Map<String, Object> responseBody = new HashMap<>();
369+
responseBody.put("error", errorBody);
370+
371+
when(mockRaw.update(any(), any()))
372+
.thenThrow(new ApiClientApiException("Error with status code 404", 404, responseBody));
373+
374+
VaultController controller = createControllerWithMock(mockApi);
375+
376+
UpdateRequestRecord updateRecord = UpdateRequestRecord.builder().skyflowId("sky-1").build();
377+
UpdateRequest request = UpdateRequest.builder()
378+
.tableName("table1")
379+
.records(Collections.singletonList(updateRecord))
380+
.build();
381+
382+
try {
383+
controller.update(request);
384+
Assert.fail(EXCEPTION_NOT_THROWN);
385+
} catch (SkyflowException e) {
386+
Assert.assertEquals(
387+
"Invalid request. Vault not found for vaultID: vault123. Specify a valid vaultID.",
388+
e.getMessage());
389+
Assert.assertEquals(404, e.getHttpCode());
390+
}
391+
}
392+
315393
@Test
316394
public void testUpdate_invalidRequestThrowsSkyflowException() throws Exception {
317395
ApiClient mockApi = Mockito.mock(ApiClient.class);

0 commit comments

Comments
 (0)