Skip to content

fix(parser): stop malformed base64 from escaping safeParse - #5666

Merged
svozza merged 1 commit into
aws-powertools:mainfrom
vishwakt:fix/5651-base64-safeparse-never-throws
Sep 7, 2026
Merged

svozza merged 1 commit into
aws-powertools:mainfrom
vishwakt:fix/5651-base64-safeparse-never-throws

Conversation

@vishwakt

@vishwakt vishwakt commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Changes

Base64Encoded started its chain with z.string() and called fromBase64 inside the transform. fromBase64 throws a TypeError on invalid base64, and Zod 4 does not catch exceptions thrown inside transforms, so safeParse threw a TypeError instead of returning { success: false }. This affected the helper directly, the built-in KinesisDataStreamSchema / KinesisEnvelope, parse(event, undefined, KinesisDataStreamSchema, true), and the decorator / Middy middleware in safeParse mode.

Through parse() it was worse: Zod's ~standard.validate catches the synchronous throw and falls back to safeParseAsync, returning a rejecting promise. parse() saw a Promise, threw the misleading "supports only synchronous validation" error, and never attached a handler to the rejected promise, so it surfaced as an unhandled rejection (Runtime.UnhandledPromiseRejection on the Lambda runtime).

  • Guard Base64Encoded with z.base64() instead of z.string(), so invalid input becomes a normal Zod issue before the transform runs. This matches the existing CloudWatchLogsSchema and KinesisFirehoseRecordSchema, which already guard with z.base64() and were not affected.
  • In parse(), attach a handler to the promise returned by an async validation before throwing, so a rejected promise from Zod's async fallback cannot leak. Dropped the /* v8 ignore */ on that branch since it is reachable, and covered it with a test.

The decorator and Middy safeParse paths route through the same schemas, so they are fixed at the source by the helper change.

Added tests:

  • Base64Encoded(...).safeParse('not base64!!') returns a failed result instead of throwing.
  • parse(event, undefined, KinesisDataStreamSchema, true) with a malformed kinesis.data returns { success: false } rather than throwing.
  • A schema whose transform throws synchronously makes parse() throw the sync-only ParseError without leaking an unhandled rejection (the run fails if the handler is removed).

Verified each test fails against the pre-fix code, and the full parser unit suite (289 tests) passes with 100% coverage on parser.ts and helpers/index.ts.

Issue number: closes #5651


By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

`Base64Encoded` started its chain with `z.string()` and called `fromBase64`
inside a transform. `fromBase64` throws a `TypeError` on invalid base64, and
Zod 4 does not catch exceptions thrown inside transforms, so `safeParse`
threw instead of returning a failed result. Through `parse()` this also made
Zod fall back to async validation, whose rejected promise leaked as an
unhandled rejection (`Runtime.UnhandledPromiseRejection` on Lambda).

Guard the chain with `z.base64()` so invalid input becomes a normal Zod
issue before the transform runs, matching `CloudWatchLogsSchema` and
`KinesisFirehoseRecordSchema`. In `parse()`, attach a handler to the promise
returned by an async validation before throwing so the rejection cannot
leak, and drop the now-reachable `v8 ignore` on that branch.

Closes aws-powertools#5651
@svozza
svozza merged commit 11a18a3 into aws-powertools:main Sep 7, 2026
43 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/M PR between 30-99 LOC

2 participants