Skip to content

DynamoDBv2 throws AmazonClientException when using ReturnValuesOnConditionCheckFailure #3764

Open
@bjornsallarp

Description

@bjornsallarp

Describe the bug

When using UpdateItemRequest with ConditionExpression and specifying ReturnValuesOnConditionCheckFailure and the existing document fail the condition and also contain a a map/dictionary where a key ends with the strings "code" or "message" JsonErrorResponseUnmarshaller throws an exception.

Regression Issue

  • Select this option if this issue appears to be a regression.

Expected Behavior

When using ReturnValuesOnConditionCheckFailure I expect ConditionalCheckFailedException to be thrown and the Item property on the exception to be set even if a dictionary key has a name ending with __type, code or message.

Current Behavior

Throws an AmazonClientException because the ConditionalCheckFailedException cannot be deserialized

Unhandled exception. Amazon.Runtime.AmazonClientException: We expected a VALUE token but got: ObjectStart
   at Amazon.Runtime.Internal.Transform.JsonUnmarshallerContext.ReadText()
   at Amazon.Runtime.Internal.Transform.SimpleTypeUnmarshaller`1.Unmarshall(JsonUnmarshallerContext context)
   at Amazon.Runtime.Internal.Transform.JsonErrorResponseUnmarshaller.GetValuesFromJsonIfPossible(JsonUnmarshallerContext context, String& type, String& message, String& code)
   at Amazon.Runtime.Internal.Transform.JsonErrorResponseUnmarshaller.Unmarshall(JsonUnmarshallerContext context)
   at Amazon.DynamoDBv2.Model.Internal.MarshallTransformations.UpdateItemResponseUnmarshaller.UnmarshallException(JsonUnmarshallerContext context, Exception innerException, HttpStatusCode statusCode)
   at Amazon.Runtime.Internal.Transform.JsonResponseUnmarshaller.UnmarshallException(UnmarshallerContext input, Exception innerException, HttpStatusCode statusCode)
   at Amazon.Runtime.Internal.HttpErrorResponseExceptionHandler.HandleExceptionStream(IRequestContext requestContext, IWebResponseData httpErrorResponse, HttpErrorResponseException exception, Stream responseStream)
   at Amazon.Runtime.Internal.HttpErrorResponseExceptionHandler.HandleExceptionAsync(IExecutionContext executionContext, HttpErrorResponseException exception)
   at Amazon.Runtime.Internal.ExceptionHandler`1.HandleAsync(IExecutionContext executionContext, Exception exception)
   at Amazon.Runtime.Internal.ErrorHandler.ProcessExceptionAsync(IExecutionContext executionContext, Exception exception)
   at Amazon.Runtime.Internal.ErrorHandler.InvokeAsync[T](IExecutionContext executionContext)
   at Amazon.Runtime.Internal.CallbackHandler.InvokeAsync[T](IExecutionContext executionContext)
   at Amazon.Runtime.Internal.Signer.InvokeAsync[T](IExecutionContext executionContext)
   at Amazon.Runtime.Internal.EndpointDiscoveryHandler.InvokeAsync[T](IExecutionContext executionContext)
   at Amazon.Runtime.Internal.EndpointDiscoveryHandler.InvokeAsync[T](IExecutionContext executionContext)
   at Amazon.Runtime.Internal.CredentialsRetriever.InvokeAsync[T](IExecutionContext executionContext)
   at Amazon.Runtime.Internal.RetryHandler.InvokeAsync[T](IExecutionContext executionContext)
   at Amazon.Runtime.Internal.RetryHandler.InvokeAsync[T](IExecutionContext executionContext)
   at Amazon.Runtime.Internal.CallbackHandler.InvokeAsync[T](IExecutionContext executionContext)
   at Amazon.Runtime.Internal.CallbackHandler.InvokeAsync[T](IExecutionContext executionContext)
   at Amazon.Runtime.Internal.ErrorCallbackHandler.InvokeAsync[T](IExecutionContext executionContext)
   at Amazon.Runtime.Internal.MetricsHandler.InvokeAsync[T](IExecutionContext executionContext)
   at DynamoDbBug.DynamoDbBug.ConditionalUpdate() in /Users/bjornsallarp/Code/DynamoDbBug/DynamoDbBug.cs:line 51
   at DynamoDbBug.DynamoDbBug.Run() in /Users/bjornsallarp/Code/DynamoDbBug/DynamoDbBug.cs:line 19
   at Program.<Main>$(String[] args) in /Users/bjornsallarp/Code/DynamoDbBug/Main.cs:line 301
   at Program.<Main>(String[] args)

Reproduction Steps

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.DataModel;
using Amazon.DynamoDBv2.Model;
using ReturnValuesOnConditionCheckFailure = Amazon.DynamoDBv2.ReturnValuesOnConditionCheckFailure;

namespace DynamoDbBug;

public class DynamoDbBug(AmazonDynamoDBClient db, IDynamoDBContext context)
{
	private const string TableName = "BuggedTable";
	private const string DocumentKey = "Key";

	public async Task Run()
	{
		await InsertDocument();
		await ConditionalUpdate();
	}

	private async Task ConditionalUpdate()
	{
		var test = new MyObject { Foo = "foo" };
		var document = context.ToDocument(test);

		var request = new UpdateItemRequest
		{
			TableName = TableName,
			Key = new Dictionary<string, AttributeValue>
			{
				{ "Key", new AttributeValue(DocumentKey) }
			},
			UpdateExpression = "SET Dictionary.#key = :document",
			ExpressionAttributeNames = new Dictionary<string, string>
			{
				{ "#key", "keyThatEndsWithCode" }
			},
			ExpressionAttributeValues = new Dictionary<string, AttributeValue>
			{
				[":document"] = new() { M = document.ToAttributeMap() },
				[":maxSize"] = new() { N = "1" }
			},
			ConditionExpression = "size(Dictionary) < :maxSize",
			ReturnValuesOnConditionCheckFailure = ReturnValuesOnConditionCheckFailure.ALL_OLD,
			ReturnValues = ReturnValue.NONE
		};

		try
		{
			await db.UpdateItemAsync(request);
		}
		catch (ConditionalCheckFailedException e)
		{
			Console.WriteLine(e.Item?["Key"]?.S);
		}
	}

	private async Task InsertDocument()
	{
		var item = new MyDocument
		{
			Key = DocumentKey,
			Dictionary = new Dictionary<string, MyObject>
			{
				["keyThatEndsWithCode"] = new MyObject { Foo = "bar" }
			}
		};

		await db.PutItemAsync(new PutItemRequest(TableName, context.ToDocument(item).ToAttributeMap()));
	}
}

public class MyDocument
{
	public required string Key { get; set; }
	public Dictionary<string, MyObject> Dictionary { get; set; } = new();
}

public class MyObject
{
	public string Foo { get; set; }
}

Possible Solution

The issue is here:

private static void GetValuesFromJsonIfPossible(JsonUnmarshallerContext context, out string type, out string message, out string code)

The loop keeps digging down into the json looking for something ending with __type, message or code. I assume an error response has a certain format, so why look for properties deep down, something like this could work:

while (TryReadContext(context))
{
    if (context.CurrentDepth > 1)
        continue;
    
    if (context.TestExpression("__type"))
    {
        type = StringUnmarshaller.GetInstance().Unmarshall(context);
        continue;
    }
    if (context.TestExpression("message"))
    {
        message = StringUnmarshaller.GetInstance().Unmarshall(context);
        continue;
    }
    if (context.TestExpression("code"))
    {
        code = StringUnmarshaller.GetInstance().Unmarshall(context);
        continue;
    }
}

Additional Information/Context

No response

AWS .NET SDK and/or Package version used

AWSSDK.DynamoDBv2 3.7.400.8

Targeted .NET Platform

.NET 9

Operating System and version

OSX

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugThis issue is a bug.dynamodbp1This is a high priority issue

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions