diff --git a/.github/labeler.yml b/.github/labeler.yml index 7ac52ce..6d477b3 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml
@@ -1,16 +1,21 @@ -# Configuration for .github/workflows/pull_request_label.yml. +# Configuration for .github/workflows/pull_request_label.yml. -'infra': - - '.github/**' +'type-infra': + - changed-files: + - any-glob-to-any-file: '.github/**' 'package:cronet_http': - - 'pkgs/cronet_http/**' + - changed-files: + - any-glob-to-any-file: 'pkgs/cronet_http/**' 'package:cupertino_http': - - 'pkgs/cupertino_http/**' + - changed-files: + - any-glob-to-any-file: 'pkgs/cupertino_http/**' 'package:http': - - 'pkgs/http/**' + - changed-files: + - any-glob-to-any-file: 'pkgs/http/**' 'package:http_client_conformance_tests': - - 'pkgs/http_client_conformance_tests/**' + - changed-files: + - any-glob-to-any-file: 'pkgs/http_client_conformance_tests/**' \ No newline at end of file
diff --git a/.github/workflows/cronet.yml b/.github/workflows/cronet.yml index 94fcf1c..1e9a81d 100644 --- a/.github/workflows/cronet.yml +++ b/.github/workflows/cronet.yml
@@ -6,10 +6,12 @@ - main - master paths: + - '.github/workflows/cronet.yaml' - 'pkgs/cronet_http/**' - 'pkgs/http_client_conformance_tests/**' pull_request: paths: + - '.github/workflows/cronet.yaml' - 'pkgs/cronet_http/**' - 'pkgs/http_client_conformance_tests/**' schedule: @@ -19,48 +21,49 @@ PUB_ENVIRONMENT: bot.github jobs: - analyze: - name: Lint and static analysis - runs-on: ubuntu-latest - defaults: - run: - working-directory: pkgs/cronet_http - steps: - - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2 - with: - # TODO: Change to 'stable' when a release version of flutter - # pins version 1.21.1 or later of 'package:test' - channel: 'master' - - id: install - name: Install dependencies - run: flutter pub get - - name: Check formatting - run: dart format --output=none --set-exit-if-changed . - if: always() && steps.install.outcome == 'success' - - name: Analyze code - run: flutter analyze --fatal-infos - if: always() && steps.install.outcome == 'success' - - test: - # Test package:cupertino_http use flutter integration tests. - needs: analyze - name: "Build and test" + verify: + name: Format & Analyze & Test runs-on: macos-latest + strategy: + matrix: + package: ['cronet_http', 'cronet_http_embedded'] steps: - - uses: actions/checkout@v4 - - uses: actions/setup-java@v3 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + - uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 with: distribution: 'zulu' java-version: '17' - - uses: subosito/flutter-action@v2 + - uses: subosito/flutter-action@2783a3f08e1baf891508463f8c6653c258246225 with: channel: 'stable' + - name: Make cronet_http_embedded copy + if: ${{ matrix.package == 'cronet_http_embedded' }} + run: | + mv pkgs/cronet_http pkgs/cronet_http_embedded + cd pkgs/cronet_http_embedded + flutter pub get && dart tool/prepare_for_embedded.dart + - id: install + name: Install dependencies + working-directory: 'pkgs/${{ matrix.package }}' + run: flutter pub get + - name: Check formatting + working-directory: 'pkgs/${{ matrix.package }}' + run: dart format --output=none --set-exit-if-changed . + if: always() && steps.install.outcome == 'success' + - name: Analyze code + working-directory: 'pkgs/${{ matrix.package }}' + run: flutter analyze --fatal-infos + if: always() && steps.install.outcome == 'success' - name: Run tests - uses: reactivecircus/android-emulator-runner@v2 + uses: reactivecircus/android-emulator-runner@6b0df4b0efb23bb0ec63d881db79aefbc976e4b2 + if: always() && steps.install.outcome == 'success' with: - api-level: 28 - target: playstore + # api-level/minSdkVersion should be help in sync in: + # - .github/workflows/cronet.yml + # - pkgs/cronet_http/android/build.gradle + # - pkgs/cronet_http/example/android/app/build.gradle + api-level: 21 arch: x86_64 + target: ${{ matrix.package == 'cronet_http_embedded' && 'default' || 'google_apis' }} profile: pixel - script: cd ./pkgs/cronet_http/example && flutter test --timeout=1200s integration_test/ + script: cd 'pkgs/${{ matrix.package }}/example' && flutter test --timeout=1200s integration_test/
diff --git a/.github/workflows/cupertino.yml b/.github/workflows/cupertino.yml index 6617635..ef2a8e8 100644 --- a/.github/workflows/cupertino.yml +++ b/.github/workflows/cupertino.yml
@@ -28,8 +28,8 @@ run: working-directory: pkgs/cupertino_http steps: - - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + - uses: subosito/flutter-action@2783a3f08e1baf891508463f8c6653c258246225 with: channel: 'stable' - id: install @@ -57,17 +57,17 @@ matrix: # Test on the minimum supported flutter version and the latest # version. - flutter-version: ["3.10.0", "any"] + flutter-version: ["3.16.0", "any"] runs-on: macos-latest defaults: run: working-directory: pkgs/cupertino_http/example steps: - - uses: actions/checkout@v4 - - uses: futureware-tech/simulator-action@v3 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + - uses: futureware-tech/simulator-action@bfa03d93ec9de6dacb0c5553bbf8da8afc6c2ee9 with: model: 'iPhone 8' - - uses: subosito/flutter-action@v2 + - uses: subosito/flutter-action@2783a3f08e1baf891508463f8c6653c258246225 with: flutter-version: ${{ matrix.flutter-version }} channel: 'stable'
diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 35af167..3de19a3 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml
@@ -1,4 +1,4 @@ -# Created with package:mono_repo v6.6.0 +# Created with package:mono_repo v6.6.1 name: Dart CI on: push: @@ -21,7 +21,7 @@ runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 + uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable" @@ -29,76 +29,37 @@ os:ubuntu-latest;pub-cache-hosted os:ubuntu-latest - name: Setup Dart SDK - uses: dart-lang/setup-dart@b64355ae6ca0b5d484f0106a033dd1388965d06d + uses: dart-lang/setup-dart@fedb1266e91cf51be2fdb382869461a434b920a3 with: sdk: stable - id: checkout name: Checkout repository - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - name: mono_repo self validate - run: dart pub global activate mono_repo 6.6.0 + run: dart pub global activate mono_repo 6.6.1 - name: mono_repo self validate run: dart pub global run mono_repo generate --validate job_002: - name: "analyze_and_format; linux; Dart 2.19.0; PKG: pkgs/http_client_conformance_tests; `dart analyze --fatal-infos`" + name: "analyze_and_format; linux; Dart 3.0.0; PKG: pkgs/http_profile; `dart analyze --fatal-infos`" runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 + uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 with: path: "~/.pub-cache/hosted" - key: "os:ubuntu-latest;pub-cache-hosted;sdk:2.19.0;packages:pkgs/http_client_conformance_tests;commands:analyze_1" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.0.0;packages:pkgs/http_profile;commands:analyze_1" restore-keys: | - os:ubuntu-latest;pub-cache-hosted;sdk:2.19.0;packages:pkgs/http_client_conformance_tests - os:ubuntu-latest;pub-cache-hosted;sdk:2.19.0 - os:ubuntu-latest;pub-cache-hosted - os:ubuntu-latest - - name: Setup Dart SDK - uses: dart-lang/setup-dart@b64355ae6ca0b5d484f0106a033dd1388965d06d - with: - sdk: "2.19.0" - - id: checkout - name: Checkout repository - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 - - id: pkgs_http_client_conformance_tests_pub_upgrade - name: pkgs/http_client_conformance_tests; dart pub upgrade - run: dart pub upgrade - if: "always() && steps.checkout.conclusion == 'success'" - working-directory: pkgs/http_client_conformance_tests - - name: "pkgs/http_client_conformance_tests; dart analyze --fatal-infos" - run: dart analyze --fatal-infos - if: "always() && steps.pkgs_http_client_conformance_tests_pub_upgrade.conclusion == 'success'" - working-directory: pkgs/http_client_conformance_tests - job_003: - name: "analyze_and_format; linux; Dart 3.0.0; PKGS: pkgs/http, pkgs/http_profile; `dart analyze --fatal-infos`" - runs-on: ubuntu-latest - steps: - - name: Cache Pub hosted dependencies - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 - with: - path: "~/.pub-cache/hosted" - key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.0.0;packages:pkgs/http-pkgs/http_profile;commands:analyze_1" - restore-keys: | - os:ubuntu-latest;pub-cache-hosted;sdk:3.0.0;packages:pkgs/http-pkgs/http_profile + os:ubuntu-latest;pub-cache-hosted;sdk:3.0.0;packages:pkgs/http_profile os:ubuntu-latest;pub-cache-hosted;sdk:3.0.0 os:ubuntu-latest;pub-cache-hosted os:ubuntu-latest - name: Setup Dart SDK - uses: dart-lang/setup-dart@b64355ae6ca0b5d484f0106a033dd1388965d06d + uses: dart-lang/setup-dart@fedb1266e91cf51be2fdb382869461a434b920a3 with: sdk: "3.0.0" - id: checkout name: Checkout repository - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 - - id: pkgs_http_pub_upgrade - name: pkgs/http; dart pub upgrade - run: dart pub upgrade - if: "always() && steps.checkout.conclusion == 'success'" - working-directory: pkgs/http - - name: "pkgs/http; dart analyze --fatal-infos" - run: dart analyze --fatal-infos - if: "always() && steps.pkgs_http_pub_upgrade.conclusion == 'success'" - working-directory: pkgs/http + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - id: pkgs_http_profile_pub_upgrade name: pkgs/http_profile; dart pub upgrade run: dart pub upgrade @@ -108,12 +69,72 @@ run: dart analyze --fatal-infos if: "always() && steps.pkgs_http_profile_pub_upgrade.conclusion == 'success'" working-directory: pkgs/http_profile + job_003: + name: "analyze_and_format; linux; Dart 3.2.0; PKG: pkgs/http_client_conformance_tests; `dart analyze --fatal-infos`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.2.0;packages:pkgs/http_client_conformance_tests;commands:analyze_1" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:3.2.0;packages:pkgs/http_client_conformance_tests + os:ubuntu-latest;pub-cache-hosted;sdk:3.2.0 + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@fedb1266e91cf51be2fdb382869461a434b920a3 + with: + sdk: "3.2.0" + - id: checkout + name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + - id: pkgs_http_client_conformance_tests_pub_upgrade + name: pkgs/http_client_conformance_tests; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/http_client_conformance_tests + - name: "pkgs/http_client_conformance_tests; dart analyze --fatal-infos" + run: dart analyze --fatal-infos + if: "always() && steps.pkgs_http_client_conformance_tests_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/http_client_conformance_tests job_004: + name: "analyze_and_format; linux; Dart 3.3.0; PKG: pkgs/http; `dart analyze --fatal-infos`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.3.0;packages:pkgs/http;commands:analyze_1" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:3.3.0;packages:pkgs/http + os:ubuntu-latest;pub-cache-hosted;sdk:3.3.0 + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@fedb1266e91cf51be2fdb382869461a434b920a3 + with: + sdk: "3.3.0" + - id: checkout + name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + - id: pkgs_http_pub_upgrade + name: pkgs/http; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/http + - name: "pkgs/http; dart analyze --fatal-infos" + run: dart analyze --fatal-infos + if: "always() && steps.pkgs_http_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/http + job_005: name: "analyze_and_format; linux; Dart dev; PKGS: pkgs/http, pkgs/http_client_conformance_tests, pkgs/http_profile; `dart analyze --fatal-infos`" runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 + uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/http-pkgs/http_client_conformance_tests-pkgs/http_profile;commands:analyze_1" @@ -123,12 +144,12 @@ os:ubuntu-latest;pub-cache-hosted os:ubuntu-latest - name: Setup Dart SDK - uses: dart-lang/setup-dart@b64355ae6ca0b5d484f0106a033dd1388965d06d + uses: dart-lang/setup-dart@fedb1266e91cf51be2fdb382869461a434b920a3 with: sdk: dev - id: checkout name: Checkout repository - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - id: pkgs_http_pub_upgrade name: pkgs/http; dart pub upgrade run: dart pub upgrade @@ -156,12 +177,12 @@ run: dart analyze --fatal-infos if: "always() && steps.pkgs_http_profile_pub_upgrade.conclusion == 'success'" working-directory: pkgs/http_profile - job_005: + job_006: name: "analyze_and_format; linux; Dart dev; PKGS: pkgs/http, pkgs/http_client_conformance_tests, pkgs/http_profile; `dart format --output=none --set-exit-if-changed .`" runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 + uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/http-pkgs/http_client_conformance_tests-pkgs/http_profile;commands:format" @@ -171,12 +192,12 @@ os:ubuntu-latest;pub-cache-hosted os:ubuntu-latest - name: Setup Dart SDK - uses: dart-lang/setup-dart@b64355ae6ca0b5d484f0106a033dd1388965d06d + uses: dart-lang/setup-dart@fedb1266e91cf51be2fdb382869461a434b920a3 with: sdk: dev - id: checkout name: Checkout repository - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - id: pkgs_http_pub_upgrade name: pkgs/http; dart pub upgrade run: dart pub upgrade @@ -204,12 +225,12 @@ run: "dart format --output=none --set-exit-if-changed ." if: "always() && steps.pkgs_http_profile_pub_upgrade.conclusion == 'success'" working-directory: pkgs/http_profile - job_006: + job_007: name: "analyze_and_format; linux; Flutter stable; PKG: pkgs/flutter_http_example; `dart format --output=none --set-exit-if-changed .`" runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 + uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:pkgs/flutter_http_example;commands:format" @@ -219,12 +240,12 @@ os:ubuntu-latest;pub-cache-hosted os:ubuntu-latest - name: Setup Flutter SDK - uses: subosito/flutter-action@48cafc24713cca54bbe03cdc3a423187d413aafa + uses: subosito/flutter-action@2783a3f08e1baf891508463f8c6653c258246225 with: channel: stable - id: checkout name: Checkout repository - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - id: pkgs_flutter_http_example_pub_upgrade name: pkgs/flutter_http_example; flutter pub upgrade run: flutter pub upgrade @@ -234,12 +255,12 @@ run: "dart format --output=none --set-exit-if-changed ." if: "always() && steps.pkgs_flutter_http_example_pub_upgrade.conclusion == 'success'" working-directory: pkgs/flutter_http_example - job_007: + job_008: name: "analyze_and_format; linux; Flutter stable; PKG: pkgs/flutter_http_example; `flutter analyze --fatal-infos`" runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 + uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:pkgs/flutter_http_example;commands:analyze_0" @@ -249,12 +270,12 @@ os:ubuntu-latest;pub-cache-hosted os:ubuntu-latest - name: Setup Flutter SDK - uses: subosito/flutter-action@48cafc24713cca54bbe03cdc3a423187d413aafa + uses: subosito/flutter-action@2783a3f08e1baf891508463f8c6653c258246225 with: channel: stable - id: checkout name: Checkout repository - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - id: pkgs_flutter_http_example_pub_upgrade name: pkgs/flutter_http_example; flutter pub upgrade run: flutter pub upgrade @@ -264,112 +285,27 @@ run: flutter analyze --fatal-infos if: "always() && steps.pkgs_flutter_http_example_pub_upgrade.conclusion == 'success'" working-directory: pkgs/flutter_http_example - job_008: - name: "unit_test; linux; Dart 3.0.0; PKG: pkgs/http; `dart run --define=no_default_http_client=true test/no_default_http_client_test.dart`" - runs-on: ubuntu-latest - steps: - - name: Cache Pub hosted dependencies - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 - with: - path: "~/.pub-cache/hosted" - key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.0.0;packages:pkgs/http;commands:command_1" - restore-keys: | - os:ubuntu-latest;pub-cache-hosted;sdk:3.0.0;packages:pkgs/http - os:ubuntu-latest;pub-cache-hosted;sdk:3.0.0 - os:ubuntu-latest;pub-cache-hosted - os:ubuntu-latest - - name: Setup Dart SDK - uses: dart-lang/setup-dart@b64355ae6ca0b5d484f0106a033dd1388965d06d - with: - sdk: "3.0.0" - - id: checkout - name: Checkout repository - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 - - id: pkgs_http_pub_upgrade - name: pkgs/http; dart pub upgrade - run: dart pub upgrade - if: "always() && steps.checkout.conclusion == 'success'" - working-directory: pkgs/http - - name: "pkgs/http; dart run --define=no_default_http_client=true test/no_default_http_client_test.dart" - run: "dart run --define=no_default_http_client=true test/no_default_http_client_test.dart" - if: "always() && steps.pkgs_http_pub_upgrade.conclusion == 'success'" - working-directory: pkgs/http - needs: - - job_001 - - job_002 - - job_003 - - job_004 - - job_005 - - job_006 - - job_007 job_009: - name: "unit_test; linux; Dart 3.0.0; PKG: pkgs/http; `dart test --platform chrome`" + name: "unit_test; linux; Dart 3.0.0; PKG: pkgs/http_profile; `dart test --platform vm`" runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 + uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 with: path: "~/.pub-cache/hosted" - key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.0.0;packages:pkgs/http;commands:test_3" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.0.0;packages:pkgs/http_profile;commands:test_2" restore-keys: | - os:ubuntu-latest;pub-cache-hosted;sdk:3.0.0;packages:pkgs/http + os:ubuntu-latest;pub-cache-hosted;sdk:3.0.0;packages:pkgs/http_profile os:ubuntu-latest;pub-cache-hosted;sdk:3.0.0 os:ubuntu-latest;pub-cache-hosted os:ubuntu-latest - name: Setup Dart SDK - uses: dart-lang/setup-dart@b64355ae6ca0b5d484f0106a033dd1388965d06d + uses: dart-lang/setup-dart@fedb1266e91cf51be2fdb382869461a434b920a3 with: sdk: "3.0.0" - id: checkout name: Checkout repository - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 - - id: pkgs_http_pub_upgrade - name: pkgs/http; dart pub upgrade - run: dart pub upgrade - if: "always() && steps.checkout.conclusion == 'success'" - working-directory: pkgs/http - - name: "pkgs/http; dart test --platform chrome" - run: dart test --platform chrome - if: "always() && steps.pkgs_http_pub_upgrade.conclusion == 'success'" - working-directory: pkgs/http - needs: - - job_001 - - job_002 - - job_003 - - job_004 - - job_005 - - job_006 - - job_007 - job_010: - name: "unit_test; linux; Dart 3.0.0; PKGS: pkgs/http, pkgs/http_profile; `dart test --platform vm`" - runs-on: ubuntu-latest - steps: - - name: Cache Pub hosted dependencies - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 - with: - path: "~/.pub-cache/hosted" - key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.0.0;packages:pkgs/http-pkgs/http_profile;commands:test_2" - restore-keys: | - os:ubuntu-latest;pub-cache-hosted;sdk:3.0.0;packages:pkgs/http-pkgs/http_profile - os:ubuntu-latest;pub-cache-hosted;sdk:3.0.0 - os:ubuntu-latest;pub-cache-hosted - os:ubuntu-latest - - name: Setup Dart SDK - uses: dart-lang/setup-dart@b64355ae6ca0b5d484f0106a033dd1388965d06d - with: - sdk: "3.0.0" - - id: checkout - name: Checkout repository - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 - - id: pkgs_http_pub_upgrade - name: pkgs/http; dart pub upgrade - run: dart pub upgrade - if: "always() && steps.checkout.conclusion == 'success'" - working-directory: pkgs/http - - name: "pkgs/http; dart test --platform vm" - run: dart test --platform vm - if: "always() && steps.pkgs_http_pub_upgrade.conclusion == 'success'" - working-directory: pkgs/http + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - id: pkgs_http_profile_pub_upgrade name: pkgs/http_profile; dart pub upgrade run: dart pub upgrade @@ -387,12 +323,130 @@ - job_005 - job_006 - job_007 + - job_008 + job_010: + name: "unit_test; linux; Dart 3.3.0; PKG: pkgs/http; `dart run --define=no_default_http_client=true test/no_default_http_client_test.dart`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.3.0;packages:pkgs/http;commands:command_1" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:3.3.0;packages:pkgs/http + os:ubuntu-latest;pub-cache-hosted;sdk:3.3.0 + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@fedb1266e91cf51be2fdb382869461a434b920a3 + with: + sdk: "3.3.0" + - id: checkout + name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + - id: pkgs_http_pub_upgrade + name: pkgs/http; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/http + - name: "pkgs/http; dart run --define=no_default_http_client=true test/no_default_http_client_test.dart" + run: "dart run --define=no_default_http_client=true test/no_default_http_client_test.dart" + if: "always() && steps.pkgs_http_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/http + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 job_011: + name: "unit_test; linux; Dart 3.3.0; PKG: pkgs/http; `dart test --platform chrome`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.3.0;packages:pkgs/http;commands:test_3" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:3.3.0;packages:pkgs/http + os:ubuntu-latest;pub-cache-hosted;sdk:3.3.0 + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@fedb1266e91cf51be2fdb382869461a434b920a3 + with: + sdk: "3.3.0" + - id: checkout + name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + - id: pkgs_http_pub_upgrade + name: pkgs/http; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/http + - name: "pkgs/http; dart test --platform chrome" + run: dart test --platform chrome + if: "always() && steps.pkgs_http_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/http + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_012: + name: "unit_test; linux; Dart 3.3.0; PKG: pkgs/http; `dart test --platform vm`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.3.0;packages:pkgs/http;commands:test_2" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:3.3.0;packages:pkgs/http + os:ubuntu-latest;pub-cache-hosted;sdk:3.3.0 + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@fedb1266e91cf51be2fdb382869461a434b920a3 + with: + sdk: "3.3.0" + - id: checkout + name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + - id: pkgs_http_pub_upgrade + name: pkgs/http; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/http + - name: "pkgs/http; dart test --platform vm" + run: dart test --platform vm + if: "always() && steps.pkgs_http_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/http + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_013: name: "unit_test; linux; Dart dev; PKG: pkgs/http; `dart run --define=no_default_http_client=true test/no_default_http_client_test.dart`" runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 + uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/http;commands:command_1" @@ -402,12 +456,12 @@ os:ubuntu-latest;pub-cache-hosted os:ubuntu-latest - name: Setup Dart SDK - uses: dart-lang/setup-dart@b64355ae6ca0b5d484f0106a033dd1388965d06d + uses: dart-lang/setup-dart@fedb1266e91cf51be2fdb382869461a434b920a3 with: sdk: dev - id: checkout name: Checkout repository - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - id: pkgs_http_pub_upgrade name: pkgs/http; dart pub upgrade run: dart pub upgrade @@ -425,12 +479,13 @@ - job_005 - job_006 - job_007 - job_012: + - job_008 + job_014: name: "unit_test; linux; Dart dev; PKG: pkgs/http; `dart test --platform chrome`" runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 + uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/http;commands:test_3" @@ -440,12 +495,12 @@ os:ubuntu-latest;pub-cache-hosted os:ubuntu-latest - name: Setup Dart SDK - uses: dart-lang/setup-dart@b64355ae6ca0b5d484f0106a033dd1388965d06d + uses: dart-lang/setup-dart@fedb1266e91cf51be2fdb382869461a434b920a3 with: sdk: dev - id: checkout name: Checkout repository - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - id: pkgs_http_pub_upgrade name: pkgs/http; dart pub upgrade run: dart pub upgrade @@ -463,12 +518,13 @@ - job_005 - job_006 - job_007 - job_013: + - job_008 + job_015: name: "unit_test; linux; Dart dev; PKGS: pkgs/http, pkgs/http_profile; `dart test --platform vm`" runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 + uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/http-pkgs/http_profile;commands:test_2" @@ -478,12 +534,12 @@ os:ubuntu-latest;pub-cache-hosted os:ubuntu-latest - name: Setup Dart SDK - uses: dart-lang/setup-dart@b64355ae6ca0b5d484f0106a033dd1388965d06d + uses: dart-lang/setup-dart@fedb1266e91cf51be2fdb382869461a434b920a3 with: sdk: dev - id: checkout name: Checkout repository - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - id: pkgs_http_pub_upgrade name: pkgs/http; dart pub upgrade run: dart pub upgrade @@ -510,12 +566,52 @@ - job_005 - job_006 - job_007 - job_014: + - job_008 + job_016: + name: "unit_test; linux; Dart dev; PKG: pkgs/http; `dart test --test-randomize-ordering-seed=random -p chrome -c dart2wasm`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/http;commands:test_4" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/http + os:ubuntu-latest;pub-cache-hosted;sdk:dev + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@fedb1266e91cf51be2fdb382869461a434b920a3 + with: + sdk: dev + - id: checkout + name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + - id: pkgs_http_pub_upgrade + name: pkgs/http; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/http + - name: "pkgs/http; dart test --test-randomize-ordering-seed=random -p chrome -c dart2wasm" + run: "dart test --test-randomize-ordering-seed=random -p chrome -c dart2wasm" + if: "always() && steps.pkgs_http_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/http + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_017: name: "unit_test; linux; Flutter stable; PKG: pkgs/flutter_http_example; `flutter test --platform chrome`" runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 + uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:pkgs/flutter_http_example;commands:test_1" @@ -525,12 +621,12 @@ os:ubuntu-latest;pub-cache-hosted os:ubuntu-latest - name: Setup Flutter SDK - uses: subosito/flutter-action@48cafc24713cca54bbe03cdc3a423187d413aafa + uses: subosito/flutter-action@2783a3f08e1baf891508463f8c6653c258246225 with: channel: stable - id: checkout name: Checkout repository - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - id: pkgs_flutter_http_example_pub_upgrade name: pkgs/flutter_http_example; flutter pub upgrade run: flutter pub upgrade @@ -548,12 +644,13 @@ - job_005 - job_006 - job_007 - job_015: + - job_008 + job_018: name: "unit_test; linux; Flutter stable; PKG: pkgs/flutter_http_example; `flutter test`" runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 + uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:pkgs/flutter_http_example;commands:command_0" @@ -563,12 +660,12 @@ os:ubuntu-latest;pub-cache-hosted os:ubuntu-latest - name: Setup Flutter SDK - uses: subosito/flutter-action@48cafc24713cca54bbe03cdc3a423187d413aafa + uses: subosito/flutter-action@2783a3f08e1baf891508463f8c6653c258246225 with: channel: stable - id: checkout name: Checkout repository - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - id: pkgs_flutter_http_example_pub_upgrade name: pkgs/flutter_http_example; flutter pub upgrade run: flutter pub upgrade @@ -586,12 +683,13 @@ - job_005 - job_006 - job_007 - job_016: + - job_008 + job_019: name: "unit_test; macos; Flutter stable; PKG: pkgs/flutter_http_example; `flutter test`" runs-on: macos-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 + uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 with: path: "~/.pub-cache/hosted" key: "os:macos-latest;pub-cache-hosted;sdk:stable;packages:pkgs/flutter_http_example;commands:command_0" @@ -601,12 +699,12 @@ os:macos-latest;pub-cache-hosted os:macos-latest - name: Setup Flutter SDK - uses: subosito/flutter-action@48cafc24713cca54bbe03cdc3a423187d413aafa + uses: subosito/flutter-action@2783a3f08e1baf891508463f8c6653c258246225 with: channel: stable - id: checkout name: Checkout repository - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - id: pkgs_flutter_http_example_pub_upgrade name: pkgs/flutter_http_example; flutter pub upgrade run: flutter pub upgrade @@ -624,17 +722,18 @@ - job_005 - job_006 - job_007 - job_017: + - job_008 + job_020: name: "unit_test; windows; Flutter stable; PKG: pkgs/flutter_http_example; `flutter test`" runs-on: windows-latest steps: - name: Setup Flutter SDK - uses: subosito/flutter-action@48cafc24713cca54bbe03cdc3a423187d413aafa + uses: subosito/flutter-action@2783a3f08e1baf891508463f8c6653c258246225 with: channel: stable - id: checkout name: Checkout repository - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - id: pkgs_flutter_http_example_pub_upgrade name: pkgs/flutter_http_example; flutter pub upgrade run: flutter pub upgrade @@ -652,3 +751,4 @@ - job_005 - job_006 - job_007 + - job_008
diff --git a/.github/workflows/java.yml b/.github/workflows/java.yml index ec1b9da..ed3f93f 100644 --- a/.github/workflows/java.yml +++ b/.github/workflows/java.yml
@@ -29,8 +29,8 @@ run: working-directory: pkgs/java_http steps: - - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + - uses: subosito/flutter-action@2783a3f08e1baf891508463f8c6653c258246225 with: channel: 'stable' @@ -55,8 +55,8 @@ working-directory: pkgs/java_http steps: - - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + - uses: subosito/flutter-action@2783a3f08e1baf891508463f8c6653c258246225 with: channel: 'stable'
diff --git a/.github/workflows/no-response.yml b/.github/workflows/no-response.yml new file mode 100644 index 0000000..ab1ac49 --- /dev/null +++ b/.github/workflows/no-response.yml
@@ -0,0 +1,37 @@ +# A workflow to close issues where the author hasn't responded to a request for +# more information; see https://github.com/actions/stale. + +name: No Response + +# Run as a daily cron. +on: + schedule: + # Every day at 8am + - cron: '0 8 * * *' + +# All permissions not specified are set to 'none'. +permissions: + issues: write + pull-requests: write + +jobs: + no-response: + runs-on: ubuntu-latest + if: ${{ github.repository_owner == 'dart-lang' }} + steps: + - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e + with: + # Don't automatically mark inactive issues+PRs as stale. + days-before-stale: -1 + # Close needs-info issues and PRs after 14 days of inactivity. + days-before-close: 14 + stale-issue-label: "needs-info" + close-issue-message: > + Without additional information we're not able to resolve this issue. + Feel free to add more info or respond to any questions above and we + can reopen the case. Thanks for your contribution! + stale-pr-label: "needs-info" + close-pr-message: > + Without additional information we're not able to resolve this PR. + Feel free to add more info or respond to any questions above. + Thanks for your contribution!
diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 0000000..f0cc574 --- /dev/null +++ b/.github/workflows/publish.yaml
@@ -0,0 +1,17 @@ +# A CI configuration to auto-publish pub packages. + +name: Publish + +on: + pull_request: + branches: [ master ] + push: + tags: [ '[A-z]+-v[0-9]+.[0-9]+.[0-9]+' ] + +jobs: + publish: + if: ${{ github.repository_owner == 'dart-lang' }} + uses: dart-lang/ecosystem/.github/workflows/publish.yaml@main + permissions: + id-token: write # Required for authentication using OIDC + pull-requests: write # Required for writing the pull request note
diff --git a/.github/workflows/pull_request_label.yml b/.github/workflows/pull_request_label.yml index 9933aad..54e3df5 100644 --- a/.github/workflows/pull_request_label.yml +++ b/.github/workflows/pull_request_label.yml
@@ -16,7 +16,7 @@ pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/labeler@ac9175f8a1f3625fd0d4fb234536d26811351594 + - uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 with: repo-token: "${{ secrets.GITHUB_TOKEN }}" sync-labels: true
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..32c6edd --- /dev/null +++ b/CONTRIBUTING.md
@@ -0,0 +1,47 @@ +# Contributing :heart: + +Want to contribute? Great! First, read this page (including the small print at +the end). + +### Before you contribute + +Before we can use your code, you must sign the +[Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual) +(CLA), which you can do online. The CLA is necessary mainly because you own the +copyright to your changes, even after your contribution becomes part of our +codebase, so we need your permission to use and distribute your code. We also +need to be sure of various other things—for instance that you'll tell us if you +know that your code infringes on other people's patents. You don't have to sign +the CLA until after you've submitted your code for review and a member has +approved it, but you must do it before we can put your code into our codebase. + +Before you start working on a larger contribution, you should get in touch with +us first through the issue tracker with your idea so that we can help out and +possibly guide you. Coordinating up front makes it much easier to avoid +frustration later on. + +### Code reviews + +All submissions, including submissions by project members, require review. + +### File headers + +All files in the project must start with the following header. + +```dart +// Copyright (c) 2024, 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. +``` + +### The small print + +Contributions made by corporations are covered by a different agreement than the +one above, the +[Software Grant and Corporate Contributor License Agreement](https://developers.google.com/open-source/cla/corporate). + +## A word about conduct + +We pledge to maintain an open and welcoming environment :hugs:. +For details, see our +[code of conduct](https://dart.dev/community/code-of-conduct).
diff --git a/README.md b/README.md index 006404d..2ce0382 100644 --- a/README.md +++ b/README.md
@@ -15,3 +15,8 @@ | [cronet_http](pkgs/cronet_http/) | An Android Flutter plugin that provides access to the [Cronet](https://developer.android.com/guide/topics/connectivity/cronet/reference/org/chromium/net/package-summary) HTTP client. | [](https://pub.dev/packages/cronet_http) | | [cupertino_http](pkgs/cupertino_http/) | A macOS/iOS Flutter plugin that provides access to the [Foundation URL Loading System](https://developer.apple.com/documentation/foundation/url_loading_system). | [](https://pub.dev/packages/cupertino_http) | | [flutter_http_example](pkgs/flutter_http_example/) | An Flutter app that demonstrates how to configure and use `package:http`. | — | + +## Contributing + +If you'd like to contribute to any of these packages, see the +[Contributing Guide](CONTRIBUTING.md).
diff --git a/analysis_options.yaml b/analysis_options.yaml index 7d741bd..e536739 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml
@@ -14,15 +14,10 @@ - avoid_returning_this - avoid_unused_constructor_parameters - cascade_invocations - - comment_references - join_return_with_assignment - missing_whitespace_between_adjacent_strings - no_adjacent_strings_in_list - no_runtimeType_toString - - prefer_const_constructors - prefer_const_declarations - prefer_expression_function_bodies - - prefer_relative_imports - - test_types_in_equals - use_string_buffers - - use_super_parameters
diff --git a/pkgs/cronet_http/CHANGELOG.md b/pkgs/cronet_http/CHANGELOG.md index 83e2701..bc92551 100644 --- a/pkgs/cronet_http/CHANGELOG.md +++ b/pkgs/cronet_http/CHANGELOG.md
@@ -1,6 +1,17 @@ +## 1.1.0 + +* Use `package:http_image_provider` in the example application. +* Support Android API 21+. +* Support `BaseResponseWithUrl`. + +## 1.0.0 + +* No functional changes. + ## 0.4.2 * Require `package:jni >= 0.7.2` to remove a potential buffer overflow. +* Fix a bug where incorrect HTTP request methods were sent. ## 0.4.1
diff --git a/pkgs/cronet_http/README.md b/pkgs/cronet_http/README.md index 0238eb6..47ebd8b 100644 --- a/pkgs/cronet_http/README.md +++ b/pkgs/cronet_http/README.md
@@ -1,31 +1,61 @@ +[](https://pub.dev/packages/cronet_http) +[](https://pub.dev/packages/cronet_http/publisher) + An Android Flutter plugin that provides access to the -[Cronet](https://developer.android.com/guide/topics/connectivity/cronet/reference/org/chromium/net/package-summary) +[Cronet][] HTTP client. Cronet is available as part of -[Google Play Services](https://developers.google.com/android/guides/overview). +[Google Play Services][]. -This package depends on -[Google Play Services](https://developers.google.com/android/guides/overview) -for its Cronet implementation. +This package depends on [Google Play Services][] for its [Cronet][] +implementation. [`package:cronet_http_embedded`](https://pub.dev/packages/cronet_http_embedded) -is functionally identical to this package but embeds Cronet directly instead -of relying on -[Google Play Services](https://developers.google.com/android/guides/overview). +is functionally identical to this package but embeds [Cronet][] directly +instead of relying on [Google Play Services][]. -## Status: Experimental +## Motivation -**NOTE**: This package is currently experimental and published under the -[labs.dart.dev](https://dart.dev/dart-team-packages) pub publisher in order to -solicit feedback. +Using [Cronet][], rather than the socket-based [dart:io HttpClient][] +implemententation, has several advantages: -For packages in the labs.dart.dev publisher we generally plan to either graduate -the package into a supported publisher (dart.dev, tools.dart.dev) after a period -of feedback and iteration, or discontinue the package. These packages have a -much higher expected rate of API and breaking changes. +1. It automatically supports Android platform features such as HTTP proxies. +2. It supports configurable caching. +3. It supports more HTTP features such as HTTP/3. -Your feedback is valuable and will help us evolve this package. -For general feedback and suggestions please comment in the -[feedback issue](https://github.com/dart-lang/http/issues/764). -For bugs, please file an issue in the -[bug tracker](https://github.com/dart-lang/http/issues). +## Using + +The easiest way to use this library is via the the high-level interface +defined by [package:http Client][]. + +This approach allows the same HTTP code to be used on all platforms, while +still allowing platform-specific setup. + +```dart +import 'package:cronet_http/cronet_http.dart'; +import 'package:http/http.dart'; +import 'package:http/io_client.dart'; + +void main() async { + final Client httpClient; + if (Platform.isAndroid) { + final engine = CronetEngine.build( + cacheMode: CacheMode.memory, + cacheMaxSize: 2 * 1024 * 1024, + userAgent: 'Book Agent'); + httpClient = CronetClient.fromCronetEngine(engine); + } else { + httpClient = IOClient(HttpClient()..userAgent = 'Book Agent'); + } + + final response = await client.get(Uri.https( + 'www.googleapis.com', + '/books/v1/volumes', + {'q': 'HTTP', 'maxResults': '40', 'printType': 'books'})); +} +``` + +[Cronet]: https://developer.android.com/guide/topics/connectivity/cronet/reference/org/chromium/net/package-summary +[dart:io HttpClient]: https://api.dart.dev/stable/dart-io/HttpClient-class.html +[Google Play Services]: https://developers.google.com/android/guides/overview +[package:http Client]: https://pub.dev/documentation/http/latest/http/Client-class.html
diff --git a/pkgs/cronet_http/android/build.gradle b/pkgs/cronet_http/android/build.gradle index 3a91d8a..af945d9 100644 --- a/pkgs/cronet_http/android/build.gradle +++ b/pkgs/cronet_http/android/build.gradle
@@ -2,14 +2,14 @@ version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.6.10' + ext.kotlin_version = '1.7.21' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.1.2' + classpath 'com.android.tools.build:gradle:7.4.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -25,6 +25,11 @@ apply plugin: 'kotlin-android' android { + // Conditional for compatibility with AGP <4.2. + if (project.android.hasProperty("namespace")) { + namespace 'io.flutter.plugins.cronet_http' + } + compileSdkVersion 31 compileOptions { @@ -41,7 +46,11 @@ } defaultConfig { - minSdkVersion 16 + // api-level/minSdkVersion should be help in sync in: + // - .github/workflows/cronet.yml + // - pkgs/cronet_http/android/build.gradle + // - pkgs/cronet_http/example/android/app/build.gradle + minSdkVersion 21 } defaultConfig { @@ -56,6 +65,5 @@ } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "com.google.android.gms:play-services-cronet:18.0.1" }
diff --git a/pkgs/cronet_http/example/android/app/build.gradle b/pkgs/cronet_http/example/android/app/build.gradle index 88b3356..dfd7427 100644 --- a/pkgs/cronet_http/example/android/app/build.gradle +++ b/pkgs/cronet_http/example/android/app/build.gradle
@@ -44,9 +44,11 @@ defaultConfig { applicationId "io.flutter.cronet_http_example" - // You can update the following values to match your application needs. - // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. - minSdkVersion flutter.minSdkVersion + // api-level/minSdkVersion should be help in sync in: + // - .github/workflows/cronet.yml + // - pkgs/cronet_http/android/build.gradle + // - pkgs/cronet_http/example/android/app/build.gradle + minSdkVersion 21 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName @@ -66,7 +68,9 @@ } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + // TODO(#1112): org.jetbrains.kotlin:kotlin-bom artifact purpose is to align kotlin stdlib and related code versions. + // This should be removed when https://github.com/flutter/flutter/issues/125062 is fixed. + implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.0")) // ""com.google.android.gms:play-services-cronet" is only present so that // `jnigen` will work. Applications should not include this line. implementation "com.google.android.gms:play-services-cronet:18.0.1"
diff --git a/pkgs/cronet_http/example/android/app/src/debug/AndroidManifest.xml b/pkgs/cronet_http/example/android/app/src/debug/AndroidManifest.xml index b17be9f..6170951 100644 --- a/pkgs/cronet_http/example/android/app/src/debug/AndroidManifest.xml +++ b/pkgs/cronet_http/example/android/app/src/debug/AndroidManifest.xml
@@ -1,4 +1,4 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.example.cronet_http_example"> + package="io.flutter.cronet_http_example"> <uses-permission android:name="android.permission.INTERNET"/> </manifest>
diff --git a/pkgs/cronet_http/example/android/app/src/main/AndroidManifest.xml b/pkgs/cronet_http/example/android/app/src/main/AndroidManifest.xml index e8d5f3f..254760d 100644 --- a/pkgs/cronet_http/example/android/app/src/main/AndroidManifest.xml +++ b/pkgs/cronet_http/example/android/app/src/main/AndroidManifest.xml
@@ -1,5 +1,5 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.example.cronet_http_example"> + package="io.flutter.cronet_http_example"> <uses-permission android:name="android.permission.INTERNET"/> <application android:label="cronet_http_example"
diff --git a/pkgs/cronet_http/example/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java b/pkgs/cronet_http/example/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java index 90015fd..3772f02 100644 --- a/pkgs/cronet_http/example/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java +++ b/pkgs/cronet_http/example/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java
@@ -16,14 +16,24 @@ private static final String TAG = "GeneratedPluginRegistrant"; public static void registerWith(@NonNull FlutterEngine flutterEngine) { try { - flutterEngine.getPlugins().add(new io.flutter.plugins.cronet_http.CronetHttpPlugin()); - } catch(Exception e) { - Log.e(TAG, "Error registering plugin cronet_http, io.flutter.plugins.cronet_http.CronetHttpPlugin", e); + flutterEngine.getPlugins().add(new dev.flutter.plugins.integration_test.IntegrationTestPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin integration_test, dev.flutter.plugins.integration_test.IntegrationTestPlugin", e); } try { - flutterEngine.getPlugins().add(new dev.flutter.plugins.integration_test.IntegrationTestPlugin()); - } catch(Exception e) { - Log.e(TAG, "Error registering plugin integration_test, dev.flutter.plugins.integration_test.IntegrationTestPlugin", e); + flutterEngine.getPlugins().add(new com.github.dart_lang.jni.JniPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin jni, com.github.dart_lang.jni.JniPlugin", e); + } + try { + flutterEngine.getPlugins().add(new io.flutter.plugins.pathprovider.PathProviderPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin path_provider_android, io.flutter.plugins.pathprovider.PathProviderPlugin", e); + } + try { + flutterEngine.getPlugins().add(new com.tekartik.sqflite.SqflitePlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin sqflite, com.tekartik.sqflite.SqflitePlugin", e); } } }
diff --git a/pkgs/cronet_http/example/android/app/src/main/kotlin/com/example/cronet_http_example/MainActivity.kt b/pkgs/cronet_http/example/android/app/src/main/kotlin/io/flutter/cronet_http_example/MainActivity.kt similarity index 70% rename from pkgs/cronet_http/example/android/app/src/main/kotlin/com/example/cronet_http_example/MainActivity.kt rename to pkgs/cronet_http/example/android/app/src/main/kotlin/io/flutter/cronet_http_example/MainActivity.kt index 2688c93..97b07c8 100644 --- a/pkgs/cronet_http/example/android/app/src/main/kotlin/com/example/cronet_http_example/MainActivity.kt +++ b/pkgs/cronet_http/example/android/app/src/main/kotlin/io/flutter/cronet_http_example/MainActivity.kt
@@ -1,4 +1,4 @@ -package com.example.cronet_http_example +package io.flutter.cronet_http_example import io.flutter.embedding.android.FlutterActivity
diff --git a/pkgs/cronet_http/example/android/app/src/profile/AndroidManifest.xml b/pkgs/cronet_http/example/android/app/src/profile/AndroidManifest.xml index b17be9f..6170951 100644 --- a/pkgs/cronet_http/example/android/app/src/profile/AndroidManifest.xml +++ b/pkgs/cronet_http/example/android/app/src/profile/AndroidManifest.xml
@@ -1,4 +1,4 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.example.cronet_http_example"> + package="io.flutter.cronet_http_example"> <uses-permission android:name="android.permission.INTERNET"/> </manifest>
diff --git a/pkgs/cronet_http/example/android/build.gradle b/pkgs/cronet_http/example/android/build.gradle index 83ae220..954fa1c 100644 --- a/pkgs/cronet_http/example/android/build.gradle +++ b/pkgs/cronet_http/example/android/build.gradle
@@ -1,12 +1,12 @@ buildscript { - ext.kotlin_version = '1.6.10' + ext.kotlin_version = '1.7.21' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.1.2' + classpath 'com.android.tools.build:gradle:7.4.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -26,6 +26,6 @@ project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir }
diff --git a/pkgs/cronet_http/example/android/gradle/wrapper/gradle-wrapper.properties b/pkgs/cronet_http/example/android/gradle/wrapper/gradle-wrapper.properties index cc5527d..cfe88f6 100644 --- a/pkgs/cronet_http/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/pkgs/cronet_http/example/android/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@ distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip
diff --git a/pkgs/cronet_http/example/integration_test/client_test.dart b/pkgs/cronet_http/example/integration_test/client_test.dart index cc6b80e..f126f84 100644 --- a/pkgs/cronet_http/example/integration_test/client_test.dart +++ b/pkgs/cronet_http/example/integration_test/client_test.dart
@@ -13,6 +13,8 @@ () => testAll( CronetClient.defaultCronetEngine, canStreamRequestBody: false, + canReceiveSetCookieHeaders: true, + canSendCookieHeaders: true, )); group('from cronet engine', () {
diff --git a/pkgs/cronet_http/example/lib/book.dart b/pkgs/cronet_http/example/lib/book.dart index 61a663b..584ae9a 100644 --- a/pkgs/cronet_http/example/lib/book.dart +++ b/pkgs/cronet_http/example/lib/book.dart
@@ -5,7 +5,7 @@ class Book { String title; String description; - String imageUrl; + Uri imageUrl; Book(this.title, this.description, this.imageUrl); @@ -21,7 +21,7 @@ 'description': final String description, 'imageLinks': {'smallThumbnail': final String thumbnail} }) { - books.add(Book(title, description, thumbnail)); + books.add(Book(title, description, Uri.parse(thumbnail))); } } }
diff --git a/pkgs/cronet_http/example/lib/main.dart b/pkgs/cronet_http/example/lib/main.dart index 61f11a7..6e1bad2 100644 --- a/pkgs/cronet_http/example/lib/main.dart +++ b/pkgs/cronet_http/example/lib/main.dart
@@ -5,21 +5,31 @@ import 'dart:convert'; import 'dart:io'; -import 'package:cached_network_image/cached_network_image.dart'; import 'package:cronet_http/cronet_http.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart'; +import 'package:http/io_client.dart'; +import 'package:http_image_provider/http_image_provider.dart'; +import 'package:provider/provider.dart'; import 'book.dart'; void main() { - var clientFactory = Client.new; // Constructs the default client. + final Client httpClient; if (Platform.isAndroid) { final engine = CronetEngine.build( - cacheMode: CacheMode.memory, userAgent: 'Book Agent'); - clientFactory = () => CronetClient.fromCronetEngine(engine); + cacheMode: CacheMode.memory, + cacheMaxSize: 2 * 1024 * 1024, + userAgent: 'Book Agent'); + httpClient = CronetClient.fromCronetEngine(engine); + } else { + httpClient = IOClient(HttpClient()..userAgent = 'Book Agent'); } - runWithClient(() => runApp(const BookSearchApp()), clientFactory); + + runApp(Provider<Client>( + create: (_) => httpClient, + child: const BookSearchApp(), + dispose: (_, client) => client.close())); } class BookSearchApp extends StatelessWidget { @@ -44,20 +54,22 @@ class _HomePageState extends State<HomePage> { List<Book>? _books; String? _lastQuery; + late Client _client; @override void initState() { super.initState(); + _client = context.read<Client>(); } // Get the list of books matching `query`. // The `get` call will automatically use the `client` configurated in `main`. Future<List<Book>> _findMatchingBooks(String query) async { - final response = await get( + final response = await _client.get( Uri.https( 'www.googleapis.com', '/books/v1/volumes', - {'q': query, 'maxResults': '40', 'printType': 'books'}, + {'q': query, 'maxResults': '20', 'printType': 'books'}, ), ); @@ -129,11 +141,10 @@ itemBuilder: (context, index) => Card( key: ValueKey(widget.books[index].title), child: ListTile( - leading: CachedNetworkImage( - placeholder: (context, url) => - const CircularProgressIndicator(), - imageUrl: - widget.books[index].imageUrl.replaceFirst('http', 'https')), + leading: Image( + image: HttpImage( + widget.books[index].imageUrl.replace(scheme: 'https'), + client: context.read<Client>())), title: Text(widget.books[index].title), subtitle: Text(widget.books[index].description), ),
diff --git a/pkgs/cronet_http/example/pubspec.yaml b/pkgs/cronet_http/example/pubspec.yaml index 568cef9..41c6611 100644 --- a/pkgs/cronet_http/example/pubspec.yaml +++ b/pkgs/cronet_http/example/pubspec.yaml
@@ -4,19 +4,20 @@ publish_to: 'none' environment: - sdk: "^3.0.0" + sdk: ^3.2.0 dependencies: - cached_network_image: ^3.2.3 cronet_http: path: ../ cupertino_icons: ^1.0.2 flutter: sdk: flutter - http: ^0.13.5 + http: ^1.0.0 + http_image_provider: ^0.0.2 + provider: ^6.1.1 dev_dependencies: - dart_flutter_team_lints: ^1.0.0 + dart_flutter_team_lints: ^2.0.0 flutter_test: sdk: flutter http_client_conformance_tests:
diff --git a/pkgs/cronet_http/lib/src/cronet_client.dart b/pkgs/cronet_http/lib/src/cronet_client.dart index c4d0d19..3c2a426 100644 --- a/pkgs/cronet_http/lib/src/cronet_client.dart +++ b/pkgs/cronet_http/lib/src/cronet_client.dart
@@ -23,6 +23,21 @@ final _digitRegex = RegExp(r'^\d+$'); const _bufferSize = 10 * 1024; // The size of the Cronet read buffer. +/// This class can be removed when `package:http` v2 is released. +class _StreamedResponseWithUrl extends StreamedResponse + implements BaseResponseWithUrl { + @override + final Uri url; + + _StreamedResponseWithUrl(super.stream, super.statusCode, + {required this.url, + super.contentLength, + super.request, + super.headers, + super.isRedirect, + super.reasonPhrase}); +} + /// The type of caching to use when making HTTP requests. enum CacheMode { disabled, @@ -163,9 +178,11 @@ case final contentLengthHeader?: contentLength = int.parse(contentLengthHeader); } - responseCompleter.complete(StreamedResponse( + responseCompleter.complete(_StreamedResponseWithUrl( responseStream!.stream, responseInfo.getHttpStatusCode(), + url: Uri.parse( + responseInfo.getUrl().toDartString(releaseOriginal: true)), contentLength: contentLength, reasonPhrase: responseInfo .getHttpStatusText() @@ -317,7 +334,7 @@ jb.UrlRequestCallbackProxy.new1( _urlRequestCallbacks(request, responseCompleter)), _executor, - ); + )..setHttpMethod(request.method.toJString()); var headers = request.headers; if (body.isNotEmpty &&
diff --git a/pkgs/cronet_http/pubspec.yaml b/pkgs/cronet_http/pubspec.yaml index 82390c8..f1a642b 100644 --- a/pkgs/cronet_http/pubspec.yaml +++ b/pkgs/cronet_http/pubspec.yaml
@@ -1,7 +1,7 @@ name: cronet_http -description: > +version: 1.1.0 +description: >- An Android Flutter plugin that provides access to the Cronet HTTP client. -version: 0.4.2 repository: https://github.com/dart-lang/http/tree/master/pkgs/cronet_http environment: @@ -11,11 +11,11 @@ dependencies: flutter: sdk: flutter - http: '>=0.13.4 <2.0.0' + http: ^1.2.0 jni: ^0.7.2 dev_dependencies: - dart_flutter_team_lints: ^1.0.0 + dart_flutter_team_lints: ^2.0.0 jnigen: ^0.7.0 xml: ^6.1.0 yaml_edit: ^2.0.3 @@ -25,4 +25,3 @@ platforms: android: ffiPlugin: true -
diff --git a/pkgs/cronet_http/tool/prepare_for_embedded.dart b/pkgs/cronet_http/tool/prepare_for_embedded.dart index e0f99d7..16a2c0b 100644 --- a/pkgs/cronet_http/tool/prepare_for_embedded.dart +++ b/pkgs/cronet_http/tool/prepare_for_embedded.dart
@@ -13,6 +13,9 @@ /// 1. Modifying the Gradle build file to reference the embedded Cronet. /// 2. Modifying the *name* and *description* in `pubspec.yaml`. /// 3. Replacing `README.md` with `README_EMBEDDED.md`. +/// 4. Change the name of `cronet_http.dart` to `cronet_http_embedded.dart`. +/// 5. Update all the imports from `package:cronet_http/cronet_http.dart` to +/// `package:cronet_http_embedded/cronet_http_embedded.dart` /// /// After running this script, `flutter pub publish` /// can be run to update package:cronet_http_embedded. @@ -26,6 +29,7 @@ import 'package:xml/xml.dart'; import 'package:yaml_edit/yaml_edit.dart'; +late final String _scriptName; late final Directory _packageDirectory; const _gmsDependencyName = 'com.google.android.gms:play-services-cronet'; @@ -39,18 +43,31 @@ 'dl.google.com', 'android/maven2/org/chromium/net/group-index.xml', ); +// Finds the Google Play Services Cronet dependency line. For example: +// ' implementation "com.google.android.gms:play-services-cronet:18.0.1"' +final implementationRegExp = RegExp( + '^\\s*implementation [\'"]' + '$_gmsDependencyName' + ':\\d+.\\d+.\\d+[\'"]', + multiLine: true, +); -void main() async { - if (Directory.current.path.endsWith('tool')) { - _packageDirectory = Directory.current.parent; - } else { - _packageDirectory = Directory.current; - } - +void main(List<String> args) async { + final script = Platform.script.toFilePath(); + _scriptName = script.split(Platform.pathSeparator).last; + _packageDirectory = Directory( + Uri.directory( + '${script.replaceAll(_scriptName, '')}' + '..${Platform.pathSeparator}', + ).toFilePath(), + ); final latestVersion = await _getLatestCronetVersion(); - updateCronetDependency(latestVersion); + updateBuildGradle(latestVersion); + updateExampleBuildGradle(); updatePubSpec(); updateReadme(); + updateLibraryName(); + updateImports(); } Future<String> _getLatestCronetVersion() async { @@ -69,37 +86,77 @@ return versions.last; } -/// Update android/build.gradle -void updateCronetDependency(String latestVersion) { - final fBuildGradle = File('${_packageDirectory.path}/android/build.gradle'); - final gradleContent = fBuildGradle.readAsStringSync(); - final implementationRegExp = RegExp( - '^\\s*implementation [\'"]' - '$_gmsDependencyName' - ':\\d+.\\d+.\\d+[\'"]', - multiLine: true, - ); +/// Update android/build.gradle. +void updateBuildGradle(String latestVersion) { + final buildGradle = File('${_packageDirectory.path}/android/build.gradle'); + final gradleContent = buildGradle.readAsStringSync(); final newImplementation = '$_embeddedDependencyName:$latestVersion'; - print('Patching $newImplementation'); + print('Updating ${buildGradle.path}: adding $newImplementation'); final newGradleContent = gradleContent.replaceAll( implementationRegExp, ' implementation "$newImplementation"', ); - fBuildGradle.writeAsStringSync(newGradleContent); + buildGradle.writeAsStringSync(newGradleContent); } -/// Update pubspec.yaml +/// Remove the cronet reference from ./example/android/app/build.gradle. +void updateExampleBuildGradle() { + final buildGradle = + File('${_packageDirectory.path}/example/android/app/build.gradle'); + final gradleContent = buildGradle.readAsStringSync(); + + print('Updating ${buildGradle.path}: removing cronet reference'); + final newGradleContent = gradleContent.replaceAll( + implementationRegExp, + ' // NOTE: removed in package:cronet_http_embedded', + ); + buildGradle.writeAsStringSync(newGradleContent); +} + +/// Update pubspec.yaml and example/pubspec.yaml. void updatePubSpec() { + print('Updating pubspec.yaml'); final fPubspec = File('${_packageDirectory.path}/pubspec.yaml'); final yamlEditor = YamlEditor(fPubspec.readAsStringSync()) ..update(['name'], _packageName) ..update(['description'], _packageDescription); fPubspec.writeAsStringSync(yamlEditor.toString()); + print('Updating example/pubspec.yaml'); + final examplePubspec = File('${_packageDirectory.path}/example/pubspec.yaml'); + final replaced = examplePubspec + .readAsStringSync() + .replaceAll('cronet_http:', 'cronet_http_embedded:'); + examplePubspec.writeAsStringSync(replaced); } -/// Move README_EMBEDDED.md to replace README.md +/// Move README_EMBEDDED.md to replace README.md. void updateReadme() { + print('Updating README.md from README_EMBEDDED.md'); File('${_packageDirectory.path}/README.md').deleteSync(); File('${_packageDirectory.path}/README_EMBEDDED.md') .renameSync('${_packageDirectory.path}/README.md'); } + +void updateImports() { + print('Updating imports in Dart files'); + for (final file in _packageDirectory.listSync(recursive: true)) { + if (file is File && + file.path.endsWith('.dart') && + !file.path.contains(_scriptName)) { + final updatedSource = file.readAsStringSync().replaceAll( + 'package:cronet_http/cronet_http.dart', + 'package:cronet_http_embedded/cronet_http_embedded.dart', + ); + file.writeAsStringSync(updatedSource); + } + } +} + +void updateLibraryName() { + print('Renaming cronet_http.dart to cronet_http_embedded.dart'); + File( + '${_packageDirectory.path}/lib/cronet_http.dart', + ).renameSync( + '${_packageDirectory.path}/lib/cronet_http_embedded.dart', + ); +}
diff --git a/pkgs/cupertino_http/CHANGELOG.md b/pkgs/cupertino_http/CHANGELOG.md index 3a69c49..34a36c5 100644 --- a/pkgs/cupertino_http/CHANGELOG.md +++ b/pkgs/cupertino_http/CHANGELOG.md
@@ -1,3 +1,15 @@ +## 1.3.1-wip + +## 1.3.0 + +* Use `package:http_image_provider` in the example application. +* Support `BaseResponseWithUrl`. + +## 1.2.0 + +* Add support for setting additional http headers in + `URLSessionConfiguration`. + ## 1.1.0 * Add websocket support to `cupertino_api`.
diff --git a/pkgs/cupertino_http/README.md b/pkgs/cupertino_http/README.md index 83514dc..77c1d79 100644 --- a/pkgs/cupertino_http/README.md +++ b/pkgs/cupertino_http/README.md
@@ -24,49 +24,25 @@ still allowing platform-specific setup. ```dart -late Client client; -if (Platform.isIOS) { - final config = URLSessionConfiguration.ephemeralSessionConfiguration() - ..allowsCellularAccess = false - ..allowsConstrainedNetworkAccess = false - ..allowsExpensiveNetworkAccess = false; - client = CupertinoClient.fromSessionConfiguration(config); -} else { - client = IOClient(); // Uses an HTTP client based on dart:io -} +import 'package:cupertino_http/cupertino_http.dart'; +import 'package:http/http.dart'; +import 'package:http/io_client.dart'; -final response = await client.get(Uri.https( - 'www.googleapis.com', - '/books/v1/volumes', - {'q': 'HTTP', 'maxResults': '40', 'printType': 'books'})); -``` - -[package:http runWithClient][] can be used to configure the -[package:http Client][] for the entire application. - -```dart -void main() { - late Client client; - if (Platform.isIOS) { - client = CupertinoClient.defaultSessionConfiguration(); +void main() async { + final Client httpClient; + if (Platform.isIOS || Platform.isMacOS) { + final config = URLSessionConfiguration.ephemeralSessionConfiguration() + ..cache = URLCache.withCapacity(memoryCapacity: 2 * 1024 * 1024) + ..httpAdditionalHeaders = {'User-Agent': 'Book Agent'}; + httpClient = CupertinoClient.fromSessionConfiguration(config); } else { - client = IOClient(); + httpClient = IOClient(HttpClient()..userAgent = 'Book Agent'); } - runWithClient(() => runApp(const MyApp()), () => client); -} - -... - -class MainPageState extends State<MainPage> { - void someMethod() { - // Will use the Client configured in main. - final response = await get(Uri.https( - 'www.googleapis.com', - '/books/v1/volumes', - {'q': 'HTTP', 'maxResults': '40', 'printType': 'books'})); - } - ... + final response = await client.get(Uri.https( + 'www.googleapis.com', + '/books/v1/volumes', + {'q': 'HTTP', 'maxResults': '40', 'printType': 'books'})); } ``` @@ -88,6 +64,5 @@ ``` [package:http Client]: https://pub.dev/documentation/http/latest/http/Client-class.html -[package:http runWithClient]: https://pub.dev/documentation/http/latest/http/runWithClient.html [Foundation URL Loading System]: https://developer.apple.com/documentation/foundation/url_loading_system [dart:io HttpClient]: https://api.dart.dev/stable/dart-io/HttpClient-class.html
diff --git a/pkgs/cupertino_http/example/integration_test/client_conformance_test.dart b/pkgs/cupertino_http/example/integration_test/client_conformance_test.dart index a0823bf..3007123 100644 --- a/pkgs/cupertino_http/example/integration_test/client_conformance_test.dart +++ b/pkgs/cupertino_http/example/integration_test/client_conformance_test.dart
@@ -11,11 +11,19 @@ IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('defaultSessionConfiguration', () { - testAll(CupertinoClient.defaultSessionConfiguration); + testAll( + CupertinoClient.defaultSessionConfiguration, + canReceiveSetCookieHeaders: true, + canSendCookieHeaders: true, + ); }); group('fromSessionConfiguration', () { final config = URLSessionConfiguration.ephemeralSessionConfiguration(); - testAll(() => CupertinoClient.fromSessionConfiguration(config), - canWorkInIsolates: false); + testAll( + () => CupertinoClient.fromSessionConfiguration(config), + canWorkInIsolates: false, + canReceiveSetCookieHeaders: true, + canSendCookieHeaders: true, + ); }); }
diff --git a/pkgs/cupertino_http/example/integration_test/client_test.dart b/pkgs/cupertino_http/example/integration_test/client_test.dart index 9247783..1becd5c 100644 --- a/pkgs/cupertino_http/example/integration_test/client_test.dart +++ b/pkgs/cupertino_http/example/integration_test/client_test.dart
@@ -2,6 +2,7 @@ // 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. +import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; @@ -45,7 +46,7 @@ } final request = StreamedRequest('POST', uri); request.sink.add(data); - request.sink.close(); + unawaited(request.sink.close()); await client.send(request); expect(serverHash, sha1.convert(data).bytes); });
diff --git a/pkgs/cupertino_http/example/integration_test/url_session_configuration_test.dart b/pkgs/cupertino_http/example/integration_test/url_session_configuration_test.dart index 85386bc..ab117c3 100644 --- a/pkgs/cupertino_http/example/integration_test/url_session_configuration_test.dart +++ b/pkgs/cupertino_http/example/integration_test/url_session_configuration_test.dart
@@ -2,10 +2,38 @@ // 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. +import 'dart:io'; + import 'package:cupertino_http/cupertino_http.dart'; import 'package:integration_test/integration_test.dart'; import 'package:test/test.dart'; +/// Make a HTTP request using the given configuration and return the headers +/// received by the server. +Future<Map<String, List<String>>> sentHeaders( + URLSessionConfiguration config) async { + final session = URLSession.sessionWithConfiguration(config); + final headers = <String, List<String>>{}; + final server = (await HttpServer.bind('localhost', 0)) + ..listen((request) async { + request.headers.forEach((k, v) => headers[k] = v); + await request.drain<void>(); + request.response.headers.set('Content-Type', 'text/plain'); + request.response.write('Hello World'); + await request.response.close(); + }); + + final task = session.dataTaskWithRequest(URLRequest.fromUrl( + Uri(scheme: 'http', host: 'localhost', port: server.port))) + ..resume(); + while (task.state != URLSessionTaskState.urlSessionTaskStateCompleted) { + await pumpEventQueue(); + } + + await server.close(); + return headers; +} + void testProperties(URLSessionConfiguration config) { group('properties', () { test('allowsCellularAccess', () { @@ -32,6 +60,22 @@ config.discretionary = false; expect(config.discretionary, false); }); + test('httpAdditionalHeaders', () async { + expect(config.httpAdditionalHeaders, isNull); + + config.httpAdditionalHeaders = { + 'User-Agent': 'My Client', + 'MyHeader': 'myvalue' + }; + expect(config.httpAdditionalHeaders, + {'User-Agent': 'My Client', 'MyHeader': 'myvalue'}); + final headers = await sentHeaders(config); + expect(headers, containsPair('user-agent', ['My Client'])); + expect(headers, containsPair('myheader', ['myvalue'])); + + config.httpAdditionalHeaders = null; + expect(config.httpAdditionalHeaders, isNull); + }); test('httpCookieAcceptPolicy', () { config.httpCookieAcceptPolicy = HTTPCookieAcceptPolicy.httpCookieAcceptPolicyAlways;
diff --git a/pkgs/cupertino_http/example/lib/book.dart b/pkgs/cupertino_http/example/lib/book.dart index f2a27fc..b47ca9e 100644 --- a/pkgs/cupertino_http/example/lib/book.dart +++ b/pkgs/cupertino_http/example/lib/book.dart
@@ -5,7 +5,7 @@ class Book { String title; String description; - String imageUrl; + Uri imageUrl; Book(this.title, this.description, this.imageUrl); @@ -21,7 +21,7 @@ 'description': final String description, 'imageLinks': {'smallThumbnail': final String thumbnail} }) { - books.add(Book(title, description, thumbnail)); + books.add(Book(title, description, Uri.parse(thumbnail))); } } }
diff --git a/pkgs/cupertino_http/example/lib/main.dart b/pkgs/cupertino_http/example/lib/main.dart index edfceac..092300a 100644 --- a/pkgs/cupertino_http/example/lib/main.dart +++ b/pkgs/cupertino_http/example/lib/main.dart
@@ -5,19 +5,30 @@ import 'dart:convert'; import 'dart:io'; -import 'package:cached_network_image/cached_network_image.dart'; import 'package:cupertino_http/cupertino_http.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart'; +import 'package:http/io_client.dart'; +import 'package:http_image_provider/http_image_provider.dart'; +import 'package:provider/provider.dart'; import 'book.dart'; void main() { - var clientFactory = Client.new; // The default Client. + final Client httpClient; if (Platform.isIOS || Platform.isMacOS) { - clientFactory = CupertinoClient.defaultSessionConfiguration.call; + final config = URLSessionConfiguration.ephemeralSessionConfiguration() + ..cache = URLCache.withCapacity(memoryCapacity: 2 * 1024 * 1024) + ..httpAdditionalHeaders = {'User-Agent': 'Book Agent'}; + httpClient = CupertinoClient.fromSessionConfiguration(config); + } else { + httpClient = IOClient(HttpClient()..userAgent = 'Book Agent'); } - runWithClient(() => runApp(const BookSearchApp()), clientFactory); + + runApp(Provider<Client>( + create: (_) => httpClient, + child: const BookSearchApp(), + dispose: (_, client) => client.close())); } class BookSearchApp extends StatelessWidget { @@ -42,20 +53,22 @@ class _HomePageState extends State<HomePage> { List<Book>? _books; String? _lastQuery; + late Client _client; @override void initState() { super.initState(); + _client = context.read<Client>(); } // Get the list of books matching `query`. // The `get` call will automatically use the `client` configurated in `main`. Future<List<Book>> _findMatchingBooks(String query) async { - final response = await get( + final response = await _client.get( Uri.https( 'www.googleapis.com', '/books/v1/volumes', - {'q': query, 'maxResults': '40', 'printType': 'books'}, + {'q': query, 'maxResults': '20', 'printType': 'books'}, ), ); @@ -127,11 +140,10 @@ itemBuilder: (context, index) => Card( key: ValueKey(widget.books[index].title), child: ListTile( - leading: CachedNetworkImage( - placeholder: (context, url) => - const CircularProgressIndicator(), - imageUrl: - widget.books[index].imageUrl.replaceFirst('http', 'https')), + leading: Image( + image: HttpImage( + widget.books[index].imageUrl.replace(scheme: 'https'), + client: context.read<Client>())), title: Text(widget.books[index].title), subtitle: Text(widget.books[index].description), ),
diff --git a/pkgs/cupertino_http/example/pubspec.yaml b/pkgs/cupertino_http/example/pubspec.yaml index be72731..8d61e62 100644 --- a/pkgs/cupertino_http/example/pubspec.yaml +++ b/pkgs/cupertino_http/example/pubspec.yaml
@@ -6,22 +6,23 @@ version: 1.0.0+1 environment: - sdk: ^3.0.0 - flutter: '>=3.10.0' + sdk: ^3.2.0 + flutter: '>=3.16.0' dependencies: - cached_network_image: ^3.2.3 cupertino_http: path: ../ cupertino_icons: ^1.0.2 flutter: sdk: flutter - http: ^0.13.5 + http: ^1.0.0 + http_image_provider: ^0.0.2 + provider: ^6.1.1 dev_dependencies: convert: ^3.1.1 crypto: ^3.0.3 - dart_flutter_team_lints: ^1.0.0 + dart_flutter_team_lints: ^2.0.0 ffi: ^2.0.1 flutter_test: sdk: flutter
diff --git a/pkgs/cupertino_http/lib/src/cupertino_api.dart b/pkgs/cupertino_http/lib/src/cupertino_api.dart index 01c4853..5cebd7e 100644 --- a/pkgs/cupertino_http/lib/src/cupertino_api.dart +++ b/pkgs/cupertino_http/lib/src/cupertino_api.dart
@@ -344,6 +344,32 @@ bool get discretionary => _nsObject.discretionary; set discretionary(bool value) => _nsObject.discretionary = value; + /// Additional headers to send with each request. + /// + /// Note that the getter for this field returns a **copy** of the headers. + /// + /// See [NSURLSessionConfiguration.HTTPAdditionalHeaders](https://developer.apple.com/documentation/foundation/nsurlsessionconfiguration/1411532-httpadditionalheaders) + Map<String, String>? get httpAdditionalHeaders { + if (_nsObject.HTTPAdditionalHeaders case var additionalHeaders?) { + final headers = ncb.NSDictionary.castFrom(additionalHeaders); + return stringDictToMap(headers); + } + return null; + } + + set httpAdditionalHeaders(Map<String, String>? headers) { + if (headers == null) { + _nsObject.HTTPAdditionalHeaders = null; + return; + } + final d = ncb.NSMutableDictionary.alloc(linkedLibs).init(); + headers.forEach((key, value) { + d.setObject_forKey_( + value.toNSString(linkedLibs), key.toNSString(linkedLibs)); + }); + _nsObject.HTTPAdditionalHeaders = d; + } + /// What policy to use when deciding whether to accept cookies. /// /// See [NSURLSessionConfiguration.HTTPCookieAcceptPolicy](https://developer.apple.com/documentation/foundation/nsurlsessionconfiguration/1408933-httpcookieacceptpolicy). @@ -441,6 +467,7 @@ 'allowsConstrainedNetworkAccess=$allowsConstrainedNetworkAccess ' 'allowsExpensiveNetworkAccess=$allowsExpensiveNetworkAccess ' 'discretionary=$discretionary ' + 'httpAdditionalHeaders=$httpAdditionalHeaders ' 'httpCookieAcceptPolicy=$httpCookieAcceptPolicy ' 'httpShouldSetCookies=$httpShouldSetCookies ' 'httpMaximumConnectionsPerHost=$httpMaximumConnectionsPerHost '
diff --git a/pkgs/cupertino_http/lib/src/cupertino_client.dart b/pkgs/cupertino_http/lib/src/cupertino_client.dart index a4c7715..b93dd5c 100644 --- a/pkgs/cupertino_http/lib/src/cupertino_client.dart +++ b/pkgs/cupertino_http/lib/src/cupertino_client.dart
@@ -17,11 +17,27 @@ final _digitRegex = RegExp(r'^\d+$'); +/// This class can be removed when `package:http` v2 is released. +class _StreamedResponseWithUrl extends StreamedResponse + implements BaseResponseWithUrl { + @override + final Uri url; + + _StreamedResponseWithUrl(super.stream, super.statusCode, + {required this.url, + super.contentLength, + super.request, + super.headers, + super.isRedirect, + super.reasonPhrase}); +} + class _TaskTracker { final responseCompleter = Completer<URLResponse>(); final BaseRequest request; final responseController = StreamController<Uint8List>(); int numRedirects = 0; + Uri? lastUrl; // The last URL redirected to. _TaskTracker(this.request); @@ -180,6 +196,7 @@ ++taskTracker.numRedirects; if (taskTracker.request.followRedirects && taskTracker.numRedirects <= taskTracker.request.maxRedirects) { + taskTracker.lastUrl = request.url; return request; } return null; @@ -292,9 +309,10 @@ ); } - return StreamedResponse( + return _StreamedResponseWithUrl( taskTracker.responseController.stream, response.statusCode, + url: taskTracker.lastUrl ?? request.url, contentLength: response.expectedContentLength == -1 ? null : response.expectedContentLength,
diff --git a/pkgs/cupertino_http/pubspec.yaml b/pkgs/cupertino_http/pubspec.yaml index 9d639a8..2a819e1 100644 --- a/pkgs/cupertino_http/pubspec.yaml +++ b/pkgs/cupertino_http/pubspec.yaml
@@ -1,23 +1,23 @@ name: cupertino_http -description: > +version: 1.3.1-wip +description: >- A macOS/iOS Flutter plugin that provides access to the Foundation URL Loading System. -version: 1.1.0 repository: https://github.com/dart-lang/http/tree/master/pkgs/cupertino_http environment: - sdk: ^3.0.0 - flutter: '>=3.10.0' # If changed, update test matrix. + sdk: ^3.2.0 + flutter: '>=3.16.0' # If changed, update test matrix. dependencies: async: ^2.5.0 ffi: ^2.1.0 flutter: sdk: flutter - http: '>=0.13.4 <2.0.0' + http: ^1.2.0 dev_dependencies: - dart_flutter_team_lints: ^1.0.0 + dart_flutter_team_lints: ^2.0.0 ffigen: ^9.0.1 flutter:
diff --git a/pkgs/flutter_http_example/README.md b/pkgs/flutter_http_example/README.md index 2c6cdfd..1a9cf9d 100644 --- a/pkgs/flutter_http_example/README.md +++ b/pkgs/flutter_http_example/README.md
@@ -9,8 +9,7 @@ including: * configuration for multiple platforms. - * using `runWithClient` and `package:provider` to pass `Client`s through - an application. + * using `package:provider` to pass `Client`s through an application. * writing tests using `MockClient`. ## The important bits @@ -34,9 +33,8 @@ * import `http_client_factory.dart` or `http_client_factory_web.dart`, depending on whether we are targeting the web browser or not. -* share a `package:http` `Client` by using `runWithClient` and - `package:provider`. -* call `package:http` functions. +* share a `package:http` `Client` by using `package:provider`. +* call `package:http` `Client` methods. ### `widget_test.dart`
diff --git a/pkgs/flutter_http_example/lib/book.dart b/pkgs/flutter_http_example/lib/book.dart index f2a27fc..b47ca9e 100644 --- a/pkgs/flutter_http_example/lib/book.dart +++ b/pkgs/flutter_http_example/lib/book.dart
@@ -5,7 +5,7 @@ class Book { String title; String description; - String imageUrl; + Uri imageUrl; Book(this.title, this.description, this.imageUrl); @@ -21,7 +21,7 @@ 'description': final String description, 'imageLinks': {'smallThumbnail': final String thumbnail} }) { - books.add(Book(title, description, thumbnail)); + books.add(Book(title, description, Uri.parse(thumbnail))); } } }
diff --git a/pkgs/flutter_http_example/lib/http_client_factory.dart b/pkgs/flutter_http_example/lib/http_client_factory.dart index cb36597..6e6ddc0 100644 --- a/pkgs/flutter_http_example/lib/http_client_factory.dart +++ b/pkgs/flutter_http_example/lib/http_client_factory.dart
@@ -7,13 +7,23 @@ import 'package:cronet_http/cronet_http.dart'; import 'package:cupertino_http/cupertino_http.dart'; import 'package:http/http.dart'; +import 'package:http/io_client.dart'; + +const _maxCacheSize = 2 * 1024 * 1024; Client httpClient() { if (Platform.isAndroid) { - return CronetClient.defaultCronetEngine(); + final engine = CronetEngine.build( + cacheMode: CacheMode.memory, + cacheMaxSize: _maxCacheSize, + userAgent: 'Book Agent'); + return CronetClient.fromCronetEngine(engine); } if (Platform.isIOS || Platform.isMacOS) { - return CupertinoClient.defaultSessionConfiguration(); + final config = URLSessionConfiguration.ephemeralSessionConfiguration() + ..cache = URLCache.withCapacity(memoryCapacity: _maxCacheSize) + ..httpAdditionalHeaders = {'User-Agent': 'Book Agent'}; + return CupertinoClient.fromSessionConfiguration(config); } - return Client(); // Return the default client. + return IOClient(HttpClient()..userAgent = 'Book Agent'); }
diff --git a/pkgs/flutter_http_example/lib/main.dart b/pkgs/flutter_http_example/lib/main.dart index 70f003c..548c3de 100644 --- a/pkgs/flutter_http_example/lib/main.dart +++ b/pkgs/flutter_http_example/lib/main.dart
@@ -4,32 +4,20 @@ import 'dart:convert'; -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart'; +import 'package:http_image_provider/http_image_provider.dart'; import 'package:provider/provider.dart'; import 'book.dart'; import 'http_client_factory.dart' - if (dart.library.html) 'http_client_factory_web.dart' as http_factory; + if (dart.library.js_interop) 'http_client_factory_web.dart' as http_factory; void main() { - // `runWithClient` is used to control which `package:http` `Client` is used - // when the `Client` constructor is called. This method allows you to choose - // the `Client` even when the package that you are using does not offer - // explicit parameterization. - // - // However, `runWithClient` does not work with Flutter tests. See - // https://github.com/flutter/flutter/issues/96939. - // - // Use `package:provider` and `runWithClient` together so that tests and - // unparameterized `Client` usages both work. - runWithClient( - () => runApp(Provider<Client>( - create: (_) => http_factory.httpClient(), - child: const BookSearchApp(), - dispose: (_, client) => client.close())), - http_factory.httpClient); + runApp(Provider<Client>( + create: (_) => http_factory.httpClient(), + child: const BookSearchApp(), + dispose: (_, client) => client.close())); } class BookSearchApp extends StatelessWidget { @@ -141,11 +129,10 @@ itemBuilder: (context, index) => Card( key: ValueKey(widget.books[index].title), child: ListTile( - leading: CachedNetworkImage( - placeholder: (context, url) => - const CircularProgressIndicator(), - imageUrl: - widget.books[index].imageUrl.replaceFirst('http', 'https')), + leading: Image( + image: HttpImage( + widget.books[index].imageUrl.replace(scheme: 'https'), + client: context.read<Client>())), title: Text(widget.books[index].title), subtitle: Text(widget.books[index].description), ),
diff --git a/pkgs/flutter_http_example/pubspec.yaml b/pkgs/flutter_http_example/pubspec.yaml index 6f3d1ba..7d0f089 100644 --- a/pkgs/flutter_http_example/pubspec.yaml +++ b/pkgs/flutter_http_example/pubspec.yaml
@@ -1,27 +1,26 @@ name: flutter_http_example +version: 1.0.0 description: Demonstrates how to use package:http in a Flutter app. publish_to: 'none' -version: 1.0.0 - environment: sdk: ^3.0.0 flutter: '>=3.10.0' dependencies: - cached_network_image: ^3.2.3 - cronet_http: ^0.4.1 - cupertino_http: ^1.1.0 + cronet_http: ^1.0.0 + cupertino_http: ^1.2.0 cupertino_icons: ^1.0.2 fetch_client: ^1.0.2 flutter: sdk: flutter - http: ^0.13.5 + http: ^1.0.0 + http_image_provider: ^0.0.2 provider: ^6.0.5 dev_dependencies: - dart_flutter_team_lints: ^1.0.0 + dart_flutter_team_lints: ^2.0.0 flutter_test: sdk: flutter integration_test:
diff --git a/pkgs/flutter_http_example/test/widget_test.dart b/pkgs/flutter_http_example/test/widget_test.dart index 9e0af25..f890a1b 100644 --- a/pkgs/flutter_http_example/test/widget_test.dart +++ b/pkgs/flutter_http_example/test/widget_test.dart
@@ -2,6 +2,8 @@ // 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. +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:flutter_http_example/main.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -17,7 +19,7 @@ "title": "Flutter Cookbook", "description": "Write, test, and publish your web, desktop...", "imageLinks": { - "smallThumbnail": "http://books.google.com/books/content?id=gcnAEAAAQBAJ&printsec=frontcover&img=1&zoom=5&edge=curl&source=gbs_api" + "smallThumbnail": "http://thumbnailurl/" } } } @@ -25,6 +27,11 @@ } '''; +final _dummyPngImage = base64Decode( + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmM' + 'IQAAAABJRU5ErkJggg==', +); + void main() { Widget app(Client client) => Provider<Client>( create: (_) => client, @@ -42,11 +49,14 @@ testWidgets('test search with one result', (WidgetTester tester) async { final mockClient = MockClient((request) async { - if (request.url.path != '/books/v1/volumes' && - request.url.queryParameters['q'] != 'Flutter') { - return Response('', 404); + if (request.url.path == '/books/v1/volumes' && + request.url.queryParameters['q'] == 'Flutter') { + return Response(_singleBookResponse, 200); + } else if (request.url == Uri.https('thumbnailurl', '/')) { + return Response.bytes(_dummyPngImage, 200, + headers: const {'Content-Type': 'image/png'}); } - return Response(_singleBookResponse, 200); + return Response('', 404); }); await tester.pumpWidget(app(mockClient));
diff --git a/pkgs/http/CHANGELOG.md b/pkgs/http/CHANGELOG.md index 6d39b10..86bc678 100644 --- a/pkgs/http/CHANGELOG.md +++ b/pkgs/http/CHANGELOG.md
@@ -1,8 +1,26 @@ +## 1.2.1 + +* Require Dart `^3.3` +* Require `package:web` `^0.5.0`. + +## 1.2.0 + +* Add `MockClient.pngResponse`, which makes it easier to fake image responses. +* Added the ability to fetch the URL of the response through `BaseResponseWithUrl`. +* Add the ability to get headers as a `Map<String, List<String>` to + `BaseResponse`. + +## 1.1.2 + +* Allow `web: '>=0.3.0 <0.5.0'`. + ## 1.1.1 * `BrowserClient` throws `ClientException` when the `'Content-Length'` header is invalid. * `IOClient` trims trailing whitespace on header values. +* Require Dart 3.2 +* Browser: support Wasm by using `package:web`. ## 1.1.0
diff --git a/pkgs/http/README.md b/pkgs/http/README.md index c889f2b..85ec291 100644 --- a/pkgs/http/README.md +++ b/pkgs/http/README.md
@@ -4,8 +4,8 @@ A composable, Future-based library for making HTTP requests. This package contains a set of high-level functions and classes that make it -easy to consume HTTP resources. It's multi-platform, and supports mobile, desktop, -and the browser. +easy to consume HTTP resources. It's multi-platform (mobile, desktop, and +browser) and supports multiple implementations. ## Using @@ -23,6 +23,11 @@ print(await http.read(Uri.https('example.com', 'foobar.txt'))); ``` +> [!NOTE] +> Flutter applications may require +> [additional configuration](https://docs.flutter.dev/data-and-backend/networking#platform-notes) +> to make HTTP requests. + If you're making multiple requests to the same server, you can keep open a persistent connection by using a [Client][] rather than making one-off requests. If you do this, make sure to close the client when you're done: @@ -41,6 +46,12 @@ } ``` +> [!TIP] +> For detailed background information and practical usage examples, see: +> - [Dart Development: Fetch data from the internet](https://dart.dev/tutorials/server/fetch-data) +> - [Flutter Cookbook: Fetch data from the internet](https://docs.flutter.dev/cookbook/networking/fetch-data) +> - [The Flutter HTTP example application][flutterhttpexample] + You can also exert more fine-grained control over your requests and responses by creating [Request][] or [StreamedRequest][] objects yourself and passing them to [Client.send][]. @@ -100,3 +111,182 @@ the [`RetryClient()`][new RetryClient] constructor. [new RetryClient]: https://pub.dev/documentation/http/latest/retry/RetryClient/RetryClient.html + +## Choosing an implementation + +There are multiple implementations of the `package:http` [`Client`][client] interface. By default, `package:http` uses [`BrowserClient`][browserclient] on the web and [`IOClient`][ioclient] on all other platforms. You an choose a different [`Client`][client] implementation based on the needs of your application. + +You can change implementations without changing your application code, except +for a few lines of [configuration](#2-configure-the-http-client). + +Some well supported implementations are: + +| Implementation | Supported Platforms | SDK | Caching | HTTP3/QUIC | Platform Native | +| -------------- | ------------------- | ----| ------- | ---------- | --------------- | +| `package:http` — [`IOClient`][ioclient] | Android, iOS, Linux, macOS, Windows | Dart, Flutter | ❌ | ❌ | ❌ | +| `package:http` — [`BrowserClient`][browserclient] | Web | Dart, Flutter | ― | ✅︎ | ✅︎ | Dart, Flutter | +| [`package:cupertino_http`][cupertinohttp] — [`CupertinoClient`][cupertinoclient] | iOS, macOS | Flutter | ✅︎ | ✅︎ | ✅︎ | +| [`package:cronet_http`][cronethttp] — [`CronetClient`][cronetclient] | Android | Flutter | ✅︎ | ✅︎ | ― | +| [`package:fetch_client`][fetch] — [`FetchClient`][fetchclient] | Web | Dart, Flutter | ✅︎ | ✅︎ | ✅︎ | + +> [!TIP] +> If you are writing a Dart package or Flutter pluggin that uses +> `package:http`, you should not depend on a particular [`Client`][client] +> implementation. Let the application author decide what implementation is +> best for their project. You can make that easier by accepting an explicit +> [`Client`][client] argument. For example: +> +> ```dart +> Future<Album> fetchAlbum({Client? client}) async { +> client ??= Client(); +> ... +> } +> ``` + +## Configuration + +To use a HTTP client implementation other than the default, you must: +1. Add the HTTP client as a dependency. +2. Configure the HTTP client. +3. Connect the HTTP client to the code that uses it. + +### 1. Add the HTTP client as a dependency. + +To add a package compatible with the Dart SDK to your project, use `dart pub add`. + +For example: + +```terminal +# Replace "fetch_client" with the package that you want to use. +dart pub add fetch_client +``` + +To add a package that requires the Flutter SDK, use `flutter pub add`. + +For example: + +```terminal +# Replace "cupertino_http" with the package that you want to use. +flutter pub add cupertino_http +``` + +### 2. Configure the HTTP client. + +Different `package:http` [`Client`][client] implementations may require +different configuration options. + +Add a function that returns a correctly configured [`Client`][client]. You can +return a different [`Client`][client] on different platforms. + +For example: + +```dart +Client httpClient() { + if (Platform.isAndroid) { + final engine = CronetEngine.build( + cacheMode: CacheMode.memory, + cacheMaxSize: 1000000); + return CronetClient.fromCronetEngine(engine); + } + if (Platform.isIOS || Platform.isMacOS) { + final config = URLSessionConfiguration.ephemeralSessionConfiguration() + ..cache = URLCache.withCapacity(memoryCapacity: 1000000); + return CupertinoClient.fromSessionConfiguration(config); + } + return IOClient(); +} +``` + +> [!TIP] +> [The Flutter HTTP example application][flutterhttpexample] demonstrates +> configuration best practices. + +#### Supporting browser and native + +If your application can be run in the browser and natively, you must put your +browser and native configurations in seperate files and import the correct file +based on the platform. + +For example: + +```dart +// -- http_client_factory.dart +Client httpClient() { + if (Platform.isAndroid) { + return CronetClient.defaultCronetEngine(); + } + if (Platform.isIOS || Platform.isMacOS) { + return CupertinoClient.defaultSessionConfiguration(); + } + return IOClient(); +} +``` + +```dart +// -- http_client_factory_web.dart +Client httpClient() => FetchClient(); +``` + +```dart +// -- main.dart +import 'http_client_factory.dart' + if (dart.library.js_interop) 'http_client_factory_web.dart' + +// The correct `httpClient` will be available. +``` + +### 3. Connect the HTTP client to the code that uses it. + +The best way to pass [`Client`][client] to the places that use it is +explicitly through arguments. + +For example: + +```dart +void main() { + final client = httpClient(); + fetchAlbum(client, ...); +} +``` + +In Flutter, you can use a one of many +[state mangement approaches][flutterstatemanagement]. + +If you depend on code that uses top-level functions (e.g. `http.post`) or +calls the [`Client()`][clientconstructor] constructor, then you can use +[`runWithClient`](runwithclient) to ensure that the correct +`Client` is used. When an [Isolate][isolate] is spawned, it does not inherit +any variables from the calling Zone, so `runWithClient` needs to be used in +each Isolate that uses `package:http`. + +You can ensure that only the `Client` that you have explicitly configured is +used by defining `no_default_http_client=true` in the environment. This will +also allow the default `Client` implementation to be removed, resulting in +a reduced application size. + +```terminal +$ flutter build appbundle --dart-define=no_default_http_client=true ... +$ dart compile exe --define=no_default_http_client=true ... +``` + +> [!TIP] +> [The Flutter HTTP example application][flutterhttpexample] demonstrates +> how to make the configured [`Client`][client] available using +> [`package:provider`][provider] and [`package:http_image_provider`][http_image_provider]. + +[browserclient]: https://pub.dev/documentation/http/latest/browser_client/BrowserClient-class.html +[client]: https://pub.dev/documentation/http/latest/http/Client-class.html +[clientconstructor]: https://pub.dev/documentation/http/latest/http/Client/Client.html +[cupertinohttp]: https://pub.dev/packages/cupertino_http +[cupertinoclient]: https://pub.dev/documentation/cupertino_http/latest/cupertino_http/CupertinoClient-class.html +[cronethttp]: https://pub.dev/packages/cronet_http +[cronetclient]: https://pub.dev/documentation/cronet_http/latest/cronet_http/CronetClient-class.html +[fetch]: https://pub.dev/packages/fetch_client +[fetchclient]: https://pub.dev/documentation/fetch_client/latest/fetch_client/FetchClient-class.html +[flutterhttpexample]: https://github.com/dart-lang/http/tree/master/pkgs/flutter_http_example +[http_image_provider]: https://pub.dev/documentation/http_image_provider +[ioclient]: https://pub.dev/documentation/http/latest/io_client/IOClient-class.html +[isolate]: https://dart.dev/language/concurrency#how-isolates-work +[flutterstatemanagement]: https://docs.flutter.dev/data-and-backend/state-mgmt/options +[provider]: https://pub.dev/packages/provider +[runwithclient]: https://pub.dev/documentation/http/latest/http/runWithClient.html
diff --git a/pkgs/http/lib/http.dart b/pkgs/http/lib/http.dart index 6200424..da35b23 100644 --- a/pkgs/http/lib/http.dart +++ b/pkgs/http/lib/http.dart
@@ -16,7 +16,8 @@ export 'src/base_client.dart'; export 'src/base_request.dart'; -export 'src/base_response.dart'; +export 'src/base_response.dart' + show BaseResponse, BaseResponseWithUrl, HeadersWithSplitValues; export 'src/byte_stream.dart'; export 'src/client.dart' hide zoneClient; export 'src/exception.dart'; @@ -25,7 +26,7 @@ export 'src/request.dart'; export 'src/response.dart'; export 'src/streamed_request.dart'; -export 'src/streamed_response.dart'; +export 'src/streamed_response.dart' show StreamedResponse; /// Sends an HTTP HEAD request with the given headers to the given URL. ///
diff --git a/pkgs/http/lib/src/base_request.dart b/pkgs/http/lib/src/base_request.dart index 70a7869..4b165c7 100644 --- a/pkgs/http/lib/src/base_request.dart +++ b/pkgs/http/lib/src/base_request.dart
@@ -132,13 +132,25 @@ try { var response = await client.send(this); var stream = onDone(response.stream, client.close); - return StreamedResponse(ByteStream(stream), response.statusCode, - contentLength: response.contentLength, - request: response.request, - headers: response.headers, - isRedirect: response.isRedirect, - persistentConnection: response.persistentConnection, - reasonPhrase: response.reasonPhrase); + + if (response case BaseResponseWithUrl(:final url)) { + return StreamedResponseV2(ByteStream(stream), response.statusCode, + contentLength: response.contentLength, + request: response.request, + headers: response.headers, + isRedirect: response.isRedirect, + url: url, + persistentConnection: response.persistentConnection, + reasonPhrase: response.reasonPhrase); + } else { + return StreamedResponse(ByteStream(stream), response.statusCode, + contentLength: response.contentLength, + request: response.request, + headers: response.headers, + isRedirect: response.isRedirect, + persistentConnection: response.persistentConnection, + reasonPhrase: response.reasonPhrase); + } } catch (_) { client.close(); rethrow;
diff --git a/pkgs/http/lib/src/base_response.dart b/pkgs/http/lib/src/base_response.dart index ed95f6c..0527461 100644 --- a/pkgs/http/lib/src/base_response.dart +++ b/pkgs/http/lib/src/base_response.dart
@@ -4,6 +4,9 @@ import 'base_client.dart'; import 'base_request.dart'; +import 'client.dart'; +import 'response.dart'; +import 'streamed_response.dart'; /// The base class for HTTP responses. /// @@ -43,10 +46,12 @@ /// // values = ['Apple', 'Banana', 'Grape'] /// ``` /// + /// To retrieve the header values as a `List<String>`, use + /// [HeadersWithSplitValues.headersSplitValues]. + /// /// If a header value contains whitespace then that whitespace may be replaced /// by a single space. Leading and trailing whitespace in header values are /// always removed. - // TODO(nweiz): make this a HttpHeaders object. final Map<String, String> headers; final bool isRedirect; @@ -68,3 +73,99 @@ } } } + +/// A [BaseResponse] with a [url] field. +/// +/// [Client] methods that return a [BaseResponse] subclass, such as [Response] +/// or [StreamedResponse], **may** return a [BaseResponseWithUrl]. +/// +/// For example: +/// +/// ```dart +/// final client = Client(); +/// final response = client.get(Uri.https('example.com', '/')); +/// Uri? finalUri; +/// if (response case BaseResponseWithUrl(:final url)) { +/// finalUri = url; +/// } +/// // Do something with `finalUri`. +/// client.close(); +/// ``` +/// +/// [url] will be added to [BaseResponse] when `package:http` version 2 is +/// released and this mixin will be deprecated. +abstract interface class BaseResponseWithUrl implements BaseResponse { + /// The [Uri] of the response returned by the server. + /// + /// If no redirects were followed, [url] will be the same as the requested + /// [Uri]. + /// + /// If redirects were followed, [url] will be the [Uri] of the last redirect + /// that was followed. + abstract final Uri url; +} + +/// "token" as defined in RFC 2616, 2.2 +/// See https://datatracker.ietf.org/doc/html/rfc2616#section-2.2 +const _tokenChars = r"!#$%&'*+\-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`" + 'abcdefghijklmnopqrstuvwxyz|~'; + +/// Splits comma-seperated header values. +var _headerSplitter = RegExp(r'[ \t]*,[ \t]*'); + +/// Splits comma-seperated "Set-Cookie" header values. +/// +/// Set-Cookie strings can contain commas. In particular, the following +/// productions defined in RFC-6265, section 4.1.1: +/// - <sane-cookie-date> e.g. "Expires=Sun, 06 Nov 1994 08:49:37 GMT" +/// - <path-value> e.g. "Path=somepath," +/// - <extension-av> e.g. "AnyString,Really," +/// +/// Some values are ambiguous e.g. +/// "Set-Cookie: lang=en; Path=/foo/" +/// "Set-Cookie: SID=x23" +/// and: +/// "Set-Cookie: lang=en; Path=/foo/,SID=x23" +/// would both be result in `response.headers` => "lang=en; Path=/foo/,SID=x23" +/// +/// The idea behind this regex is that ",<valid token>=" is more likely to +/// start a new <cookie-pair> then be part of <path-value> or <extension-av>. +/// +/// See https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1 +var _setCookieSplitter = RegExp(r'[ \t]*,[ \t]*(?=[' + _tokenChars + r']+=)'); + +extension HeadersWithSplitValues on BaseResponse { + /// The HTTP headers returned by the server. + /// + /// The header names are converted to lowercase and stored with their + /// associated header values. + /// + /// Cookies can be parsed using the dart:io `Cookie` class: + /// + /// ```dart + /// import "dart:io"; + /// import "package:http/http.dart"; + /// + /// void main() async { + /// final response = await Client().get(Uri.https('example.com', '/')); + /// final cookies = [ + /// for (var value i + /// in response.headersSplitValues['set-cookie'] ?? <String>[]) + /// Cookie.fromSetCookieValue(value) + /// ]; + Map<String, List<String>> get headersSplitValues { + var headersWithFieldLists = <String, List<String>>{}; + headers.forEach((key, value) { + if (!value.contains(',')) { + headersWithFieldLists[key] = [value]; + } else { + if (key == 'set-cookie') { + headersWithFieldLists[key] = value.split(_setCookieSplitter); + } else { + headersWithFieldLists[key] = value.split(_headerSplitter); + } + } + }); + return headersWithFieldLists; + } +}
diff --git a/pkgs/http/lib/src/browser_client.dart b/pkgs/http/lib/src/browser_client.dart index 9345be0..07e2323 100644 --- a/pkgs/http/lib/src/browser_client.dart +++ b/pkgs/http/lib/src/browser_client.dart
@@ -3,8 +3,9 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:async'; -import 'dart:html'; -import 'dart:typed_data'; +import 'dart:js_interop'; + +import 'package:web/web.dart'; import 'base_client.dart'; import 'base_request.dart'; @@ -25,8 +26,8 @@ return BrowserClient(); } -/// A `dart:html`-based HTTP client that runs in the browser and is backed by -/// XMLHttpRequests. +/// A `package:web`-based HTTP client that runs in the browser and is backed by +/// [XMLHttpRequest]. /// /// This client inherits some of the limitations of XMLHttpRequest. It ignores /// the [BaseRequest.contentLength], [BaseRequest.persistentConnection], @@ -37,7 +38,7 @@ /// The currently active XHRs. /// /// These are aborted if the client is closed. - final _xhrs = <HttpRequest>{}; + final _xhrs = <XMLHttpRequest>{}; /// Whether to send credentials such as cookies or authorization headers for /// cross-site requests. @@ -55,30 +56,36 @@ 'HTTP request failed. Client is already closed.', request.url); } var bytes = await request.finalize().toBytes(); - var xhr = HttpRequest(); + var xhr = XMLHttpRequest(); _xhrs.add(xhr); xhr - ..open(request.method, '${request.url}', async: true) + ..open(request.method, '${request.url}', true) ..responseType = 'arraybuffer' ..withCredentials = withCredentials; - request.headers.forEach(xhr.setRequestHeader); + for (var header in request.headers.entries) { + xhr.setRequestHeader(header.key, header.value); + } var completer = Completer<StreamedResponse>(); unawaited(xhr.onLoad.first.then((_) { - if (xhr.responseHeaders['content-length'] case final contentLengthHeader? - when !_digitRegex.hasMatch(contentLengthHeader)) { + if (xhr.responseHeaders['content-length'] case final contentLengthHeader + when contentLengthHeader != null && + !_digitRegex.hasMatch(contentLengthHeader)) { completer.completeError(ClientException( 'Invalid content-length header [$contentLengthHeader].', request.url, )); return; } - var body = (xhr.response as ByteBuffer).asUint8List(); - completer.complete(StreamedResponse( - ByteStream.fromBytes(body), xhr.status!, + var body = (xhr.response as JSArrayBuffer).toDart.asUint8List(); + var responseUrl = xhr.responseURL; + var url = responseUrl.isNotEmpty ? Uri.parse(responseUrl) : request.url; + completer.complete(StreamedResponseV2( + ByteStream.fromBytes(body), xhr.status, contentLength: body.length, request: request, + url: url, headers: xhr.responseHeaders, reasonPhrase: xhr.statusText)); })); @@ -91,7 +98,7 @@ StackTrace.current); })); - xhr.send(bytes); + xhr.send(bytes.toJS); try { return await completer.future; @@ -112,3 +119,30 @@ _xhrs.clear(); } } + +extension on XMLHttpRequest { + Map<String, String> get responseHeaders { + // from Closure's goog.net.Xhrio.getResponseHeaders. + var headers = <String, String>{}; + var headersString = getAllResponseHeaders(); + var headersList = headersString.split('\r\n'); + for (var header in headersList) { + if (header.isEmpty) { + continue; + } + + var splitIdx = header.indexOf(': '); + if (splitIdx == -1) { + continue; + } + var key = header.substring(0, splitIdx).toLowerCase(); + var value = header.substring(splitIdx + 2); + if (headers.containsKey(key)) { + headers[key] = '${headers[key]}, $value'; + } else { + headers[key] = value; + } + } + return headers; + } +}
diff --git a/pkgs/http/lib/src/client.dart b/pkgs/http/lib/src/client.dart index 9bceb88..7429ca8 100644 --- a/pkgs/http/lib/src/client.dart +++ b/pkgs/http/lib/src/client.dart
@@ -12,7 +12,7 @@ import 'base_client.dart'; import 'base_request.dart'; import 'client_stub.dart' - if (dart.library.html) 'browser_client.dart' + if (dart.library.js_interop) 'browser_client.dart' if (dart.library.io) 'io_client.dart'; import 'exception.dart'; import 'response.dart'; @@ -37,7 +37,8 @@ /// Creates a new platform appropriate client. /// /// Creates an `IOClient` if `dart:io` is available and a `BrowserClient` if - /// `dart:html` is available, otherwise it will throw an unsupported error. + /// `dart:js_interop` is available, otherwise it will throw an unsupported + /// error. factory Client() => zoneClient ?? createClient(); /// Sends an HTTP HEAD request with the given headers to the given URL.
diff --git a/pkgs/http/lib/src/client_stub.dart b/pkgs/http/lib/src/client_stub.dart index 1a34d50..6384fd0 100644 --- a/pkgs/http/lib/src/client_stub.dart +++ b/pkgs/http/lib/src/client_stub.dart
@@ -6,4 +6,4 @@ /// Implemented in `browser_client.dart` and `io_client.dart`. BaseClient createClient() => throw UnsupportedError( - 'Cannot create a client without dart:html or dart:io.'); + 'Cannot create a client without dart:js_interop or dart:io.');
diff --git a/pkgs/http/lib/src/io_client.dart b/pkgs/http/lib/src/io_client.dart index 247cc8c..fe4834b 100644 --- a/pkgs/http/lib/src/io_client.dart +++ b/pkgs/http/lib/src/io_client.dart
@@ -6,6 +6,7 @@ import 'base_client.dart'; import 'base_request.dart'; +import 'base_response.dart'; import 'client.dart'; import 'exception.dart'; import 'io_streamed_response.dart'; @@ -46,6 +47,22 @@ String toString() => 'ClientException with $cause, uri=$uri'; } +class _IOStreamedResponseV2 extends IOStreamedResponse + implements BaseResponseWithUrl { + @override + final Uri url; + + _IOStreamedResponseV2(super.stream, super.statusCode, + {required this.url, + super.contentLength, + super.request, + super.headers, + super.isRedirect, + super.persistentConnection, + super.reasonPhrase, + super.inner}); +} + /// A `dart:io`-based HTTP [Client]. /// /// If there is a socket-level failure when communicating with the server @@ -72,6 +89,18 @@ /// The underlying `dart:io` HTTP client. HttpClient? _inner; + /// Create a new `dart:io`-based HTTP [Client]. + /// + /// If [inner] is provided then it can be used to provide configuration + /// options for the client. + /// + /// For example: + /// ```dart + /// final httpClient = HttpClient() + /// ..userAgent = 'Book Agent' + /// ..idleTimeout = const Duration(seconds: 5); + /// final client = IOClient(httpClient); + /// ``` IOClient([HttpClient? inner]) : _inner = inner ?? HttpClient(); /// Sends an HTTP request and asynchronously returns the response. @@ -104,7 +133,7 @@ headers[key] = values.map((value) => value.trimRight()).join(','); }); - return IOStreamedResponse( + return _IOStreamedResponseV2( response.handleError((Object error) { final httpException = error as HttpException; throw ClientException(httpException.message, httpException.uri); @@ -115,6 +144,9 @@ request: request, headers: headers, isRedirect: response.isRedirect, + url: response.redirects.isNotEmpty + ? response.redirects.last.location + : request.url, persistentConnection: response.persistentConnection, reasonPhrase: response.reasonPhrase, inner: response);
diff --git a/pkgs/http/lib/src/mock_client.dart b/pkgs/http/lib/src/mock_client.dart index bf2df40..52f108a 100644 --- a/pkgs/http/lib/src/mock_client.dart +++ b/pkgs/http/lib/src/mock_client.dart
@@ -2,6 +2,8 @@ // 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. +import 'dart:convert'; + import 'base_client.dart'; import 'base_request.dart'; import 'byte_stream.dart'; @@ -10,6 +12,11 @@ import 'streamed_request.dart'; import 'streamed_response.dart'; +final _pngImageData = base64Decode( + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDw' + 'AEhQGAhKmMIQAAAABJRU5ErkJggg==', +); + // TODO(nweiz): once Dart has some sort of Rack- or WSGI-like standard for // server APIs, MockClient should conform to it. @@ -69,6 +76,17 @@ var bodyStream = request.finalize(); return await _handler(request, bodyStream); } + + /// Return a response containing a PNG image. + static Response pngResponse({BaseRequest? request}) { + final headers = { + 'content-type': 'image/png', + 'content-length': '${_pngImageData.length}' + }; + + return Response.bytes(_pngImageData, 200, + request: request, headers: headers, reasonPhrase: 'OK'); + } } /// A handler function that receives [StreamedRequest]s and sends
diff --git a/pkgs/http/lib/src/streamed_response.dart b/pkgs/http/lib/src/streamed_response.dart index 8cc0c76..44389d7 100644 --- a/pkgs/http/lib/src/streamed_response.dart +++ b/pkgs/http/lib/src/streamed_response.dart
@@ -26,3 +26,20 @@ super.reasonPhrase}) : stream = toByteStream(stream); } + +/// This class is private to `package:http` and will be removed when +/// `package:http` v2 is released. +class StreamedResponseV2 extends StreamedResponse + implements BaseResponseWithUrl { + @override + final Uri url; + + StreamedResponseV2(super.stream, super.statusCode, + {required this.url, + super.contentLength, + super.request, + super.headers, + super.isRedirect, + super.persistentConnection, + super.reasonPhrase}); +}
diff --git a/pkgs/http/mono_pkg.yaml b/pkgs/http/mono_pkg.yaml index 0e2f9d8..06f79d9 100644 --- a/pkgs/http/mono_pkg.yaml +++ b/pkgs/http/mono_pkg.yaml
@@ -18,3 +18,5 @@ - command: dart run --define=no_default_http_client=true test/no_default_http_client_test.dart os: - linux + - test: --test-randomize-ordering-seed=random -p chrome -c dart2wasm + sdk: dev
diff --git a/pkgs/http/pubspec.yaml b/pkgs/http/pubspec.yaml index ec23e2d..1874d5c 100644 --- a/pkgs/http/pubspec.yaml +++ b/pkgs/http/pubspec.yaml
@@ -1,18 +1,19 @@ name: http -version: 1.1.1-wip +version: 1.2.1 description: A composable, multi-platform, Future-based API for HTTP requests. repository: https://github.com/dart-lang/http/tree/master/pkgs/http environment: - sdk: ^3.0.0 + sdk: ^3.3.0 dependencies: async: ^2.5.0 http_parser: ^4.0.0 meta: ^1.3.0 + web: ^0.5.0 dev_dependencies: - dart_flutter_team_lints: ^1.0.0 + dart_flutter_team_lints: ^2.0.0 fake_async: ^1.2.0 http_client_conformance_tests: path: ../http_client_conformance_tests/
diff --git a/pkgs/http/test/html/utils.dart b/pkgs/http/test/html/utils.dart index abe5808..11170e1 100644 --- a/pkgs/http/test/html/utils.dart +++ b/pkgs/http/test/html/utils.dart
@@ -2,7 +2,7 @@ // 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. -import 'dart:html'; +import 'package:web/web.dart'; export '../utils.dart';
diff --git a/pkgs/http/test/io/client_conformance_test.dart b/pkgs/http/test/io/client_conformance_test.dart index 5d8f7f5..65368e5 100644 --- a/pkgs/http/test/io/client_conformance_test.dart +++ b/pkgs/http/test/io/client_conformance_test.dart
@@ -10,5 +10,9 @@ import 'package:test/test.dart'; void main() { - testAll(IOClient.new); + testAll( + IOClient.new, preservesMethodCase: false, // https://dartbug.com/54187 + canReceiveSetCookieHeaders: true, + canSendCookieHeaders: true, + ); }
diff --git a/pkgs/http/test/io/request_test.dart b/pkgs/http/test/io/request_test.dart index ac6b44c..226781f 100644 --- a/pkgs/http/test/io/request_test.dart +++ b/pkgs/http/test/io/request_test.dart
@@ -46,15 +46,22 @@ final response = await request.send(); expect(response.statusCode, equals(302)); + expect( + response, + isA<http.BaseResponseWithUrl>() + .having((r) => r.url, 'url', serverUrl.resolve('/redirect'))); }); test('with redirects', () async { final request = http.Request('GET', serverUrl.resolve('/redirect')); final response = await request.send(); - expect(response.statusCode, equals(200)); final bytesString = await response.stream.bytesToString(); expect(bytesString, parse(containsPair('path', '/'))); + expect( + response, + isA<http.BaseResponseWithUrl>() + .having((r) => r.url, 'url', serverUrl.resolve('/'))); }); test('exceeding max redirects', () async {
diff --git a/pkgs/http/test/mock_client_test.dart b/pkgs/http/test/mock_client_test.dart index db561c5..625285c 100644 --- a/pkgs/http/test/mock_client_test.dart +++ b/pkgs/http/test/mock_client_test.dart
@@ -5,6 +5,7 @@ import 'dart:convert'; import 'package:http/http.dart' as http; +import 'package:http/src/request.dart'; import 'package:http/testing.dart'; import 'package:test/test.dart'; @@ -43,4 +44,25 @@ expect(await client.read(Uri.http('example.com', '/foo')), equals('you did it')); }); + + test('pngResponse with default options', () { + final response = MockClient.pngResponse(); + expect(response.statusCode, 200); + expect(response.bodyBytes.take(8), + [137, 80, 78, 71, 13, 10, 26, 10] // PNG header + ); + expect(response.request, null); + expect(response.headers, containsPair('content-type', 'image/png')); + }); + + test('pngResponse with request', () { + final request = Request('GET', Uri.https('example.com')); + final response = MockClient.pngResponse(request: request); + expect(response.statusCode, 200); + expect(response.bodyBytes.take(8), + [137, 80, 78, 71, 13, 10, 26, 10] // PNG header + ); + expect(response.request, request); + expect(response.headers, containsPair('content-type', 'image/png')); + }); }
diff --git a/pkgs/http/test/response_test.dart b/pkgs/http/test/response_test.dart index 38061c1..1bd9fd8 100644 --- a/pkgs/http/test/response_test.dart +++ b/pkgs/http/test/response_test.dart
@@ -70,4 +70,73 @@ expect(response.bodyBytes, equals([104, 101, 108, 108, 111])); }); }); + + group('.headersSplitValues', () { + test('no headers', () async { + var response = http.Response('Hello, world!', 200); + expect(response.headersSplitValues, const <String, List<String>>{}); + }); + + test('one header', () async { + var response = + http.Response('Hello, world!', 200, headers: {'fruit': 'apple'}); + expect(response.headersSplitValues, const { + 'fruit': ['apple'] + }); + }); + + test('two headers', () async { + var response = http.Response('Hello, world!', 200, + headers: {'fruit': 'apple,banana'}); + expect(response.headersSplitValues, const { + 'fruit': ['apple', 'banana'] + }); + }); + + test('two headers with lots of spaces', () async { + var response = http.Response('Hello, world!', 200, + headers: {'fruit': 'apple \t , \tbanana'}); + expect(response.headersSplitValues, const { + 'fruit': ['apple', 'banana'] + }); + }); + + test('one set-cookie', () async { + var response = http.Response('Hello, world!', 200, headers: { + 'set-cookie': 'id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT' + }); + expect(response.headersSplitValues, const { + 'set-cookie': ['id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT'] + }); + }); + + test('two set-cookie, with comma in expires', () async { + var response = http.Response('Hello, world!', 200, headers: { + // ignore: missing_whitespace_between_adjacent_strings + 'set-cookie': 'id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT,' + 'sessionId=e8bb43229de9; Domain=foo.example.com' + }); + expect(response.headersSplitValues, const { + 'set-cookie': [ + 'id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT', + 'sessionId=e8bb43229de9; Domain=foo.example.com' + ] + }); + }); + + test('two set-cookie, with lots of commas', () async { + var response = http.Response('Hello, world!', 200, headers: { + 'set-cookie': + // ignore: missing_whitespace_between_adjacent_strings + 'id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/,,HE,=L=LO,' + 'sessionId=e8bb43229de9; Domain=foo.example.com' + }); + expect(response.headersSplitValues, const { + 'set-cookie': [ + 'id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/,,HE,=L=LO', + 'sessionId=e8bb43229de9; Domain=foo.example.com' + ] + }); + }); + }); }
diff --git a/pkgs/http_client_conformance_tests/bin/generate_server_wrappers.dart b/pkgs/http_client_conformance_tests/bin/generate_server_wrappers.dart index 74f9d00..6e86737 100644 --- a/pkgs/http_client_conformance_tests/bin/generate_server_wrappers.dart +++ b/pkgs/http_client_conformance_tests/bin/generate_server_wrappers.dart
@@ -10,12 +10,17 @@ import 'package:dart_style/dart_style.dart'; -const vm = '''// Generated by generate_server_wrappers.dart. Do not edit. +const _export = '''export 'server_queue_helpers.dart' + show StreamQueueOfNullableObjectExtension;'''; + +const _vm = '''// Generated by generate_server_wrappers.dart. Do not edit. import 'package:stream_channel/stream_channel.dart'; import '<server_file_placeholder>'; +$_export + /// Starts the redirect test HTTP server in the same process. Future<StreamChannel<Object?>> startServer() async { final controller = StreamChannelController<Object?>(sync: true); @@ -24,11 +29,13 @@ } '''; -const web = '''// Generated by generate_server_wrappers.dart. Do not edit. +const _web = '''// Generated by generate_server_wrappers.dart. Do not edit. import 'package:stream_channel/stream_channel.dart'; import 'package:test/test.dart'; +$_export + /// Starts the redirect test HTTP server out-of-process. Future<StreamChannel<Object?>> startServer() async => spawnHybridUri(Uri( scheme: 'package', @@ -41,11 +48,11 @@ files.where((file) => file.path.endsWith('_server.dart')).forEach((file) { final vmPath = file.path.replaceAll('_server.dart', '_server_vm.dart'); - File(vmPath).writeAsStringSync(formatter.format(vm.replaceAll( + File(vmPath).writeAsStringSync(formatter.format(_vm.replaceAll( '<server_file_placeholder>', file.uri.pathSegments.last))); final webPath = file.path.replaceAll('_server.dart', '_server_web.dart'); - File(webPath).writeAsStringSync(formatter.format(web.replaceAll( + File(webPath).writeAsStringSync(formatter.format(_web.replaceAll( '<server_file_placeholder>', file.uri.pathSegments.last))); }); }
diff --git a/pkgs/http_client_conformance_tests/lib/http_client_conformance_tests.dart b/pkgs/http_client_conformance_tests/lib/http_client_conformance_tests.dart index 3d00319..07903b5 100644 --- a/pkgs/http_client_conformance_tests/lib/http_client_conformance_tests.dart +++ b/pkgs/http_client_conformance_tests/lib/http_client_conformance_tests.dart
@@ -11,9 +11,12 @@ import 'src/redirect_tests.dart'; import 'src/request_body_streamed_tests.dart'; import 'src/request_body_tests.dart'; +import 'src/request_cookies_test.dart'; import 'src/request_headers_tests.dart'; +import 'src/request_methods_tests.dart'; import 'src/response_body_streamed_test.dart'; import 'src/response_body_tests.dart'; +import 'src/response_cookies_test.dart'; import 'src/response_headers_tests.dart'; import 'src/response_status_line_tests.dart'; import 'src/server_errors_test.dart'; @@ -26,9 +29,12 @@ export 'src/redirect_tests.dart' show testRedirect; export 'src/request_body_streamed_tests.dart' show testRequestBodyStreamed; export 'src/request_body_tests.dart' show testRequestBody; +export 'src/request_cookies_test.dart' show testRequestCookies; export 'src/request_headers_tests.dart' show testRequestHeaders; +export 'src/request_methods_tests.dart' show testRequestMethods; export 'src/response_body_streamed_test.dart' show testResponseBodyStreamed; export 'src/response_body_tests.dart' show testResponseBody; +export 'src/response_cookies_test.dart' show testResponseCookies; export 'src/response_headers_tests.dart' show testResponseHeaders; export 'src/response_status_line_tests.dart' show testResponseStatusLine; export 'src/server_errors_test.dart' show testServerErrors; @@ -49,14 +55,28 @@ /// If [canWorkInIsolates] is `false` then tests that require that the [Client] /// work in Isolates other than the main isolate will be skipped. /// +/// If [preservesMethodCase] is `false` then tests that assume that the +/// [Client] preserves custom request method casing will be skipped. +/// +/// If [canSendCookieHeaders] is `false` then tests that require that "cookie" +/// headers be sent by the client will not be run. +/// +/// If [canReceiveSetCookieHeaders] is `false` then tests that require that +/// "set-cookie" headers be received by the client will not be run. +/// /// The tests are run against a series of HTTP servers that are started by the /// tests. If the tests are run in the browser, then the test servers are /// started in another process. Otherwise, the test servers are run in-process. -void testAll(Client Function() clientFactory, - {bool canStreamRequestBody = true, - bool canStreamResponseBody = true, - bool redirectAlwaysAllowed = false, - bool canWorkInIsolates = true}) { +void testAll( + Client Function() clientFactory, { + bool canStreamRequestBody = true, + bool canStreamResponseBody = true, + bool redirectAlwaysAllowed = false, + bool canWorkInIsolates = true, + bool preservesMethodCase = false, + bool canSendCookieHeaders = false, + bool canReceiveSetCookieHeaders = false, +}) { testRequestBody(clientFactory()); testRequestBodyStreamed(clientFactory(), canStreamRequestBody: canStreamRequestBody); @@ -65,6 +85,7 @@ testResponseBodyStreamed(clientFactory(), canStreamResponseBody: canStreamResponseBody); testRequestHeaders(clientFactory()); + testRequestMethods(clientFactory(), preservesMethodCase: preservesMethodCase); testResponseHeaders(clientFactory()); testResponseStatusLine(clientFactory()); testRedirect(clientFactory(), redirectAlwaysAllowed: redirectAlwaysAllowed); @@ -73,4 +94,8 @@ testMultipleClients(clientFactory); testClose(clientFactory); testIsolate(clientFactory, canWorkInIsolates: canWorkInIsolates); + testRequestCookies(clientFactory(), + canSendCookieHeaders: canSendCookieHeaders); + testResponseCookies(clientFactory(), + canReceiveSetCookieHeaders: canReceiveSetCookieHeaders); }
diff --git a/pkgs/http_client_conformance_tests/lib/src/close_tests.dart b/pkgs/http_client_conformance_tests/lib/src/close_tests.dart index 040b338..39324ad 100644 --- a/pkgs/http_client_conformance_tests/lib/src/close_tests.dart +++ b/pkgs/http_client_conformance_tests/lib/src/close_tests.dart
@@ -8,7 +8,7 @@ import 'package:test/test.dart'; import 'request_body_server_vm.dart' - if (dart.library.html) 'request_body_server_web.dart'; + if (dart.library.js_interop) 'request_body_server_web.dart'; /// Tests that the [Client] correctly implements [Client.close]. void testClose(Client Function() clientFactory) { @@ -20,7 +20,7 @@ setUpAll(() async { httpServerChannel = await startServer(); httpServerQueue = StreamQueue(httpServerChannel.stream); - host = 'localhost:${await httpServerQueue.next}'; + host = 'localhost:${await httpServerQueue.nextAsInt}'; }); tearDownAll(() => httpServerChannel.sink.add(null));
diff --git a/pkgs/http_client_conformance_tests/lib/src/compressed_response_body_server_vm.dart b/pkgs/http_client_conformance_tests/lib/src/compressed_response_body_server_vm.dart index 2bb2c16..a5ae1e0 100644 --- a/pkgs/http_client_conformance_tests/lib/src/compressed_response_body_server_vm.dart +++ b/pkgs/http_client_conformance_tests/lib/src/compressed_response_body_server_vm.dart
@@ -4,6 +4,8 @@ import 'compressed_response_body_server.dart'; +export 'server_queue_helpers.dart' show StreamQueueOfNullableObjectExtension; + /// Starts the redirect test HTTP server in the same process. Future<StreamChannel<Object?>> startServer() async { final controller = StreamChannelController<Object?>(sync: true);
diff --git a/pkgs/http_client_conformance_tests/lib/src/compressed_response_body_server_web.dart b/pkgs/http_client_conformance_tests/lib/src/compressed_response_body_server_web.dart index f880799..7b1d1a6 100644 --- a/pkgs/http_client_conformance_tests/lib/src/compressed_response_body_server_web.dart +++ b/pkgs/http_client_conformance_tests/lib/src/compressed_response_body_server_web.dart
@@ -3,6 +3,8 @@ import 'package:stream_channel/stream_channel.dart'; import 'package:test/test.dart'; +export 'server_queue_helpers.dart' show StreamQueueOfNullableObjectExtension; + /// Starts the redirect test HTTP server out-of-process. Future<StreamChannel<Object?>> startServer() async => spawnHybridUri(Uri( scheme: 'package',
diff --git a/pkgs/http_client_conformance_tests/lib/src/compressed_response_body_tests.dart b/pkgs/http_client_conformance_tests/lib/src/compressed_response_body_tests.dart index 3ce871b..538b3ba 100644 --- a/pkgs/http_client_conformance_tests/lib/src/compressed_response_body_tests.dart +++ b/pkgs/http_client_conformance_tests/lib/src/compressed_response_body_tests.dart
@@ -8,7 +8,7 @@ import 'package:test/test.dart'; import 'compressed_response_body_server_vm.dart' - if (dart.library.html) 'compressed_response_body_server_web.dart'; + if (dart.library.js_interop) 'compressed_response_body_server_web.dart'; /// Tests that the [Client] correctly implements HTTP responses with compressed /// bodies. @@ -32,7 +32,7 @@ setUpAll(() async { httpServerChannel = await startServer(); httpServerQueue = StreamQueue(httpServerChannel.stream); - host = 'localhost:${await httpServerQueue.next}'; + host = 'localhost:${await httpServerQueue.nextAsInt}'; }); tearDownAll(() => httpServerChannel.sink.add(null));
diff --git a/pkgs/http_client_conformance_tests/lib/src/isolate_test.dart b/pkgs/http_client_conformance_tests/lib/src/isolate_test.dart index b4ac8b2..1723ab5 100644 --- a/pkgs/http_client_conformance_tests/lib/src/isolate_test.dart +++ b/pkgs/http_client_conformance_tests/lib/src/isolate_test.dart
@@ -2,7 +2,7 @@ // 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. -import 'dart:isolate' if (dart.library.html) 'dummy_isolate.dart'; +import 'dart:isolate' if (dart.library.js_interop) 'dummy_isolate.dart'; import 'package:async/async.dart'; import 'package:http/http.dart'; @@ -10,7 +10,7 @@ import 'package:test/test.dart'; import 'request_body_server_vm.dart' - if (dart.library.html) 'request_body_server_web.dart'; + if (dart.library.js_interop) 'request_body_server_web.dart'; Future<void> _testPost(Client Function() clientFactory, String host) async { await Isolate.run( @@ -31,7 +31,7 @@ setUpAll(() async { httpServerChannel = await startServer(); httpServerQueue = StreamQueue(httpServerChannel.stream); - host = 'localhost:${await httpServerQueue.next}'; + host = 'localhost:${await httpServerQueue.nextAsInt}'; }); tearDownAll(() => httpServerChannel.sink.add(null));
diff --git a/pkgs/http_client_conformance_tests/lib/src/multiple_clients_server_vm.dart b/pkgs/http_client_conformance_tests/lib/src/multiple_clients_server_vm.dart index c689212..f00f4ba 100644 --- a/pkgs/http_client_conformance_tests/lib/src/multiple_clients_server_vm.dart +++ b/pkgs/http_client_conformance_tests/lib/src/multiple_clients_server_vm.dart
@@ -4,6 +4,8 @@ import 'multiple_clients_server.dart'; +export 'server_queue_helpers.dart' show StreamQueueOfNullableObjectExtension; + /// Starts the redirect test HTTP server in the same process. Future<StreamChannel<Object?>> startServer() async { final controller = StreamChannelController<Object?>(sync: true);
diff --git a/pkgs/http_client_conformance_tests/lib/src/multiple_clients_server_web.dart b/pkgs/http_client_conformance_tests/lib/src/multiple_clients_server_web.dart index 91cfc76..3f71aa7 100644 --- a/pkgs/http_client_conformance_tests/lib/src/multiple_clients_server_web.dart +++ b/pkgs/http_client_conformance_tests/lib/src/multiple_clients_server_web.dart
@@ -3,6 +3,8 @@ import 'package:stream_channel/stream_channel.dart'; import 'package:test/test.dart'; +export 'server_queue_helpers.dart' show StreamQueueOfNullableObjectExtension; + /// Starts the redirect test HTTP server out-of-process. Future<StreamChannel<Object?>> startServer() async => spawnHybridUri(Uri( scheme: 'package',
diff --git a/pkgs/http_client_conformance_tests/lib/src/multiple_clients_tests.dart b/pkgs/http_client_conformance_tests/lib/src/multiple_clients_tests.dart index be9c4d9..ad40d4a 100644 --- a/pkgs/http_client_conformance_tests/lib/src/multiple_clients_tests.dart +++ b/pkgs/http_client_conformance_tests/lib/src/multiple_clients_tests.dart
@@ -8,7 +8,7 @@ import 'package:test/test.dart'; import 'multiple_clients_server_vm.dart' - if (dart.library.html) 'multiple_clients_server_web.dart'; + if (dart.library.js_interop) 'multiple_clients_server_web.dart'; /// Tests that the [Client] works correctly if there are many used /// simultaneously. @@ -21,7 +21,7 @@ setUpAll(() async { httpServerChannel = await startServer(); httpServerQueue = StreamQueue(httpServerChannel.stream); - host = 'localhost:${await httpServerQueue.next}'; + host = 'localhost:${await httpServerQueue.nextAsInt}'; }); tearDownAll(() => httpServerChannel.sink.add(null));
diff --git a/pkgs/http_client_conformance_tests/lib/src/redirect_server_vm.dart b/pkgs/http_client_conformance_tests/lib/src/redirect_server_vm.dart index 7f9cf8c..4a9450a 100644 --- a/pkgs/http_client_conformance_tests/lib/src/redirect_server_vm.dart +++ b/pkgs/http_client_conformance_tests/lib/src/redirect_server_vm.dart
@@ -4,6 +4,8 @@ import 'redirect_server.dart'; +export 'server_queue_helpers.dart' show StreamQueueOfNullableObjectExtension; + /// Starts the redirect test HTTP server in the same process. Future<StreamChannel<Object?>> startServer() async { final controller = StreamChannelController<Object?>(sync: true);
diff --git a/pkgs/http_client_conformance_tests/lib/src/redirect_server_web.dart b/pkgs/http_client_conformance_tests/lib/src/redirect_server_web.dart index 0fbe8a3..a5fb0f2 100644 --- a/pkgs/http_client_conformance_tests/lib/src/redirect_server_web.dart +++ b/pkgs/http_client_conformance_tests/lib/src/redirect_server_web.dart
@@ -3,6 +3,8 @@ import 'package:stream_channel/stream_channel.dart'; import 'package:test/test.dart'; +export 'server_queue_helpers.dart' show StreamQueueOfNullableObjectExtension; + /// Starts the redirect test HTTP server out-of-process. Future<StreamChannel<Object?>> startServer() async => spawnHybridUri(Uri( scheme: 'package',
diff --git a/pkgs/http_client_conformance_tests/lib/src/redirect_tests.dart b/pkgs/http_client_conformance_tests/lib/src/redirect_tests.dart index e21a5c2..a33d077 100644 --- a/pkgs/http_client_conformance_tests/lib/src/redirect_tests.dart +++ b/pkgs/http_client_conformance_tests/lib/src/redirect_tests.dart
@@ -8,7 +8,7 @@ import 'package:test/test.dart'; import 'redirect_server_vm.dart' - if (dart.library.html) 'redirect_server_web.dart'; + if (dart.library.js_interop) 'redirect_server_web.dart'; /// Tests that the [Client] correctly implements HTTP redirect logic. /// @@ -23,16 +23,30 @@ setUpAll(() async { httpServerChannel = await startServer(); httpServerQueue = StreamQueue(httpServerChannel.stream); - host = 'localhost:${await httpServerQueue.next}'; + host = 'localhost:${await httpServerQueue.nextAsInt}'; }); tearDownAll(() => httpServerChannel.sink.add(null)); + test('no redirect', () async { + final request = Request('GET', Uri.http(host, '/')) + ..followRedirects = false; + final response = await client.send(request); + expect(response.statusCode, 200); + expect(response.isRedirect, false); + if (response case BaseResponseWithUrl(url: final url)) { + expect(url, Uri.http(host, '/')); + } + }); + test('disallow redirect', () async { final request = Request('GET', Uri.http(host, '/1')) ..followRedirects = false; final response = await client.send(request); expect(response.statusCode, 302); expect(response.isRedirect, true); + if (response case BaseResponseWithUrl(url: final url)) { + expect(url, Uri.http(host, '/1')); + } }, skip: redirectAlwaysAllowed ? 'redirects always allowed' : false); test('disallow redirect, 0 maxRedirects', () async { @@ -42,6 +56,9 @@ final response = await client.send(request); expect(response.statusCode, 302); expect(response.isRedirect, true); + if (response case BaseResponseWithUrl(url: final url)) { + expect(url, Uri.http(host, '/1')); + } }, skip: redirectAlwaysAllowed ? 'redirects always allowed' : false); test('allow redirect', () async { @@ -50,6 +67,9 @@ final response = await client.send(request); expect(response.statusCode, 200); expect(response.isRedirect, false); + if (response case BaseResponseWithUrl(url: final url)) { + expect(url, Uri.http(host, '/')); + } }); test('allow redirect, 0 maxRedirects', () async { @@ -69,6 +89,9 @@ final response = await client.send(request); expect(response.statusCode, 200); expect(response.isRedirect, false); + if (response case BaseResponseWithUrl(url: final url)) { + expect(url, Uri.http(host, '/')); + } }, skip: redirectAlwaysAllowed ? 'redirects always allowed' : false); test('too many redirects', () async {
diff --git a/pkgs/http_client_conformance_tests/lib/src/request_body_server_vm.dart b/pkgs/http_client_conformance_tests/lib/src/request_body_server_vm.dart index 2260766..d2e1e4a 100644 --- a/pkgs/http_client_conformance_tests/lib/src/request_body_server_vm.dart +++ b/pkgs/http_client_conformance_tests/lib/src/request_body_server_vm.dart
@@ -4,6 +4,8 @@ import 'request_body_server.dart'; +export 'server_queue_helpers.dart' show StreamQueueOfNullableObjectExtension; + /// Starts the redirect test HTTP server in the same process. Future<StreamChannel<Object?>> startServer() async { final controller = StreamChannelController<Object?>(sync: true);
diff --git a/pkgs/http_client_conformance_tests/lib/src/request_body_server_web.dart b/pkgs/http_client_conformance_tests/lib/src/request_body_server_web.dart index 250bd52..6b6ab00 100644 --- a/pkgs/http_client_conformance_tests/lib/src/request_body_server_web.dart +++ b/pkgs/http_client_conformance_tests/lib/src/request_body_server_web.dart
@@ -3,6 +3,8 @@ import 'package:stream_channel/stream_channel.dart'; import 'package:test/test.dart'; +export 'server_queue_helpers.dart' show StreamQueueOfNullableObjectExtension; + /// Starts the redirect test HTTP server out-of-process. Future<StreamChannel<Object?>> startServer() async => spawnHybridUri(Uri( scheme: 'package',
diff --git a/pkgs/http_client_conformance_tests/lib/src/request_body_streamed_server_vm.dart b/pkgs/http_client_conformance_tests/lib/src/request_body_streamed_server_vm.dart index 9f58119..c343d68 100644 --- a/pkgs/http_client_conformance_tests/lib/src/request_body_streamed_server_vm.dart +++ b/pkgs/http_client_conformance_tests/lib/src/request_body_streamed_server_vm.dart
@@ -4,6 +4,8 @@ import 'request_body_streamed_server.dart'; +export 'server_queue_helpers.dart' show StreamQueueOfNullableObjectExtension; + /// Starts the redirect test HTTP server in the same process. Future<StreamChannel<Object?>> startServer() async { final controller = StreamChannelController<Object?>(sync: true);
diff --git a/pkgs/http_client_conformance_tests/lib/src/request_body_streamed_server_web.dart b/pkgs/http_client_conformance_tests/lib/src/request_body_streamed_server_web.dart index 97e8fbc..41477ee 100644 --- a/pkgs/http_client_conformance_tests/lib/src/request_body_streamed_server_web.dart +++ b/pkgs/http_client_conformance_tests/lib/src/request_body_streamed_server_web.dart
@@ -3,6 +3,8 @@ import 'package:stream_channel/stream_channel.dart'; import 'package:test/test.dart'; +export 'server_queue_helpers.dart' show StreamQueueOfNullableObjectExtension; + /// Starts the redirect test HTTP server out-of-process. Future<StreamChannel<Object?>> startServer() async => spawnHybridUri(Uri( scheme: 'package',
diff --git a/pkgs/http_client_conformance_tests/lib/src/request_body_streamed_tests.dart b/pkgs/http_client_conformance_tests/lib/src/request_body_streamed_tests.dart index 560858d..0f43505 100644 --- a/pkgs/http_client_conformance_tests/lib/src/request_body_streamed_tests.dart +++ b/pkgs/http_client_conformance_tests/lib/src/request_body_streamed_tests.dart
@@ -11,7 +11,7 @@ import 'package:test/test.dart'; import 'request_body_streamed_server_vm.dart' - if (dart.library.html) 'request_body_streamed_server_web.dart'; + if (dart.library.js_interop) 'request_body_streamed_server_web.dart'; /// Tests that the [Client] correctly implements streamed request body /// uploading. @@ -29,7 +29,7 @@ setUp(() async { httpServerChannel = await startServer(); httpServerQueue = StreamQueue(httpServerChannel.stream); - host = 'localhost:${await httpServerQueue.next}'; + host = 'localhost:${await httpServerQueue.nextAsInt}'; }); tearDown(() => httpServerChannel.sink.add(null));
diff --git a/pkgs/http_client_conformance_tests/lib/src/request_body_tests.dart b/pkgs/http_client_conformance_tests/lib/src/request_body_tests.dart index e211da5..901f7f7 100644 --- a/pkgs/http_client_conformance_tests/lib/src/request_body_tests.dart +++ b/pkgs/http_client_conformance_tests/lib/src/request_body_tests.dart
@@ -10,7 +10,7 @@ import 'package:test/test.dart'; import 'request_body_server_vm.dart' - if (dart.library.html) 'request_body_server_web.dart'; + if (dart.library.js_interop) 'request_body_server_web.dart'; class _Plus2Decoder extends Converter<List<int>, String> { @override @@ -47,7 +47,7 @@ setUp(() async { httpServerChannel = await startServer(); httpServerQueue = StreamQueue(httpServerChannel.stream); - host = 'localhost:${await httpServerQueue.next}'; + host = 'localhost:${await httpServerQueue.nextAsInt}'; }); tearDown(() => httpServerChannel.sink.add(null)); @@ -260,6 +260,47 @@ expect(serverReceivedBody.codeUnits, <int>[]); }); + test('client.send() with persistentConnection', () async { + // Do five requests to verify that the connection persistance logic is + // correct. + for (var i = 0; i < 5; ++i) { + final request = Request('POST', Uri.http(host, '')) + ..headers['Content-Type'] = 'text/plain; charset=utf-8' + ..persistentConnection = true + ..body = 'Hello World $i'; + + final response = await client.send(request); + expect(response.statusCode, 200); + + final serverReceivedContentType = await httpServerQueue.next; + final serverReceivedBody = await httpServerQueue.next as String; + + expect(serverReceivedContentType, ['text/plain; charset=utf-8']); + expect(serverReceivedBody, 'Hello World $i'); + } + }); + + test('client.send() with persistentConnection and body >64K', () async { + // 64KiB is special for the HTTP network API: + // https://fetch.spec.whatwg.org/#http-network-or-cache-fetch + // See https://github.com/dart-lang/http/issues/977 + final body = ''.padLeft(64 * 1024, 'XYZ'); + + final request = Request('POST', Uri.http(host, '')) + ..headers['Content-Type'] = 'text/plain; charset=utf-8' + ..persistentConnection = true + ..body = body; + + final response = await client.send(request); + expect(response.statusCode, 200); + + final serverReceivedContentType = await httpServerQueue.next; + final serverReceivedBody = await httpServerQueue.next as String; + + expect(serverReceivedContentType, ['text/plain; charset=utf-8']); + expect(serverReceivedBody, body); + }); + test('client.send() GET with non-empty stream', () async { final request = StreamedRequest('GET', Uri.http(host, '')); request.headers['Content-Type'] = 'image/png';
diff --git a/pkgs/http_client_conformance_tests/lib/src/request_cookies_server.dart b/pkgs/http_client_conformance_tests/lib/src/request_cookies_server.dart new file mode 100644 index 0000000..44653a7 --- /dev/null +++ b/pkgs/http_client_conformance_tests/lib/src/request_cookies_server.dart
@@ -0,0 +1,55 @@ +// Copyright (c) 2024, 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. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:stream_channel/stream_channel.dart'; + +/// Starts an HTTP server that captures "cookie" headers. +/// +/// Channel protocol: +/// On Startup: +/// - send port +/// On Request Received: +/// - send a list of header lines starting with "cookie:" +/// When Receive Anything: +/// - exit +void hybridMain(StreamChannel<Object?> channel) async { + late ServerSocket server; + + server = (await ServerSocket.bind('localhost', 0)) + ..listen((Socket socket) async { + final request = utf8.decoder.bind(socket).transform(const LineSplitter()); + + final cookies = <String>[]; + request.listen((line) { + if (line.toLowerCase().startsWith('cookie:')) { + cookies.add(line); + } + + if (line.isEmpty) { + // A blank line indicates the end of the headers. + channel.sink.add(cookies); + } + }); + + socket.writeAll( + [ + 'HTTP/1.1 200 OK', + 'Access-Control-Allow-Origin: *', + 'Content-Length: 0', + '\r\n', // Add \r\n at the end of this header section. + ], + '\r\n', // Separate each field by \r\n. + ); + await socket.close(); + }); + + channel.sink.add(server.port); + await channel + .stream.first; // Any writes indicates that the server should exit. + unawaited(server.close()); +}
diff --git a/pkgs/http_client_conformance_tests/lib/src/request_cookies_server_vm.dart b/pkgs/http_client_conformance_tests/lib/src/request_cookies_server_vm.dart new file mode 100644 index 0000000..1f30e5f --- /dev/null +++ b/pkgs/http_client_conformance_tests/lib/src/request_cookies_server_vm.dart
@@ -0,0 +1,14 @@ +// Generated by generate_server_wrappers.dart. Do not edit. + +import 'package:stream_channel/stream_channel.dart'; + +import 'request_cookies_server.dart'; + +export 'server_queue_helpers.dart' show StreamQueueOfNullableObjectExtension; + +/// Starts the redirect test HTTP server in the same process. +Future<StreamChannel<Object?>> startServer() async { + final controller = StreamChannelController<Object?>(sync: true); + hybridMain(controller.foreign); + return controller.local; +}
diff --git a/pkgs/http_client_conformance_tests/lib/src/request_cookies_server_web.dart b/pkgs/http_client_conformance_tests/lib/src/request_cookies_server_web.dart new file mode 100644 index 0000000..31d961b --- /dev/null +++ b/pkgs/http_client_conformance_tests/lib/src/request_cookies_server_web.dart
@@ -0,0 +1,11 @@ +// Generated by generate_server_wrappers.dart. Do not edit. + +import 'package:stream_channel/stream_channel.dart'; +import 'package:test/test.dart'; + +export 'server_queue_helpers.dart' show StreamQueueOfNullableObjectExtension; + +/// Starts the redirect test HTTP server out-of-process. +Future<StreamChannel<Object?>> startServer() async => spawnHybridUri(Uri( + scheme: 'package', + path: 'http_client_conformance_tests/src/request_cookies_server.dart'));
diff --git a/pkgs/http_client_conformance_tests/lib/src/request_cookies_test.dart b/pkgs/http_client_conformance_tests/lib/src/request_cookies_test.dart new file mode 100644 index 0000000..a4eb78c --- /dev/null +++ b/pkgs/http_client_conformance_tests/lib/src/request_cookies_test.dart
@@ -0,0 +1,56 @@ +// Copyright (c) 2024, 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. + +import 'package:async/async.dart'; +import 'package:http/http.dart'; +import 'package:stream_channel/stream_channel.dart'; +import 'package:test/test.dart'; + +import 'request_cookies_server_vm.dart' + if (dart.library.js_interop) 'request_cookies_server_web.dart'; + +// The an HTTP header into [name, value]. +final headerSplitter = RegExp(':[ \t]+'); + +/// Tests that the [Client] correctly sends "cookie" headers in the request. +/// +/// If [canSendCookieHeaders] is `false` then tests that require that "cookie" +/// headers be sent by the client will not be run. +void testRequestCookies(Client client, + {bool canSendCookieHeaders = false}) async { + group('request cookies', () { + late final String host; + late final StreamChannel<Object?> httpServerChannel; + late final StreamQueue<Object?> httpServerQueue; + + setUpAll(() async { + httpServerChannel = await startServer(); + httpServerQueue = StreamQueue(httpServerChannel.stream); + host = 'localhost:${await httpServerQueue.nextAsInt}'; + }); + tearDownAll(() => httpServerChannel.sink.add(null)); + + test('one cookie', () async { + await client + .get(Uri.http(host, ''), headers: {'cookie': 'SID=298zf09hf012fh2'}); + + final cookies = (await httpServerQueue.next as List).cast<String>(); + expect(cookies, hasLength(1)); + final [header, value] = cookies[0].split(headerSplitter); + expect(header.toLowerCase(), 'cookie'); + expect(value, 'SID=298zf09hf012fh2'); + }, skip: canSendCookieHeaders ? false : 'cannot send cookie headers'); + + test('multiple cookies semicolon separated', () async { + await client.get(Uri.http(host, ''), + headers: {'cookie': 'SID=298zf09hf012fh2; lang=en-US'}); + + final cookies = (await httpServerQueue.next as List).cast<String>(); + expect(cookies, hasLength(1)); + final [header, value] = cookies[0].split(headerSplitter); + expect(header.toLowerCase(), 'cookie'); + expect(value, 'SID=298zf09hf012fh2; lang=en-US'); + }, skip: canSendCookieHeaders ? false : 'cannot send cookie headers'); + }); +}
diff --git a/pkgs/http_client_conformance_tests/lib/src/request_headers_server_vm.dart b/pkgs/http_client_conformance_tests/lib/src/request_headers_server_vm.dart index 44e6565..dc930dc 100644 --- a/pkgs/http_client_conformance_tests/lib/src/request_headers_server_vm.dart +++ b/pkgs/http_client_conformance_tests/lib/src/request_headers_server_vm.dart
@@ -4,6 +4,8 @@ import 'request_headers_server.dart'; +export 'server_queue_helpers.dart' show StreamQueueOfNullableObjectExtension; + /// Starts the redirect test HTTP server in the same process. Future<StreamChannel<Object?>> startServer() async { final controller = StreamChannelController<Object?>(sync: true);
diff --git a/pkgs/http_client_conformance_tests/lib/src/request_headers_server_web.dart b/pkgs/http_client_conformance_tests/lib/src/request_headers_server_web.dart index 62e8d9e..a15b69b 100644 --- a/pkgs/http_client_conformance_tests/lib/src/request_headers_server_web.dart +++ b/pkgs/http_client_conformance_tests/lib/src/request_headers_server_web.dart
@@ -3,6 +3,8 @@ import 'package:stream_channel/stream_channel.dart'; import 'package:test/test.dart'; +export 'server_queue_helpers.dart' show StreamQueueOfNullableObjectExtension; + /// Starts the redirect test HTTP server out-of-process. Future<StreamChannel<Object?>> startServer() async => spawnHybridUri(Uri( scheme: 'package',
diff --git a/pkgs/http_client_conformance_tests/lib/src/request_headers_tests.dart b/pkgs/http_client_conformance_tests/lib/src/request_headers_tests.dart index 8adf98c..24d94d8 100644 --- a/pkgs/http_client_conformance_tests/lib/src/request_headers_tests.dart +++ b/pkgs/http_client_conformance_tests/lib/src/request_headers_tests.dart
@@ -8,7 +8,7 @@ import 'package:test/test.dart'; import 'request_headers_server_vm.dart' - if (dart.library.html) 'request_headers_server_web.dart'; + if (dart.library.js_interop) 'request_headers_server_web.dart'; /// Tests that the [Client] correctly sends headers in the request. void testRequestHeaders(Client client) async { @@ -20,7 +20,7 @@ setUpAll(() async { httpServerChannel = await startServer(); httpServerQueue = StreamQueue(httpServerChannel.stream); - host = 'localhost:${await httpServerQueue.next}'; + host = 'localhost:${await httpServerQueue.nextAsInt}'; }); tearDownAll(() => httpServerChannel.sink.add(null));
diff --git a/pkgs/http_client_conformance_tests/lib/src/request_methods_server.dart b/pkgs/http_client_conformance_tests/lib/src/request_methods_server.dart new file mode 100644 index 0000000..bf05ec0 --- /dev/null +++ b/pkgs/http_client_conformance_tests/lib/src/request_methods_server.dart
@@ -0,0 +1,41 @@ +// Copyright (c) 2023, 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. + +import 'dart:async'; +import 'dart:io'; + +import 'package:stream_channel/stream_channel.dart'; + +/// Starts an HTTP server that captures the request headers. +/// +/// Channel protocol: +/// On Startup: +/// - send port +/// On Request Received: +/// - send the received request method (e.g. GET) as a String +/// When Receive Anything: +/// - exit +void hybridMain(StreamChannel<Object?> channel) async { + late HttpServer server; + + server = (await HttpServer.bind('localhost', 0)) + ..listen((request) async { + request.response.headers.set('Access-Control-Allow-Origin', '*'); + if (request.method == 'OPTIONS') { + // Handle a CORS preflight request: + // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#preflighted_requests + request.response.headers + ..set('Access-Control-Allow-Methods', '*') + ..set('Access-Control-Allow-Headers', '*'); + } else { + channel.sink.add(request.method); + } + unawaited(request.response.close()); + }); + + channel.sink.add(server.port); + await channel + .stream.first; // Any writes indicates that the server should exit. + unawaited(server.close()); +}
diff --git a/pkgs/http_client_conformance_tests/lib/src/request_methods_server_vm.dart b/pkgs/http_client_conformance_tests/lib/src/request_methods_server_vm.dart new file mode 100644 index 0000000..fa25735 --- /dev/null +++ b/pkgs/http_client_conformance_tests/lib/src/request_methods_server_vm.dart
@@ -0,0 +1,14 @@ +// Generated by generate_server_wrappers.dart. Do not edit. + +import 'package:stream_channel/stream_channel.dart'; + +import 'request_methods_server.dart'; + +export 'server_queue_helpers.dart' show StreamQueueOfNullableObjectExtension; + +/// Starts the redirect test HTTP server in the same process. +Future<StreamChannel<Object?>> startServer() async { + final controller = StreamChannelController<Object?>(sync: true); + hybridMain(controller.foreign); + return controller.local; +}
diff --git a/pkgs/http_client_conformance_tests/lib/src/request_methods_server_web.dart b/pkgs/http_client_conformance_tests/lib/src/request_methods_server_web.dart new file mode 100644 index 0000000..f9c924e --- /dev/null +++ b/pkgs/http_client_conformance_tests/lib/src/request_methods_server_web.dart
@@ -0,0 +1,11 @@ +// Generated by generate_server_wrappers.dart. Do not edit. + +import 'package:stream_channel/stream_channel.dart'; +import 'package:test/test.dart'; + +export 'server_queue_helpers.dart' show StreamQueueOfNullableObjectExtension; + +/// Starts the redirect test HTTP server out-of-process. +Future<StreamChannel<Object?>> startServer() async => spawnHybridUri(Uri( + scheme: 'package', + path: 'http_client_conformance_tests/src/request_methods_server.dart'));
diff --git a/pkgs/http_client_conformance_tests/lib/src/request_methods_tests.dart b/pkgs/http_client_conformance_tests/lib/src/request_methods_tests.dart new file mode 100644 index 0000000..802f57e --- /dev/null +++ b/pkgs/http_client_conformance_tests/lib/src/request_methods_tests.dart
@@ -0,0 +1,88 @@ +// Copyright (c) 2023, 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. + +import 'package:async/async.dart'; +import 'package:http/http.dart'; +import 'package:stream_channel/stream_channel.dart'; +import 'package:test/test.dart'; + +import 'request_methods_server_vm.dart' + if (dart.library.js_interop) 'request_methods_server_web.dart'; + +/// Tests that the [Client] correctly sends HTTP request methods +/// (e.g. GET, HEAD). +/// +/// If [preservesMethodCase] is `false` then tests that assume that the +/// [Client] preserves custom request method casing will be skipped. +void testRequestMethods(Client client, + {bool preservesMethodCase = true}) async { + group('request methods', () { + late final String host; + late final StreamChannel<Object?> httpServerChannel; + late final StreamQueue<Object?> httpServerQueue; + + setUpAll(() async { + httpServerChannel = await startServer(); + httpServerQueue = StreamQueue(httpServerChannel.stream); + host = 'localhost:${await httpServerQueue.nextAsInt}'; + }); + tearDownAll(() => httpServerChannel.sink.add(null)); + + test('custom method - not case preserving', () async { + await client.send(Request( + 'CuStOm', + Uri.http(host, ''), + )); + final method = await httpServerQueue.next as String; + expect('CUSTOM', method.toUpperCase()); + }); + + test('custom method case preserving', () async { + await client.send(Request( + 'CuStOm', + Uri.http(host, ''), + )); + final method = await httpServerQueue.next as String; + expect('CuStOm', method); + }, + skip: preservesMethodCase + ? false + : 'does not preserve HTTP request method case'); + + test('delete', () async { + await client.delete(Uri.http(host, '')); + final method = await httpServerQueue.next as String; + expect('DELETE', method); + }); + + test('get', () async { + await client.get(Uri.http(host, '')); + final method = await httpServerQueue.next as String; + expect('GET', method); + }); + test('head', () async { + await client.head(Uri.http(host, '')); + final method = await httpServerQueue.next as String; + expect('HEAD', method); + }); + + test('patch', () async { + await client.patch(Uri.http(host, '')); + final method = await httpServerQueue.next as String; + expect('PATCH', method); + }); + + test('post', () async { + await client.post(Uri.http(host, '')); + final method = await httpServerQueue.next as String; + expect('POST', method); + }); + + test('put', () async { + await client.put(Uri.http(host, '')); + final method = await httpServerQueue.next as String; + expect('PUT', method); + }); + }); +}
diff --git a/pkgs/http_client_conformance_tests/lib/src/response_body_server_vm.dart b/pkgs/http_client_conformance_tests/lib/src/response_body_server_vm.dart index f88e065..a12b6fb 100644 --- a/pkgs/http_client_conformance_tests/lib/src/response_body_server_vm.dart +++ b/pkgs/http_client_conformance_tests/lib/src/response_body_server_vm.dart
@@ -4,6 +4,8 @@ import 'response_body_server.dart'; +export 'server_queue_helpers.dart' show StreamQueueOfNullableObjectExtension; + /// Starts the redirect test HTTP server in the same process. Future<StreamChannel<Object?>> startServer() async { final controller = StreamChannelController<Object?>(sync: true);
diff --git a/pkgs/http_client_conformance_tests/lib/src/response_body_server_web.dart b/pkgs/http_client_conformance_tests/lib/src/response_body_server_web.dart index 94bdaa9..4d23a48 100644 --- a/pkgs/http_client_conformance_tests/lib/src/response_body_server_web.dart +++ b/pkgs/http_client_conformance_tests/lib/src/response_body_server_web.dart
@@ -3,6 +3,8 @@ import 'package:stream_channel/stream_channel.dart'; import 'package:test/test.dart'; +export 'server_queue_helpers.dart' show StreamQueueOfNullableObjectExtension; + /// Starts the redirect test HTTP server out-of-process. Future<StreamChannel<Object?>> startServer() async => spawnHybridUri(Uri( scheme: 'package',
diff --git a/pkgs/http_client_conformance_tests/lib/src/response_body_streamed_server_vm.dart b/pkgs/http_client_conformance_tests/lib/src/response_body_streamed_server_vm.dart index 01d84a1..4e4eaff 100644 --- a/pkgs/http_client_conformance_tests/lib/src/response_body_streamed_server_vm.dart +++ b/pkgs/http_client_conformance_tests/lib/src/response_body_streamed_server_vm.dart
@@ -4,6 +4,8 @@ import 'response_body_streamed_server.dart'; +export 'server_queue_helpers.dart' show StreamQueueOfNullableObjectExtension; + /// Starts the redirect test HTTP server in the same process. Future<StreamChannel<Object?>> startServer() async { final controller = StreamChannelController<Object?>(sync: true);
diff --git a/pkgs/http_client_conformance_tests/lib/src/response_body_streamed_server_web.dart b/pkgs/http_client_conformance_tests/lib/src/response_body_streamed_server_web.dart index a9ce00b..e04ebd6 100644 --- a/pkgs/http_client_conformance_tests/lib/src/response_body_streamed_server_web.dart +++ b/pkgs/http_client_conformance_tests/lib/src/response_body_streamed_server_web.dart
@@ -3,6 +3,8 @@ import 'package:stream_channel/stream_channel.dart'; import 'package:test/test.dart'; +export 'server_queue_helpers.dart' show StreamQueueOfNullableObjectExtension; + /// Starts the redirect test HTTP server out-of-process. Future<StreamChannel<Object?>> startServer() async => spawnHybridUri(Uri( scheme: 'package',
diff --git a/pkgs/http_client_conformance_tests/lib/src/response_body_streamed_test.dart b/pkgs/http_client_conformance_tests/lib/src/response_body_streamed_test.dart index 28686fa..f355d6c 100644 --- a/pkgs/http_client_conformance_tests/lib/src/response_body_streamed_test.dart +++ b/pkgs/http_client_conformance_tests/lib/src/response_body_streamed_test.dart
@@ -10,7 +10,7 @@ import 'package:test/test.dart'; import 'response_body_streamed_server_vm.dart' - if (dart.library.html) 'response_body_streamed_server_web.dart'; + if (dart.library.js_interop) 'response_body_streamed_server_web.dart'; /// Tests that the [Client] correctly implements HTTP responses with bodies of /// unbounded size. @@ -28,7 +28,7 @@ setUpAll(() async { httpServerChannel = await startServer(); httpServerQueue = StreamQueue(httpServerChannel.stream); - host = 'localhost:${await httpServerQueue.next}'; + host = 'localhost:${await httpServerQueue.nextAsInt}'; }); tearDownAll(() => httpServerChannel.sink.add(null));
diff --git a/pkgs/http_client_conformance_tests/lib/src/response_body_tests.dart b/pkgs/http_client_conformance_tests/lib/src/response_body_tests.dart index 91ee549..34c29f6 100644 --- a/pkgs/http_client_conformance_tests/lib/src/response_body_tests.dart +++ b/pkgs/http_client_conformance_tests/lib/src/response_body_tests.dart
@@ -8,7 +8,7 @@ import 'package:test/test.dart'; import 'response_body_server_vm.dart' - if (dart.library.html) 'response_body_server_web.dart'; + if (dart.library.js_interop) 'response_body_server_web.dart'; /// Tests that the [Client] correctly implements HTTP responses with bodies. /// @@ -26,7 +26,7 @@ setUpAll(() async { httpServerChannel = await startServer(); httpServerQueue = StreamQueue(httpServerChannel.stream); - host = 'localhost:${await httpServerQueue.next}'; + host = 'localhost:${await httpServerQueue.nextAsInt}'; }); tearDownAll(() => httpServerChannel.sink.add(null));
diff --git a/pkgs/http_client_conformance_tests/lib/src/response_cookies_server.dart b/pkgs/http_client_conformance_tests/lib/src/response_cookies_server.dart new file mode 100644 index 0000000..392e228 --- /dev/null +++ b/pkgs/http_client_conformance_tests/lib/src/response_cookies_server.dart
@@ -0,0 +1,44 @@ +// Copyright (c) 2024, 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. + +import 'dart:async'; +import 'dart:io'; + +import 'package:async/async.dart'; +import 'package:stream_channel/stream_channel.dart'; + +/// Starts an HTTP server that returns a custom status line. +/// +/// Channel protocol: +/// On Startup: +/// - send port +/// On Request Received: +/// - load response status line from channel +/// - exit +void hybridMain(StreamChannel<Object?> channel) async { + late HttpServer server; + final clientQueue = StreamQueue(channel.stream); + + server = (await HttpServer.bind('localhost', 0)) + ..listen((request) async { + await request.drain<void>(); + final socket = await request.response.detachSocket(writeHeaders: false); + + final headers = (await clientQueue.next) as List; + socket.writeAll( + [ + 'HTTP/1.1 200 OK', + 'Access-Control-Allow-Origin: *', + 'Content-Length: 0', + ...headers, + '\r\n', // Add \r\n at the end of this header section. + ], + '\r\n', // Separate each field by \r\n. + ); + await socket.close(); + unawaited(server.close()); + }); + + channel.sink.add(server.port); +}
diff --git a/pkgs/http_client_conformance_tests/lib/src/response_cookies_server_vm.dart b/pkgs/http_client_conformance_tests/lib/src/response_cookies_server_vm.dart new file mode 100644 index 0000000..2edbb45 --- /dev/null +++ b/pkgs/http_client_conformance_tests/lib/src/response_cookies_server_vm.dart
@@ -0,0 +1,14 @@ +// Generated by generate_server_wrappers.dart. Do not edit. + +import 'package:stream_channel/stream_channel.dart'; + +import 'response_cookies_server.dart'; + +export 'server_queue_helpers.dart' show StreamQueueOfNullableObjectExtension; + +/// Starts the redirect test HTTP server in the same process. +Future<StreamChannel<Object?>> startServer() async { + final controller = StreamChannelController<Object?>(sync: true); + hybridMain(controller.foreign); + return controller.local; +}
diff --git a/pkgs/http_client_conformance_tests/lib/src/response_cookies_server_web.dart b/pkgs/http_client_conformance_tests/lib/src/response_cookies_server_web.dart new file mode 100644 index 0000000..cb8e384 --- /dev/null +++ b/pkgs/http_client_conformance_tests/lib/src/response_cookies_server_web.dart
@@ -0,0 +1,11 @@ +// Generated by generate_server_wrappers.dart. Do not edit. + +import 'package:stream_channel/stream_channel.dart'; +import 'package:test/test.dart'; + +export 'server_queue_helpers.dart' show StreamQueueOfNullableObjectExtension; + +/// Starts the redirect test HTTP server out-of-process. +Future<StreamChannel<Object?>> startServer() async => spawnHybridUri(Uri( + scheme: 'package', + path: 'http_client_conformance_tests/src/response_cookies_server.dart'));
diff --git a/pkgs/http_client_conformance_tests/lib/src/response_cookies_test.dart b/pkgs/http_client_conformance_tests/lib/src/response_cookies_test.dart new file mode 100644 index 0000000..f8e154d --- /dev/null +++ b/pkgs/http_client_conformance_tests/lib/src/response_cookies_test.dart
@@ -0,0 +1,92 @@ +// Copyright (c) 2024, 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. + +import 'package:async/async.dart'; +import 'package:http/http.dart'; +import 'package:stream_channel/stream_channel.dart'; +import 'package:test/test.dart'; + +import 'response_cookies_server_vm.dart' + if (dart.library.js_interop) 'response_cookies_server_web.dart'; + +/// Tests that the [Client] correctly receives "set-cookie-headers" +/// +/// If [canReceiveSetCookieHeaders] is `false` then tests that require that +/// "set-cookie" headers be received by the client will not be run. +void testResponseCookies(Client client, + {required bool canReceiveSetCookieHeaders}) async { + group('response cookies', () { + late String host; + late StreamChannel<Object?> httpServerChannel; + late StreamQueue<Object?> httpServerQueue; + + setUp(() async { + httpServerChannel = await startServer(); + httpServerQueue = StreamQueue(httpServerChannel.stream); + host = 'localhost:${await httpServerQueue.nextAsInt}'; + }); + + test('single session cookie', () async { + httpServerChannel.sink.add(['Set-Cookie: SID=1231AB3']); + final response = await client.get(Uri.http(host, '')); + + expect(response.headers['set-cookie'], 'SID=1231AB3'); + }, + skip: canReceiveSetCookieHeaders + ? false + : 'cannot receive set-cookie headers'); + + test('multiple session cookies', () async { + // RFC-2616 4.2 says: + // "The field value MAY be preceded by any amount of LWS, though a single + // SP is preferred." and + // "The field-content does not include any leading or trailing LWS ..." + httpServerChannel.sink + .add(['Set-Cookie: SID=1231AB3', 'Set-Cookie: lang=en_US']); + final response = await client.get(Uri.http(host, '')); + + expect( + response.headers['set-cookie'], + matches(r'SID=1231AB3' + r'[ \t]*,[ \t]*' + r'lang=en_US')); + }, + skip: canReceiveSetCookieHeaders + ? false + : 'cannot receive set-cookie headers'); + + test('permanent cookie with expires', () async { + httpServerChannel.sink + .add(['Set-Cookie: id=a3fWa; Expires=Wed, 10 Jan 2024 07:28:00 GMT']); + final response = await client.get(Uri.http(host, '')); + + expect(response.headers['set-cookie'], + 'id=a3fWa; Expires=Wed, 10 Jan 2024 07:28:00 GMT'); + }, + skip: canReceiveSetCookieHeaders + ? false + : 'cannot receive set-cookie headers'); + + test('multiple permanent cookies with expires', () async { + // RFC-2616 4.2 says: + // "The field value MAY be preceded by any amount of LWS, though a single + // SP is preferred." and + // "The field-content does not include any leading or trailing LWS ..." + httpServerChannel.sink.add([ + 'Set-Cookie: id=a3fWa; Expires=Wed, 10 Jan 2024 07:28:00 GMT', + 'Set-Cookie: id=2fasd; Expires=Wed, 21 Oct 2025 07:28:00 GMT' + ]); + final response = await client.get(Uri.http(host, '')); + + expect( + response.headers['set-cookie'], + matches(r'id=a3fWa; Expires=Wed, 10 Jan 2024 07:28:00 GMT' + r'[ \t]*,[ \t]*' + r'id=2fasd; Expires=Wed, 21 Oct 2025 07:28:00 GMT')); + }, + skip: canReceiveSetCookieHeaders + ? false + : 'cannot receive set-cookie headers'); + }); +}
diff --git a/pkgs/http_client_conformance_tests/lib/src/response_headers_server_vm.dart b/pkgs/http_client_conformance_tests/lib/src/response_headers_server_vm.dart index b7d4a01..c99a021 100644 --- a/pkgs/http_client_conformance_tests/lib/src/response_headers_server_vm.dart +++ b/pkgs/http_client_conformance_tests/lib/src/response_headers_server_vm.dart
@@ -4,6 +4,8 @@ import 'response_headers_server.dart'; +export 'server_queue_helpers.dart' show StreamQueueOfNullableObjectExtension; + /// Starts the redirect test HTTP server in the same process. Future<StreamChannel<Object?>> startServer() async { final controller = StreamChannelController<Object?>(sync: true);
diff --git a/pkgs/http_client_conformance_tests/lib/src/response_headers_server_web.dart b/pkgs/http_client_conformance_tests/lib/src/response_headers_server_web.dart index 8ee938a..0e6dabd 100644 --- a/pkgs/http_client_conformance_tests/lib/src/response_headers_server_web.dart +++ b/pkgs/http_client_conformance_tests/lib/src/response_headers_server_web.dart
@@ -3,6 +3,8 @@ import 'package:stream_channel/stream_channel.dart'; import 'package:test/test.dart'; +export 'server_queue_helpers.dart' show StreamQueueOfNullableObjectExtension; + /// Starts the redirect test HTTP server out-of-process. Future<StreamChannel<Object?>> startServer() async => spawnHybridUri(Uri( scheme: 'package',
diff --git a/pkgs/http_client_conformance_tests/lib/src/response_headers_tests.dart b/pkgs/http_client_conformance_tests/lib/src/response_headers_tests.dart index 4f92042..84f0fb6 100644 --- a/pkgs/http_client_conformance_tests/lib/src/response_headers_tests.dart +++ b/pkgs/http_client_conformance_tests/lib/src/response_headers_tests.dart
@@ -8,7 +8,7 @@ import 'package:test/test.dart'; import 'response_headers_server_vm.dart' - if (dart.library.html) 'response_headers_server_web.dart'; + if (dart.library.js_interop) 'response_headers_server_web.dart'; /// Tests that the [Client] correctly processes response headers. void testResponseHeaders(Client client) async { @@ -20,7 +20,7 @@ setUp(() async { httpServerChannel = await startServer(); httpServerQueue = StreamQueue(httpServerChannel.stream); - host = 'localhost:${await httpServerQueue.next}'; + host = 'localhost:${await httpServerQueue.nextAsInt}'; }); test('single header', () async {
diff --git a/pkgs/http_client_conformance_tests/lib/src/response_status_line_server_vm.dart b/pkgs/http_client_conformance_tests/lib/src/response_status_line_server_vm.dart index ff2ea84..053bd11 100644 --- a/pkgs/http_client_conformance_tests/lib/src/response_status_line_server_vm.dart +++ b/pkgs/http_client_conformance_tests/lib/src/response_status_line_server_vm.dart
@@ -4,6 +4,8 @@ import 'response_status_line_server.dart'; +export 'server_queue_helpers.dart' show StreamQueueOfNullableObjectExtension; + /// Starts the redirect test HTTP server in the same process. Future<StreamChannel<Object?>> startServer() async { final controller = StreamChannelController<Object?>(sync: true);
diff --git a/pkgs/http_client_conformance_tests/lib/src/response_status_line_server_web.dart b/pkgs/http_client_conformance_tests/lib/src/response_status_line_server_web.dart index f1ebbcb..d70a325 100644 --- a/pkgs/http_client_conformance_tests/lib/src/response_status_line_server_web.dart +++ b/pkgs/http_client_conformance_tests/lib/src/response_status_line_server_web.dart
@@ -3,6 +3,8 @@ import 'package:stream_channel/stream_channel.dart'; import 'package:test/test.dart'; +export 'server_queue_helpers.dart' show StreamQueueOfNullableObjectExtension; + /// Starts the redirect test HTTP server out-of-process. Future<StreamChannel<Object?>> startServer() async => spawnHybridUri(Uri( scheme: 'package',
diff --git a/pkgs/http_client_conformance_tests/lib/src/response_status_line_tests.dart b/pkgs/http_client_conformance_tests/lib/src/response_status_line_tests.dart index b618fa0..6eb70c5 100644 --- a/pkgs/http_client_conformance_tests/lib/src/response_status_line_tests.dart +++ b/pkgs/http_client_conformance_tests/lib/src/response_status_line_tests.dart
@@ -8,7 +8,7 @@ import 'package:test/test.dart'; import 'response_status_line_server_vm.dart' - if (dart.library.html) 'response_status_line_server_web.dart'; + if (dart.library.js_interop) 'response_status_line_server_web.dart'; /// Tests that the [Client] correctly processes the response status line (e.g. /// 'HTTP/1.1 200 OK\r\n'). @@ -23,7 +23,7 @@ setUp(() async { httpServerChannel = await startServer(); httpServerQueue = StreamQueue(httpServerChannel.stream); - host = 'localhost:${await httpServerQueue.next}'; + host = 'localhost:${await httpServerQueue.nextAsInt}'; }); test('complete', () async {
diff --git a/pkgs/http_client_conformance_tests/lib/src/server_errors_server_vm.dart b/pkgs/http_client_conformance_tests/lib/src/server_errors_server_vm.dart index 257adcf..e5aa09f 100644 --- a/pkgs/http_client_conformance_tests/lib/src/server_errors_server_vm.dart +++ b/pkgs/http_client_conformance_tests/lib/src/server_errors_server_vm.dart
@@ -4,6 +4,8 @@ import 'server_errors_server.dart'; +export 'server_queue_helpers.dart' show StreamQueueOfNullableObjectExtension; + /// Starts the redirect test HTTP server in the same process. Future<StreamChannel<Object?>> startServer() async { final controller = StreamChannelController<Object?>(sync: true);
diff --git a/pkgs/http_client_conformance_tests/lib/src/server_errors_server_web.dart b/pkgs/http_client_conformance_tests/lib/src/server_errors_server_web.dart index cc763e3..9614f36 100644 --- a/pkgs/http_client_conformance_tests/lib/src/server_errors_server_web.dart +++ b/pkgs/http_client_conformance_tests/lib/src/server_errors_server_web.dart
@@ -3,6 +3,8 @@ import 'package:stream_channel/stream_channel.dart'; import 'package:test/test.dart'; +export 'server_queue_helpers.dart' show StreamQueueOfNullableObjectExtension; + /// Starts the redirect test HTTP server out-of-process. Future<StreamChannel<Object?>> startServer() async => spawnHybridUri(Uri( scheme: 'package',
diff --git a/pkgs/http_client_conformance_tests/lib/src/server_errors_test.dart b/pkgs/http_client_conformance_tests/lib/src/server_errors_test.dart index 65de499..1a83696 100644 --- a/pkgs/http_client_conformance_tests/lib/src/server_errors_test.dart +++ b/pkgs/http_client_conformance_tests/lib/src/server_errors_test.dart
@@ -8,7 +8,7 @@ import 'package:test/test.dart'; import 'server_errors_server_vm.dart' - if (dart.library.html) 'server_errors_server_web.dart'; + if (dart.library.js_interop) 'server_errors_server_web.dart'; /// Tests that the [Client] correctly handles server errors. void testServerErrors(Client client, {bool redirectAlwaysAllowed = false}) { @@ -20,7 +20,7 @@ setUpAll(() async { httpServerChannel = await startServer(); httpServerQueue = StreamQueue(httpServerChannel.stream); - host = 'localhost:${await httpServerQueue.next}'; + host = 'localhost:${await httpServerQueue.nextAsInt}'; }); tearDownAll(() => httpServerChannel.sink.add(null));
diff --git a/pkgs/http_client_conformance_tests/lib/src/server_queue_helpers.dart b/pkgs/http_client_conformance_tests/lib/src/server_queue_helpers.dart new file mode 100644 index 0000000..df87ddd --- /dev/null +++ b/pkgs/http_client_conformance_tests/lib/src/server_queue_helpers.dart
@@ -0,0 +1,10 @@ +// Copyright (c) 2023, 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. + +import 'package:async/async.dart'; + +extension StreamQueueOfNullableObjectExtension on StreamQueue<Object?> { + /// When run under dart2wasm, JSON numbers are always returned as [double]. + Future<int> get nextAsInt async => ((await next) as num).toInt(); +}
diff --git a/pkgs/http_client_conformance_tests/pubspec.yaml b/pkgs/http_client_conformance_tests/pubspec.yaml index 4264059..f904b2d 100644 --- a/pkgs/http_client_conformance_tests/pubspec.yaml +++ b/pkgs/http_client_conformance_tests/pubspec.yaml
@@ -2,18 +2,19 @@ description: >- A library that tests whether implementations of package:http's `Client` class behave as expected. -publish_to: none repository: https://github.com/dart-lang/http/tree/master/pkgs/http_client_conformance_tests +publish_to: none + environment: - sdk: '>=2.19.0 <3.0.0' + sdk: ^3.2.0 dependencies: async: ^2.8.2 dart_style: ^2.2.3 - http: '>=0.13.4 <2.0.0' + http: ^1.2.0 stream_channel: ^2.1.1 test: ^1.21.2 dev_dependencies: - dart_flutter_team_lints: ^1.0.0 + dart_flutter_team_lints: ^2.0.0
diff --git a/pkgs/http_profile/pubspec.yaml b/pkgs/http_profile/pubspec.yaml index 9e3b69e..1b5891e 100644 --- a/pkgs/http_profile/pubspec.yaml +++ b/pkgs/http_profile/pubspec.yaml
@@ -1,10 +1,11 @@ name: http_profile description: >- - A library used by HTTP client authors to integrate with the DevTools - Network tab. -publish_to: none + A library used by HTTP client authors to integrate with the DevTools Network + tab. repository: https://github.com/dart-lang/http/tree/master/pkgs/http_profile +publish_to: none + environment: sdk: ^3.0.0
diff --git a/pkgs/java_http/pubspec.yaml b/pkgs/java_http/pubspec.yaml index ecba4f1..c04429a 100644 --- a/pkgs/java_http/pubspec.yaml +++ b/pkgs/java_http/pubspec.yaml
@@ -1,7 +1,9 @@ name: java_http -description: A Dart package for making HTTP requests using java.net.HttpURLConnection. version: 0.0.1 +description: >- + A Dart package for making HTTP requests using java.net.HttpURLConnection. repository: https://github.com/dart-lang/http/tree/master/pkgs/java_http + publish_to: none environment: @@ -14,9 +16,8 @@ path: ^1.8.0 dev_dependencies: - dart_flutter_team_lints: ^1.0.0 + dart_flutter_team_lints: ^2.0.0 http_client_conformance_tests: path: ../http_client_conformance_tests/ jnigen: ^0.5.0 - lints: ^2.0.0 test: ^1.21.0
diff --git a/tool/ci.sh b/tool/ci.sh index 26395d2..d4cc8d2 100755 --- a/tool/ci.sh +++ b/tool/ci.sh
@@ -1,9 +1,10 @@ #!/bin/bash -# Created with package:mono_repo v6.6.0 +# Created with package:mono_repo v6.6.1 # Support built in commands on windows out of the box. + # When it is a flutter repo (check the pubspec.yaml for "sdk: flutter") -# then "flutter" is called instead of "pub". +# then "flutter pub" is called instead of "dart pub". # This assumes that the Flutter SDK has been installed in a previous step. function pub() { if grep -Fq "sdk: flutter" "${PWD}/pubspec.yaml"; then @@ -12,18 +13,13 @@ command dart pub "$@" fi } -# When it is a flutter repo (check the pubspec.yaml for "sdk: flutter") -# then "flutter" is called instead of "pub". -# This assumes that the Flutter SDK has been installed in a previous step. + function format() { - if grep -Fq "sdk: flutter" "${PWD}/pubspec.yaml"; then - command flutter format "$@" - else - command dart format "$@" - fi + command dart format "$@" } + # When it is a flutter repo (check the pubspec.yaml for "sdk: flutter") -# then "flutter" is called instead of "pub". +# then "flutter analyze" is called instead of "dart analyze". # This assumes that the Flutter SDK has been installed in a previous step. function analyze() { if grep -Fq "sdk: flutter" "${PWD}/pubspec.yaml"; then @@ -99,6 +95,10 @@ echo 'dart test --platform chrome' dart test --platform chrome || EXIT_CODE=$? ;; + test_4) + echo 'dart test --test-randomize-ordering-seed=random -p chrome -c dart2wasm' + dart test --test-randomize-ordering-seed=random -p chrome -c dart2wasm || EXIT_CODE=$? + ;; *) echo -e "\033[31mUnknown TASK '${TASK}' - TERMINATING JOB\033[0m" exit 64