Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wrapping exceptions thrown in custom serializers as a CosmosException with StatusCode 400 #40797

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ public final class CosmosEncryptionAsyncContainer {
private final EncryptionProcessor encryptionProcessor;

private final CosmosEncryptionAsyncClient cosmosEncryptionAsyncClient;

private final static ImplementationBridgeHelpers.CosmosItemSerializerHelper.CosmosItemSerializerAccessor itemSerializerAccessor =
ImplementationBridgeHelpers.CosmosItemSerializerHelper.getCosmosItemSerializerAccessor();
private final static ImplementationBridgeHelpers.CosmosItemResponseHelper.CosmosItemResponseBuilderAccessor cosmosItemResponseBuilderAccessor = ImplementationBridgeHelpers.CosmosItemResponseHelper.getCosmosItemResponseBuilderAccessor();
private final static ImplementationBridgeHelpers.CosmosItemRequestOptionsHelper.CosmosItemRequestOptionsAccessor cosmosItemRequestOptionsAccessor = ImplementationBridgeHelpers.CosmosItemRequestOptionsHelper.getCosmosItemRequestOptionsAccessor();
private final static ImplementationBridgeHelpers.CosmosQueryRequestOptionsHelper.CosmosQueryRequestOptionsAccessor cosmosQueryRequestOptionsAccessor = ImplementationBridgeHelpers.CosmosQueryRequestOptionsHelper.getCosmosQueryRequestOptionsAccessor();
Expand Down Expand Up @@ -1379,10 +1382,9 @@ public Mono<CosmosBatchResponse> executeCosmosBatch(CosmosBatch cosmosBatch, Cos
.flatMap(encryptedIdPartitionKeyTuple -> {


Map<String, Object> jsonTree = effectiveItemSerializer
.serialize(
itemBatchOperation.getItem()
);
Map<String, Object> jsonTree = itemSerializerAccessor.serializeSafe(
effectiveItemSerializer,
itemBatchOperation.getItem());

ObjectNode objectNode = jsonTree instanceof ObjectNodeMap
? ((ObjectNodeMap)jsonTree).getObjectNode().deepCopy()
Expand Down Expand Up @@ -1579,8 +1581,9 @@ public <TContext> Flux<CosmosBulkOperationResponse<TContext>> executeBulkOperati
}
})
.flatMap(encryptedIdPartitionKeyTuple -> {
Map<String, Object> jsonTree = effectiveItemSerializer
.serialize(cosmosItemOperation.getItem());
Map<String, Object> jsonTree = itemSerializerAccessor.serializeSafe(
effectiveItemSerializer,
cosmosItemOperation.getItem());

ObjectNode objectNode = jsonTree instanceof ObjectNodeMap
? ((ObjectNodeMap)jsonTree).getObjectNode().deepCopy()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.azure.cosmos.BridgeInternal;
import com.azure.cosmos.CosmosItemSerializer;
import com.azure.cosmos.implementation.CosmosPagedFluxOptions;
import com.azure.cosmos.implementation.ImplementationBridgeHelpers;
import com.azure.cosmos.implementation.ObjectNodeMap;
import com.azure.cosmos.implementation.PrimitiveJsonNodeMap;
import com.azure.cosmos.implementation.Utils;
Expand All @@ -25,6 +26,8 @@
import static com.azure.cosmos.implementation.guava25.base.Preconditions.checkNotNull;

public class CosmosEncryptionQueryTransformer<T> implements Transformer<T> {
private final static ImplementationBridgeHelpers.CosmosItemSerializerHelper.CosmosItemSerializerAccessor itemSerializerAccessor =
ImplementationBridgeHelpers.CosmosItemSerializerHelper.getCosmosItemSerializerAccessor();
private final Scheduler encryptionScheduler;
private final EncryptionProcessor encryptionProcessor;
private final Class<T> classType;
Expand Down Expand Up @@ -81,7 +84,8 @@ private <TTransform> Function<CosmosPagedFluxOptions, Flux<FeedResponse<TTransfo
classType);
}

return effectiveSerializer.deserialize(
return itemSerializerAccessor.deserializeSafe(
effectiveSerializer,
decryptedJsonTree,
classType);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -774,7 +774,9 @@ public <T> Map<String, Object> serialize(T item) {
}

if (passThroughOnSerialize) {
return CosmosItemSerializer.DEFAULT_SERIALIZER.serialize(item);
return ImplementationBridgeHelpers.CosmosItemSerializerHelper.getCosmosItemSerializerAccessor().serializeSafe(
CosmosItemSerializer.DEFAULT_SERIALIZER,
item);
}

Map<String, Object> unwrappedJsonTree = CosmosItemSerializer.DEFAULT_SERIALIZER.serialize(item);
Expand Down Expand Up @@ -802,8 +804,13 @@ public <T> T deserialize(Map<String, Object> jsonNodeMap, Class<T> classType) {
assertThat(jsonNodeMap.get("_trackingId")).isNotNull();
}

TestDocumentWrappedInEnvelope envelope =
CosmosItemSerializer.DEFAULT_SERIALIZER.deserialize(jsonNodeMap, TestDocumentWrappedInEnvelope.class);
TestDocumentWrappedInEnvelope envelope = ImplementationBridgeHelpers
.CosmosItemSerializerHelper
.getCosmosItemSerializerAccessor()
.deserializeSafe(
CosmosItemSerializer.DEFAULT_SERIALIZER,
jsonNodeMap,
TestDocumentWrappedInEnvelope.class);

if (envelope == null || envelope.wrappedContent == null) {
return null;
Expand All @@ -816,9 +823,13 @@ public <T> T deserialize(Map<String, Object> jsonNodeMap, Class<T> classType) {
throw new IllegalStateException("Double wrapped");
}

return CosmosItemSerializer.DEFAULT_SERIALIZER.deserialize(
unwrappedContent,
classType);
return ImplementationBridgeHelpers
.CosmosItemSerializerHelper
.getCosmosItemSerializerAccessor()
.deserializeSafe(
CosmosItemSerializer.DEFAULT_SERIALIZER,
unwrappedContent,
classType);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

package com.azure.cosmos;

import com.azure.cosmos.implementation.ImplementationBridgeHelpers;
import com.azure.cosmos.implementation.TestConfigurations;
import com.azure.cosmos.models.CosmosBatch;
import com.azure.cosmos.models.CosmosBatchOperationResult;
Expand Down Expand Up @@ -181,6 +182,15 @@ public Object[][] testConfigs_requestLevelSerializer() {
};
}

@DataProvider(name = "testConfigs_onlyCustomSerializer")
public Object[][] testConfigs_onlyCustomSerializer() {
return new Object[][] {
new Object[] {
EnvelopWrappingItemSerializer.INSTANCE_NO_TRACKING_ID_VALIDATION
},
};
}

@Override
public String resolveTestNameSuffix(Object[] row) {
String prefix = nonIdempotentWriteRetriesEnabled
Expand Down Expand Up @@ -479,6 +489,171 @@ public void batchAndChangeFeedWithPojo(CosmosItemSerializer requestLevelSerializ
);
}

@Test(groups = { "fast", "emulator" }, dataProvider = "testConfigs_onlyCustomSerializer", timeOut = TIMEOUT * 1000000)
public void handleCustomSerializationExceptionPojo(CosmosItemSerializer requestLevelSerializer) {
String id = "serializationFailure" + UUID.randomUUID();
TestDocument doc = TestDocument.create(id);
Consumer<TestDocument> onBeforeReplace = item -> item.someNumber = 999;
BiFunction<TestDocument, Boolean, CosmosPatchOperations> onBeforePatch = (item, isEnvelopeWrapped) -> {

doc.someNumber = 555;
if (!isEnvelopeWrapped) {
return CosmosPatchOperations
.create()
.add("/someNumber", 555);
} else {
return CosmosPatchOperations
.create()
.add("/wrappedContent/someNumber", 555);
}
};

try {
runPointOperationAndQueryTestCase(
doc,
id,
onBeforeReplace,
onBeforePatch,
requestLevelSerializer,
TestDocument.class);

fail("A custom serialization exception should have been thrown.");
} catch (CosmosException cosmosException) {
assertThat(cosmosException).isNotNull();
assertThat(cosmosException.getStatusCode()).isEqualTo(400);
assertThat(cosmosException.getCause()).isNotNull();
assertThat(cosmosException.getCause()).isInstanceOf(RuntimeException.class);
assertThat(cosmosException.getCause().getCause()).isNotNull();
assertThat(cosmosException.getCause().getCause()).isInstanceOf(OutOfMemoryError.class);
assertThat(cosmosException.getCause().getCause().getMessage())
.isEqualTo("Some dummy Error thrown in custom serializer during serialization.");
}
}

@Test(groups = { "fast", "emulator" }, dataProvider = "testConfigs_onlyCustomSerializer", timeOut = TIMEOUT * 1000000)
public void handleCustomSerializationExceptionObjectNode(CosmosItemSerializer requestLevelSerializer) {
String id = "serializationFailure" + UUID.randomUUID();
ObjectNode doc = TestDocument.createAsObjectNode(id);
Consumer<ObjectNode> onBeforeReplace = item -> item.put("someNumber", 999);
BiFunction<ObjectNode, Boolean, CosmosPatchOperations> onBeforePatch = (item, isEnvelopeWrapped) -> {

item.put("someNumber", 555);

if (!isEnvelopeWrapped) {
return CosmosPatchOperations
.create()
.add("/someNumber", 555);
} else {
return CosmosPatchOperations
.create()
.add("/wrappedContent/someNumber", 555);
}
};

try {
runPointOperationAndQueryTestCase(
doc,
id,
onBeforeReplace,
onBeforePatch,
requestLevelSerializer,
ObjectNode.class);

fail("A custom serialization exception should have been thrown.");
} catch (CosmosException cosmosException) {
assertThat(cosmosException).isNotNull();
assertThat(cosmosException.getStatusCode()).isEqualTo(400);
assertThat(cosmosException.getCause()).isNotNull();
assertThat(cosmosException.getCause()).isInstanceOf(RuntimeException.class);
assertThat(cosmosException.getCause().getCause()).isNotNull();
assertThat(cosmosException.getCause().getCause()).isInstanceOf(OutOfMemoryError.class);
assertThat(cosmosException.getCause().getCause().getMessage())
.isEqualTo("Some dummy Error thrown in custom serializer during serialization.");
}
}

@Test(groups = { "fast", "emulator" }, dataProvider = "testConfigs_onlyCustomSerializer", timeOut = TIMEOUT * 1000000)
public void handleCustomDeserializationExceptionPojo(CosmosItemSerializer requestLevelSerializer) {
String id = "deserializationFailure" + UUID.randomUUID();
TestDocument doc = TestDocument.create(id);
Consumer<TestDocument> onBeforeReplace = item -> item.someNumber = 999;
BiFunction<TestDocument, Boolean, CosmosPatchOperations> onBeforePatch = (item, isEnvelopeWrapped) -> {

doc.someNumber = 555;
if (!isEnvelopeWrapped) {
return CosmosPatchOperations
.create()
.add("/someNumber", 555);
} else {
return CosmosPatchOperations
.create()
.add("/wrappedContent/someNumber", 555);
}
};

try {
runPointOperationAndQueryTestCase(
doc,
id,
onBeforeReplace,
onBeforePatch,
requestLevelSerializer,
TestDocument.class);

fail("A custom deserialization exception should have been thrown.");
} catch (CosmosException cosmosException) {
assertThat(cosmosException).isNotNull();
assertThat(cosmosException.getStatusCode()).isEqualTo(400);
assertThat(cosmosException.getCause()).isNotNull();
assertThat(cosmosException.getCause()).isInstanceOf(RuntimeException.class);
assertThat(cosmosException.getCause().getCause()).isNotNull();
assertThat(cosmosException.getCause().getCause()).isInstanceOf(OutOfMemoryError.class);
assertThat(cosmosException.getCause().getCause().getMessage())
.isEqualTo("Some dummy Error thrown in custom serializer during deserialization.");
}
}

@Test(groups = { "fast", "emulator" }, dataProvider = "testConfigs_onlyCustomSerializer", timeOut = TIMEOUT * 1000000)
public void handleCustomDeserializationExceptionObjectNode(CosmosItemSerializer requestLevelSerializer) {
String id = "deserializationFailure" + UUID.randomUUID();
ObjectNode doc = TestDocument.createAsObjectNode(id);
Consumer<ObjectNode> onBeforeReplace = item -> item.put("someNumber", 999);
BiFunction<ObjectNode, Boolean, CosmosPatchOperations> onBeforePatch = (item, isEnvelopeWrapped) -> {

item.put("someNumber", 555);

if (!isEnvelopeWrapped) {
return CosmosPatchOperations
.create()
.add("/someNumber", 555);
} else {
return CosmosPatchOperations
.create()
.add("/wrappedContent/someNumber", 555);
}
};

try {
runPointOperationAndQueryTestCase(
doc,
id,
onBeforeReplace,
onBeforePatch,
requestLevelSerializer,
ObjectNode.class);

fail("A custom deserialization exception should have been thrown.");
} catch (CosmosException cosmosException) {
assertThat(cosmosException).isNotNull();
assertThat(cosmosException.getStatusCode()).isEqualTo(400);
assertThat(cosmosException.getCause()).isNotNull();
assertThat(cosmosException.getCause()).isInstanceOf(RuntimeException.class);
assertThat(cosmosException.getCause().getCause()).isNotNull();
assertThat(cosmosException.getCause().getCause()).isInstanceOf(OutOfMemoryError.class);
assertThat(cosmosException.getCause().getCause().getMessage())
.isEqualTo("Some dummy Error thrown in custom serializer during deserialization.");
}
}

private <T> void runBatchAndChangeFeedTestCase(
Function<String, T> docGenerator,
Expand Down Expand Up @@ -733,14 +908,20 @@ public <T> Map<String, Object> serialize(T item) {
}

if (passThroughOnSerialize) {
return CosmosItemSerializer.DEFAULT_SERIALIZER.serialize(item);
return ImplementationBridgeHelpers.CosmosItemSerializerHelper.getCosmosItemSerializerAccessor().serializeSafe(
CosmosItemSerializer.DEFAULT_SERIALIZER,
item);
}

Map<String, Object> unwrappedJsonTree = CosmosItemSerializer.DEFAULT_SERIALIZER.serialize(item);
if (unwrappedJsonTree.containsKey("wrappedContent")) {
throw new IllegalStateException("Double wrapping");
}

if (unwrappedJsonTree.get("id") != null && unwrappedJsonTree.get("id").toString().startsWith("serializationFailure")) {
throw new OutOfMemoryError("Some dummy Error thrown in custom serializer during serialization.");
}

Map<String, Object> wrappedJsonTree = new ConcurrentHashMap<>();
wrappedJsonTree.put("id", unwrappedJsonTree.get("id"));
wrappedJsonTree.put("mypk", unwrappedJsonTree.get("mypk"));
Expand All @@ -760,8 +941,13 @@ public <T> T deserialize(Map<String, Object> jsonNodeMap, Class<T> classType) {
assertThat(jsonNodeMap.get("_trackingId")).isNotNull();
}

TestDocumentWrappedInEnvelope envelope =
CosmosItemSerializer.DEFAULT_SERIALIZER.deserialize(jsonNodeMap, TestDocumentWrappedInEnvelope.class);
TestDocumentWrappedInEnvelope envelope = ImplementationBridgeHelpers
.CosmosItemSerializerHelper
.getCosmosItemSerializerAccessor()
.deserializeSafe(
CosmosItemSerializer.DEFAULT_SERIALIZER,
jsonNodeMap,
TestDocumentWrappedInEnvelope.class);

if (envelope == null || envelope.wrappedContent == null) {
return null;
Expand All @@ -774,9 +960,17 @@ public <T> T deserialize(Map<String, Object> jsonNodeMap, Class<T> classType) {
throw new IllegalStateException("Double wrapped");
}

return CosmosItemSerializer.DEFAULT_SERIALIZER.deserialize(
unwrappedContent,
classType);
if (unwrappedContent.get("id") != null && unwrappedContent.get("id").toString().startsWith("deserializationFailure")) {
throw new OutOfMemoryError("Some dummy Error thrown in custom serializer during deserialization.");
}

return ImplementationBridgeHelpers
.CosmosItemSerializerHelper
.getCosmosItemSerializerAccessor()
.deserializeSafe(
CosmosItemSerializer.DEFAULT_SERIALIZER,
unwrappedContent,
classType);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -765,7 +765,13 @@ public <T> T deserialize(Map<String, Object> jsonNodeMap, Class<T> classType) {
return (T)jsonNodeMap.get("id");
}

return CosmosItemSerializer.DEFAULT_SERIALIZER.deserialize(jsonNodeMap, classType);
return ImplementationBridgeHelpers
.CosmosItemSerializerHelper
.getCosmosItemSerializerAccessor()
.deserializeSafe(
CosmosItemSerializer.DEFAULT_SERIALIZER,
jsonNodeMap,
classType);
}
});

Expand Down
1 change: 1 addition & 0 deletions sdk/cosmos/azure-cosmos/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

#### Other Changes
* Added diagnostic fields for `quorumAckedLSN` and `currentReplicaSetSize`. Changed `replicaStatusList` to include all replicas and more information. - See [PR 39844](https://github.com/Azure/azure-sdk-for-java/pull/39844)
* Ensured that exceptions thrown in custom serializers are being wraped as a CosmosException with StatusCode 400. - See [PR 40797](https://github.com/Azure/azure-sdk-for-java/pull/40797)

### 4.61.1 (2024-05-31)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -620,5 +620,6 @@ public static void initializeAllAccessors() {
CosmosDiagnosticsThresholds.initialize();
CosmosContainerProactiveInitConfig.initialize();
SessionRetryOptions.initialize();
CosmosItemSerializer.initialize();
}
}
Loading