Expected Behavior
safeParse should never throw. A Kinesis record whose kinesis.data is not valid base64 should produce { success: false, error: ParseError, originalEvent } from KinesisDataStreamSchema.safeParse, KinesisEnvelope.safeParse, parse(event, undefined, KinesisDataStreamSchema, true), and from the decorator or Middy middleware in safeParse mode. The same applies to any customer schema built with the Base64Encoded helper.
Current Behavior
Base64Encoded is z.string().transform(...) with no base64 validation, and the transform calls fromBase64 outside a try. fromBase64 throws a TypeError for bad padding or invalid characters, and Zod 4 does not catch exceptions thrown inside transforms, so:
Base64Encoded(z.any()).safeParse('not base64!!') throws TypeError: Invalid base64 string.
KinesisDataStreamSchema.safeParse(event) and KinesisEnvelope.safeParse(event, schema) throw the same TypeError.
parse(event, undefined, KinesisDataStreamSchema, true) throws ParseError: Schema parsing supports only synchronous validation and leaks an unhandled promise rejection. Zod's ~standard.validate catches the synchronous throw and falls back to safeParseAsync, which returns a rejecting Promise. parse() sees a Promise, throws the misleading sync-only error, and never attaches a handler to the rejected promise. On the Lambda Node.js runtime this surfaces as Runtime.UnhandledPromiseRejection. The /* v8 ignore next */ on that branch in parser.ts marks it as unreachable, but it is reachable with the library's own built-in schema.
- The Middy middleware with
safeParse: true throws from before instead of handing the handler a failed result.
CloudWatchLogsSchema and KinesisFirehoseRecordSchema already guard with z.base64() before decoding and are not affected.
Code snippet
import { parse } from '@aws-lambda-powertools/parser';
import { Base64Encoded } from '@aws-lambda-powertools/parser/helpers';
import { KinesisDataStreamSchema } from '@aws-lambda-powertools/parser/schemas/kinesis';
import { z } from 'zod';
// 1. helper alone
Base64Encoded(z.any()).safeParse('not base64!!');
// throws TypeError: Invalid base64 string. (expected { success: false })
// 2. built-in schema through parse() with safeParse
const event = {
Records: [
{
eventSource: 'aws:kinesis',
eventVersion: '1.0',
eventID: 'shardId-000000000006:49590338271490256608559692538361571095921575989136588898',
eventName: 'aws:kinesis:record',
awsRegion: 'us-east-2',
invokeIdentityArn: 'arn:aws:iam::123456789012:role/lambda-role',
eventSourceARN: 'arn:aws:kinesis:us-east-2:123456789012:stream/lambda-stream',
kinesis: {
kinesisSchemaVersion: '1.0',
partitionKey: '1',
sequenceNumber: '49590338271490256608559692538361571095921575989136588898',
approximateArrivalTimestamp: 1545084650.987,
data: 'not base64!!',
},
},
],
};
parse(event, undefined, KinesisDataStreamSchema, true);
// throws ParseError: Schema parsing supports only synchronous validation
// and the process emits an unhandledRejection: TypeError: Invalid base64 string.
Steps to Reproduce
- Create a Node.js 22.x or 24.x project with
@aws-lambda-powertools/parser and zod@4.
- Run the snippet above with
node --unhandled-rejections=strict.
- Observe the
TypeError escaping safeParse in step 1, and the ParseError plus a process-level unhandled rejection in step 2.
- Wire the same event through
parser({ schema: KinesisDataStreamSchema, safeParse: true }) as a Middy middleware and observe before throwing instead of setting request.event to a failed result.
Possible Solution
- In
Base64Encoded, start the chain with z.base64() instead of z.string() so invalid input becomes a normal Zod issue before the transform runs, matching CloudWatchLogsSchema and KinesisFirehoseRecordSchema. Alternatively wrap the fromBase64 call and report via ctx.addIssue with fatal: true.
- In
parse(), when ~standard.validate returns a Promise, attach a handler before throwing so a rejected promise from Zod's async fallback cannot leak. Dropping the v8 ignore and covering the branch with a test would keep it honest.
Powertools for AWS Lambda (TypeScript) version
2.35.0 (main at 9bab0299)
AWS Lambda function runtime
22.x, 24.x
Packaging format used
npm
Execution logs
FAIL parse() with safeParse never throws when a Kinesis record carries malformed base64
ParseError: Schema parsing supports only synchronous validation
❯ parse src/parser.ts:124:11
⎯⎯⎯⎯ Unhandled Rejection ⎯⎯⎯⎯⎯
TypeError: Invalid base64 string.
❯ fromBase64 ../commons/lib/esm/fromBase64.js:27:15
❯ Object.transform src/helpers/index.ts:108:38
❯ inst._zod.parse ../../node_modules/zod/v4/classic/schemas.js:1182:28
❯ handlePipeResult ../../node_modules/zod/v4/core/schemas.js:2174:22
Expected Behavior
safeParseshould never throw. A Kinesis record whosekinesis.datais not valid base64 should produce{ success: false, error: ParseError, originalEvent }fromKinesisDataStreamSchema.safeParse,KinesisEnvelope.safeParse,parse(event, undefined, KinesisDataStreamSchema, true), and from the decorator or Middy middleware insafeParsemode. The same applies to any customer schema built with theBase64Encodedhelper.Current Behavior
Base64Encodedisz.string().transform(...)with no base64 validation, and the transform callsfromBase64outside atry.fromBase64throws aTypeErrorfor bad padding or invalid characters, and Zod 4 does not catch exceptions thrown inside transforms, so:Base64Encoded(z.any()).safeParse('not base64!!')throwsTypeError: Invalid base64 string.KinesisDataStreamSchema.safeParse(event)andKinesisEnvelope.safeParse(event, schema)throw the sameTypeError.parse(event, undefined, KinesisDataStreamSchema, true)throwsParseError: Schema parsing supports only synchronous validationand leaks an unhandled promise rejection. Zod's~standard.validatecatches the synchronous throw and falls back tosafeParseAsync, which returns a rejectingPromise.parse()sees aPromise, throws the misleading sync-only error, and never attaches a handler to the rejected promise. On the Lambda Node.js runtime this surfaces asRuntime.UnhandledPromiseRejection. The/* v8 ignore next */on that branch inparser.tsmarks it as unreachable, but it is reachable with the library's own built-in schema.safeParse: truethrows frombeforeinstead of handing the handler a failed result.CloudWatchLogsSchemaandKinesisFirehoseRecordSchemaalready guard withz.base64()before decoding and are not affected.Code snippet
Steps to Reproduce
@aws-lambda-powertools/parserandzod@4.node --unhandled-rejections=strict.TypeErrorescapingsafeParsein step 1, and theParseErrorplus a process-level unhandled rejection in step 2.parser({ schema: KinesisDataStreamSchema, safeParse: true })as a Middy middleware and observebeforethrowing instead of settingrequest.eventto a failed result.Possible Solution
Base64Encoded, start the chain withz.base64()instead ofz.string()so invalid input becomes a normal Zod issue before the transform runs, matchingCloudWatchLogsSchemaandKinesisFirehoseRecordSchema. Alternatively wrap thefromBase64call and report viactx.addIssuewithfatal: true.parse(), when~standard.validatereturns aPromise, attach a handler before throwing so a rejected promise from Zod's async fallback cannot leak. Dropping thev8 ignoreand covering the branch with a test would keep it honest.Powertools for AWS Lambda (TypeScript) version
2.35.0 (main at
9bab0299)AWS Lambda function runtime
22.x, 24.x
Packaging format used
npm
Execution logs
FAIL parse() with safeParse never throws when a Kinesis record carries malformed base64 ParseError: Schema parsing supports only synchronous validation ❯ parse src/parser.ts:124:11 ⎯⎯⎯⎯ Unhandled Rejection ⎯⎯⎯⎯⎯ TypeError: Invalid base64 string. ❯ fromBase64 ../commons/lib/esm/fromBase64.js:27:15 ❯ Object.transform src/helpers/index.ts:108:38 ❯ inst._zod.parse ../../node_modules/zod/v4/classic/schemas.js:1182:28 ❯ handlePipeResult ../../node_modules/zod/v4/core/schemas.js:2174:22