Skip to content

feat(cupertino_http): shared session#1876

Open
mertalev wants to merge 16 commits intodart-lang:masterfrom
mertalev:feat/external-session-cupertino
Open

feat(cupertino_http): shared session#1876
mertalev wants to merge 16 commits intodart-lang:masterfrom
mertalev:feat/external-session-cupertino

Conversation

@mertalev
Copy link

@mertalev mertalev commented Feb 4, 2026

This PR enables sharing of a single URLSession with platform and across isolates. The existing implementation's delegate approach tightly couples a session to a single isolate. As a solution, it adds an implementation based on the bytes(for:) API, which does not require delegate-based callbacks. This allows users to bring their own session configured with their own delegate. It enables my goal to use a single shared session in my app; this is optimal for multiplexing and caching and means a single place to configure authentication like client certificates.

I've tested these changes in the app and made sure the conformance tests are passing with this implementation. A few changes were needed in the tests because bytes(for:) does its own chunking internally before yielding bytes. I have not tested its performance. I'd guess it performs better since the implementation is more contained in Swift with less Dart trampolining/bookkeeping, but it'd be good to profile it.

From a maintenance standpoint, I think this implementation is simpler than the delegate-based approach and might be able to replace it at some point, perhaps when the minimum iOS version is bumped to 15? It's relatively self-contained as it is, though, and should have no effect on existing users of cupertino_http.

Fixes #1002


  • I’ve reviewed the contributor guide and applied the relevant portions to this PR.
Contribution guidelines:

Many Dart repos have a weekly cadence for reviewing PRs - please allow for some latency before initial review feedback.

Note: The Dart team is trialing Gemini Code Assist. Don't take its comments as final Dart team feedback. Use the suggestions if they're helpful; otherwise, wait for a human reviewer.

@github-actions
Copy link

github-actions bot commented Feb 6, 2026

PR Health

License Headers ✔️
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
Files
no missing headers

All source files should start with a license header.

Unrelated files missing license headers
Files
pkgs/http/example/main.dart
pkgs/http_multi_server/test/cert.dart

This check can be disabled by tagging the PR with skip-license-check.

API leaks ✔️

The following packages contain symbols visible in the public API, but not exported by the library. Export these symbols or remove them from your publicly visible API.

Package Leaked API symbol Leaking sources

This check can be disabled by tagging the PR with skip-leaking-check.

Breaking changes ✔️
Package Change Current Version New Version Needed Version Looking good?
cupertino_http Breaking 2.4.0 3.0.0-wip 3.0.0-wip ✔️

This check can be disabled by tagging the PR with skip-breaking-check.

Unused Dependencies ⚠️
Package Status
cupertino_http
❗ Show Issues
These packages are used outside lib/ but are not dev_dependencies:
* convert
* crypto
* flutter_test
* http_client_conformance_tests
* http_image_provider
* integration_test
* provider
* test
* web_socket_conformance_tests
These packages are only used outside lib/ and should be downgraded to dev_dependencies:
* flutter
These packages may be unused, or you may be using assets from these packages:
* dart_flutter_team_lints
Failed to update packages.

For details on how to fix these, see dependency_validator.

This check can be disabled by tagging the PR with skip-unused-dependencies-check.

Coverage ⚠️
File Coverage
pkgs/cupertino_http/example/integration_test/client_conformance_test.dart 💔 Not covered
pkgs/cupertino_http/example/integration_test/client_test.dart 💔 Not covered
pkgs/cupertino_http/lib/src/cupertino_api.dart 💔 Not covered
pkgs/cupertino_http/lib/src/cupertino_client.dart 💔 Not covered
pkgs/cupertino_http/lib/src/cupertino_web_socket.dart 💔 Not covered
pkgs/cupertino_http/lib/src/native_cupertino_bindings.dart 💔 Not covered

This check for test coverage is informational (issues shown here will not fail the PR).

This check can be disabled by tagging the PR with skip-coverage-check.

Changelog Entry
Package Changed Files
package:cupertino_http pkgs/cupertino_http/lib/src/cupertino_api.dart
pkgs/cupertino_http/lib/src/cupertino_client.dart
pkgs/cupertino_http/lib/src/cupertino_web_socket.dart
pkgs/cupertino_http/lib/src/native_cupertino_bindings.dart

Changes to files need to be accounted for in their respective changelogs.

This check can be disabled by tagging the PR with skip-changelog-check.

@mertalev
Copy link
Author

mertalev commented Feb 8, 2026

I just looked into per-task delegates and I think they'd also work for this. Any hooks the task delegate doesn't override go to the session-level delegate, so things like client certificates would still work. It'd avoid the limitations with websockets and redirect tracking, so maybe that's the way to go. I'll give it a try later.

@brianquinlan
Copy link
Collaborator

Hi @mertalev this is very high quality but I'll need a while to review it.

From a maintenance standpoint, I think this implementation is simpler than the delegate-based approach and might be able to replace it at some point, perhaps when the minimum iOS version is bumped to 15? It's relatively self-contained as it is, though, and should have no effect on existing users of cupertino_http.

...avoid the limitations with websockets and redirect tracking, so maybe that's the way to go. I'll give it a try later.

If I understand correctly, the current approach can't be made to honor max-redirects, right? But will the redirect handler attached to the session be called?

Also, I guess that you are doing something like:

final sharedSession = URLSession.sessionWithConfiguration(
 ..., onFinishedDownloading: onFinished);

void foo() {
 sharedSession.downloadTaskWithRequest(...);
}  

void bar() {  // Possibly in another isolate
 final client = CupertinoClient.fromSharedSession(sharedSession);
 client.
}

The startWithLegacyFallback code path could be implemented in Dart using dataTaskWithCompletionHandler, right?

@liamappelbe is there a way to generate Dart bindings for Swift-only methods like these: https://developer.apple.com/documentation/foundation/urlsession/bytes(from:delegate:)?language=objc

@liamappelbe
Copy link

@liamappelbe is there a way to generate Dart bindings for Swift-only methods like these: https://developer.apple.com/documentation/foundation/urlsession/bytes(from:delegate:)?language=objc

In theory you could run this through swiftgen, and it would take care of generating an @objc compatibility layer for you. But it looks like it returns a tuple, which isn't supported yet. I think that's the only blocker to this particular method.

@mertalev
Copy link
Author

mertalev commented Feb 10, 2026

If I understand correctly, the current approach can't be made to honor max-redirects, right? But will the redirect handler attached to the session be called?

Yup, the maxRedirects / followRedirect settings couldn't be respected without a delegate. If the external session's delegate happens to handle redirects, this would still be respected, though. I just changed it to a task-level delegate approach that should respect those settings. As a bonus, it seems to have a modest performance improvement in a quick benchmark.

Dart delegate vs bytes(for:):

flutter: ╔══════════════════╤════════════╤══════════╤══════════╤══════════╤══════════════╤════════╗
flutter: ║ Label            │ Payload    │  p50(µs) │  p90(µs) │  p99(µs) │ Throughput   │ Chunks ║
flutter: ╠══════════════════╪════════════╪══════════╪══════════╪══════════╪══════════════╪════════╣
flutter: ║ owned-session    │ 1.0 KB     │      487 │      792 │     1084 │     2.0 MB/s │      1 ║
flutter: ║ shared-session   │ 1.0 KB     │      490 │      763 │     2156 │     2.0 MB/s │      1 ║
flutter: ║ owned-session    │ 64.0 KB    │     1018 │     1312 │     2264 │    61.4 MB/s │      1 ║
flutter: ║ shared-session   │ 64.0 KB    │     1064 │     1400 │     2248 │    58.7 MB/s │      1 ║
flutter: ║ owned-session    │ 1.0 MB     │     5886 │     6464 │     8677 │   169.9 MB/s │      3 ║
flutter: ║ shared-session   │ 1.0 MB     │     6325 │     6809 │     8960 │   158.1 MB/s │     16 ║
flutter: ║ owned-session    │ 16.0 MB    │    88930 │    91404 │   106781 │   179.9 MB/s │    168 ║
flutter: ║ shared-session   │ 16.0 MB    │    87860 │    90202 │    98826 │   182.1 MB/s │    256 ║

Dart delegate vs task-level delegate:

flutter: ╔══════════════════╤════════════╤══════════╤══════════╤══════════╤══════════════╤════════╗
flutter: ║ Label            │ Payload    │  p50(µs) │  p90(µs) │  p99(µs) │ Throughput   │ Chunks ║
flutter: ╠══════════════════╪════════════╪══════════╪══════════╪══════════╪══════════════╪════════╣
flutter: ║ owned-session    │ 1.0 KB     │      461 │      647 │     1183 │     2.1 MB/s │      1 ║
flutter: ║ shared-session   │ 1.0 KB     │      399 │      562 │     1736 │     2.4 MB/s │      1 ║
flutter: ║ owned-session    │ 64.0 KB    │      846 │     1054 │     1695 │    73.9 MB/s │      1 ║
flutter: ║ shared-session   │ 64.0 KB    │      843 │     1051 │     1806 │    74.1 MB/s │      1 ║
flutter: ║ owned-session    │ 1.0 MB     │     5733 │     6199 │     8526 │   174.4 MB/s │      3 ║
flutter: ║ shared-session   │ 1.0 MB     │     5641 │     5979 │     8730 │   177.3 MB/s │     16 ║
flutter: ║ owned-session    │ 16.0 MB    │    85992 │    88690 │    97948 │   186.1 MB/s │    168 ║
flutter: ║ shared-session   │ 16.0 MB    │    82941 │    84355 │   102873 │   192.9 MB/s │    256 ║
flutter: ╚══════════════════╧════════════╧══════════╧══════════╧══════════╧══════════════╧════════╝

As for usage, I create a session in AppDelegate.swift at bootstrap and have each isolate (including the main isolate) call fromSharedSession to use it.

The startWithLegacyFallback code path could be implemented in Dart using dataTaskWithCompletionHandler, right?

Probably yes!

@mertalev
Copy link
Author

I moved the legacy path to use Dart with dataTaskWithCompletionHandler and did some refactoring to simplify the code. The biggest change is to unify to using the task-level delegate approach - there's actually not much point to having the owned vs streamed paths since the new implementation has feature parity and is slightly more efficient. I can revert that if you'd rather have both versions, though.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sharing CupertinoClient's URLSession

3 participants