Skip to content

feat: React Native 0.83 new architecture, OkHttp 5.3.2, Brotli, and SSL overhaul#165

Merged
enahum merged 15 commits into
masterfrom
rn83
May 7, 2026
Merged

feat: React Native 0.83 new architecture, OkHttp 5.3.2, Brotli, and SSL overhaul#165
enahum merged 15 commits into
masterfrom
rn83

Conversation

@enahum
Copy link
Copy Markdown
Contributor

@enahum enahum commented May 6, 2026

Summary

  • React Native 0.83 / New Architecture — migrates iOS and Android to the new RN architecture
  • OkHttp 5.3.2 upgrade — upgrades to OkHttp 5.3.2 with Brotli encoding support (Accept-Encoding: br,gzip)
  • SSL/certificate overhaul — comprehensive fix for multiple open certificate issues in mattermost-mobile (see details below)
  • Android fixes — retry logic improvements, buffer usage in toWritableMap, removal of ReactApplicationContext from ForwardingCookieHandler
  • metrics collection fix on iOS

SSL / Certificate Changes (mattermost-mobile issues)

  • [Android/iOS] AIA caIssuers chain completion: fetches missing intermediate CAs when a server sends an incomplete chain (mattermost-mobile#9153, mattermost-mobile#6643, mattermost-mobile#9623, mattermost-mobile#6062)
  • [Android] Trust user-installed and MDM root CAs via AndroidCAStore on API 24+ (mattermost-mobile#9337)
  • [Android] Replace okhttp-tls HandshakeCertificates with standard javax.net.ssl APIs; removes okhttp-tls dependency
  • [Android/iOS] SSL error events now include certificate subject, issuer and expiry date (mattermost-mobile#8461)
  • [Android/iOS] CLIENT_ERROR events are deduplicated — only one event fires per session regardless of retries (mattermost-mobile#7658)
  • [Android] Explicit Content-Type set on POST request bodies to prevent WAF rejections
  • [iOS] Fix memory leak in Session extension properties on session invalidation

enahum added 10 commits April 24, 2026 08:34
- [Android/iOS] AIA caIssuers chain completion for servers with incomplete chains
  (mattermost-mobile#9153, mattermost-mobile#6643, mattermost-mobile#9623,
  mattermost-mobile#6062)
- [Android] Trust user-installed and MDM root CAs via AndroidCAStore (mattermost-mobile#9337)
- [Android] Replace okhttp-tls HandshakeCertificates with javax.net.ssl APIs
- [Android/iOS] Include cert subject, issuer and expiry in SSL error events (mattermost-mobile#8461)
- [Android/iOS] Deduplicate CLIENT_ERROR events on retried cert failures (mattermost-mobile#7658)
- [Android] Set explicit Content-Type on POST bodies to avoid WAF rejections
- [iOS] Fix Session extension property leak on session invalidation
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

Caution

Review failed

The head commit changed during the review from da3e3c4 to ecdeb5c.

📝 Walkthrough

Walkthrough

Refactors TLS/SSL certificate handling (AIA-based chain completion, TrustManager/KeyManager and client P12 support), rewrites retry interceptor control flow and moves tests to a JVM runner, modernizes Android/iOS example projects and build/tooling, updates TypeScript schemas and configs, replaces DocumentPicker in examples, and migrates to an ESLint flat config (deleted .eslintignore).

Changes

TLS/SSL: Android and iOS certificate chain & trust wiring

Layer / File(s) Summary
Data / Helpers
android/src/main/java/.../helpers/CertificateChainHelper.kt, android/src/main/java/.../helpers/TrustManagerHelper.kt, ios/Utilities/AiaCertHelper.swift
Added AIA caIssuers DER parsing, intermediate certificate fetching, chain completion and trust/key-manager builders for Android and iOS.
KeyStore surface
android/src/main/java/.../helpers/KeyStoreHelper.kt
Replaced PKCS12 extraction API: removed getClientCertificates(...); added getClientCertKeyStore(...) returning KeyStore+password pair for mTLS usage.
SSL config / wiring
android/src/main/java/.../NetworkClient.kt
Introduced buildSslConfig(), applyClientSslConfiguration(options), configureSsl(); integrated TrustManagerHelper and P12 handling; rebuilds OkHttp client when client certs are configured; added brotli and media-type constants.
Handshake / validation
android/src/main/java/.../NetworkClient.kt, ios/Delegates/ApiClientSessionDelegate.swift
Handshake verification attempts AIA-based chain augmentation before failing; iOS challenge handling now can augment chain and conditionally trust augmented chain.
Error reporting / state
android/src/main/java/.../NetworkClient.kt, ios/Extensions/Session+Extensions.swift, ios/SessionManager.swift
Added per-host deduplication (Android) and per-session certErrorEmitted/lastServerCertSummary storage (iOS); session invalidation now cleans per-session extension state.

Build, dependencies and platform configs

Layer / File(s) Summary
Android build defaults & resolution
android/build.gradle, example/android/app/build.gradle
Kotlin default bumped to 2.2.21; OkHttp moved to 5.3.2 family; added configurations.all resolutionStrategy forcing OkHttp/Okio/Kotlin stdlib versions; removed legacy OkHttp 4.x test deps.
Example Android SDK / wrapper
example/android/build.gradle, example/android/gradle/wrapper/gradle-wrapper.properties, example/android/gradle.properties
Bumped buildTools/compile/target SDK and NDK; Gradle wrapper updated to 9.0.0; introduced edgeToEdgeEnabled property.
iOS platform & pods
react-native-network-client.podspec, example/ios/Podfile
Podspec raised iOS minimum to 15.1 and Alamofire to ~> 5.11.2; Podfile sets env flags and sets SWIFT_ENABLE_EXPLICIT_MODULES = 'NO' for targets.
Project scripts
example/android/gradlew, example/android/gradlew.bat
Gradle wrapper scripts now invoke wrapper JAR via -jar; APP_HOME, CLASSPATH and header/license tweaks applied.
iOS project cleanup
ios/NetworkClient.xcodeproj/project.pbxproj
Removed the NetworkClient iOS Xcode project configuration (project file deleted).

Example app modernization and UI/tooling updates

Layer / File(s) Summary
Android host bootstrap
example/android/.../MainApplication.kt
Replaced anonymous ReactNativeHost with lazy ReactHost initialization and loadReactNative(this); wired OkHttpClientProvider/RCTOkHttpClientFactory usage.
iOS AppDelegate migration
example/ios/NetworkClientExample/AppDelegate.swift, deleted Obj-C files/bridging header
Migrated Objective‑C bootstrap to Swift AppDelegate using RCTReactNativeFactory/ReactNativeDelegate; removed Obj‑C AppDelegate, main and bridging header.
Safe area & imports
example/src/App.tsx, example/src/screens/*
Wrapped example app with SafeAreaProvider; moved SafeAreaView to react-native-safe-area-context; reorganized imports to named hooks and RN primitives.
File picker replacement
example/src/components/FilePickerButtonGroup.tsx, example/src/components/P12Inputs.tsx
Replaced DocumentPicker flows with @react-native-documents/picker APIs (pick, keepLocalCopy, isErrorWithCode).
Metro / Babel / patches
example/metro.config.js, babel.config.js, example/babel.config.js, example/patches/*
Metro resolver maps package to TypeScript src, uses blockList and extraNodeModules; added @babel/plugin-transform-export-namespace-from to root and example Babel configs; example patches updated (type-only imports, lifecycle fixes).
Example package & Gemfile
example/package.json, example/Gemfile
Upgraded many dependencies/devDependencies for example app and adjusted CocoaPods/Ruby constraints.

Retry logic, streaming reads and test migration

Layer / File(s) Summary
Retry runtime
android/src/main/java/.../interfaces/RetryInterceptor.kt
Refactored intercept loop: explicit attempts counter, IOException-aware retry loop capturing lastException with rethrow after exhaustion, response cleanup, and retriesExhausted marking for final non-successful responses.
Response streaming
android/src/main/java/.../Extensions.kt
Added CountingInputStream and streaming-based response body reading to avoid full in-memory buffering and to compute metrics from actual bytes read.
Tests: native → JVM runner
deleted android/src/test/.../ExponentialRetryInterceptorTest.kt, added test-runner/src/test/kotlin/.../RetryInterceptorTest.kt, test-runner/build.gradle.kts
Removed Android unit tests; added JVM-hosted retry tests using MockWebServer and Mockito validating exponential backoff and retry behavior; added mockito dependency to test-runner.
Android test deletions
android/src/test/java/.../WebSocketClientModuleImplTest.kt
Removed the malformed-URL promise rejection test file.

Linting, TypeScript schemas and tooling

Layer / File(s) Summary
ESLint migration
.eslintignore (deleted), eslint.config.js, .eslintrc.json
Deleted .eslintignore; introduced eslint.config.js (flat config array: ignores, TypeScript+Prettier rules, test globals); .eslintrc.json left unchanged.
Babel root plugin
babel.config.js, example/babel.config.js
Added @babel/plugin-transform-export-namespace-from to root and example Babel configs; example Babel config simplified.
Schemas / types
src/schemas.tsx, src/APIClient/NativeApiClient.ts
Expanded API client schemas: added headers and nested configuration options; RequestOptionsSchema adds headers and tighter first union variant; MultipartUploadConfigSchema simplified; SessionConfiguration type now includes optional collectMetrics?: boolean.
TypeScript config
tsconfig.json, tsconfig.build.json
Changed moduleResolution to bundler, extended exclude (example, node_modules, Pods); added rootDir: "./src" in tsconfig.build.json.
Project package scripts
package.json
Changed lint script to eslint ., bumped many devDependencies, and added overrides mapping for release tooling.
Git ignore
.gitignore
Added .kotlin ignore entry under Android/IJ section.

Sequence Diagram(s)

sequenceDiagram
    participant Client as App/JS
    participant Network as NetworkClient (Kotlin/Swift)
    participant Trust as TrustManager / SecTrust
    participant Chain as CertificateChainHelper / AiaCertHelper
    participant JS as JS Event Emitter

    Client->>Network: Make HTTPS request
    Network->>Trust: Validate server certificate
    alt Trust validation succeeds
        Trust-->>Network: Validation OK
        Network->>Client: Proceed with response
    else Trust validation fails (recoverable)
        Trust->>Chain: Fetch AIA caIssuers & attempt chain completion
        Chain-->>Trust: Augmented chain or nil
        alt Augmented chain valid
            Trust-->>Network: Validation succeeded with augmented chain
            Network->>Client: Proceed with response
        else Augmentation failed
            Network->>JS: emitInvalidCertificateError(chain, cause)
            JS-->>Network: (event handled)
            Network->>Client: Fail request with certificate error
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch rn83

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 20

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
example/android/app/src/main/java/com/example/reactnativenetworkclient/MainApplication.kt (1)

24-31: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Move OkHttpClientProvider.setOkHttpClientFactory() before loadReactNative(this).

OkHttpClientProvider lazily initializes its client on the first network request. NetworkingModule (created on first JS network access) requests the client from OkHttpClientProvider during construction. If loadReactNative(this) is called first and triggers any network activity during React Native initialization, the default client will be created before your factory is registered, bypassing SSL/Brotli/cookie customizations. Setting the factory beforehand guarantees it's in place before any NetworkingModule instance can be created.

Suggested fix
    override fun onCreate() {
        super.onCreate()
-       loadReactNative(this)
-
        // Tells React Native to use our RCTOkHttpClientFactory which builds an OKHttpClient
        // with a cookie jar defined in APIClientModule and an interceptor to intercept all
        // requests that originate from React Native's OKHttpClient
        OkHttpClientProvider.setOkHttpClientFactory(RCTOkHttpClientFactory())
+       loadReactNative(this)
    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@example/android/app/src/main/java/com/example/reactnativenetworkclient/MainApplication.kt`
around lines 24 - 31, Move the OkHttpClientProvider factory registration so it
runs before React Native initializes: call
OkHttpClientProvider.setOkHttpClientFactory(RCTOkHttpClientFactory()) prior to
invoking loadReactNative(this) in onCreate(), ensuring the custom factory is
registered before any NetworkingModule or first network request can lazily
create the default client and bypass your SSL/Brotli/cookie customizations.
android/src/main/java/com/mattermost/networkclient/NetworkClient.kt (1)

403-436: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Apply the bodyless-POST fallback even when options is null.

The new empty JSON body is only added inside if (options != null). A call like post(..., null) still builds a POST with body == null, so it misses the WAF workaround and can still trip OkHttp's method/body validation.

💡 Proposed fix
 private fun prepareRequestBody(method: String, options: ReadableMap?): RequestBody? {
     var requestBody: RequestBody? = null
 
     if (options != null) {
         if (options.hasKey("body")) {
             when (options.getType("body")) {
@@
-            } else if (method.uppercase(Locale.ENGLISH) == "POST") {
-                // Explicit Content-Type ensures WAFs don't reject bodyless POSTs (`#9689`).
-                // Caller can override by including Content-Type in the request headers.
-                requestBody = "".toRequestBody(MEDIA_TYPE_JSON)
-            }
+            }
+    }
+
+    if (requestBody == null && method.uppercase(Locale.ENGLISH) == "POST") {
+        // Explicit Content-Type ensures WAFs don't reject bodyless POSTs (`#9689`).
+        // Caller can override by including Content-Type in the request headers.
+        requestBody = "".toRequestBody(MEDIA_TYPE_JSON)
     }
 
     return requestBody
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@android/src/main/java/com/mattermost/networkclient/NetworkClient.kt` around
lines 403 - 436, In prepareRequestBody, the POST-empty-body fallback is only
applied when options != null so post(..., null) can produce a null body; update
prepareRequestBody to ensure when method.uppercase(Locale.ENGLISH) == "POST" and
requestBody is still null (or options is null) you assign an empty JSON body
(e.g., "".toRequestBody(MEDIA_TYPE_JSON)) so the WAF workaround always runs;
locate prepareRequestBody and the existing options null branch and move or add
the POST fallback after the options handling (or check for options == null) so
the empty-body assignment executes whenever no body was produced.
ios/ApiClient/ApiClient.mm (1)

238-241: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

requestAdapterConfiguration is built and then discarded.

In the new-architecture path, bearerAuthTokenResponseHeader never reaches wrapper because adapterDictionary is never stored in dict. That breaks request-adapter configuration for codegen clients.

💡 Proposed fix
     if (config.requestAdapterConfiguration().has_value()) {
         NSMutableDictionary *adapterDictionary = [[NSMutableDictionary alloc] init];
         adapterDictionary[@"bearerAuthTokenResponseHeader"] = config.requestAdapterConfiguration().value().bearerAuthTokenResponseHeader();
+        dict[@"requestAdapterConfiguration"] = adapterDictionary;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ios/ApiClient/ApiClient.mm` around lines 238 - 241, The code creates
adapterDictionary in the requestAdapterConfiguration() branch but never attaches
it to the outer dict/wrapper, so the bearerAuthTokenResponseHeader never flows
to wrapper; update the branch that builds adapterDictionary (referencing
requestAdapterConfiguration(), adapterDictionary) to insert it into the main
dict/wrapper (e.g., set dict[@"requestAdapterConfiguration"] = adapterDictionary
or otherwise merge adapterDictionary into dict before using wrapper) so the
request-adapter configuration reaches wrapper.
🧹 Nitpick comments (3)
example/android/gradlew (1)

117-117: ⚡ Quick win

Simplify wrapper launch by removing unused classpath plumbing.

Both example/android/gradlew and example/android/gradlew.bat set a CLASSPATH variable and pass it via -classpath flag, even though the wrapper is invoked with -jar. This unnecessary plumbing—setting CLASSPATH to an empty value, converting it for Cygwin/MSYS, and passing it as an argument—adds avoidable complexity to the shell path handling without functional benefit. Remove the CLASSPATH declaration (line 117), its conversion (line 175), and the -classpath argument (line 215) to align with the standard Gradle 9 wrapper pattern.

Suggested diff
-CLASSPATH="\\\"\\\""
@@
 if "$cygwin" || "$msys" ; then
     APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
-    CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
@@
 set -- \
         "-Dorg.gradle.appname=$APP_BASE_NAME" \
-        -classpath "$CLASSPATH" \
         -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
         "$@"

Apply the same cleanup to example/android/gradlew.bat (remove line 78 and the -classpath "%CLASSPATH%" argument from line 82).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@example/android/gradlew` at line 117, Remove the unused CLASSPATH plumbing:
delete the CLASSPATH declaration that sets CLASSPATH="\"\"" in the shell
wrapper, remove the block that converts CLASSPATH for Cygwin/MSYS, and remove
the -classpath argument passed to the java invocation that launches the wrapper
JAR; do the same in the Windows wrapper by deleting the CLASSPATH variable and
removing the -classpath "%CLASSPATH%" argument so the wrapper launches with -jar
only (locate the CLASSPATH assignment, the cygpath conversion block, and the
java invocation that includes -classpath to modify).
example/android/app/build.gradle (1)

73-73: ⚡ Quick win

Pin the JSC artefact to an exact version.

Using 2026004.+ makes the example build non-reproducible and can start pulling a different JSC patch release on the next install. Given the rest of this PR is explicitly pinning OkHttp/Kotlin, this one should be locked to the exact version you validated too.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@example/android/app/build.gradle` at line 73, The jscFlavor variable is using
a wildcard version ('io.github.react-native-community:jsc-android:2026004.+')
which makes builds non-reproducible; change the jscFlavor assignment to pin the
exact validated artifact version (replace the '2026004.+' range with the
specific patch version you tested) so the build pulls an immutable dependency
(update the jscFlavor definition accordingly).
eslint.config.js (1)

9-16: ⚡ Quick win

example/** is fully excluded from linting.

This ignore means the upgraded sample app won't be linted at all, even though this PR changes its Gradle, package, and iOS bootstrap files. If that isn't intentional, trim the ignore or add a dedicated flat-config entry for example/ so regressions in the RN 0.83 migration are still caught.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@eslint.config.js` around lines 9 - 16, The current ESLint ignores array
contains "example/**" which fully excludes the sample app from linting; remove
or narrow that pattern (or add a dedicated flat-config entry for example/) so
changes to the sample app (the RN 0.83 migration files) are linted; edit the
ignores array (the ignores symbol) to either delete "example/**" or replace it
with a more specific pattern (e.g., exclude generated/artifacts only) or add a
separate flat-config entry targeting "example/" to ensure the example project is
still validated.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@android/build.gradle`:
- Around line 61-67: The forced Kotlin stdlib version in configurations.all
(resolutionStrategy forcing "org.jetbrains.kotlin:kotlin-stdlib:2.2.21") is
hard-coded and can diverge from the plugin/dependency Kotlin version; change the
force call to use the project's Kotlin version accessor (e.g., call
getKotlinVersion() or reference rootProject.ext.kotlinVersion) so the forced
artifact matches the Kotlin version used by the gradle-plugin and implementation
dependencies in this build.

In `@android/src/main/java/com/mattermost/networkclient/Extensions.kt`:
- Around line 83-115: The code calls source.request(Long.MAX_VALUE) which forces
the entire response into memory before you check bufferSize, so remove or
replace that call: either use responseBody.contentLength() if available or call
source.request(MAX_IN_MEMORY_SIZE + 1) to probe whether the body exceeds the
threshold without loading everything. Then use source (not
source.buffer.clone()) to stream-read into readBufferInChunks when the probe
indicates a large body, and only call buffer.readUtf8() for the small-body path;
update references to source.request, buffer.clone(), bufferSize,
MAX_IN_MEMORY_SIZE, and readBufferInChunks accordingly.
- Around line 31-45: readBufferInChunks currently decodes each fixed-size byte
chunk independently which can split multibyte UTF-8 characters and produce
U+FFFD; fix by switching to a streaming decode using a CharsetDecoder so partial
multibyte sequences are preserved across iterations. In readBufferInChunks,
create a CharsetDecoder from StandardCharsets.UTF_8.newDecoder(), on each loop
read the chunk bytes into a ByteBuffer, call decoder.decode(byteBuffer,
charBuffer, false) appending decoded chars to the StringBuilder and retain any
remaining undecoded bytes between iterations (by compacting or slicing the
ByteBuffer), and after the loop call decoder.decode(..., true) and
decoder.flush(...) to finish decoding; reference readBufferInChunks, CHUNK_SIZE,
buffer.read... and StandardCharsets.UTF_8 in your changes.

In
`@android/src/main/java/com/mattermost/networkclient/helpers/CertificateChainHelper.kt`:
- Around line 112-120: The fetchCertificate function currently reads the entire
response via readBytes(), risking OOM from a malicious AIA endpoint; change it
to stream and enforce a hard byte limit (e.g., MAX_AIA_BYTES ~ a few MB) when
reading the InputStream instead of using readBytes(), reading in a buffered loop
into a ByteArrayOutputStream and aborting (throwing/logging and closing the
connection) if the limit is exceeded, then pass the collected byte array to
CertificateFactory.generateCertificate; keep existing timeouts and ensure all
streams are closed on both success and error.
- Around line 91-103: Validate DER lengths before slicing and return null on
malformed data: in the AIA parsing block (where CA_ISSUERS_OID is compared)
guard the copyOfRange and String construction by checking oidLen and uriLen
against remaining bounds (use seqEnd and der.size and current pos) before
calling lengthAt/lengthSize results; ensure pos + oidLen and pos + uriLen are <=
seqEnd and <= der.size and that pos is valid when testing tags (der[pos])—if any
check fails, bail out/return null instead of performing the slice or
constructing the String.

In
`@android/src/main/java/com/mattermost/networkclient/helpers/KeyStoreHelper.kt`:
- Around line 87-94: getClientCertKeyStore currently assumes the PKCS12 file
exists and doesn't close the InputStream; update getClientCertKeyStore to safely
handle missing files by catching FileNotFoundException from
ApiClientModuleImpl.context.openFileInput(p12Alias), returning null if missing,
and ensure the opened InputStream is closed in a finally/use block;
additionally, update deleteClientCertificates to also clear the stored password
via ApiClientModuleImpl.retrieveValue/remove method so the persisted secret and
file can't get out of sync.

In `@android/src/main/java/com/mattermost/networkclient/NetworkClient.kt`:
- Around line 422-424: The code currently maps ReadableType.Null to
RequestBody.EMPTY which forces bodyless verbs like GET/HEAD to have a body and
breaks Request.Builder.method(...); change the ReadableType.Null branch to set
requestBody = null (make the requestBody variable nullable if needed) so that
callers of the surrounding function (the logic building the Request via
Request.Builder.method(...)) can apply their verb-specific fallback behavior;
keep the reference to RequestBody.EMPTY only for places that truly require an
empty body, and ensure the surrounding method (where requestBody is used)
handles a null body appropriately.
- Around line 620-626: The SSLContext is being initialized with the original
trustManager inside TrustManagerHelper.buildSslSocketFactory so the
later-wrapped trust manager returned by NetworkClient.buildSslConfig via
getTrustManager(trustManager) is not used; fix by ensuring the SSLContext is
initialized with the wrapped trust manager: either wrap the trustManager before
calling TrustManagerHelper.buildSslSocketFactory (i.e., call
getTrustManager(...) in NetworkClient.buildSslConfig and pass that wrapped
manager into buildSslSocketFactory), or modify
TrustManagerHelper.buildSslSocketFactory so it accepts/creates the wrapped trust
manager and calls sslContext.init(keyManagers, arrayOf(wrappedTrustManager),
SecureRandom()); update references to TrustManagerHelper.buildSslSocketFactory,
NetworkClient.buildSslConfig, getTrustManager, and the sslContext.init call
accordingly.

In `@example/android/gradle/wrapper/gradle-wrapper.properties`:
- Line 3: The Gradle wrapper was bumped to 9.0.0 but the project AGP is still on
a 7.x line; confirm the Android Gradle Plugin version used by both the root
android/build.gradle and example/android/build.gradle (look for the
com.android.tools.build:gradle dependency or plugin declaration) and resolve the
incompatibility by either setting distributionUrl in
example/android/gradle/wrapper/gradle-wrapper.properties back to a compatible
Gradle 8.x release or updating the AGP declaration(s) to 8.0+ (and adjust any
AGP-related build config if required) so Gradle 9.0.0 is supported.

In `@example/src/components/FilePickerButtonGroup.tsx`:
- Around line 36-50: The code currently reads result.fileCopyUri which doesn't
exist on keepLocalCopy's response; update the flow in the pick/keepLocalCopy
sequence to check the discriminant (status) on the keepLocalCopy response (the
returned result) and, when status indicates success, read the success field's
localUri and use that as the NativeFile.uri (instead of result.fileCopyUri);
reference the functions/variables pick, keepLocalCopy, selectedFile, result and
the constructed file: NativeFile to locate and change the logic so it only
accesses localUri on the successful branch.
- Line 2: Update the import to include errorCodes from
"@react-native-documents/picker" and change the catch logic that currently uses
isErrorWithCode to swallow all coded errors: use isErrorWithCode(err,
errorCodes.OPERATION_CANCELED) (or compare err.code ===
errorCodes.OPERATION_CANCELED) so only OPERATION_CANCELED is swallowed and all
other errors (e.g., IN_PROGRESS, NULL_PRESENTER, UNABLE_TO_OPEN_FILE_TYPE) are
re-thrown or propagated; reference the existing
pick/keepLocalCopy/isErrorWithCode usage to locate where to adjust the catch
handling.

In `@example/src/components/P12Inputs.tsx`:
- Around line 48-63: The code is reading a non-existent result.fileCopyUri and
swallowing picker errors; update the keepLocalCopy handling to inspect the
discriminant (result.status), throw or propagate when status !== "success", and
on success use the returned localUri (e.g. copyResult.localUri) to call
props.onSelectP12. In the error handler, avoid silently ignoring errors matched
by isErrorWithCode(err); rethrow or surface picker/keepLocalCopy failures so the
caller can show feedback. Locate the logic around pick(), keepLocalCopy(...) and
props.onSelectP12 and replace the fileCopyUri usage with the union-safe status
check and localUri property.
- Line 7: Update the error handling in P12Inputs and FilePickerButtonGroup where
isErrorWithCode(err) is used: import errorCodes from
"@react-native-documents/picker" and only suppress errors when err.code ===
errorCodes.OPERATION_CANCELED; for any other isErrorWithCode(err) values (e.g.,
IN_PROGRESS, UNABLE_TO_OPEN_FILE_TYPE) re-throw the error so real failures are
not silently ignored. Locate the checks around the picker calls (functions using
pick/keepLocalCopy and the isErrorWithCode branch) and replace the blanket
suppression with an explicit err.code === errorCodes.OPERATION_CANCELED check
before swallowing the error.

In `@example/src/components/ResponseSuccessOverlay.tsx`:
- Around line 5-6: The component imports Text from "react-native" but uses the
react-native-elements-specific prop h4 in ResponseSuccessOverlay (several Text
usages), causing runtime errors; change the import to pull Text from
"react-native-elements" (or replace h4 usages with appropriate react-native
styles) so the Text component matches the h4 prop expected by the code—update
the import statement that currently imports Text from "react-native" and ensure
only the react-native-elements Text is used by functions/components like
ResponseSuccessOverlay and any local Text usages.

In `@example/src/screens/APIClientRequestScreen.tsx`:
- Line 99: The console.log('RESPONSE', response) in APIClientRequestScreen.tsx
must be removed or gated to development and redact sensitive fields; locate the
logging inside the APIClientRequestScreen component (or the function handling
the network response) and either delete the console.log or wrap it with a
development-only guard (e.g., __DEV__ or NODE_ENV==='development') and ensure
any logged object strips tokens/PII (redact Authorization, token, user
identifiers) before logging.

In `@ios/Extensions/Session`+Extensions.swift:
- Around line 15-16: The two global dictionaries certErrorEmitted_FILEPRIVATE
and lastServerCertSummary_FILEPRIVATE are mutated from multiple threads
(delegate callbacks and invalidation) and must be made thread-safe: create a
dedicated serial DispatchQueue or an NSLock (e.g. certStateLockQueue or
certStateLock) and use it to synchronise all reads and writes to
certErrorEmitted_FILEPRIVATE and lastServerCertSummary_FILEPRIVATE (including
places that clear them during invalidation and any delegate-path accesses);
alternatively move these dictionaries onto a thread-safe owner object and gate
access through that owner. Ensure every site that touches these symbols
(including the locations around the other mentioned block using the same
globals) is updated to use the chosen lock/queue.

In `@ios/patches/apply_patches.rb`:
- Line 12: The current directory_arg assignment can return nil or pick the wrong
Pods dir; replace the one-liner that uses Dir.glob(...).first.sub(...) with
deterministic logic: collect all matches from
Dir.glob(Pathname(repo_root).join("**/**/Pods")), fail fast with a clear
error/log and exit if empty, and if multiple matches exist prefer in order:
"#{repo_root}/ios/Pods", "#{repo_root}/Pods", otherwise choose the shortest
relative path (closest to repo_root) and log a warning about multiple
candidates; then set directory_arg to the chosen path with the "#{repo_root}/"
prefix removed. Update the code around variable directory_arg and use
repo_root/Pathname references accordingly.

In `@ios/Utilities/AiaCertHelper.swift`:
- Around line 154-164: The fetchCertificate(from:) function starts a URLSession
dataTask but doesn't retain the task or cancel it on semaphore timeout, leaving
background requests running; update fetchCertificate(from:) to store the data
task in a local variable (e.g., let task = URLSession.shared.dataTask(...)),
call task.resume(), wait on the semaphore, and if the wait returns .timedOut
call task.cancel() before returning nil; also ensure the completion handler
always signals the semaphore so the wait unblocks and use the retained task
variable in the cancel call.

In
`@test-runner/src/test/kotlin/com/mattermost/networkclient/RetryInterceptorTest.kt`:
- Around line 135-136: The test uses verify(retryInterceptor,
times(0)).getWaitInterval(0) and verify(...).waitForMilliseconds(0) which only
assert those methods were not called with the literal 0; change these to
verify(retryInterceptor, never()).getWaitInterval(<any-int-matcher>) and
verify(retryInterceptor, never()).waitForMilliseconds(<any-int-matcher>) so the
test asserts no invocations with any argument; update the three other
occurrences mentioned (the assertions at lines 153-154 and 232-233) accordingly
and import/use the appropriate Mockito/Kotlin any-int matcher and never()
matcher used elsewhere in RetryInterceptorTest.

---

Outside diff comments:
In `@android/src/main/java/com/mattermost/networkclient/NetworkClient.kt`:
- Around line 403-436: In prepareRequestBody, the POST-empty-body fallback is
only applied when options != null so post(..., null) can produce a null body;
update prepareRequestBody to ensure when method.uppercase(Locale.ENGLISH) ==
"POST" and requestBody is still null (or options is null) you assign an empty
JSON body (e.g., "".toRequestBody(MEDIA_TYPE_JSON)) so the WAF workaround always
runs; locate prepareRequestBody and the existing options null branch and move or
add the POST fallback after the options handling (or check for options == null)
so the empty-body assignment executes whenever no body was produced.

In
`@example/android/app/src/main/java/com/example/reactnativenetworkclient/MainApplication.kt`:
- Around line 24-31: Move the OkHttpClientProvider factory registration so it
runs before React Native initializes: call
OkHttpClientProvider.setOkHttpClientFactory(RCTOkHttpClientFactory()) prior to
invoking loadReactNative(this) in onCreate(), ensuring the custom factory is
registered before any NetworkingModule or first network request can lazily
create the default client and bypass your SSL/Brotli/cookie customizations.

In `@ios/ApiClient/ApiClient.mm`:
- Around line 238-241: The code creates adapterDictionary in the
requestAdapterConfiguration() branch but never attaches it to the outer
dict/wrapper, so the bearerAuthTokenResponseHeader never flows to wrapper;
update the branch that builds adapterDictionary (referencing
requestAdapterConfiguration(), adapterDictionary) to insert it into the main
dict/wrapper (e.g., set dict[@"requestAdapterConfiguration"] = adapterDictionary
or otherwise merge adapterDictionary into dict before using wrapper) so the
request-adapter configuration reaches wrapper.

---

Nitpick comments:
In `@eslint.config.js`:
- Around line 9-16: The current ESLint ignores array contains "example/**" which
fully excludes the sample app from linting; remove or narrow that pattern (or
add a dedicated flat-config entry for example/) so changes to the sample app
(the RN 0.83 migration files) are linted; edit the ignores array (the ignores
symbol) to either delete "example/**" or replace it with a more specific pattern
(e.g., exclude generated/artifacts only) or add a separate flat-config entry
targeting "example/" to ensure the example project is still validated.

In `@example/android/app/build.gradle`:
- Line 73: The jscFlavor variable is using a wildcard version
('io.github.react-native-community:jsc-android:2026004.+') which makes builds
non-reproducible; change the jscFlavor assignment to pin the exact validated
artifact version (replace the '2026004.+' range with the specific patch version
you tested) so the build pulls an immutable dependency (update the jscFlavor
definition accordingly).

In `@example/android/gradlew`:
- Line 117: Remove the unused CLASSPATH plumbing: delete the CLASSPATH
declaration that sets CLASSPATH="\"\"" in the shell wrapper, remove the block
that converts CLASSPATH for Cygwin/MSYS, and remove the -classpath argument
passed to the java invocation that launches the wrapper JAR; do the same in the
Windows wrapper by deleting the CLASSPATH variable and removing the -classpath
"%CLASSPATH%" argument so the wrapper launches with -jar only (locate the
CLASSPATH assignment, the cygpath conversion block, and the java invocation that
includes -classpath to modify).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: eb070022-06d7-45e0-843f-c48377c66219

📥 Commits

Reviewing files that changed from the base of the PR and between 3211cc4 and 7d2ab61.

⛔ Files ignored due to path filters (5)
  • example/Gemfile.lock is excluded by !**/*.lock
  • example/android/gradle/wrapper/gradle-wrapper.jar is excluded by !**/*.jar
  • example/ios/Podfile.lock is excluded by !**/*.lock
  • example/package-lock.json is excluded by !**/package-lock.json
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (72)
  • .eslintignore
  • .eslintrc.json
  • .gitignore
  • README.md
  • android/build.gradle
  • android/src/main/java/com/mattermost/networkclient/ApiClientModuleImpl.kt
  • android/src/main/java/com/mattermost/networkclient/Extensions.kt
  • android/src/main/java/com/mattermost/networkclient/NetworkClient.kt
  • android/src/main/java/com/mattermost/networkclient/helpers/CertificateChainHelper.kt
  • android/src/main/java/com/mattermost/networkclient/helpers/KeyStoreHelper.kt
  • android/src/main/java/com/mattermost/networkclient/helpers/TrustManagerHelper.kt
  • android/src/main/java/com/mattermost/networkclient/interfaces/RetryInterceptor.kt
  • android/src/test/java/com/mattermost/networkclient/ExponentialRetryInterceptorTest.kt
  • android/src/test/java/com/mattermost/networkclient/WebSocketClientModuleImplTest.kt
  • babel.config.js
  • eslint.config.js
  • example/Gemfile
  • example/android/app/build.gradle
  • example/android/app/src/main/java/com/example/reactnativenetworkclient/MainApplication.kt
  • example/android/build.gradle
  • example/android/gradle.properties
  • example/android/gradle/wrapper/gradle-wrapper.properties
  • example/android/gradlew
  • example/android/gradlew.bat
  • example/babel.config.js
  • example/ios/NetworkClientExample-Bridging-Header.h
  • example/ios/NetworkClientExample.xcodeproj/project.pbxproj
  • example/ios/NetworkClientExample/AppDelegate.h
  • example/ios/NetworkClientExample/AppDelegate.mm
  • example/ios/NetworkClientExample/AppDelegate.swift
  • example/ios/NetworkClientExample/Info.plist
  • example/ios/NetworkClientExample/SDWebImageDownloaderOperation+Swizzle.swift
  • example/ios/NetworkClientExample/main.m
  • example/ios/Podfile
  • example/metro.config.js
  • example/package.json
  • example/patches/react-native-image-picker+7.2.3.patch
  • example/patches/react-native-markdown-renderer+3.2.8.patch
  • example/src/App.tsx
  • example/src/components/FilePickerButtonGroup.tsx
  • example/src/components/P12Inputs.tsx
  • example/src/components/ResponseSuccessOverlay.tsx
  • example/src/screens/APIClientDownloadScreen.tsx
  • example/src/screens/APIClientFastImageScreen.tsx
  • example/src/screens/APIClientImportP12Screen.tsx
  • example/src/screens/APIClientRequestScreen.tsx
  • example/src/screens/APIClientScreen.tsx
  • example/src/screens/APIClientUploadScreen.tsx
  • example/src/screens/ClientListScreen.tsx
  • example/src/screens/CreateAPIClientScreen.tsx
  • example/src/screens/CreateWebSocketClientScreen.tsx
  • example/src/screens/GenericClientRequestScreen.tsx
  • example/src/screens/MattermostClientUploadScreen.tsx
  • example/src/screens/WebSocketClientScreen.tsx
  • ios/ApiClient/ApiClient.h
  • ios/ApiClient/ApiClient.mm
  • ios/ApiClient/ApiClientWrapper.swift
  • ios/Delegates/ApiClientSessionDelegate.swift
  • ios/Extensions/Session+Extensions.swift
  • ios/NetworkClient.xcodeproj/project.pbxproj
  • ios/SessionManager.swift
  • ios/Utilities/AiaCertHelper.swift
  • ios/patches/Alamofire+5.11.2.diff
  • ios/patches/apply_patches.rb
  • package.json
  • react-native-network-client.podspec
  • src/APIClient/NativeApiClient.ts
  • src/schemas.tsx
  • test-runner/build.gradle.kts
  • test-runner/src/test/kotlin/com/mattermost/networkclient/RetryInterceptorTest.kt
  • tsconfig.build.json
  • tsconfig.json
💤 Files with no reviewable changes (11)
  • example/ios/NetworkClientExample-Bridging-Header.h
  • example/ios/NetworkClientExample/AppDelegate.h
  • example/ios/NetworkClientExample/main.m
  • ios/NetworkClient.xcodeproj/project.pbxproj
  • example/patches/react-native-markdown-renderer+3.2.8.patch
  • .eslintignore
  • example/ios/NetworkClientExample/AppDelegate.mm
  • .eslintrc.json
  • android/src/test/java/com/mattermost/networkclient/WebSocketClientModuleImplTest.kt
  • android/src/test/java/com/mattermost/networkclient/ExponentialRetryInterceptorTest.kt
  • example/patches/react-native-image-picker+7.2.3.patch

Comment thread android/build.gradle Outdated
Comment thread android/src/main/java/com/mattermost/networkclient/Extensions.kt Outdated
Comment thread android/src/main/java/com/mattermost/networkclient/Extensions.kt Outdated
Comment thread example/src/screens/APIClientRequestScreen.tsx Outdated
Comment thread ios/Extensions/Session+Extensions.swift
Comment thread ios/patches/apply_patches.rb
Comment thread ios/Utilities/AiaCertHelper.swift Outdated
Comment thread test-runner/src/test/kotlin/com/mattermost/networkclient/RetryInterceptorTest.kt Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
android/src/main/java/com/mattermost/networkclient/NetworkClient.kt (2)

400-433: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Apply the bodyless POST fallback even when options is null.

The empty JSON body is only added inside if (options != null). A reachable call like request("POST", ..., null, ...) still leaves body == null, so the WAF workaround added in this PR is skipped on that path.

Suggested fix
     private fun prepareRequestBody(method: String, options: ReadableMap?): RequestBody? {
         var requestBody: RequestBody? = null

         if (options != null) {
             if (options.hasKey("body")) {
@@
-            } else if (method.uppercase(Locale.ENGLISH) == "POST") {
-                // Explicit Content-Type ensures WAFs don't reject bodyless POSTs (`#9689`).
-                // Caller can override by including Content-Type in the request headers.
-                requestBody = "".toRequestBody(MEDIA_TYPE_JSON)
             }
         }
+
+        if (requestBody == null && method.uppercase(Locale.ENGLISH) == "POST") {
+            // Explicit Content-Type ensures WAFs don't reject bodyless POSTs (`#9689`).
+            // Caller can override by including Content-Type in the request headers.
+            requestBody = "".toRequestBody(MEDIA_TYPE_JSON)
+        }

         return requestBody
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@android/src/main/java/com/mattermost/networkclient/NetworkClient.kt` around
lines 400 - 433, The POST body fallback is only applied when options != null, so
a call like request("POST", ..., null, ...) skips the empty JSON body; update
prepareRequestBody to ensure that when method.uppercase(Locale.ENGLISH) ==
"POST" and no body was created (i.e., options is null or options.hasKey("body")
is false), you still set requestBody = "".toRequestBody(MEDIA_TYPE_JSON) so the
empty JSON body is applied; locate this logic in prepareRequestBody and move or
add the POST fallback check outside/after the options != null branch (or check
for requestBody == null) while preserving the existing comment about WAFs and
allowing header overrides.

199-210: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve the configured hostname policy when rebuilding after P12 import.

This path always reinstalls the default verifier. If trustSelfSignedServerCertificate was enabled on the original client, importing a P12 at runtime flips the client back to strict hostname checks and breaks the self-signed flow only on the rebuild path.

Suggested fix
         if (baseUrl != null) {
             val (sslSocketFactory, trustManager) = buildSslConfig()
-            okHttpClient = okHttpClient.newBuilder()
-                    .sslSocketFactory(sslSocketFactory, trustManager)
-                    .hostnameVerifier { hostname, session ->
-                        val hv = HttpsURLConnection.getDefaultHostnameVerifier()
-                        val result = hv.verify(hostname, session)
-                        if (!result) {
-                            emitInvalidCertificateError()
-                        }
-                        result
-                    }
-                    .build()
+            val rebuilt = okHttpClient.newBuilder()
+                    .sslSocketFactory(sslSocketFactory, trustManager)
+
+            if (trustSelfSignedServerCertificate) {
+                rebuilt.hostnameVerifier { _, _ -> true }
+            } else {
+                rebuilt.hostnameVerifier { hostname, session ->
+                    val hv = HttpsURLConnection.getDefaultHostnameVerifier()
+                    val result = hv.verify(hostname, session)
+                    if (!result) {
+                        emitInvalidCertificateError()
+                    }
+                    result
+                }
+            }
+
+            okHttpClient = rebuilt.build()
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@android/src/main/java/com/mattermost/networkclient/NetworkClient.kt` around
lines 199 - 210, When rebuilding okHttpClient after importing a P12 (in the
branch where baseUrl != null and buildSslConfig() is called), preserve and reuse
the existing hostname verifier instead of always installing the default one;
capture the current client's hostnameVerifier (from the existing okHttpClient
instance) and only replace it when necessary (e.g., when a new explicit policy
is intended), otherwise reapply that verifier in the newBuilder() call so
trustSelfSignedServerCertificate behavior isn’t lost; ensure
emitInvalidCertificateError() is still invoked on verification failure as
before.
♻️ Duplicate comments (1)
android/src/main/java/com/mattermost/networkclient/helpers/CertificateChainHelper.kt (1)

84-106: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject zero-length DER lengths before continuing the AIA scan.

lengthAt() returns 0 on malformed/truncated input. In this loop, that can make seqEnd == pos, so the continue path re-enters with the same offset and spins during the TLS handshake instead of bailing out cleanly. Treat non-positive seqLen/oidLen/uriLen as invalid and return null before reusing pos.

Suggested fix
             val seqLen = lengthAt(der, pos)
             val seqLenSize = lengthSize(der, pos)
+            if (seqLen <= 0 || pos + seqLenSize > der.size) return null
             pos += seqLenSize
             val seqEnd = pos + seqLen
             if (seqEnd > der.size) return null
@@
             pos++
             val oidLen = lengthAt(der, pos)
-            pos += lengthSize(der, pos)
-            if (oidLen > seqEnd - pos) return null
+            val oidLenSize = lengthSize(der, pos)
+            if (oidLen <= 0 || pos + oidLenSize > seqEnd || pos + oidLenSize > der.size) return null
+            pos += oidLenSize
+            if (pos + oidLen > seqEnd || pos + oidLen > der.size) return null
             val oidBytes = der.copyOfRange(pos, pos + oidLen)
             pos += oidLen
@@
                 if (pos < seqEnd && der[pos].toInt() and 0xFF == 0x86) {
                     pos++
                     val uriLen = lengthAt(der, pos)
-                    pos += lengthSize(der, pos)
-                    if (uriLen > seqEnd - pos) return null
+                    val uriLenSize = lengthSize(der, pos)
+                    if (uriLen <= 0 || pos + uriLenSize > seqEnd || pos + uriLenSize > der.size) return null
+                    pos += uriLenSize
+                    if (pos + uriLen > seqEnd || pos + uriLen > der.size) return null
                     return String(der, pos, uriLen, Charsets.US_ASCII)
                 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@android/src/main/java/com/mattermost/networkclient/helpers/CertificateChainHelper.kt`
around lines 84 - 106, The loop in CertificateChainHelper that parses AIA must
reject zero or negative lengths from lengthAt() to avoid infinite loops; update
the parsing logic around lengthAt/lengthSize usage (for seqLen, oidLen and
uriLen) so that after computing seqLen and seqLenSize you return null if seqLen
<= 0 (and likewise return null if oidLen <= 0 or uriLen <= 0) before advancing
pos or continuing; ensure these checks occur immediately after computing each
length (references: lengthAt, lengthSize, CA_ISSUERS_OID and the AIA parsing
block) so malformed/truncated DER causes an early null return instead of reusing
the same pos.
🧹 Nitpick comments (1)
test-runner/src/test/kotlin/com/mattermost/networkclient/RetryInterceptorTest.kt (1)

113-113: 💤 Low value

Consider closing the Response to avoid resource warnings.

The Response object returned by execute() should be closed to release connection resources. While MockWebServer handles server-side cleanup, leaving Response objects unclosed can generate warnings and leak connections in more complex test scenarios.

♻️ Suggested fix
-            client.newCall(request).execute()
+            client.newCall(request).execute().close()

Same pattern applies to lines 136 and 154 where the response is not inspected.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@test-runner/src/test/kotlin/com/mattermost/networkclient/RetryInterceptorTest.kt`
at line 113, The test calls client.newCall(request).execute() without closing
the returned Response; update each invocation (including the occurrences at
client.newCall(request).execute() around lines noted and the similar calls at
lines 136 and 154) to close the Response—preferably by using Kotlin's use { ...
} on the Response (e.g., client.newCall(request).execute().use { /* optionally
assert or ignore */ }) or by calling response.close()—so the connection
resources are properly released in RetryInterceptorTest.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@android/build.gradle`:
- Line 2: The default Kotlin version returned by
getKotlinVersion()/rootProject.ext ('kotlinVersion') is set to "2.2.21", which
is incompatible with the pinned Android Gradle Plugin (AGP) 7.3.1; either change
the default Kotlin version to one supported by AGP 7.3.1 or upgrade the
AGP/Gradle versions together. Locate the getKotlinVersion()/kotlinVersion
default and either lower the default string to a Kotlin 1.x/1.8.x version
compatible with AGP 7.3.1, or update the AGP plugin dependency (the classpath
artifact that currently pins 7.3.1) and the Gradle wrapper to the matching
versions required for Kotlin 2.2.21 (AGP 8.10+), ensuring both changes are
coherent across places that call getKotlinVersion().

In
`@android/src/main/java/com/mattermost/networkclient/helpers/CertificateChainHelper.kt`:
- Around line 120-125: In fetchCertificate, validate the URL scheme before
calling URL(url).openConnection(): parse the input string (e.g., via URL or URI)
and only allow "http" or "https"; if the scheme is anything else, reject it
(return null or throw) to prevent local/resource schemes like file: or jar:
being used for AIA fetches. Apply this check at the top of the fetchCertificate
function so conn is only opened for http(s) URLs.

In `@android/src/main/java/com/mattermost/networkclient/NetworkClient.kt`:
- Around line 604-620: Before attempting to apply a new clientP12Configuration,
clear any previously stored client certificate/state so a failed import does not
leave a stale identity in place; specifically, in the block that reads
clientP12Configuration and calls importClientP12(path, password) (and in the
importClientP12 implementation if needed), reset the p12Alias/any keystore
entries used by configureSsl() (or set the variable that configureSsl() reads to
null/empty) before calling importClientP12, and ensure that on Exception (or if
importClientP12 no-ops) you leave that alias cleared so configureSsl() will not
load the old certificate.

In `@ios/Utilities/AiaCertHelper.swift`:
- Around line 209-220: The lengthAt(_:_: ) function can crash when the DER
length byte is 0x80 because numBytes becomes 0 and the for loop uses
1...numBytes; update lengthAt to explicitly guard for numBytes == 0
(indefinite-length) before constructing the range and treat it as a
malformed-length error—e.g., return a safe error tuple (such as (0,1) or (0,1 +
numBytes)) or otherwise bail out—also ensure you validate that pos + numBytes
does not exceed der.count before reading bytes; reference lengthAt, first, and
numBytes when making the checks.

---

Outside diff comments:
In `@android/src/main/java/com/mattermost/networkclient/NetworkClient.kt`:
- Around line 400-433: The POST body fallback is only applied when options !=
null, so a call like request("POST", ..., null, ...) skips the empty JSON body;
update prepareRequestBody to ensure that when method.uppercase(Locale.ENGLISH)
== "POST" and no body was created (i.e., options is null or
options.hasKey("body") is false), you still set requestBody =
"".toRequestBody(MEDIA_TYPE_JSON) so the empty JSON body is applied; locate this
logic in prepareRequestBody and move or add the POST fallback check
outside/after the options != null branch (or check for requestBody == null)
while preserving the existing comment about WAFs and allowing header overrides.
- Around line 199-210: When rebuilding okHttpClient after importing a P12 (in
the branch where baseUrl != null and buildSslConfig() is called), preserve and
reuse the existing hostname verifier instead of always installing the default
one; capture the current client's hostnameVerifier (from the existing
okHttpClient instance) and only replace it when necessary (e.g., when a new
explicit policy is intended), otherwise reapply that verifier in the
newBuilder() call so trustSelfSignedServerCertificate behavior isn’t lost;
ensure emitInvalidCertificateError() is still invoked on verification failure as
before.

---

Duplicate comments:
In
`@android/src/main/java/com/mattermost/networkclient/helpers/CertificateChainHelper.kt`:
- Around line 84-106: The loop in CertificateChainHelper that parses AIA must
reject zero or negative lengths from lengthAt() to avoid infinite loops; update
the parsing logic around lengthAt/lengthSize usage (for seqLen, oidLen and
uriLen) so that after computing seqLen and seqLenSize you return null if seqLen
<= 0 (and likewise return null if oidLen <= 0 or uriLen <= 0) before advancing
pos or continuing; ensure these checks occur immediately after computing each
length (references: lengthAt, lengthSize, CA_ISSUERS_OID and the AIA parsing
block) so malformed/truncated DER causes an early null return instead of reusing
the same pos.

---

Nitpick comments:
In
`@test-runner/src/test/kotlin/com/mattermost/networkclient/RetryInterceptorTest.kt`:
- Line 113: The test calls client.newCall(request).execute() without closing the
returned Response; update each invocation (including the occurrences at
client.newCall(request).execute() around lines noted and the similar calls at
lines 136 and 154) to close the Response—preferably by using Kotlin's use { ...
} on the Response (e.g., client.newCall(request).execute().use { /* optionally
assert or ignore */ }) or by calling response.close()—so the connection
resources are properly released in RetryInterceptorTest.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7f53c262-291b-4d40-b269-a1ce2c1b3630

📥 Commits

Reviewing files that changed from the base of the PR and between 7d2ab61 and 9b0ac0a.

📒 Files selected for processing (13)
  • android/build.gradle
  • android/src/main/java/com/mattermost/networkclient/Extensions.kt
  • android/src/main/java/com/mattermost/networkclient/NetworkClient.kt
  • android/src/main/java/com/mattermost/networkclient/helpers/CertificateChainHelper.kt
  • android/src/main/java/com/mattermost/networkclient/helpers/KeyStoreHelper.kt
  • android/src/main/java/com/mattermost/networkclient/helpers/TrustManagerHelper.kt
  • example/src/components/FilePickerButtonGroup.tsx
  • example/src/components/P12Inputs.tsx
  • example/src/components/ResponseSuccessOverlay.tsx
  • example/src/screens/APIClientRequestScreen.tsx
  • ios/Extensions/Session+Extensions.swift
  • ios/Utilities/AiaCertHelper.swift
  • test-runner/src/test/kotlin/com/mattermost/networkclient/RetryInterceptorTest.kt

Comment thread android/build.gradle
Comment thread android/src/main/java/com/mattermost/networkclient/NetworkClient.kt
Comment thread ios/Utilities/AiaCertHelper.swift
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
android/src/main/java/com/mattermost/networkclient/NetworkClient.kt (3)

597-600: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep hostname verification enabled for trustSelfSignedServerCertificate.

getTrustManager() already handles the self-signed opt-in during trust evaluation. Returning true here additionally accepts certificates for the wrong host, which is a much broader MITM bypass than the option name implies.

Suggested fix
         if (trustSelf) {
             trustSelfSignedServerCertificate = true
-            builder.hostnameVerifier { _, _ -> true }
-        } else {
-            buildHostnameVerifier()
         }
+        buildHostnameVerifier()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@android/src/main/java/com/mattermost/networkclient/NetworkClient.kt` around
lines 597 - 600, The current branch sets builder.hostnameVerifier { _, _ -> true
} when trustSelf is true, which disables hostname verification and creates a
broad MITM risk; instead remove the hostnameVerifier override and rely on the
existing trustSelfSignedServerCertificate flag and getTrustManager() logic to
handle self-signed certificates during trust evaluation. Locate the block that
sets trustSelfSignedServerCertificate and remove the builder.hostnameVerifier
lines, ensuring getTrustManager() remains responsible for evaluating self-signed
opt-in behavior and the TLS hostname checks are preserved.

196-203: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear the previous client identity before importing a replacement.

If importClientP12() throws—or just no-ops because the URI cannot be resolved—buildSslConfig() will keep loading the old p12Alias. applyClientSslConfiguration() already clears the alias first; this path should do the same so a failed replacement does not keep presenting a stale client cert.

Suggested fix
 fun importClientP12AndRebuildClient(p12FilePath: String, password: String) {
+    KeyStoreHelper.deleteClientCertificates(p12Alias)
     importClientP12(p12FilePath, password)

     if (baseUrl != null) {
         val (sslSocketFactory, trustManager) = buildSslConfig()
         okHttpClient = okHttpClient.newBuilder()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@android/src/main/java/com/mattermost/networkclient/NetworkClient.kt` around
lines 196 - 203, The importClientP12AndRebuildClient path must clear the
existing client identity before attempting a replacement so a failed import
doesn't leave the old p12Alias in use; modify importClientP12AndRebuildClient to
clear/reset the stored client identity (e.g. set p12Alias/nullify the alias or
call the same clear logic used by applyClientSslConfiguration) before calling
importClientP12(p12FilePath, password), and only set the alias back after a
successful import so buildSslConfig() cannot load a stale client cert.

419-432: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Apply the POST fallback after the body switch.

Line 419 correctly keeps an explicit null body as null, but Lines 429-432 only add the empty JSON body when the body key is absent. {"body": null} and options == null still skip the workaround and leave POSTs bodyless.

Suggested fix
 private fun prepareRequestBody(method: String, options: ReadableMap?): RequestBody? {
     var requestBody: RequestBody? = null

-    if (options != null) {
-        if (options.hasKey("body")) {
+    if (options?.hasKey("body") == true) {
             when (options.getType("body")) {
                 ReadableType.Array -> {
                     val jsonBody = JSONArray(options.getArray("body")!!.toArrayList())
                     requestBody = jsonBody.toString().toRequestBody(MEDIA_TYPE_JSON)
                 }
@@
                 ReadableType.Number -> {
                     requestBody = options.getDouble("body").toString().toRequestBody(MEDIA_TYPE_JSON)
                 }
             }
-        } else if (method.uppercase(Locale.ENGLISH) == "POST") {
-            // Explicit Content-Type ensures WAFs don't reject bodyless POSTs (`#9689`).
-            // Caller can override by including Content-Type in the request headers.
-            requestBody = "".toRequestBody(MEDIA_TYPE_JSON)
-        }
+    }
+
+    if (requestBody == null && method.uppercase(Locale.ENGLISH) == "POST") {
+        // Explicit Content-Type ensures WAFs don't reject bodyless POSTs (`#9689`).
+        // Caller can override by including Content-Type in the request headers.
+        requestBody = "".toRequestBody(MEDIA_TYPE_JSON)
     }

     return requestBody
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@android/src/main/java/com/mattermost/networkclient/NetworkClient.kt` around
lines 419 - 432, The POST empty-body workaround should run after the
body-parsing switch and must also handle options == null; move the existing POST
fallback so it executes after you parse options into requestBody and apply it
only when method.uppercase(Locale.ENGLISH) == "POST" && (options == null ||
!options.hasKey("body")), leaving cases where the caller explicitly provided
body: null (handled by ReadableType.Null setting requestBody = null) untouched;
update code around the requestBody construction in NetworkClient.kt (the
requestBody variable, the body switch, MEDIA_TYPE_JSON, and the POST fallback)
accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ios/Utilities/AiaCertHelper.swift`:
- Around line 126-136: The DER length parser lengthAt should return (0, 0) on
parse error and callers must validate the returned length-size before advancing
or slicing; update lengthAt (the function performing the early break at the old
line 225) to detect insufficient bytes and return (0,0) on error, and then in
all call sites (the blocks using octetLenSize, seqLenSize, descLenSize,
oidLenSize and variables pos) check lenSize > 0 immediately after the call and
bail out (or set pos = descEnd / break) if it is zero so you never advance pos
or slice with insufficient bytes—apply this to the sequences at the uses of
octetLenSize, seqLenSize, descLenSize and oidLenSize to fail fast on
malformed/truncated DER.

---

Duplicate comments:
In `@android/src/main/java/com/mattermost/networkclient/NetworkClient.kt`:
- Around line 597-600: The current branch sets builder.hostnameVerifier { _, _
-> true } when trustSelf is true, which disables hostname verification and
creates a broad MITM risk; instead remove the hostnameVerifier override and rely
on the existing trustSelfSignedServerCertificate flag and getTrustManager()
logic to handle self-signed certificates during trust evaluation. Locate the
block that sets trustSelfSignedServerCertificate and remove the
builder.hostnameVerifier lines, ensuring getTrustManager() remains responsible
for evaluating self-signed opt-in behavior and the TLS hostname checks are
preserved.
- Around line 196-203: The importClientP12AndRebuildClient path must clear the
existing client identity before attempting a replacement so a failed import
doesn't leave the old p12Alias in use; modify importClientP12AndRebuildClient to
clear/reset the stored client identity (e.g. set p12Alias/nullify the alias or
call the same clear logic used by applyClientSslConfiguration) before calling
importClientP12(p12FilePath, password), and only set the alias back after a
successful import so buildSslConfig() cannot load a stale client cert.
- Around line 419-432: The POST empty-body workaround should run after the
body-parsing switch and must also handle options == null; move the existing POST
fallback so it executes after you parse options into requestBody and apply it
only when method.uppercase(Locale.ENGLISH) == "POST" && (options == null ||
!options.hasKey("body")), leaving cases where the caller explicitly provided
body: null (handled by ReadableType.Null setting requestBody = null) untouched;
update code around the requestBody construction in NetworkClient.kt (the
requestBody variable, the body switch, MEDIA_TYPE_JSON, and the POST fallback)
accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0e7179c7-0ffa-4ca2-9d6e-61202f126fc3

📥 Commits

Reviewing files that changed from the base of the PR and between 9b0ac0a and 79256b1.

📒 Files selected for processing (3)
  • android/src/main/java/com/mattermost/networkclient/NetworkClient.kt
  • android/src/main/java/com/mattermost/networkclient/helpers/CertificateChainHelper.kt
  • ios/Utilities/AiaCertHelper.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • android/src/main/java/com/mattermost/networkclient/helpers/CertificateChainHelper.kt

Comment thread ios/Utilities/AiaCertHelper.swift
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
ios/Utilities/AiaCertHelper.swift (2)

22-22: 💤 Low value

Remove unused host parameter or document its intended purpose.

The host parameter is declared but never referenced in the function body. The SSL policies (including hostname verification) are already copied from the original serverTrust at line 45, so the host appears unnecessary.

Suggested fix
-    static func completingChain(for serverTrust: SecTrust, host: String) -> SecTrust? {
+    static func completingChain(for serverTrust: SecTrust) -> SecTrust? {

Also update the call site(s) accordingly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ios/Utilities/AiaCertHelper.swift` at line 22, The parameter host on
completingChain(for: host:) is unused; either remove the host parameter from the
static function signature (completingChain(for:)) and update all call sites to
pass only the SecTrust, or if host is intended for future hostname policy logic,
add a brief doc comment explaining its purpose and why it is unused now. Locate
the function named completingChain(for: host:) and adjust its signature and any
callers accordingly (or add documentation above the function) so there are no
unused-parameter warnings.

42-42: 💤 Low value

Cache original certificate count to avoid redundant call.

existingCertificates(from:) is called twice with the same trust object. Store the count at line 23 for a minor efficiency improvement.

Suggested fix
     static func completingChain(for serverTrust: SecTrust, host: String) -> SecTrust? {
-        var certs = existingCertificates(from: serverTrust)
+        let originalCerts = existingCertificates(from: serverTrust)
+        var certs = originalCerts
         guard let leaf = certs.first else { return nil }
         // ...
-        guard certs.count > existingCertificates(from: serverTrust).count else { return nil }
+        guard certs.count > originalCerts.count else { return nil }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ios/Utilities/AiaCertHelper.swift` at line 42, The guard calls
existingCertificates(from:) twice on the same serverTrust causing redundant
work; cache the original count in a local variable (e.g., let originalCount =
existingCertificates(from: serverTrust).count) near where certs is computed,
then change the guard to compare certs.count > originalCount; reference
existingCertificates(from:), certs, serverTrust, and the guard statement to
locate and update the code.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@ios/Utilities/AiaCertHelper.swift`:
- Line 22: The parameter host on completingChain(for: host:) is unused; either
remove the host parameter from the static function signature
(completingChain(for:)) and update all call sites to pass only the SecTrust, or
if host is intended for future hostname policy logic, add a brief doc comment
explaining its purpose and why it is unused now. Locate the function named
completingChain(for: host:) and adjust its signature and any callers accordingly
(or add documentation above the function) so there are no unused-parameter
warnings.
- Line 42: The guard calls existingCertificates(from:) twice on the same
serverTrust causing redundant work; cache the original count in a local variable
(e.g., let originalCount = existingCertificates(from: serverTrust).count) near
where certs is computed, then change the guard to compare certs.count >
originalCount; reference existingCertificates(from:), certs, serverTrust, and
the guard statement to locate and update the code.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 72ae4433-5d78-4e0f-8a48-89df5c2c245a

📥 Commits

Reviewing files that changed from the base of the PR and between 79256b1 and aa1d0a5.

📒 Files selected for processing (1)
  • ios/Utilities/AiaCertHelper.swift

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

Caution

Review failed

An error occurred during the review process. Please try again later.

📝 Walkthrough

Walkthrough

Refactors TLS/SSL certificate handling (AIA-based chain completion, TrustManager/KeyManager and client P12 support), rewrites retry interceptor control flow and moves tests to a JVM runner, modernizes Android/iOS example projects and build/tooling, updates TypeScript schemas and configs, replaces DocumentPicker in examples, and migrates to an ESLint flat config (deleted .eslintignore).

Changes

TLS/SSL: Android and iOS certificate chain & trust wiring

Layer / File(s) Summary
Data / Helpers
android/src/main/java/.../helpers/CertificateChainHelper.kt, android/src/main/java/.../helpers/TrustManagerHelper.kt, ios/Utilities/AiaCertHelper.swift
Added AIA caIssuers DER parsing, intermediate certificate fetching, chain completion and trust/key-manager builders for Android and iOS.
KeyStore surface
android/src/main/java/.../helpers/KeyStoreHelper.kt
Replaced PKCS12 extraction API: removed getClientCertificates(...); added getClientCertKeyStore(...) returning KeyStore+password pair for mTLS usage.
SSL config / wiring
android/src/main/java/.../NetworkClient.kt
Introduced buildSslConfig(), applyClientSslConfiguration(options), configureSsl(); integrated TrustManagerHelper and P12 handling; rebuilds OkHttp client when client certs are configured; added brotli and media-type constants.
Handshake / validation
android/src/main/java/.../NetworkClient.kt, ios/Delegates/ApiClientSessionDelegate.swift
Handshake verification attempts AIA-based chain augmentation before failing; iOS challenge handling now can augment chain and conditionally trust augmented chain.
Error reporting / state
android/src/main/java/.../NetworkClient.kt, ios/Extensions/Session+Extensions.swift, ios/SessionManager.swift
Added per-host deduplication (Android) and per-session certErrorEmitted/lastServerCertSummary storage (iOS); session invalidation now cleans per-session extension state.

Build, dependencies and platform configs

Layer / File(s) Summary
Android build defaults & resolution
android/build.gradle, example/android/app/build.gradle
Kotlin default bumped to 2.2.21; OkHttp moved to 5.3.2 family; added configurations.all resolutionStrategy forcing OkHttp/Okio/Kotlin stdlib versions; removed legacy OkHttp 4.x test deps.
Example Android SDK / wrapper
example/android/build.gradle, example/android/gradle/wrapper/gradle-wrapper.properties, example/android/gradle.properties
Bumped buildTools/compile/target SDK and NDK; Gradle wrapper updated to 9.0.0; introduced edgeToEdgeEnabled property.
iOS platform & pods
react-native-network-client.podspec, example/ios/Podfile
Podspec raised iOS minimum to 15.1 and Alamofire to ~> 5.11.2; Podfile sets env flags and sets SWIFT_ENABLE_EXPLICIT_MODULES = 'NO' for targets.
Project scripts
example/android/gradlew, example/android/gradlew.bat
Gradle wrapper scripts now invoke wrapper JAR via -jar; APP_HOME, CLASSPATH and header/license tweaks applied.
iOS project cleanup
ios/NetworkClient.xcodeproj/project.pbxproj
Removed the NetworkClient iOS Xcode project configuration (project file deleted).

Example app modernization and UI/tooling updates

Layer / File(s) Summary
Android host bootstrap
example/android/.../MainApplication.kt
Replaced anonymous ReactNativeHost with lazy ReactHost initialization and loadReactNative(this); wired OkHttpClientProvider/RCTOkHttpClientFactory usage.
iOS AppDelegate migration
example/ios/NetworkClientExample/AppDelegate.swift, deleted Obj-C files/bridging header
Migrated Objective‑C bootstrap to Swift AppDelegate using RCTReactNativeFactory/ReactNativeDelegate; removed Obj‑C AppDelegate, main and bridging header.
Safe area & imports
example/src/App.tsx, example/src/screens/*
Wrapped example app with SafeAreaProvider; moved SafeAreaView to react-native-safe-area-context; reorganized imports to named hooks and RN primitives.
File picker replacement
example/src/components/FilePickerButtonGroup.tsx, example/src/components/P12Inputs.tsx
Replaced DocumentPicker flows with @react-native-documents/picker APIs (pick, keepLocalCopy, isErrorWithCode).
Metro / Babel / patches
example/metro.config.js, babel.config.js, example/babel.config.js, example/patches/*
Metro resolver maps package to TypeScript src, uses blockList and extraNodeModules; added @babel/plugin-transform-export-namespace-from to root and example Babel configs; example patches updated (type-only imports, lifecycle fixes).
Example package & Gemfile
example/package.json, example/Gemfile
Upgraded many dependencies/devDependencies for example app and adjusted CocoaPods/Ruby constraints.

Retry logic, streaming reads and test migration

Layer / File(s) Summary
Retry runtime
android/src/main/java/.../interfaces/RetryInterceptor.kt
Refactored intercept loop: explicit attempts counter, IOException-aware retry loop capturing lastException with rethrow after exhaustion, response cleanup, and retriesExhausted marking for final non-successful responses.
Response streaming
android/src/main/java/.../Extensions.kt
Added CountingInputStream and streaming-based response body reading to avoid full in-memory buffering and to compute metrics from actual bytes read.
Tests: native → JVM runner
deleted android/src/test/.../ExponentialRetryInterceptorTest.kt, added test-runner/src/test/kotlin/.../RetryInterceptorTest.kt, test-runner/build.gradle.kts
Removed Android unit tests; added JVM-hosted retry tests using MockWebServer and Mockito validating exponential backoff and retry behavior; added mockito dependency to test-runner.
Android test deletions
android/src/test/java/.../WebSocketClientModuleImplTest.kt
Removed the malformed-URL promise rejection test file.

Linting, TypeScript schemas and tooling

Layer / File(s) Summary
ESLint migration
.eslintignore (deleted), eslint.config.js, .eslintrc.json
Deleted .eslintignore; introduced eslint.config.js (flat config array: ignores, TypeScript+Prettier rules, test globals); .eslintrc.json left unchanged.
Babel root plugin
babel.config.js, example/babel.config.js
Added @babel/plugin-transform-export-namespace-from to root and example Babel configs; example Babel config simplified.
Schemas / types
src/schemas.tsx, src/APIClient/NativeApiClient.ts
Expanded API client schemas: added headers and nested configuration options; RequestOptionsSchema adds headers and tighter first union variant; MultipartUploadConfigSchema simplified; SessionConfiguration type now includes optional collectMetrics?: boolean.
TypeScript config
tsconfig.json, tsconfig.build.json
Changed moduleResolution to bundler, extended exclude (example, node_modules, Pods); added rootDir: "./src" in tsconfig.build.json.
Project package scripts
package.json
Changed lint script to eslint ., bumped many devDependencies, and added overrides mapping for release tooling.
Git ignore
.gitignore
Added .kotlin ignore entry under Android/IJ section.

Sequence Diagram(s)

sequenceDiagram
    participant Client as App/JS
    participant Network as NetworkClient (Kotlin/Swift)
    participant Trust as TrustManager / SecTrust
    participant Chain as CertificateChainHelper / AiaCertHelper
    participant JS as JS Event Emitter

    Client->>Network: Make HTTPS request
    Network->>Trust: Validate server certificate
    alt Trust validation succeeds
        Trust-->>Network: Validation OK
        Network->>Client: Proceed with response
    else Trust validation fails (recoverable)
        Trust->>Chain: Fetch AIA caIssuers & attempt chain completion
        Chain-->>Trust: Augmented chain or nil
        alt Augmented chain valid
            Trust-->>Network: Validation succeeded with augmented chain
            Network->>Client: Proceed with response
        else Augmentation failed
            Network->>JS: emitInvalidCertificateError(chain, cause)
            JS-->>Network: (event handled)
            Network->>Client: Fail request with certificate error
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch rn83

@enahum enahum requested a review from larkox May 6, 2026 13:48
@enahum enahum merged commit f12004c into master May 7, 2026
5 checks passed
@enahum enahum deleted the rn83 branch May 7, 2026 10:48
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