Skip to content

Commit a712983

Browse files
authored
Base64 encode amplify errors in serialized form (#2404)
1 parent f20e8a3 commit a712983

File tree

3 files changed

+177
-78
lines changed

3 files changed

+177
-78
lines changed

.changeset/metal-taxis-sing.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@aws-amplify/platform-core': patch
3+
---
4+
5+
Base64 encode serialized Amplify Errors

packages/platform-core/src/errors/amplify_error.test.ts

Lines changed: 83 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -85,67 +85,113 @@ and some after the error message
8585
assert.deepStrictEqual(actual?.cause?.message, testError.cause?.message);
8686
});
8787

88-
void it('deserialize when string is encoded with single quote and has double quotes in it', () => {
89-
const sampleStderr = `some random stderr
88+
void describe('V1 deserialization', () => {
89+
void it('deserialize when string is encoded with single quote and has double quotes in it', () => {
90+
const sampleStderr = `some random stderr
9091
${util.inspect({
9192
serializedError:
9293
'{"name":"SyntaxError","classification":"ERROR","options":{"message":"test error message","resolution":"test resolution"}}',
9394
})}
9495
and some after the error message
9596
`;
96-
const actual = AmplifyError.fromStderr(sampleStderr);
97-
assert.deepStrictEqual(actual?.name, 'SyntaxError');
98-
assert.deepStrictEqual(actual?.classification, 'ERROR');
99-
assert.deepStrictEqual(actual?.message, 'test error message');
100-
assert.deepStrictEqual(actual?.resolution, 'test resolution');
101-
});
97+
const actual = AmplifyError.fromStderr(sampleStderr);
98+
assert.deepStrictEqual(actual?.name, 'SyntaxError');
99+
assert.deepStrictEqual(actual?.classification, 'ERROR');
100+
assert.deepStrictEqual(actual?.message, 'test error message');
101+
assert.deepStrictEqual(actual?.resolution, 'test resolution');
102+
});
102103

103-
void it('deserialize when string is encoded with single quote and has double quotes escaped in between', () => {
104-
const sampleStderr = `some random stderr
104+
void it('deserialize when string is encoded with single quote and has double quotes escaped in between', () => {
105+
const sampleStderr = `some random stderr
105106
${util.inspect({
106107
serializedError:
107108
'{"name":"SyntaxError","classification":"ERROR","options":{"message":"paths must start with \\"/\\" and end with \\"/*","resolution":"test resolution"}}',
108109
})}
109110
and some after the error message
110111
`;
111-
const actual = AmplifyError.fromStderr(sampleStderr);
112-
assert.deepStrictEqual(actual?.name, 'SyntaxError');
113-
assert.deepStrictEqual(actual?.classification, 'ERROR');
114-
assert.deepStrictEqual(
115-
actual?.message,
116-
'paths must start with "/" and end with "/*'
117-
);
118-
assert.deepStrictEqual(actual?.resolution, 'test resolution');
119-
});
112+
const actual = AmplifyError.fromStderr(sampleStderr);
113+
assert.deepStrictEqual(actual?.name, 'SyntaxError');
114+
assert.deepStrictEqual(actual?.classification, 'ERROR');
115+
assert.deepStrictEqual(
116+
actual?.message,
117+
'paths must start with "/" and end with "/*'
118+
);
119+
assert.deepStrictEqual(actual?.resolution, 'test resolution');
120+
});
120121

121-
void it('deserialize when string is encoded with double quote and has double quotes string in it', () => {
122-
const sampleStderr = `some random stderr
122+
void it('deserialize when string is encoded with double quote and has double quotes string in it', () => {
123+
const sampleStderr = `some random stderr
123124
serializedError: "{\\"name\\":\\"SyntaxError\\",\\"classification\\":\\"ERROR\\",\\"options\\":{\\"message\\":\\"test error message\\",\\"resolution\\":\\"test resolution\\"}}"
124125
and some after the error message
125126
`;
126-
const actual = AmplifyError.fromStderr(sampleStderr);
127-
assert.deepStrictEqual(actual?.name, 'SyntaxError');
128-
assert.deepStrictEqual(actual?.classification, 'ERROR');
129-
assert.deepStrictEqual(actual?.message, 'test error message');
130-
assert.deepStrictEqual(actual?.resolution, 'test resolution');
131-
});
127+
const actual = AmplifyError.fromStderr(sampleStderr);
128+
assert.deepStrictEqual(actual?.name, 'SyntaxError');
129+
assert.deepStrictEqual(actual?.classification, 'ERROR');
130+
assert.deepStrictEqual(actual?.message, 'test error message');
131+
assert.deepStrictEqual(actual?.resolution, 'test resolution');
132+
});
132133

133-
void it('deserialize when string has single quotes in between', () => {
134-
const sampleStderr = `some random stderr
134+
void it('deserialize when string has single quotes in between', () => {
135+
const sampleStderr = `some random stderr
135136
${util.inspect({
136137
serializedError:
137138
'{"name":"SyntaxError","classification":"ERROR","options":{"message":"Cannot read properties of undefined (reading \'data\')","resolution":"test resolution"}}',
138139
})}
139140
and some after the error message
140141
`;
141-
const actual = AmplifyError.fromStderr(sampleStderr);
142-
assert.deepStrictEqual(actual?.name, 'SyntaxError');
143-
assert.deepStrictEqual(actual?.classification, 'ERROR');
144-
assert.deepStrictEqual(
145-
actual?.message,
146-
`Cannot read properties of undefined (reading 'data')`
147-
);
148-
assert.deepStrictEqual(actual?.resolution, 'test resolution');
142+
const actual = AmplifyError.fromStderr(sampleStderr);
143+
assert.deepStrictEqual(actual?.name, 'SyntaxError');
144+
assert.deepStrictEqual(actual?.classification, 'ERROR');
145+
assert.deepStrictEqual(
146+
actual?.message,
147+
`Cannot read properties of undefined (reading 'data')`
148+
);
149+
assert.deepStrictEqual(actual?.resolution, 'test resolution');
150+
});
151+
});
152+
153+
void describe('V2 deserialization', () => {
154+
void it('deserialize when string is encoded with single quote', () => {
155+
const sampleStderr = `some random stderr
156+
serializedError: '${Buffer.from(
157+
'{"name":"SyntaxError","classification":"ERROR","options":{"message":"test error message","resolution":"test resolution"}}'
158+
).toString('base64')}',
159+
and some after the error message
160+
`;
161+
const actual = AmplifyError.fromStderr(sampleStderr);
162+
assert.deepStrictEqual(actual?.name, 'SyntaxError');
163+
assert.deepStrictEqual(actual?.classification, 'ERROR');
164+
assert.deepStrictEqual(actual?.message, 'test error message');
165+
assert.deepStrictEqual(actual?.resolution, 'test resolution');
166+
});
167+
168+
void it('deserialize when string is encoded with double quote', () => {
169+
const sampleStderr = `some random stderr
170+
serializedError: "${Buffer.from(
171+
'{"name":"SyntaxError","classification":"ERROR","options":{"message":"test error message","resolution":"test resolution"}}'
172+
).toString('base64')}",
173+
and some after the error message
174+
`;
175+
const actual = AmplifyError.fromStderr(sampleStderr);
176+
assert.deepStrictEqual(actual?.name, 'SyntaxError');
177+
assert.deepStrictEqual(actual?.classification, 'ERROR');
178+
assert.deepStrictEqual(actual?.message, 'test error message');
179+
assert.deepStrictEqual(actual?.resolution, 'test resolution');
180+
});
181+
182+
void it('deserialize when string is encoded with back ticks', () => {
183+
const sampleStderr = `some random stderr
184+
serializedError: \`${Buffer.from(
185+
'{"name":"SyntaxError","classification":"ERROR","options":{"message":"test error message","resolution":"test resolution"}}'
186+
).toString('base64')}\`,
187+
and some after the error message
188+
`;
189+
const actual = AmplifyError.fromStderr(sampleStderr);
190+
assert.deepStrictEqual(actual?.name, 'SyntaxError');
191+
assert.deepStrictEqual(actual?.classification, 'ERROR');
192+
assert.deepStrictEqual(actual?.message, 'test error message');
193+
assert.deepStrictEqual(actual?.resolution, 'test resolution');
194+
});
149195
});
150196
});
151197

packages/platform-core/src/errors/amplify_error.ts

Lines changed: 89 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -47,55 +47,41 @@ export abstract class AmplifyError<T extends string = string> extends Error {
4747
if (cause && AmplifyError.isAmplifyError(cause)) {
4848
cause.serializedError = undefined;
4949
}
50-
this.serializedError = JSON.stringify(
51-
{
52-
name,
53-
classification,
54-
options,
55-
cause,
56-
},
57-
errorSerializer
58-
);
50+
this.serializedError = Buffer.from(
51+
JSON.stringify(
52+
{
53+
name,
54+
classification,
55+
options,
56+
cause,
57+
},
58+
errorSerializer
59+
)
60+
).toString('base64');
5961
}
6062

6163
static fromStderr = (_stderr: string): AmplifyError | undefined => {
62-
/**
63-
* `["']?serializedError["']?:[ ]?` captures the start of the serialized error. The quotes depend on which OS is being used
64-
* `(?:`(.+?)`|'(.+?)'|"((?:\\"|[^"])*?)")` captures the rest of the serialized string enclosed in either single quote,
65-
* double quotes or back-ticks.
66-
*/
67-
const extractionRegex =
68-
/["']?serializedError["']?:[ ]?(?:`(.+?)`|'(.+?)'|"((?:\\"|[^"])*?)")/;
69-
const serialized = _stderr.match(extractionRegex);
70-
if (serialized && serialized.length === 4) {
71-
// 4 because 1 match and 3 capturing groups
72-
try {
73-
const serializedString = serialized
74-
.slice(1)
75-
.find((item) => item && item.length > 0)
76-
?.replaceAll('\\"', '"')
77-
.replaceAll("\\'", "'");
64+
try {
65+
const serializedString = tryFindSerializedErrorJSONString(_stderr);
7866

79-
if (!serializedString) {
80-
return undefined;
81-
}
67+
if (!serializedString) {
68+
return undefined;
69+
}
8270

83-
const { name, classification, options, cause } =
84-
JSON.parse(serializedString);
71+
const { name, classification, options, cause } =
72+
JSON.parse(serializedString);
8573

86-
let serializedCause = cause;
87-
if (cause && ErrorSerializerDeserializer.isSerializedErrorType(cause)) {
88-
serializedCause = ErrorSerializerDeserializer.deserialize(cause);
89-
}
90-
return classification === 'ERROR'
91-
? new AmplifyUserError(name, options, serializedCause)
92-
: new AmplifyFault(name, options, serializedCause);
93-
} catch (error) {
94-
// cannot deserialize
95-
return undefined;
74+
let serializedCause = cause;
75+
if (cause && ErrorSerializerDeserializer.isSerializedErrorType(cause)) {
76+
serializedCause = ErrorSerializerDeserializer.deserialize(cause);
9677
}
78+
return classification === 'ERROR'
79+
? new AmplifyUserError(name, options, serializedCause)
80+
: new AmplifyFault(name, options, serializedCause);
81+
} catch (error) {
82+
// cannot deserialize
83+
return undefined;
9784
}
98-
return undefined;
9985
};
10086

10187
/**
@@ -207,6 +193,68 @@ export abstract class AmplifyError<T extends string = string> extends Error {
207193
};
208194
}
209195

196+
const tryFindSerializedErrorJSONString = (
197+
_stderr: string
198+
): string | undefined => {
199+
let errorJSONString = tryFindSerializedErrorJSONStringV2(_stderr);
200+
if (!errorJSONString) {
201+
errorJSONString = tryFindSerializedErrorJSONStringV1(_stderr);
202+
}
203+
return errorJSONString;
204+
};
205+
206+
/**
207+
* Tries to find serialized string assuming that it is in a form of serialized JSON encoded with base64.
208+
*/
209+
const tryFindSerializedErrorJSONStringV2 = (
210+
_stderr: string
211+
): string | undefined => {
212+
/**
213+
* `["']?serializedError["']?:[ ]?` captures the start of the serialized error. The quotes depend on which OS is being used
214+
* `(?:`([a-zA-Z0-9+/=]+?)`|'([a-zA-Z0-9+/=]+?)'|"([a-zA-Z0-9+/=]+?)")` captures the rest of the serialized string enclosed in either single quote,
215+
* double quotes or back-ticks.
216+
*/
217+
const extractionRegex =
218+
/["']?serializedError["']?:[ ]?(?:`([a-zA-Z0-9+/=]+?)`|'([a-zA-Z0-9+/=]+?)'|"([a-zA-Z0-9+/=]+?)")/;
219+
const serialized = _stderr.match(extractionRegex);
220+
if (serialized && serialized.length === 4) {
221+
// 4 because 1 match and 3 capturing groups
222+
const base64SerializedString = serialized
223+
.slice(1)
224+
.find((item) => item && item.length > 0);
225+
if (base64SerializedString) {
226+
return Buffer.from(base64SerializedString, 'base64').toString('utf-8');
227+
}
228+
}
229+
return undefined;
230+
};
231+
232+
/**
233+
* Tries to find serialized string assuming that it is in a form of serialized JSON.
234+
* @deprecated This is old format left for backwards compatibility in case that synth-time components are using older version of platform-core.
235+
*/
236+
const tryFindSerializedErrorJSONStringV1 = (
237+
_stderr: string
238+
): string | undefined => {
239+
/**
240+
* `["']?serializedError["']?:[ ]?` captures the start of the serialized error. The quotes depend on which OS is being used
241+
* `(?:`(.+?)`|'(.+?)'|"((?:\\"|[^"])*?)")` captures the rest of the serialized string enclosed in either single quote,
242+
* double quotes or back-ticks.
243+
*/
244+
const extractionRegex =
245+
/["']?serializedError["']?:[ ]?(?:`(.+?)`|'(.+?)'|"((?:\\"|[^"])*?)")/;
246+
const serialized = _stderr.match(extractionRegex);
247+
if (serialized && serialized.length === 4) {
248+
// 4 because 1 match and 3 capturing groups
249+
return serialized
250+
.slice(1)
251+
.find((item) => item && item.length > 0)
252+
?.replaceAll('\\"', '"')
253+
.replaceAll("\\'", "'");
254+
}
255+
return undefined;
256+
};
257+
210258
const isCredentialsError = (err?: Error): boolean => {
211259
return (
212260
!!err &&

0 commit comments

Comments
 (0)