Add `BaseResponseWithUrl.url` (#1109)
diff --git a/pkgs/http/CHANGELOG.md b/pkgs/http/CHANGELOG.md
index 90eb36a..9421143 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<String, List<String>` to
`BaseResponse`.
diff --git a/pkgs/http/lib/http.dart b/pkgs/http/lib/http.dart
index bd039c8..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' 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/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 e1796e1..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.
///
@@ -71,6 +74,37 @@
}
}
+/// 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 80db8b1..cbbada6 100644
--- a/pkgs/http/lib/src/browser_client.dart
+++ b/pkgs/http/lib/src/browser_client.dart
@@ -79,10 +79,13 @@
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 db66b02..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
@@ -116,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);
@@ -127,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/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/pubspec.yaml b/pkgs/http/pubspec.yaml
index 31746fc..a531a63 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 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 {