Skip to content

fix(gotrue): parse expires_in as num in Session.fromJson - #1719

Merged
spydon merged 2 commits into
release/gotrue-2.27.xfrom
hotfix/gotrue-wasm-expires-in
Aug 14, 2026
Merged

fix(gotrue): parse expires_in as num in Session.fromJson#1719
spydon merged 2 commits into
release/gotrue-2.27.xfrom
hotfix/gotrue-wasm-expires-in

Conversation

@spydon

@spydon spydon commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

What

Hotfix for the gotrue 2.27.x line, backporting the Wasm Session.fromJson crash fix from #1716.

The base is release/gotrue-2.27.x, a maintenance branch cut at the gotrue-v2.27.1 tag. It cannot target main, because main has since renamed the package to supabase_auth (#1697) and renamed the public API (#1712), so a PR against main would read as reverting everything merged since the tag.

Resolves #1687 for the 2.x line.

The bug

Session.fromJson cast json['expires_in'] straight to int?. That map does not always come from jsonDecode. GoTrueClient._mayStartBroadcastChannel also feeds it payloads that crossed the JavaScript interop boundary through dartify() in broadcast_web.dart, where every JavaScript number arrives as a double.

Under dart2js this was invisible, because Dart int and double share a JavaScript Number at runtime, so 3600.0 as int? succeeded. Under dart2wasm they are distinct runtime types and the cast throws:

TypeError: type 'double' is not a subtype of type 'int?' in type cast

The json.decode(json.encode(dataMap)) round trip in broadcast_web.dart does not rescue this: 3600.0 encodes to "3600.0" and decodes back to a double.

Because the throw happened inside the BroadcastChannel message listener, outside the setup try/catch, the rest of the listener was skipped. No _saveSession or _removeSession ran, and notifyAllSubscribers never fired, so receiving tabs silently failed to synchronize login, logout, and token refresh.

The fix

expires_in is now parsed as (json['expires_in'] as num?)?.toInt(), which accepts int, double, and null. JwtPayload.fromJson (exp, nbf, iat) and OAuthClientListResponse.fromJson (nextPage, lastPage, total) get the same treatment for their numeric fields.

I traced the rest of the reachable surface. dartify() is called in exactly one place in the repository, and the only types built from that data are Session, User, UserIdentity, and Factor. User.fromJson has no numeric fields, its timestamps are ISO 8601 strings, so after this change nothing reachable from the interop boundary casts to int. Everything else in the workspace decodes from a string through dart:convert, where integer literals stay int on every backend.

Pipeline fixes

The tag this branch is frozen at no longer builds against the current toolchain, so the second commit carries three unrelated fixes needed to get a green run. All three were verified to be pre-existing drift rather than fallout from this change, by comparing against #1717, an equivalent change on main whose run passed minutes apart.

  • Flutter stable now ships AGP 9, which rejects the example app's old Gradle DSL. The example's Gradle configuration is ported from main (AGP 8.13.1 to 9.1.0, Gradle 8.13 to 9.3.1, Kotlin 2.1.20 to 2.4.0). The example is publish_to: none, so nothing published changes.
  • dart analyze --fatal-infos now reports use_super_parameters on SupabaseStorageClient. main resolved this as part of the fetch layer refactor in refactor: share the HTTP request pieces between the fetch layers #1647, which gave StorageBucketApi a stored client field. At this tag the superclass stores nothing, so the local field is still needed and the lint is suppressed instead of backporting that refactor.
  • The compliance workflow validates against supabase/sdk@main, whose canonical capability identifiers keep moving, so a branch frozen at an old release can never satisfy them. Its pull_request trigger is now scoped to pull requests that target main.

Release notes

melos version on this branch proposes gotrue 2.27.2, plus supabase 2.16.1 and supabase_flutter 2.17.2 as dependency cascades. Those cascades are required, not incidental: the published supabase 2.16.0 pins gotrue: 2.27.1 exactly and supabase_flutter 2.17.1 pins supabase: 2.16.0 exactly, so publishing gotrue alone would reach nobody using the higher level packages.

Note that release-tag.yml only triggers on pushes to main, so merging the version pull request into this maintenance branch will not create the tags. They need to be pushed manually, or that workflow needs a workflow_dispatch trigger, before release-publish.yml can run against gotrue-v2.27.2.

Testing

  • dart pub get resolves the workspace cleanly.
  • dart analyze lib test in packages/gotrue: no issues.
  • dart analyze --fatal-infos packages/storage_client: no issues.
  • dart test test/src/types/session_test.dart test/src/helper_test.dart: 57 passing, including two new tests covering a double expires_in and double exp, nbf, and iat.
  • dart format: clean.
  • The Android build is verified by CI only, it was not built locally.

Parse expires_in using (json['expires_in'] as num?)?.toInt() so that
Session.fromJson handles double values crossing JS interop (such as
BroadcastChannel cross-tab sync) on Wasm without throwing.

JwtPayload.fromJson and OAuthClientListResponse.fromJson get the same
treatment for their numeric fields.

Fixes #1687
@spydon
spydon requested a review from a team as a code owner August 14, 2026 13:33
@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f2c36039-54d6-4c08-9335-94ad13c577a8

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the auth This issue or pull request is related to authentication label Aug 14, 2026
The tag this branch is frozen at no longer builds against the current
toolchain and the current canonical capability IDs:

- Flutter stable now ships AGP 9, which rejects the example app's old
  Gradle DSL. Ported the example's Gradle config from main.
- dart analyze --fatal-infos now reports use_super_parameters in
  SupabaseStorageClient. main resolved this as part of the fetch layer
  refactor, which is too large to backport, so the lint is suppressed
  locally instead.
- The compliance workflow validates against supabase/sdk@main, whose
  capability IDs keep moving, so a frozen branch can never satisfy them.
  It now only runs for pull requests that target main.
@spydon
spydon force-pushed the hotfix/gotrue-wasm-expires-in branch from d51f2a5 to 5b68061 Compare August 14, 2026 14:27
@spydon
spydon merged commit 6c8047e into release/gotrue-2.27.x Aug 14, 2026
37 checks passed
@spydon
spydon deleted the hotfix/gotrue-wasm-expires-in branch August 14, 2026 14:47
spydon added a commit that referenced this pull request Aug 14, 2026
Release for the `gotrue` 2.27.x maintenance line, targeting
`release/gotrue-2.27.x`. Ships the Wasm `Session.fromJson` fix merged in
#1719.

## Versions

| Package | Version | Reason |
| --- | --- | --- |
| `gotrue` | 2.27.1 to 2.27.2 | The fix itself |
| `supabase` | 2.16.0 to 2.16.1 | Dependency was updated |
| `supabase_flutter` | 2.17.1 to 2.17.2 | Dependency was updated |

The two dependent bumps are required rather than incidental. The
published `supabase` 2.16.0 pins `gotrue: 2.27.1` exactly and
`supabase_flutter` 2.17.1 pins `supabase: 2.16.0` exactly, so publishing
`gotrue` on its own would not reach anyone using the higher level
packages. The release commit rewrites those pins to 2.27.2 and 2.16.1
respectively.

## The example integration tests needed #1643 backported

This branch is cut at the `gotrue-v2.27.1` tag, which is commit #1642,
and #1643 landed immediately after it. That is the commit that made
these tests pass in the first place, so the branch sits at the last
commit where they were still broken.

The suite is gated on `startsWith(github.event.pull_request.title,
'chore(release)')`, so it only runs on release pull requests. That is
why #1719 skipped it and why this pull request is the first thing on the
branch to trigger it, in the same way #1642 was the first trigger ever.

The first run here failed on four platforms at three different lines,
matching the causes #1643 describes:

- `linux` at line 76, `find.text(createdTitle)` matched the create
dialog's own text field while it was still animating away, so the
following `ListTile` lookup had nothing to descend from.
- `macos` at line 50, the seeded `Book flights` row was below the fold.
- `windows` at line 97, the renamed row still matched the old title.

`1862f74c` is cherry picked here unchanged. Its parent is exactly this
branch's base commit, so it applied without conflict.

## storage_client is deliberately excluded

#1719 was squash merged, so its single `fix(gotrue):` commit also
touches `packages/storage_client/lib/src/storage_client.dart` and the
`supabase_flutter` example's Gradle files. Left alone, `melos version`
therefore also proposed `storage_client` 2.8.1.

That bump was suppressed with `--ignore storage_client`, because the
only change to that package is an `// ignore: use_super_parameters`
comment added to keep the pipeline green. Publishing it would ship a
no-op version whose changelog entry reads "FIX(gotrue): parse expires_in
as num in Session.fromJson", and would consume 2.8.1 on a line where
`main` is still at 2.8.0.

## Before publishing

`release-tag.yml` only triggers on pushes to `main`, so merging this
will not create the tags. They need to be pushed manually, or that
workflow needs a `workflow_dispatch` trigger, before
`release-publish.yml` can be dispatched against `gotrue-v2.27.2`.

## How this was generated

`melos version --yes --ignore="storage_client" --no-git-tag-version`,
matching the flags `release-prepare.yml` uses apart from the added
filter, which the workflow has no input for. `dart pub get` resolves the
workspace and `dart analyze packages/gotrue` is clean at these versions.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auth This issue or pull request is related to authentication

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants