diff --git a/pkgs/http/CHANGELOG.md b/pkgs/http/CHANGELOG.md index 4a82828e9f..aaee9e5e67 100644 --- a/pkgs/http/CHANGELOG.md +++ b/pkgs/http/CHANGELOG.md @@ -4,7 +4,8 @@ to use utf8 instead of latin1, ensuring proper JSON decoding. * Avoid references to `window` in `BrowserClient`, restoring support for web workers and NodeJS. - +* Added caching options for `BrowserClient`. + ## 1.3.0 * Fixed unintended HTML tags in doc comments. diff --git a/pkgs/http/lib/browser_client.dart b/pkgs/http/lib/browser_client.dart index 2cd0e5c7a4..a9f4d0a8a4 100644 --- a/pkgs/http/lib/browser_client.dart +++ b/pkgs/http/lib/browser_client.dart @@ -2,4 +2,4 @@ // 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. -export 'src/browser_client.dart' show BrowserClient; +export 'src/browser_client.dart' show BrowserClient, CacheMode; diff --git a/pkgs/http/lib/src/browser_client.dart b/pkgs/http/lib/src/browser_client.dart index acf233448a..64a8840cd5 100644 --- a/pkgs/http/lib/src/browser_client.dart +++ b/pkgs/http/lib/src/browser_client.dart @@ -4,7 +4,6 @@ import 'dart:async'; import 'dart:js_interop'; - import 'package:web/web.dart' show AbortController, @@ -30,6 +29,23 @@ BaseClient createClient() { return BrowserClient(); } +/// Caching mode used by the [BrowserClient]. +/// +/// Sets the request cache value of the browser Fetch API. +/// [`Request.cache`](https://developer.mozilla.org/en-US/docs/Web/API/Request/cache) property. +enum CacheMode { + defaultType('default'), + reload('reload'), + noStore('no-store'), + noCache('no-cache'), + forceCache('force-cache'), + onlyIfCached('only-if-cached'); + + final String cacheType; + + const CacheMode(this.cacheType); +} + @JS('fetch') external JSPromise _fetch( RequestInfo input, [ @@ -51,6 +67,20 @@ external JSPromise _fetch( class BrowserClient extends BaseClient { final _abortController = AbortController(); + final CacheMode _cacheMode; + + /// Create a new browser-based HTTP Client. + /// + /// If [cacheMode] is provided then it can be used to cache the request + /// in the browser. + /// + /// For example: + /// ```dart + /// final client = BrowserClient(cacheMode: CacheMode.reload); + /// ``` + BrowserClient({CacheMode cacheMode = CacheMode.defaultType}) + : _cacheMode = cacheMode; + /// Whether to send credentials such as cookies or authorization headers for /// cross-site requests. /// @@ -74,6 +104,7 @@ class BrowserClient extends BaseClient { RequestInit( method: request.method, body: bodyBytes.isNotEmpty ? bodyBytes.toJS : null, + cache: _cacheMode.cacheType, credentials: withCredentials ? 'include' : 'same-origin', headers: { if (request.contentLength case final contentLength?) diff --git a/pkgs/http/test/html/cache_test.dart b/pkgs/http/test/html/cache_test.dart new file mode 100644 index 0000000000..1dae3fba84 --- /dev/null +++ b/pkgs/http/test/html/cache_test.dart @@ -0,0 +1,83 @@ +// Copyright (c) 2025, 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. + +@TestOn('browser') +library; + +import 'dart:async'; + +import 'package:http/browser_client.dart'; +import 'package:http/http.dart' as http; +import 'package:http/http.dart'; +import 'package:test/test.dart'; + +import 'utils.dart'; + +void main() { + late Uri url; + setUp(() async { + final channel = spawnHybridUri(Uri(path: '/test/stub_server.dart')); + var port = (await channel.stream.first as num).toInt(); + url = echoUrl.replace(port: port); + }); + + test('#send a GET with default type', () async { + var client = BrowserClient(cacheMode: CacheMode.defaultType); + await client.get(url); + var response = await client.get(url); + client.close(); + + expect(response.statusCode, 200); + expect(response.reasonPhrase, 'OK'); + expect(response.body, parse(allOf(containsPair('numOfRequests', 1)))); + }); + + test('#send a GET Request with reload type', () async { + var client = BrowserClient(cacheMode: CacheMode.reload); + await client.get(url); + var response = await client.get(url); + expect(response.body, parse(allOf(containsPair('numOfRequests', 2)))); + client.close(); + }); + + test('#send a GET with no-cache type', () async { + var client = BrowserClient(cacheMode: CacheMode.noCache); + + await client.get(url); + var response = await client.get(url); + client.close(); + expect( + response.body, + parse(anyOf(containsPair('numOfRequests', 2), + containsPair('cache-control', ['max-age=0'])))); + }); + + test('#send a GET with no-store type', () async { + var client = BrowserClient(cacheMode: CacheMode.noStore); + + await client.get(url); + var response = await client.get(url); + client.close(); + expect(response.body, parse(allOf(containsPair('numOfRequests', 2)))); + }); + + test('#send a GET with force-store type', () async { + var client = BrowserClient(cacheMode: CacheMode.forceCache); + + await client.get(url); + var response = await client.get(url); + client.close(); + expect(response.body, parse(allOf(containsPair('numOfRequests', 1)))); + }); + + test('#send a StreamedRequest with only-if-cached type', () { + var client = BrowserClient(cacheMode: CacheMode.onlyIfCached); + var request = http.StreamedRequest('GET', url); + + expectLater(client.send(request), throwsA(isA())); + unawaited(request.sink.close()); + + client.close(); + }); +} diff --git a/pkgs/http/test/html/client_test.dart b/pkgs/http/test/html/client_test.dart index f62e75c119..10f0c0bfb5 100644 --- a/pkgs/http/test/html/client_test.dart +++ b/pkgs/http/test/html/client_test.dart @@ -14,20 +14,30 @@ import 'package:test/test.dart'; import 'utils.dart'; void main() { + late int port; + setUpAll(() async { + final channel = + spawnHybridUri(Uri(path: '/test/stub_server.dart'), stayAlive: true); + port = (await channel.stream.first as num).toInt(); + }); + test('#send a StreamedRequest', () async { var client = BrowserClient(); - var request = http.StreamedRequest('POST', echoUrl); + var request = http.StreamedRequest('POST', echoUrl.replace(port: port)); var responseFuture = client.send(request); request.sink.add('{"hello": "world"}'.codeUnits); unawaited(request.sink.close()); var response = await responseFuture; + var bytesString = await response.stream.bytesToString(); + client.close(); - expect(bytesString, equals('{"hello": "world"}')); - }, skip: 'Need to fix server tests for browser'); + expect(bytesString, + parse(allOf(containsPair('body', '{"hello": "world"}'.codeUnits)))); + }); test('#send with an invalid URL', () { var client = BrowserClient(); diff --git a/pkgs/http/test/html/streamed_request_test.dart b/pkgs/http/test/html/streamed_request_test.dart index 1668656046..63729451eb 100644 --- a/pkgs/http/test/html/streamed_request_test.dart +++ b/pkgs/http/test/html/streamed_request_test.dart @@ -14,27 +14,35 @@ import 'package:test/test.dart'; import 'utils.dart'; void main() { + late Uri url; + setUpAll(() async { + final channel = + spawnHybridUri(Uri(path: '/test/stub_server.dart'), stayAlive: true); + var port = (await channel.stream.first as num).toInt(); + url = echoUrl.replace(port: port); + }); group('contentLength', () { test("works when it's set", () async { - var request = http.StreamedRequest('POST', echoUrl) + var request = http.StreamedRequest('POST', url) ..contentLength = 10 ..sink.add([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); unawaited(request.sink.close()); final response = await BrowserClient().send(request); - expect(await response.stream.toBytes(), - equals([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])); + expect(await response.stream.bytesToString(), + parse(allOf(containsPair('body', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10])))); }); test("works when it's not set", () async { - var request = http.StreamedRequest('POST', echoUrl); + var request = http.StreamedRequest('POST', url); request.sink.add([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); unawaited(request.sink.close()); final response = await BrowserClient().send(request); - expect(await response.stream.toBytes(), - equals([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])); + + expect(await response.stream.bytesToString(), + parse(allOf(containsPair('body', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10])))); }); - }, skip: 'Need to fix server tests for browser'); + }); } diff --git a/pkgs/http/test/stub_server.dart b/pkgs/http/test/stub_server.dart index a53f77d375..a70e64e529 100644 --- a/pkgs/http/test/stub_server.dart +++ b/pkgs/http/test/stub_server.dart @@ -11,11 +11,15 @@ import 'package:http/src/utils.dart'; import 'package:stream_channel/stream_channel.dart'; void hybridMain(StreamChannel channel) async { + var numOfRequests = 0; final server = await HttpServer.bind('localhost', 0); final url = Uri.http('localhost:${server.port}', ''); server.listen((request) async { var path = request.uri.path; var response = request.response; + response.headers + ..set('Cache-Control', 'public, max-age=30, immutable') + ..set('etag', '312424'); if (path == '/error') { response @@ -53,6 +57,15 @@ void hybridMain(StreamChannel channel) async { return; } + // For browser runtime testing... + if (path == '/echo') { + ++numOfRequests; + + response + ..headers.add('Access-Control-Allow-Origin', '*') + ..headers.add('Access-Control-Allow-Methods', 'POST, GET'); + } + var requestBodyBytes = await ByteStream(request).toBytes(); var encodingName = request.uri.queryParameters['response-encoding']; var outputEncoding = @@ -88,6 +101,7 @@ void hybridMain(StreamChannel channel) async { 'path': request.uri.path, if (requestBody != null) 'body': requestBody, 'headers': headers, + if (path == '/echo') 'numOfRequests': numOfRequests }; var body = json.encode(content);