amplify-category-api
amplify-category-api copied to clipboard
Conflict Detection with Optimistic Concurrency does not return the current version of the object
How did you install the Amplify CLI?
npm
If applicable, what version of Node.js are you using?
20.9.0
Amplify CLI Version
12.10.1
What operating system are you using?
Mac
Did you make any manual changes to the cloud resources managed by Amplify? Please describe the changes made.
No manual changes made
Describe the bug
When using an Amplify Graphql API with conflict detection enabled and set to Optimistic Concurrency the generated resolver only returns the error message ConflictUnhandled. It does not return the current version of the object. Without it, the client cannot recover from conflict.
The response appears as:
{
{
"data": null
},
"errors": [
{
"path": [
"updateMyItem"
],
"data": null,
"errorType": "ConflictUnhandled",
"errorInfo": null,
"locations": [
{
"line": 2,
"column": 3,
"sourceName": null
}
],
"message": "Conflict resolver rejects mutation."
}
]
}
Expected behavior
I would expect the resolver to return the error message AND the current version of the object in the database as this is what is described in the AppSync documentation.
Optimistic Concurrency Optimistic Concurrency is a conflict resolution strategy that AWS AppSync provides for versioned data sources. When the conflict resolver is set to Optimistic Concurrency, if an incoming mutation is detected to have a version that differs from the actual version of the object, the conflict handler will simply reject the incoming request. Inside the GraphQL response, the existing item on the server that has the latest version will be provided. The client is then expected to handle this conflict locally and retry the mutation with the updated version of the item.
Source: AppSync Docs - Conflict Detection https://docs.aws.amazon.com/appsync/latest/devguide/conflict-detection-and-sync.html#conflict-detection-and-resolution
I would therefore expect the response to be:
{
"data": {
"updateMyItem": {
"id": "XXXXXX-XXXXXX-XXXXXX-XXXXXX-XXXXXX",
"_version": 51
}
},
"errors": [
{
"path": [
"updateMyItem"
],
"data": null,
"errorType": "ConflictUnhandled",
"errorInfo": null,
"locations": [
{
"line": 2,
"column": 3,
"sourceName": null
}
],
"message": "Conflict resolver rejects mutation."
}
]
}
I would expect it to return $ctx.result alongside the error when the rejection is due to the conflict (and not something else like an auth error).
Reproduction steps
- Create and deploy any basic schema
- Enable Conflict Detection, choose Optimistic Concurrency
- Observe that the generated resolvers call
$util.error()and does not return the result - Create and Item then try to update it with an incorrect
_version. Observe that the current version of the item is not returned.
Project Identifier
No response
Log output
No response
Additional information
Workaround:
The current workaround is to manually override each response template to use appendError() and to return the result.
## [Start] ResponseTemplate. **
$util.qr($ctx.result.put("__operation", "Mutation"))
#if( $ctx.error )
$util.error($ctx.error.message, $ctx.error.type, $ctx.result)
#else
$util.toJson($ctx.result)
#end
## [End] ResponseTemplate. **
to:
## [Start] ResponseTemplate. **
$util.qr($ctx.result.put("__operation", "Mutation"))
#if( $ctx.error )
$util.appendError($ctx.error.message, $ctx.error.type, $ctx.result)
$util.toJson($ctx.result)
#else
$util.toJson($ctx.result)
#end
## [End] ResponseTemplate. **
Possible cause:
I suspect that this section of the resolver is being generated by:
/**
* Generate common response template used by most of the resolvers.
* Append operation if response is coming from a mutation, this is to protect field resolver for subscriptions
*/
export const generateDefaultResponseMappingTemplate = (isSyncEnabled: boolean, mutation = false): string => {
const statements: Expression[] = [];
if (mutation) statements.push(qref(methodCall(ref('ctx.result.put'), str(OPERATION_KEY), str('Mutation'))));
if (isSyncEnabled) {
statements.push(
ifElse(
ref('ctx.error'),
/* here >>>>>> */ methodCall(ref('util.error'), ref('ctx.error.message'), ref('ctx.error.type'), ref('ctx.result')),
/* here >>>>>> */ toJson(ref('ctx.result')),
),
);
} else {
statements.push(
ifElse(ref('ctx.error'), methodCall(ref('util.error'), ref('ctx.error.message'), ref('ctx.error.type')), toJson(ref('ctx.result'))),
);
}
return printBlock('ResponseTemplate')(compoundExpression(statements));
};
Proposed solution:
The solution would be to check to check if the error is ConflictUnhandled only and return the result only in that case.
Before submitting, please confirm:
- [X] I have done my best to include a minimal, self-contained set of instructions for consistently reproducing the issue.
- [X] I have removed any sensitive information from my code snippets and submission.