Skip to content

fix(client): GETEX PXAT with Date must encode milliseconds#3317

Merged
nkaradzhov merged 1 commit into
redis:masterfrom
spokodev:w32/redis-getex-pxat
Jul 3, 2026
Merged

fix(client): GETEX PXAT with Date must encode milliseconds#3317
nkaradzhov merged 1 commit into
redis:masterfrom
spokodev:w32/redis-getex-pxat

Conversation

@spokodev

@spokodev spokodev commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Description

GETEX with the modern { type: 'PXAT', value: Date } option encodes the expiry in seconds instead of milliseconds.

In parseCommand, EXAT and PXAT shared a single switch case that ran both through transformEXAT, which does Math.floor(date.getTime() / 1000). PXAT is the millisecond absolute-timestamp variant, so a Date is sent about 1000x too small. Redis reads that number as a millisecond timestamp far in the past and deletes the key immediately instead of extending its TTL.

For a numeric value there is no visible difference (both transforms pass a number straight through), which is why the existing number-only test passed and the bug went unnoticed.

Reproduction

await client.set('session:123', 'token');
// read the value and refresh it to an absolute expiry
await client.getEx('session:123', { type: 'PXAT', value: new Date(Date.now() + 60_000) });
// key is gone: the PXAT timestamp was encoded as seconds

Fix

Route PXAT through transformPXAT (milliseconds); EXAT keeps transformEXAT (seconds). transformPXAT was already imported. Numeric values are unchanged.

Test

Added a PXAT date case to GETEX.spec.ts asserting the modern type: 'PXAT' + Date path encodes getTime() (milliseconds). It fails before the change (seconds value) and passes after (milliseconds value).


Note

Low Risk
Small, targeted fix in GETEX argument parsing with a regression test; no auth or broad API surface changes.

Overview
Fixes GETEX when using { type: 'PXAT', value: Date }: the command encoder no longer treats PXAT like EXAT. PXAT now goes through transformPXAT (millisecond absolute timestamps); EXAT still uses transformPXAT’s second-based sibling transformEXAT.

Previously both shared one branch that always applied second encoding, so a Date on the modern PXAT path was sent ~1000× too small and Redis could expire the key immediately. Numeric PXAT values were unaffected.

Adds a GETEX.spec.ts case asserting type: 'PXAT' + Date serializes getTime() as the argument.

Reviewed by Cursor Bugbot for commit 69cc385. Bugbot is set up for automated code reviews on this repo. Configure here.

The modern { type: 'PXAT', value: Date } branch of GETEX routed through
transformEXAT, which floors the timestamp to seconds. PXAT expects a
millisecond Unix timestamp, so a Date was sent ~1000x too small and Redis
treated it as an already-elapsed expiry, immediately deleting the key.
Route PXAT through transformPXAT (milliseconds); EXAT still uses seconds.

@nkaradzhov nkaradzhov left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @spokodev, this looks good!

@nkaradzhov nkaradzhov merged commit 2c33cc5 into redis:master Jul 3, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants