Skip to content

[pigeon] Treat NSNull as null for non-null Flutter API returns - #12531

Open
Xelorium wants to merge 2 commits into
flutter:mainfrom
Xelorium:pigeon-swift-nsnull-null-check
Open

[pigeon] Treat NSNull as null for non-null Flutter API returns#12531
Xelorium wants to merge 2 commits into
flutter:mainfrom
Xelorium:pigeon-swift-nsnull-null-check

Conversation

@Xelorium

@Xelorium Xelorium commented Aug 21, 2026

Copy link
Copy Markdown

Description

FlutterStandardReader substitutes NSNull for a nil element when it decodes a list, so a null reply for a non-null return value reaches the generated Swift code as NSNull rather than as nil. The generated guard only checks listResponse[0] == nil, so NSNull falls through to the force cast below it and aborts the process:

} else if listResponse[0] == nil {
  completion(.failure(PigeonError(code: "null-error", ...)))
} else {
  let result = listResponse[0] as! AuthenticationChallengeResponse  // 💥

This crashes production apps through webview_flutter_wkwebview: the plugin clears its native instance manager in WebViewFlutterPlugin.tearDownProxyAPIRegistrar(), which iOS triggers on sceneDidDisconnect / applicationWillTerminate. A WKNavigationDelegate.didReceiveAuthenticationChallenge reply that is still in flight then carries an identifier the native instance manager can no longer resolve, the codec reader returns nil, and the app dies with:

Could not cast value of type 'NSNull' (0x...) to 'webview_flutter_wkwebview.AuthenticationChallengeResponse' (0x...).

The same shape was reported before for other types (URLRequestWrapper in flutter/flutter#162437), which is expected: the generator emits this guard for every non-null return value, so any unresolvable instance crashes instead of reporting an error.

This PR treats NSNull as a null reply, so the existing null-error path handles it.

Verification

  • Added a Swift generator unit test asserting the generated guard covers NSNull.
  • Added a native regression test in platform_tests that replies NSNull to a non-null Flutter API return. It passes with this change; with the generated guard reverted it aborts with Could not cast value of type 'NSNull' (0x...) to 'test_plugin.AllNullableTypes' (0x...).
  • dart test and flutter_plugin_tools format pass locally.

Fixes flutter/flutter#191254

Pre-Review Checklist

@google-cla

google-cla Bot commented Aug 21, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request updates Pigeon to version 27.3.3. It modifies the Swift generator to check if a Flutter API response is nil or NSNull when validating non-nullable return values, preventing crashes when FlutterStandardReader substitutes NSNull for nil. It also updates the generated Swift files, adds a regression test in NullableReturnsTests.swift, and adds a generator unit test in swift_generator_test.dart. There are no review comments, so no further feedback is provided.

@Xelorium
Xelorium force-pushed the pigeon-swift-nsnull-null-check branch from fbfef09 to 274fd02 Compare August 21, 2026 10:58
`FlutterStandardReader` substitutes `NSNull` for a `nil` element of a
list, so a null reply for a non-null return value arrives as `NSNull`
rather than as `nil`. The generated `listResponse[0] == nil` check did
not catch that, and the following force cast aborted the process.

In `webview_flutter_wkwebview` this crashes apps whenever the native
instance manager cannot resolve the returned instance, which the plugin
itself causes by clearing the manager on scene disconnect and app
termination while an authentication challenge is in flight.
@Xelorium
Xelorium force-pushed the pigeon-swift-nsnull-null-check branch from 274fd02 to a3cd890 Compare August 25, 2026 08:09
@Xelorium

Copy link
Copy Markdown
Author

Thanks for the pointer on flutter/flutter#191254 — opened this as suggested there.

Small status note in case it helps whoever picks it up: the branch is rebased onto the 28.0.0 async change, so it is mergeable again, and the CLA check is green. The test matrix hasn't run yet since the CICD label isn't something I can add myself.

No rush — happy to make any changes you'd like when you get a chance to look.

@Piinks
Piinks requested a review from bparrishMines August 25, 2026 20:41

@bparrishMines bparrishMines left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for the contribution!

I think this may be the true cause for flutter/flutter#162437. Thanks for debugging this.

@tarrinneal Where are you with the FFI/JNI integration? Can this land without disrupting #11352?

Comment on lines +67 to +80
@Test
func nullReplyForNonNullReturnFailsWithoutCrashing() async throws {
let binaryMessenger = MockBinaryMessenger<NSNull>(codec: codec)
binaryMessenger.result = NSNull()
let api = FlutterIntegrationCoreApi(binaryMessenger: binaryMessenger)

do {
// `sendMultipleNullableTypes` has a non-null return value.
_ = try await api.sendMultipleNullableTypes(aBool: nil, anInt: nil, aString: nil)
Issue.record("Expected a null-error but the call succeeded.")
} catch let error as PigeonError {
#expect(error.code == "null-error")
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This should be moved in to the NullableReturnsTests or move this class to a separate file.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Moving into the NullableReturnsTests class would be preferred.

@bparrishMines bparrishMines added the CICD Run CI/CD label Sep 2, 2026
@tarrinneal

Copy link
Copy Markdown
Contributor

@tarrinneal Where are you with the FFI/JNI integration? Can this land without disrupting #11352?

I actually already added logic to fix this in the ffi pr. If I haven't already changed it to solve this problem, I can.

@tarrinneal

Copy link
Copy Markdown
Contributor

This pr can land first, it doesn't really matter

Comment on lines +1754 to +1756
// `FlutterStandardReader` substitutes `NSNull` for a `nil` element of
// a list, so a null reply can arrive as either. See
// https://github.com/flutter/flutter/issues/191254.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this isn't needed

Comment on lines +1175 to +1177
// `FlutterStandardReader` substitutes `NSNull` for a `nil` element of a
// list, so both need to be treated as a null reply before the value is
// cast. See https://github.com/flutter/flutter/issues/191254.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this isn't needed either

@tarrinneal

Copy link
Copy Markdown
Contributor

Make sure to run format and analyze

Comment on lines +57 to +68
/// Regression test for https://github.com/flutter/flutter/issues/191254.
///
/// `FlutterStandardReader` substitutes `NSNull` for a `nil` element of a list,
/// so a null reply for a non-null return value arrives as `NSNull` rather than
/// as `nil`. Before the fix that value was force-cast to the return type, which
/// aborted the process instead of reporting an error.
@MainActor
struct NullReplyForNonNullReturnTests {
let codec = FlutterStandardMessageCodec.sharedInstance()

@Test
func nullReplyForNonNullReturnFailsWithoutCrashing() async throws {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

the comments aren't needed if the test name is something like nonNullReturnFailsOnNSNullResponse.

@flutter-dashboard flutter-dashboard Bot removed the CICD Run CI/CD label Sep 3, 2026
@Xelorium

Xelorium commented Sep 3, 2026

Copy link
Copy Markdown
Author

Thanks for the reviews! Addressed all the comments: dropped the explanatory comments in the generator and the generator test, and folded the regression test into NullableReturnsTests as nonNullReturnFailsOnNSNullResponse. Ran format, analyze and the Dart + iOS unit tests locally, all green.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

3 participants