diff --git a/pkgs/http/CHANGELOG.md b/pkgs/http/CHANGELOG.md index 90eb36aa6b..9421143702 100644 --- a/pkgs/http/CHANGELOG.md +++ b/pkgs/http/CHANGELOG.md @@ -1,6 +1,7 @@ -## 1.2.0-wip +## 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` to `BaseResponse`. diff --git a/pkgs/http/lib/http.dart b/pkgs/http/lib/http.dart index bd039c8519..da35b23a87 100644 --- a/pkgs/http/lib/http.dart +++ b/pkgs/http/lib/http.dart @@ -16,7 +16,8 @@ import 'src/streamed_request.dart'; export 'src/base_client.dart'; export 'src/base_request.dart'; -export 'src/base_response.dart' show BaseResponse, HeadersWithSplitValues; +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/multipart_request.dart'; 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 70a78695aa..4b165c7587 100644 --- a/pkgs/http/lib/src/base_request.dart +++ b/pkgs/http/lib/src/base_request.dart @@ -132,13 +132,25 @@ abstract class BaseRequest { 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 e1796e1b36..0527461dbb 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. /// @@ -71,6 +74,37 @@ abstract class BaseResponse { } } +/// 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^_`" diff --git a/pkgs/http/lib/src/browser_client.dart b/pkgs/http/lib/src/browser_client.dart index 80db8b1291..cbbada65f8 100644 --- a/pkgs/http/lib/src/browser_client.dart +++ b/pkgs/http/lib/src/browser_client.dart @@ -79,10 +79,13 @@ class BrowserClient extends BaseClient { return; } var body = (xhr.response as JSArrayBuffer).toDart.asUint8List(); - completer.complete(StreamedResponse( + 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)); })); diff --git a/pkgs/http/lib/src/io_client.dart b/pkgs/http/lib/src/io_client.dart index db66b028c4..fe4834bb24 100644 --- a/pkgs/http/lib/src/io_client.dart +++ b/pkgs/http/lib/src/io_client.dart @@ -6,6 +6,7 @@ import 'dart:io'; 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 @@ class _ClientSocketException extends ClientException 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 @@ -116,7 +133,7 @@ class IOClient extends BaseClient { 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); @@ -127,6 +144,9 @@ class IOClient extends BaseClient { 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/streamed_response.dart b/pkgs/http/lib/src/streamed_response.dart index 8cc0c76f75..44389d7061 100644 --- a/pkgs/http/lib/src/streamed_response.dart +++ b/pkgs/http/lib/src/streamed_response.dart @@ -26,3 +26,20 @@ class StreamedResponse extends BaseResponse { 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/pubspec.yaml b/pkgs/http/pubspec.yaml index 31746fcb2d..a531a6373c 100644 --- a/pkgs/http/pubspec.yaml +++ b/pkgs/http/pubspec.yaml @@ -1,5 +1,5 @@ name: http -version: 1.2.0-wip +version: 1.2.0 description: A composable, multi-platform, Future-based API for HTTP requests. repository: https://github.com/dart-lang/http/tree/master/pkgs/http diff --git a/pkgs/http/test/io/request_test.dart b/pkgs/http/test/io/request_test.dart index ac6b44c3fd..226781fbeb 100644 --- a/pkgs/http/test/io/request_test.dart +++ b/pkgs/http/test/io/request_test.dart @@ -46,15 +46,22 @@ void main() { final response = await request.send(); expect(response.statusCode, equals(302)); + expect( + response, + isA() + .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() + .having((r) => r.url, 'url', serverUrl.resolve('/'))); }); test('exceeding max redirects', () async {