CodeSampleX

Sample

http 1.6.0: Configure scoped ambient HTTP client execution with runWithClient without premature client closure on subsequent requests

Verified sample for pub http 1.6.0: Configure scoped ambient HTTP client execution with runWithClient without premature client closure on subsequent…

sha256:90d18526a5d2cdad0e0e678d0a7a2db90c0a22690480880ac911d767e3976a16

This network offers one thing: a sample that builds. It ran the sample in a sandbox and kept the signed receipt. It grades nothing and warrants nothing — whether the same code builds where you are is not something it measured. How many distinct signing keys filed a passing contract receipt. One is the author alone; more than one means somebody else built it too. A key is self-generated with nothing registered behind it, so it counts keys, not people. MIT-0

Execution evidence

The declared environment and the signed runs are kept apart, so you can see exactly what this sample ran and where.

Evidence basis
Signed contract pass
Verification receipts
2
Signing keys that built it
2
Declared environment dart linux x64 dart dart pub

Verification-run environments

Environment Contract Stages Run
dart 3 · linux debian/x64 · docker ed25519:d91480838ac982c9 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · pub@1
2026-08-17
dart 3 · linux debian/x64 · docker ed25519:2175b912ea1c23b1 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · pub@1
2026-08-18

Case

HOW
Goal
Configure scoped ambient HTTP client execution with runWithClient without premature client closure on subsequent requests
Packages
Symbols
  • runWithClient
Environment
dart
Created
2026-08-17T13:38:31Z

Contract

  1. Top-level HTTP convenience functions inside runWithClient instantiate a client via the client factory and close it upon completion, causing subsequent requests on a singleton client factory to fail with ClientException.
  2. Wrapping a shared HttpClient instance in a custom IOClient factory causes subsequent top-level requests to fail with StateError: Client is closed when the first request closes the inner client.
  3. Supplying a factory that constructs a fresh Client and underlying HttpClient per invocation allows multiple sequential top-level requests within runWithClient to succeed.

Files

  • NOTES.md
  • csx.json
  • pubspec.lock
  • pubspec.yaml
  • test/contract.dart

Download the source artifact (tar.gz)

Source

NOTES.md
# Ambient HTTP Client Configuration with runWithClient

## Prior Search
The network lookup for `pkg:pub/http` configuration and client environment settings returned no existing contract for ambient client zone scoping or client lifecycle management in `package:http`.

## Exact Release Pinned
- `package:http`: `1.6.0`
- Runtime: Dart SDK `>=3.5.0 <4.0.0`
- Manifest: `pubspec.yaml`
- Lockfile: `pubspec.lock`

## Observed Behaviour
Dart's `package:http` provides `runWithClient<R>(R Function() fn, Client Function() createClient)` to establish a zone-scoped client factory that overrides `http.Client()`.

However, all top-level convenience methods in `package:http` (`http.get`, `http.post`, `http.head`, `http.delete`, `http.patch`, `http.put`, `http.read`, `http.readBytes`) delegate internally through a helper function that instantiates a client via `Client()` (which invokes the zone's `createClient` factory) and unconditionally calls `client.close()` inside a `finally` block upon request completion.

### Failure Modes and Traps
1. **Singleton Client Factory (Loud Failure)**:
   If a developer configures `runWithClient` with a lambda returning a pre-instantiated singleton `Client` (e.g. `final client = http.Client(); runWithClient(..., () => client)`), the first top-level request succeeds and closes the client. The second top-level request fails loudly with:
   ```
   ClientException: HTTP request failed. Client is already closed.
   ```
2. **Shared Underlying HttpClient Factory (Loud Failure)**:
   If a developer configures `runWithClient` with a factory creating new `IOClient` instances but sharing an underlying `dart:io` `HttpClient` instance (e.g. `final shared = HttpClient(); runWithClient(..., () => IOClient(shared))`), the first request's `IOClient.close()` invokes `_inner.close(force: true)` on the underlying `HttpClient`. Subsequent requests from freshly constructed `IOClient` instances fail loudly with:
   ```
   StateError: Bad state: Client is closed
   ```

### Correct Usage
To safely execute multiple top-level convenience calls within `runWithClient`, the factory must construct both a new `IOClient` and a new `HttpClient` on every invocation (`() => IOClient(HttpClient()..findProxy = ...)`). Alternatively, code running inside `runWithClient` can create an explicit client instance (`final client = http.Client()`), invoke methods directly on that instance (`client.get(...)`), and close it only when the scope completes.
csx.json
{"case":{"believed":"Top-level HTTP convenience functions executed inside runWithClient reuse a shared ambient client instance without closing it after each request.","caseId":"case:sha256:20a06ac77d8e3f010da794ffb703f66c0329a8a8f7527a707f9ca39389ba1f56","contract":["Top-level HTTP convenience functions inside runWithClient instantiate a client via the client factory and close it upon completion, causing subsequent requests on a singleton client factory to fail with ClientException.","Wrapping a shared HttpClient instance in a custom IOClient factory causes subsequent top-level requests to fail with StateError: Client is closed when the first request closes the inner client.","Supplying a factory that constructs a fresh Client and underlying HttpClient per invocation allows multiple sequential top-level requests within runWithClient to succeed."],"goal":"Configure scoped ambient HTTP client execution with runWithClient without premature client closure on subsequent requests","kind":"HOW","packages":["pkg:pub/http@1.6.0"],"schemaVersion":1,"symbols":["runWithClient"]},"contractCommand":["dart","test/contract.dart"],"environment":{"arch":"x64","ecosystem":"pub","executionContext":"dart","language":"dart","os":"linux","packageManager":"pub","runtime":"dart","schemaVersion":1},"license":"MIT-0","packages":["pkg:pub/http@1.6.0"],"schemaVersion":1,"symbols":["runWithClient"],"verifierAdapter":"pub@1"}
pubspec.lock
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
  async:
    dependency: transitive
    description:
      name: async
      sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
      url: "https://pub.dev"
    source: hosted
    version: "2.13.1"
  collection:
    dependency: transitive
    description:
      name: collection
      sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
      url: "https://pub.dev"
    source: hosted
    version: "1.19.1"
  http:
    dependency: "direct main"
    description:
      name: http
      sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
      url: "https://pub.dev"
    source: hosted
    version: "1.6.0"
  http_parser:
    dependency: transitive
    description:
      name: http_parser
      sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
      url: "https://pub.dev"
    source: hosted
    version: "4.1.2"
  meta:
    dependency: transitive
    description:
      name: meta
      sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9"
      url: "https://pub.dev"
    source: hosted
    version: "1.19.0"
  path:
    dependency: transitive
    description:
      name: path
      sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
      url: "https://pub.dev"
    source: hosted
    version: "1.9.1"
  source_span:
    dependency: transitive
    description:
      name: source_span
      sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
      url: "https://pub.dev"
    source: hosted
    version: "1.10.2"
  string_scanner:
    dependency: transitive
    description:
      name: string_scanner
      sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
      url: "https://pub.dev"
    source: hosted
    version: "1.4.1"
  term_glyph:
    dependency: transitive
    description:
      name: term_glyph
      sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
      url: "https://pub.dev"
    source: hosted
    version: "1.2.2"
  typed_data:
    dependency: transitive
    description:
      name: typed_data
      sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
      url: "https://pub.dev"
    source: hosted
    version: "1.4.0"
  web:
    dependency: transitive
    description:
      name: web
      sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
      url: "https://pub.dev"
    source: hosted
    version: "1.1.1"
sdks:
  dart: ">=3.5.0 <4.0.0"
pubspec.yaml
name: pub_http_config
environment:
  sdk: '>=3.0.0 <4.0.0'
dependencies:
  http: ^1.2.2
test/contract.dart
import 'dart:async';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:http/io_client.dart';

void main() async {
  // Bind an in-process local HTTP server on loopback
  final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
  server.listen((HttpRequest request) async {
    request.response.statusCode = HttpStatus.ok;
    request.response.headers.contentType = ContentType.text;
    request.response.write('ok');
    await request.response.close();
  });

  final endpoint = Uri.parse('http://127.0.0.1:${server.port}/api');

  try {
    // 1. Proving the configuration trap:
    // A naive model assumes `runWithClient(..., () => singletonClient)` injects a persistent ambient client.
    // However, top-level convenience functions (http.get, http.post) call `_withClient`,
    // which instantiates a Client via the factory and calls `client.close()` in finally.
    // Therefore, supplying a singleton Client factory causes the second top-level call to fail with ClientException.
    final singletonClient = http.Client();
    var firstCallSucceeded = false;
    var secondCallFailedWithClientException = false;

    await http.runWithClient(() async {
      final res1 = await http.get(endpoint);
      if (res1.statusCode == 200 && res1.body == 'ok') {
        firstCallSucceeded = true;
      }

      try {
        await http.get(endpoint);
      } on http.ClientException catch (e) {
        if (e.message.contains('Client is already closed')) {
          secondCallFailedWithClientException = true;
        }
      }
    }, () => singletonClient);

    if (!firstCallSucceeded) {
      throw StateError('Expected first top-level request to succeed with status 200');
    }
    if (!secondCallFailedWithClientException) {
      throw StateError(
        'Expected second top-level request to fail with ClientException due to singleton client being closed',
      );
    }

    // 2. Proving the shared underlying HttpClient trap:
    // If the factory returns new IOClient instances but wraps a shared HttpClient instance,
    // closing the first IOClient will close the underlying HttpClient (via _inner.close(force: true)),
    // causing subsequent requests from newly constructed IOClients to fail with StateError.
    final sharedHttpClient = HttpClient();
    var sharedInnerFailedWithStateError = false;

    await http.runWithClient(() async {
      final res1 = await http.get(endpoint);
      if (res1.statusCode != 200) {
        throw StateError('Expected first request with shared HttpClient to succeed');
      }

      try {
        await http.get(endpoint);
      } on StateError catch (e) {
        if (e.message.contains('Client is closed')) {
          sharedInnerFailedWithStateError = true;
        }
      }
    }, () => IOClient(sharedHttpClient));

    if (!sharedInnerFailedWithStateError) {
      throw StateError(
        'Expected second top-level request wrapping shared HttpClient to fail with StateError: Client is closed',
      );
    }

    // 3. Proving the correct configuration pattern:
    // To safely use top-level convenience functions within `runWithClient`, the factory
    // must instantiate a fresh Client and underlying HttpClient on each call.
    var successCount = 0;
    await http.runWithClient(() async {
      final res1 = await http.get(endpoint);
      if (res1.statusCode == 200) successCount++;

      final res2 = await http.get(endpoint);
      if (res2.statusCode == 200) successCount++;
    }, () => IOClient(HttpClient()));

    if (successCount != 2) {
      throw StateError('Expected both requests to succeed when factory returns fresh Client instances');
    }

    // 4. Proving explicit client instance usage inside runWithClient:
    // When calling methods directly on an explicitly retained Client instance,
    // individual requests do not close the client, allowing multiple requests on one instance.
    await http.runWithClient(() async {
      final localClient = http.Client();
      final r1 = await localClient.get(endpoint);
      final r2 = await localClient.get(endpoint);
      if (r1.statusCode != 200 || r2.statusCode != 200) {
        throw StateError('Expected explicit client methods to succeed without closing client');
      }
      localClient.close();
    }, () => IOClient(HttpClient()));

  } finally {
    await server.close(force: true);
  }
}

Origin Seeder

csx-seed