Skip to content

fix(cookies): parse ISO 8601 expires with milliseconds and fail loudly on invalid dates - #339

Open
jingjing2222 wants to merge 1 commit into
mainfrom
fix/cookies-expires-iso-parse
Open

fix(cookies): parse ISO 8601 expires with milliseconds and fail loudly on invalid dates#339
jingjing2222 wants to merge 1 commit into
mainfrom
fix/cookies-expires-iso-parse

Conversation

@jingjing2222

Copy link
Copy Markdown
Collaborator

What

Fixes expires date parsing in the @granite-js/cookies native modules on both platforms:

  • iOS: parseExpiresDate used a default ISO8601DateFormatter, which cannot parse ISO 8601 strings containing fractional seconds. Since JavaScript's Date.prototype.toISOString() always emits milliseconds (e.g. 2024-01-01T00:00:00.000Z), the most common way to produce an expires value from JS never parsed. Now an ISO8601DateFormatter with .withFractionalSeconds is tried first, falling back to the non-fractional variant.
  • Android: the only ISO pattern was yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ, whose zone letters cannot reliably match the literal Z suffix of toISOString() output. Added literal-'Z' UTC patterns (with and without milliseconds) tried first, keeping the existing offset/RFC 1123/timestamp fallbacks.
  • Both: on parse failure, the native modules used to silently drop the expires attribute and store the cookie as a session cookie — the caller got true back and had no way to know the expiration was lost. set now rejects an unparseable expires with a descriptive error listing the supported formats.

Why

The cookie expires field is documented as "ISO string or timestamp", but passing the canonical JS ISO string silently produced a session cookie. Session cookies live only in memory, so when iOS kills the WebKit network process of a backgrounded app, the cookie disappears — surfacing downstream as apps losing state they believed was persisted for weeks. The silent fallback made this effectively undebuggable from the JS side, since CookieManager.set resolved successfully.

How it was verified

Ran replicas of the exact parsing logic (old vs. new) as standalone scripts:

  • iOS (swift on macOS, same Foundation ISO8601DateFormatter), also constructing the resulting HTTPCookie to check isSessionOnly:
input old parse new parse resulting cookie
2024-01-01T00:00:00.000Z (toISOString) nil → session cookie ✅ parsed persistent (isSessionOnly=false)
2024-01-01T00:00:00Z parsed ✅ parsed persistent
Mon, 01 Jan 2024 00:00:00 GMT (RFC 1123) parsed ✅ parsed persistent
Unix timestamp (ms) parsed ✅ parsed persistent
garbage string nil → session cookie nil → error thrown
  • Android (java with the same java.text.SimpleDateFormat semantics): the new literal-'Z' patterns parse toISOString() output (with and without milliseconds) to the correct epoch; RFC 1123 and millisecond-timestamp behavior is unchanged.

  • swiftc -parse passes on the modified Swift file; yarn workspace @granite-js/cookies build (brick-codegen + tsdown) succeeds with no generated-file drift.

Notes for reviewers

  • Rejecting an invalid expires (instead of silently storing a session cookie) is a behavior change for callers that were passing unparseable dates — but those callers were already not getting what they asked for; they were just never told.
  • Android's parseExpiresDate intentionally keeps lenient SimpleDateFormat parsing: strict mode would reject RFC 1123 dates whose weekday doesn't match the date, which the previous implementation accepted.

@vercel

vercel Bot commented Aug 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
granite-docs Ready Ready Preview Aug 3, 2026 9:25am

Request Review

@changeset-bot

changeset-bot Bot commented Aug 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 27c3ad3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 28 packages
Name Type
@granite-js/cookies Patch
babel-preset-granite Patch
create-granite-app Patch
@granite-js/blur-view Patch
@granite-js/brownfield-module Patch
@granite-js/cli Patch
@granite-js/screen Patch
@granite-js/image Patch
@granite-js/jest Patch
@granite-js/lottie Patch
@granite-js/mpack Patch
@granite-js/native Patch
@granite-js/naver-map Patch
@granite-js/plugin-core Patch
@granite-js/plugin-env Patch
@granite-js/plugin-hermes Patch
@granite-js/plugin-micro-frontend Patch
@granite-js/plugin-router Patch
@granite-js/plugin-rozenite Patch
@granite-js/plugin-sentry Patch
@granite-js/react-native Patch
@granite-js/style-utils Patch
@granite-js/utils Patch
@granite-js/video Patch
@granite-js/vitest Patch
@granite-js/deployment-manager Patch
@granite-js/forge-cli Patch
@granite-js/pulumi-aws Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Aug 3, 2026

Copy link
Copy Markdown
@granite-js/deployment-manager

yarn add https://pkg.pr.new/@granite-js/deployment-manager@339.tgz

@granite-js/forge-cli

yarn add https://pkg.pr.new/@granite-js/forge-cli@339.tgz

@granite-js/pulumi-aws

yarn add https://pkg.pr.new/@granite-js/pulumi-aws@339.tgz

babel-preset-granite

yarn add https://pkg.pr.new/babel-preset-granite@339.tgz

@granite-js/blur-view

yarn add https://pkg.pr.new/@granite-js/blur-view@339.tgz

@granite-js/brownfield-module

yarn add https://pkg.pr.new/@granite-js/brownfield-module@339.tgz

@granite-js/cli

yarn add https://pkg.pr.new/@granite-js/cli@339.tgz

@granite-js/cookies

yarn add https://pkg.pr.new/@granite-js/cookies@339.tgz

create-granite-app

yarn add https://pkg.pr.new/create-granite-app@339.tgz

@granite-js/screen

yarn add https://pkg.pr.new/@granite-js/screen@339.tgz

@granite-js/image

yarn add https://pkg.pr.new/@granite-js/image@339.tgz

@granite-js/jest

yarn add https://pkg.pr.new/@granite-js/jest@339.tgz

@granite-js/lottie

yarn add https://pkg.pr.new/@granite-js/lottie@339.tgz

@granite-js/mpack

yarn add https://pkg.pr.new/@granite-js/mpack@339.tgz

@granite-js/native

yarn add https://pkg.pr.new/@granite-js/native@339.tgz

@granite-js/naver-map

yarn add https://pkg.pr.new/@granite-js/naver-map@339.tgz

@granite-js/plugin-core

yarn add https://pkg.pr.new/@granite-js/plugin-core@339.tgz

@granite-js/plugin-env

yarn add https://pkg.pr.new/@granite-js/plugin-env@339.tgz

@granite-js/plugin-hermes

yarn add https://pkg.pr.new/@granite-js/plugin-hermes@339.tgz

@granite-js/plugin-micro-frontend

yarn add https://pkg.pr.new/@granite-js/plugin-micro-frontend@339.tgz

@granite-js/plugin-router

yarn add https://pkg.pr.new/@granite-js/plugin-router@339.tgz

@granite-js/plugin-rozenite

yarn add https://pkg.pr.new/@granite-js/plugin-rozenite@339.tgz

@granite-js/plugin-sentry

yarn add https://pkg.pr.new/@granite-js/plugin-sentry@339.tgz

@granite-js/react-native

yarn add https://pkg.pr.new/@granite-js/react-native@339.tgz

@granite-js/style-utils

yarn add https://pkg.pr.new/@granite-js/style-utils@339.tgz

@granite-js/utils

yarn add https://pkg.pr.new/@granite-js/utils@339.tgz

@granite-js/video

yarn add https://pkg.pr.new/@granite-js/video@339.tgz

@granite-js/vitest

yarn add https://pkg.pr.new/@granite-js/vitest@339.tgz

commit: 27c3ad3

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.

1 participant