You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Found while fixing the iOS bridge ABI mismatches in #416. That PR corrected fifteen methods and pinned both halves with a guard, but forwardMessage cannot be fixed the same way: it needs a contract change across TypeScript, Swift and Kotlin, so it is filed rather than folded in.
What happens
forwardMessage is the one bridge method with a genuinely optional number. TypeScript passes an explicit null whenever the caller omits the priority (bindings/react-native/src/index.ts:1268):
Swift takes it as NSNumber? (OfflineProtocolModule.swift:1448-1450) and the shim declares it (NSNumber *)priority without a nullability qualifier (OfflineProtocolModule.m:65). That is the only spelling that matches the Swift side, and React Native refuses it.
React Native forces every NSNumber argument to non-null regardless of how it is declared (React/Base/RCTModuleMethod.mm:415-431):
/** * Special case - Numbers are not nullable in Android, so we * don't support this for now. In future we may allow it.*/if ([typeName isEqualToString:@"NSNumber"]) {
BOOL unspecified = (nullability == RCTNullabilityUnspecified);
if (!argument.unused && (nullability == RCTNullable || unspecified)) {
RCTLogArgumentError(... "but React requires that all NSNumber arguments are explicitly marked as `nonnull` to ensure compatibility with Android.");
}
nullability = RCTNonnullable;
}
A nonnull argument that arrives nil makes the argument block log "must not be null" and return NO (RCTModuleMethod.mm:451-452), and invokeWithBridge:module:arguments: then aborts before[_invocation invokeWithTarget:module] (RCTModuleMethod.mm:558-564):
if (!block(bridge, index, RCTNilIfNull(json))) {
// Invalid argument, abortRCTLogArgumentError(self, index, json, "could not be processed. Aborting method call.");
returnnil;
}
The Swift method never runs. Neither resolver nor rejecter is called, so the promise never settles and await forwardMessage(...) hangs forever, behind a redbox.
Scope
Debug builds only. The whole nullability block is inside #if RCT_DEBUG. In a release build it is compiled out, nil reaches [RCTConvert NSNumber:], and the method works as intended. So this is a developer-experience bug, not a shipped-app bug, which is why it is not a release blocker.
iOS only. Android takes priority: Int? and null-checks it (OfflineProtocolModule.kt:1609-1620).
Only forwardMessage. It is the sole bridge method with a nullable NSNumber; every other number argument is required. The guard added in fix(bindings): restore the eight iOS methods React Native could not reach #416 (react_native_ios_objc_shim_and_swift_agree_on_every_selector) checks that the two halves agree on the ABI class, and they do agree here: both sides say Object. The mismatch is with React Native's own rule, not between our two files.
same rejection, and now the declaration also claims something false
(nullable NSNumber *)priority
RCTLogArgumentError at module load, then forced non-null anyway
(NSInteger)priority
matches no Swift NSNumber?; also cannot represent absence
React Native's message names the reason: numbers are not nullable on Android, so the bridge refuses to make them nullable on iOS. The contract has to stop relying on a nullable number.
Options
Sentinel value. TypeScript sends -1 for absent, Swift and Kotlin map -1 to nil before calling the core. Smallest change; the shim becomes (NSInteger)priority and the ABI table gets simpler. Costs a magic number in three languages.
Two methods.forwardMessage and forwardMessageWithPriority, each with a total signature. No sentinel, but a new selector to keep in sync and a branch in the TypeScript.
Priority inside the options object.forwardMessage already takes originalMessageJson; an options dictionary matching sendMessageRich would carry priority as an optional field, where nil is expressible. Most consistent with the rest of the API and the largest diff.
Option 1 or 3. Whichever is chosen, the change lands in TypeScript, Swift and Kotlin together, and bindings/react-native/ios/BRIDGE_MAINTENANCE.md should lose the paragraph that currently documents the breakage and gain the rule instead.
Reproduction
Debug build, iOS:
awaitprotocol.forwardMessage({originalMessageJson: json,newRecipient: addr,// priority omitted -> TypeScript sends null});// never resolves, never rejects
Found while fixing the iOS bridge ABI mismatches in #416. That PR corrected fifteen methods and pinned both halves with a guard, but
forwardMessagecannot be fixed the same way: it needs a contract change across TypeScript, Swift and Kotlin, so it is filed rather than folded in.What happens
forwardMessageis the one bridge method with a genuinely optional number. TypeScript passes an explicitnullwhenever the caller omits the priority (bindings/react-native/src/index.ts:1268):Swift takes it as
NSNumber?(OfflineProtocolModule.swift:1448-1450) and the shim declares it(NSNumber *)prioritywithout a nullability qualifier (OfflineProtocolModule.m:65). That is the only spelling that matches the Swift side, and React Native refuses it.React Native forces every
NSNumberargument to non-null regardless of how it is declared (React/Base/RCTModuleMethod.mm:415-431):A
nonnullargument that arrivesnilmakes the argument block log"must not be null"and returnNO(RCTModuleMethod.mm:451-452), andinvokeWithBridge:module:arguments:then aborts before[_invocation invokeWithTarget:module](RCTModuleMethod.mm:558-564):The Swift method never runs. Neither
resolvernorrejecteris called, so the promise never settles andawait forwardMessage(...)hangs forever, behind a redbox.Scope
#if RCT_DEBUG. In a release build it is compiled out,nilreaches[RCTConvert NSNumber:], and the method works as intended. So this is a developer-experience bug, not a shipped-app bug, which is why it is not a release blocker.priority: Int?and null-checks it (OfflineProtocolModule.kt:1609-1620).forwardMessage. It is the sole bridge method with a nullableNSNumber; every other number argument is required. The guard added in fix(bindings): restore the eight iOS methods React Native could not reach #416 (react_native_ios_objc_shim_and_swift_agree_on_every_selector) checks that the two halves agree on the ABI class, and they do agree here: both sides say Object. The mismatch is with React Native's own rule, not between our two files.Why there is no shim-only fix
Every available spelling loses:
(NSNumber *)priority(today)(nonnull NSNumber *)priority(nullable NSNumber *)priorityRCTLogArgumentErrorat module load, then forced non-null anyway(NSInteger)priorityNSNumber?; also cannot represent absenceReact Native's message names the reason: numbers are not nullable on Android, so the bridge refuses to make them nullable on iOS. The contract has to stop relying on a nullable number.
Options
-1for absent, Swift and Kotlin map-1tonilbefore calling the core. Smallest change; the shim becomes(NSInteger)priorityand the ABI table gets simpler. Costs a magic number in three languages.forwardMessageandforwardMessageWithPriority, each with a total signature. No sentinel, but a new selector to keep in sync and a branch in the TypeScript.forwardMessagealready takesoriginalMessageJson; an options dictionary matchingsendMessageRichwould carry priority as an optional field, wherenilis expressible. Most consistent with the rest of the API and the largest diff.Option 1 or 3. Whichever is chosen, the change lands in TypeScript, Swift and Kotlin together, and
bindings/react-native/ios/BRIDGE_MAINTENANCE.mdshould lose the paragraph that currently documents the breakage and gain the rule instead.Reproduction
Debug build, iOS: