Skip to content

Commit 81a558f

Browse files
authored
fix(stepfunctions): cannot use intrinsic functions in Fail state (#30210)
### Issue # (if applicable) Closes #30063 ### Reason for this change In the Fail state, we can specify intrinsic functions and json paths as the CausePath and ErrorPath properties. Currently, however, specifying intrinsic functions as a string will result in an error. https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-fail-state.html ```ts export class SampleStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const fail = new stepfunctions.Fail(this, "Fail", { errorPath: "$.error", // OK causePath: "States.Format('cause: {}', $.cause)", // Error }); const sm = new stepfunctions.StateMachine(this, "StateMachine", { definitionBody: stepfunctions.DefinitionBody.fromChainable(fail), timeout: cdk.Duration.minutes(5) }); } } ``` ``` Error: Expected JSON path to start with '$', got: States.Format('cause: {}', $.cause) ``` ### Description of changes The value passed to the `renderJsonPath` function is expected to be a string starting with `$` if it is not a token. However, if you pass intrinsic functions as strings to the CausePath and ErrorPath properties, they will never start with `$`. Therefore, I fixed not to call the `renderJsonPath` function if the intrinsic functions are specified as strings. Another change was the addition of validation since error and errorPath, cause and causePath cannot be specified simultaneously. ### Description of how you validated changes I added unit tests to verify that passing intrinsic functions as strings do not cause an error. Tests were also added to verify that errors occur when errors and paths are specified at the same time and when cause and cause paths are specified at the same time. https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-fail-state.html#:~:text=%2C%20and%20States.UUID.-,Important,-You%20can%20specify%20either%20Cause https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-fail-state.html#:~:text=%2C%20and%20States.UUID.-,Important,-You%20can%20specify%20either%20Error ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 8b234b7 commit 81a558f

File tree

3 files changed

+145
-4
lines changed

3 files changed

+145
-4
lines changed

packages/aws-cdk-lib/aws-stepfunctions/README.md

+10
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,16 @@ const fail = new sfn.Fail(this, 'Fail', {
487487
});
488488
```
489489

490+
You can also use an intrinsic function that returns a string to specify CausePath and ErrorPath.
491+
The available functions include States.Format, States.JsonToString, States.ArrayGetItem, States.Base64Encode, States.Base64Decode, States.Hash, and States.UUID.
492+
493+
```ts
494+
const fail = new sfn.Fail(this, 'Fail', {
495+
errorPath: sfn.JsonPath.format('error: {}.', sfn.JsonPath.stringAt('$.someError')),
496+
causePath: "States.Format('cause: {}.', $.someCause)",
497+
});
498+
```
499+
490500
### Map
491501

492502
A `Map` state can be used to run a set of steps for each element of an input array.

packages/aws-cdk-lib/aws-stepfunctions/lib/states/fail.ts

+53-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Construct } from 'constructs';
22
import { StateType } from './private/state-type';
33
import { renderJsonPath, State } from './state';
4+
import { Token } from '../../../core';
45
import { INextable } from '../types';
56

67
/**
@@ -31,6 +32,9 @@ export interface FailProps {
3132
/**
3233
* JsonPath expression to select part of the state to be the error to this state.
3334
*
35+
* You can also use an intrinsic function that returns a string to specify this property.
36+
* The allowed functions include States.Format, States.JsonToString, States.ArrayGetItem, States.Base64Encode, States.Base64Decode, States.Hash, and States.UUID.
37+
*
3438
* @default - No error path
3539
*/
3640
readonly errorPath?: string;
@@ -45,6 +49,9 @@ export interface FailProps {
4549
/**
4650
* JsonPath expression to select part of the state to be the cause to this state.
4751
*
52+
* You can also use an intrinsic function that returns a string to specify this property.
53+
* The allowed functions include States.Format, States.JsonToString, States.ArrayGetItem, States.Base64Encode, States.Base64Decode, States.Hash, and States.UUID.
54+
*
4855
* @default - No cause path
4956
*/
5057
readonly causePath?: string;
@@ -56,6 +63,16 @@ export interface FailProps {
5663
* Reaching a Fail state terminates the state execution in failure.
5764
*/
5865
export class Fail extends State {
66+
private static allowedIntrinsics = [
67+
'States.Format',
68+
'States.JsonToString',
69+
'States.ArrayGetItem',
70+
'States.Base64Encode',
71+
'States.Base64Decode',
72+
'States.Hash',
73+
'States.UUID',
74+
];
75+
5976
public readonly endStates: INextable[] = [];
6077

6178
private readonly error?: string;
@@ -80,9 +97,42 @@ export class Fail extends State {
8097
Type: StateType.FAIL,
8198
Comment: this.comment,
8299
Error: this.error,
83-
ErrorPath: renderJsonPath(this.errorPath),
100+
ErrorPath: this.isIntrinsicString(this.errorPath) ? this.errorPath : renderJsonPath(this.errorPath),
84101
Cause: this.cause,
85-
CausePath: renderJsonPath(this.causePath),
102+
CausePath: this.isIntrinsicString(this.causePath) ? this.causePath : renderJsonPath(this.causePath),
86103
};
87104
}
88-
}
105+
106+
/**
107+
* Validate this state
108+
*/
109+
protected validateState(): string[] {
110+
const errors = super.validateState();
111+
112+
if (this.errorPath && this.isIntrinsicString(this.errorPath) && !this.isAllowedIntrinsic(this.errorPath)) {
113+
errors.push(`You must specify a valid intrinsic function in errorPath. Must be one of ${Fail.allowedIntrinsics.join(', ')}`);
114+
}
115+
116+
if (this.causePath && this.isIntrinsicString(this.causePath) && !this.isAllowedIntrinsic(this.causePath)) {
117+
errors.push(`You must specify a valid intrinsic function in causePath. Must be one of ${Fail.allowedIntrinsics.join(', ')}`);
118+
}
119+
120+
if (this.error && this.errorPath) {
121+
errors.push('Fail state cannot have both error and errorPath');
122+
}
123+
124+
if (this.cause && this.causePath) {
125+
errors.push('Fail state cannot have both cause and causePath');
126+
}
127+
128+
return errors;
129+
}
130+
131+
private isIntrinsicString(jsonPath?: string): boolean {
132+
return !Token.isUnresolved(jsonPath) && !jsonPath?.startsWith('$');
133+
}
134+
135+
private isAllowedIntrinsic(intrinsic: string): boolean {
136+
return Fail.allowedIntrinsics.some(allowed => intrinsic.startsWith(allowed));
137+
}
138+
}

packages/aws-cdk-lib/aws-stepfunctions/test/state-machine-resources.test.ts

+82-1
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,88 @@ describe('State Machine Resources', () => {
9696
});
9797
}),
9898

99+
test.each([
100+
[
101+
"States.Format('error: {}.', $.error)",
102+
"States.Format('cause: {}.', $.cause)",
103+
],
104+
[
105+
stepfunctions.JsonPath.format('error: {}.', stepfunctions.JsonPath.stringAt('$.error')),
106+
stepfunctions.JsonPath.format('cause: {}.', stepfunctions.JsonPath.stringAt('$.cause')),
107+
],
108+
])('Fail should render ErrorPath / CausePath correctly when specifying ErrorPath / CausePath using intrinsics', (errorPath, causePath) => {
109+
// GIVEN
110+
const app = new cdk.App();
111+
const stack = new cdk.Stack(app);
112+
const fail = new stepfunctions.Fail(stack, 'Fail', {
113+
errorPath,
114+
causePath,
115+
});
116+
117+
// WHEN
118+
const failState = stack.resolve(fail.toStateJson());
119+
120+
// THEN
121+
expect(failState).toStrictEqual({
122+
CausePath: "States.Format('cause: {}.', $.cause)",
123+
ErrorPath: "States.Format('error: {}.', $.error)",
124+
Type: 'Fail',
125+
});
126+
expect(() => app.synth()).not.toThrow();
127+
}),
128+
129+
test('fails in synthesis if error and errorPath are defined in Fail state', () => {
130+
// GIVEN
131+
const app = new cdk.App();
132+
const stack = new cdk.Stack(app);
133+
134+
// WHEN
135+
new stepfunctions.Fail(stack, 'Fail', {
136+
error: 'error',
137+
errorPath: '$.error',
138+
});
139+
140+
expect(() => app.synth()).toThrow(/Fail state cannot have both error and errorPath/);
141+
}),
142+
143+
test('fails in synthesis if cause and causePath are defined in Fail state', () => {
144+
// GIVEN
145+
const app = new cdk.App();
146+
const stack = new cdk.Stack(app);
147+
148+
// WHEN
149+
new stepfunctions.Fail(stack, 'Fail', {
150+
cause: 'cause',
151+
causePath: '$.cause',
152+
});
153+
154+
expect(() => app.synth()).toThrow(/Fail state cannot have both cause and causePath/);
155+
}),
156+
157+
test.each([
158+
'States.Array($.Id)',
159+
'States.ArrayPartition($.inputArray, 4)',
160+
'States.ArrayContains($.inputArray, $.lookingFor)',
161+
'States.ArrayRange(1, 9, 2)',
162+
'States.ArrayLength($.inputArray)',
163+
'States.JsonMerge($.json1, $.json2, false)',
164+
'States.StringToJson($.escapedJsonString)',
165+
'plainString',
166+
])('fails in synthesis if specifying invalid intrinsic functions in the causePath and errorPath (%s)', (intrinsic) => {
167+
// GIVEN
168+
const app = new cdk.App();
169+
const stack = new cdk.Stack(app);
170+
171+
// WHEN
172+
new stepfunctions.Fail(stack, 'Fail', {
173+
causePath: intrinsic,
174+
errorPath: intrinsic,
175+
});
176+
177+
expect(() => app.synth()).toThrow(/You must specify a valid intrinsic function in causePath. Must be one of States.Format, States.JsonToString, States.ArrayGetItem, States.Base64Encode, States.Base64Decode, States.Hash, States.UUID/);
178+
expect(() => app.synth()).toThrow(/You must specify a valid intrinsic function in errorPath. Must be one of States.Format, States.JsonToString, States.ArrayGetItem, States.Base64Encode, States.Base64Decode, States.Hash, States.UUID/);
179+
}),
180+
99181
testDeprecated('Task should render InputPath / Parameters / OutputPath correctly', () => {
100182
// GIVEN
101183
const stack = new cdk.Stack();
@@ -721,7 +803,6 @@ describe('State Machine Resources', () => {
721803
],
722804
});
723805
});
724-
725806
});
726807

727808
interface FakeTaskProps extends stepfunctions.TaskStateBaseProps {

0 commit comments

Comments
 (0)