Skip to content

Next Release - #3788

Merged
isekovanic merged 2 commits into
mainfrom
develop
Aug 27, 2026
Merged

Next Release#3788
isekovanic merged 2 commits into
mainfrom
develop

Conversation

@isekovanic

Copy link
Copy Markdown
Contributor

🎯 Goal

πŸ›  Implementation details

🎨 UI Changes

iOS
Before After
Android
Before After

πŸ§ͺ Testing

β˜‘οΈ Checklist

  • I have signed the Stream CLA (required)
  • PR targets the develop branch
  • Documentation is updated
  • New code is tested in main example apps, including all possible scenarios
    • SampleApp iOS and Android
    • Expo iOS and Android

## 🎯 Goal

Support React Native 0.87.

0.87 makes the Strict TypeScript API the default and drops several types
from the root `react-native` export, which breaks our type-check. Its
Babel preset also miscompiles one of our shipped files.

The SDK now builds, type-checks and runs on **both 0.86 and 0.87**. Peer
range (`>= 0.76.0`) naturally remains unchanged, no public API removed.

## πŸ›  Implementation details

**Strict TypeScript API** - new `src/types/react-native-compat.ts`
exporting `ViewRef`/`TextInputRef`/`ScrollViewRef`/`FlatListRef` etc. as
`React.ComponentRef<typeof X>`. The "fix" is in the semantics, as it
resolves to the old class-instance types on `<= 0.86` and the new ones
on `0.87`, so we keep the `0.76` floor. Using RN's own `*Instance` names
would have dropped everything below `0.87`. Same file redeclares
`KeyboardEventListener`, `ViewToken` and `ViewabilityConfig`, which 0.87
no longer exports.

**API shape changes** - `useColorScheme()` and `findNodeHandle()` have
become `null`able, `AppState.currentState` widened to `string | null |
undefined`, `FlatList`'s `data` and `ListFooterComponent` have stopped
accepting `null`, style objects have become `Readonly`,
`NativeEventEmitter` has become generic. `onAccessibilityAction` moved
to `ViewProps`.

**Type leaks** - explicit annotations on `Input`,
`useAnimatedGalleryStyle`, `SafeAreaViewWrapper` and
`ImageGalleryFooter`, plus a `ThemeStyle` union, to stop RN-internal
names leaking into `lib/typescript`.

**`SqliteClient` runtime fix** - statics were arrow class fields calling
`this.foo()`. `0.87`'s Babel preset hoists that to a module-level `var
_this = this`, so `lib/commonjs` shipped calls against the wrong object
and threw. Now references the class explicitly.

**Build config** - both wrappers' `tsconfig.json` opt into legacy
deep-import types (the codegen spec must keep the deep
`codegenNativeComponent` import; the root export only exists from 0.80).
Android: Java 1.8 => 17, Kotlin 1.7 => 2.2, `minSdk` 21 => 24,
`compileSdk` fallback 31 => 34.

`compileSdk` is 34 rather than 37 on purpose, as `0.87`'s
`react-android` declares `minCompileSdk=34` and a library's `compileSdk`
caps what *we* may call, it isn't a floor on integrators. It's only
reached when the app doesn't set `rootProject.ext.compileSdkVersion`,
which basically any app always does.

**Jest** - two `moduleNameMapper` shims, both third-party:

For our integration tests to work I had to do some mental gymnastics to
get everything off the ground.

- `react-native-svg` (<= `15.15.5`) reads `Touchable.Mixin` off the RN
root. `0.87` keeps that export alive for exactly this call site but
installs it late and non-enumerably, so Babel's ESM interop drops it
under Jest.
- reanimated `4.6.0`'s `initializeReanimatedModule()` calls
`setCSSEventHandler()` unconditionally, which throws on the JS only
backend Jest uses.

`moduleNameMapper` rather than `jest.mock('react-native', …)` because
it's resolver-level, so it also covers the separate registry Jest builds
for automocks and it doesn't shadow per suite RN mocks.

**Example apps** - SampleApp gets the AGP 9 proguard fix
(`proguard-android-optimize.txt`; AGP 9 rejects `proguard-android.txt`
outright and since it's evaluated at configuration time it breaks the
*debug* build too), AGP 9 opt-outs, SDK 37/36, and 13 strict-API fixes
in its own code. ExpoMessaging deliberately stays on Expo 57 / RN 0.86
for now, while Expo `58` remains canary only and holding it back doubles
as backwards-compat coverage for now. Once 58 ships we can likely bump
that too pretty easily.

**Native source** - no changes. None of `0.87`'s removed native APIs are
used, headers already use the namespaced `<React/...>` form, and
`StreamShimmerViewComponentView` already sets `_props = defaultProps` in
`initWithFrame` β€” the thing `0.87` now asserts on.

## 🎨 UI Changes

## πŸ§ͺ Testing

## β˜‘οΈ Checklist

- [x] I have signed the [Stream
CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform)
(required)
- [x] PR targets the `develop` branch
- [ ] Documentation is updated
- [x] New code is tested in main example apps, including all possible
scenarios
  - [x] SampleApp iOS and Android
  - [ ] Expo iOS and Android
## 🎯 Goal

This PR fixes iOS attachment uploads hanging for 60s and then failing
with `-1001` (timed out) after the server has already accepted the file.

Affects apps using `useNativeMultipartUpload` specifically.

The `CFNetwork` trace of a failing upload looks something like this:

```
resuming, timeouts(60.0, …)
received response, status 201        ← server took the file
…60 s of nothing…
finished with error [-1001]
summary for task failure { response_status=201, request_bytes=2810041,
                           response_duration_ms=0, protocol="http/1.1" }
```

So the upload succeeds and the client throws it away a minute later. The
user sees a failed attachment (and the message with the attachment is
not sent consequently).

## πŸ›  Implementation details

The root cause is unfortunately not as simple. The multipart body was
pretty much a hand rolled `NSInputStream` subclass. `CFNetwork` drives
an `HTTP/1.1` request body through the `CFReadStream` interface, which a
plain `InputStream` subclass cannot participate in, so it can never
report end-of-stream. `CFNetwork` stops calling `read` the moment
`Content-Length` is satisfied, so the subclass never returned 0, never
reached `.atEnd` and so `CFNetwork` never learned the body had ended.
The transaction stayed open until the timeout (which is 60 seconds
later).

This isn't something that would fire all the time, however. It happens
whenever the connection ends up on `HTTP/1.1`. That's decided on a much
lower level (during the TLS handshake), so it's server and network
determined rather than something the app controls, so if an endpoint
that doesn't advertise `h2`, a TLS intercepting proxy or a local
debugging proxy such as `Charles` or `Proxyman` will all put us there.
Over `HTTP/2` and `HTTP/3` the request body is framed and terminated by
`Content-Length`, so the missing end-of-stream signal never mattered and
the bug stayed dormant. Hence, why this has gone unnoticed for so long.
Naturally, this is an edge case altogether but I've decided to rewrite
chunks of the body stream class. There have always existed some certain
parts of it that bothered me and we attempt to address them here as
well.

`makeStream()` now hands `URLSession` the read end of a
`CFStreamCreateBoundPair`, so a real `CFReadStream` that reports every
event and a new `StreamMultipartBodyProducer` feeds the write end from
the same element list. Closing the write end is what tells `CFNetwork`
the body is complete.

The producer is driven by GCD (`CFWriteStreamSetClient` +
`CFWriteStreamSetDispatchQueue`) rather than a run loop, so it owns no
thread and can't outlive its work.

Because a bound pair has no error channel, body production failures are
recorded in a `StreamMultipartBodyErrorBox` and preferred over the
transport error in `didCompleteWithError`. The box is **per attempt** so
then `URLSession` can request a fresh body stream on retry and an
abandoned attempt's failure must not fail a later one that succeeds.

Aside from a bit of code complexity, this also costs about `128 KiB` of
extra memory per request (which is nothing). In turn we handle a lot of
odd edge cases, like:

- no longer relying on kind of undocumented behaviour
- failures actually get reported every time now
- backpressure is now being handled instead of just falsely quitting the
upload
- retry scoping is now strictly 

And naturally, `HTTP/1.1` no longer fails uploads.

## 🎨 UI Changes

## πŸ§ͺ Testing
## β˜‘οΈ Checklist

- [x] I have signed the [Stream
CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform)
(required)
- [x] PR targets the `develop` branch
- [ ] Documentation is updated
- [x] New code is tested in main example apps, including all possible
scenarios
  - [x] SampleApp iOS and Android
  - [x] Expo iOS and Android
@github-actions

Copy link
Copy Markdown

Next releases

v9.8.1

9.8.1 (2026-08-27)

Bug Fixes

  • timeout on ios uploads on http 1.1 connection (#3787) (824818b)

Refactors

sampleapp@v4.15.1

4.15.1 (2026-08-27)

Refactors

@Stream-SDK-Bot

Copy link
Copy Markdown
Contributor

SDK Size

title develop branch diff status
js_bundle_size 2031 KB 2031 KB 0 B 🟒

@isekovanic
isekovanic merged commit 387b041 into main Aug 27, 2026
18 checks passed
@stream-ci-bot

Copy link
Copy Markdown
Contributor

πŸŽ‰ This PR is included in version 9.8.1 πŸŽ‰

The release is available on:

Your semantic-release bot πŸ“¦πŸš€

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants