diff --git a/lib/src/_internal/web_utils.dart b/lib/src/_internal/web_utils.dart index f8062b8..46f8b63 100644 --- a/lib/src/_internal/web_utils.dart +++ b/lib/src/_internal/web_utils.dart @@ -20,6 +20,8 @@ extension type Headers._(JSObject _) implements web.Headers { external ArrayIterator keys(); external ArrayIterator values(); + factory Headers.fromHost(web.Headers host) => Headers._(host); + factory Headers.fromEntries(Iterable> entries) { final headers = Headers(); for (final MapEntry(key: name, :value) in entries) { diff --git a/lib/src/fetch/headers.js.dart b/lib/src/fetch/headers.js.dart index 5e557bd..93c34fd 100644 --- a/lib/src/fetch/headers.js.dart +++ b/lib/src/fetch/headers.js.dart @@ -102,3 +102,6 @@ class Headers } } } + +Headers headersFromHost(dom.Headers host) => + Headers._(web.Headers.fromHost(host)); diff --git a/lib/src/fetch/response.io.dart b/lib/src/fetch/response.io.dart index f86c9a9..7ffd1cc 100644 --- a/lib/src/fetch/response.io.dart +++ b/lib/src/fetch/response.io.dart @@ -22,21 +22,42 @@ final class NativeResponseHost extends ResponseHost { const NativeResponseHost(super.value); } +final class _ResponseMetadata { + _ResponseMetadata({ + required this.redirected, + required this.status, + required this.statusText, + required this.type, + required this.url, + }); + + factory _ResponseMetadata.from( + native.Response response, [ + native.ResponseInit? init, + ]) { + return _ResponseMetadata( + redirected: response.redirected, + status: init?.status ?? response.status, + statusText: init?.statusText ?? response.statusText, + type: response.type, + url: response.url, + ); + } + + final bool redirected; + final int status; + final String statusText; + final native.ResponseType type; + final String url; +} + class Response implements native.Response { Response._( this._host, { + _ResponseMetadata? metadata, io_headers.Headers? headers, - bool? redirected, - int? status, - String? statusText, - native.ResponseType? type, - String? url, - }) : _headers = headers, - _redirected = redirected, - _status = status, - _statusText = statusText, - _type = type, - _url = url; + }) : _metadata = metadata, + _headers = headers; factory Response([Object? body, native.ResponseInit? init]) { return switch ((body, init)) { @@ -70,13 +91,9 @@ class Response implements native.Response { } final ResponseHost _host; + final _ResponseMetadata? _metadata; io_headers.Headers? _headers; Body? _body; - final bool? _redirected; - final int? _status; - final String? _statusText; - final native.ResponseType? _type; - final String? _url; @override io_headers.Headers get headers { @@ -107,8 +124,8 @@ class Response implements native.Response { @override bool get ok { - final status = _status; - if (status != null) return HttpStatus.isSuccess(status); + final metadata = _metadata; + if (metadata != null) return HttpStatus.isSuccess(metadata.status); return switch (_host) { final HttpClientResponseHost host => HttpStatus.isSuccess( @@ -120,8 +137,8 @@ class Response implements native.Response { @override bool get redirected { - final redirected = _redirected; - if (redirected != null) return redirected; + final metadata = _metadata; + if (metadata != null) return metadata.redirected; return switch (_host) { final HttpClientResponseHost host => host.value.redirects.isNotEmpty, @@ -131,8 +148,8 @@ class Response implements native.Response { @override int get status { - final status = _status; - if (status != null) return status; + final metadata = _metadata; + if (metadata != null) return metadata.status; return switch (_host) { final HttpClientResponseHost host => host.value.statusCode, @@ -142,8 +159,8 @@ class Response implements native.Response { @override String get statusText { - final statusText = _statusText; - if (statusText != null) return statusText; + final metadata = _metadata; + if (metadata != null) return metadata.statusText; return switch (_host) { final HttpClientResponseHost host => host.value.reasonPhrase, @@ -153,8 +170,8 @@ class Response implements native.Response { @override native.ResponseType get type { - final type = _type; - if (type != null) return type; + final metadata = _metadata; + if (metadata != null) return metadata.type; return switch (_host) { final HttpClientResponseHost _ => native.ResponseType.default_, @@ -164,8 +181,8 @@ class Response implements native.Response { @override String get url { - final url = _url; - if (url != null) return url; + final metadata = _metadata; + if (metadata != null) return metadata.url; return switch (_host) { final HttpClientResponseHost _ => '', @@ -232,15 +249,12 @@ class Response implements native.Response { @override Response clone() { + final metadata = _ResponseMetadata.from(this); return switch (_host) { final NativeResponseHost host => Response._( NativeResponseHost(host.value.clone()), + metadata: metadata, headers: io_headers.Headers(headers), - redirected: redirected, - status: status, - statusText: statusText, - type: type, - url: url, ), final HttpClientResponseHost _ => Response._( NativeResponseHost( @@ -253,9 +267,7 @@ class Response implements native.Response { ), ), ), - redirected: redirected, - type: type, - url: url, + metadata: metadata, ), }; } @@ -272,24 +284,11 @@ class Response implements native.Response { Response response, native.ResponseInit? init, ) { - if (init?.status == null && response.status == 0) { - final clone = response.clone(); - return Response._( - clone._host, - headers: io_headers.Headers(init?.headers ?? response.headers), - redirected: response.redirected, - status: response.status, - statusText: init?.statusText ?? response.statusText, - type: response.type, - url: response.url, - ); - } - - return Response._( - NativeResponseHost(_nativeResponseFromWrappedResponse(response, init)), - redirected: response.redirected, - type: response.type, - url: response.url, + return _responseFromNativeCopySource( + response, + init, + cloneHost: () => response.clone()._host, + body: response._bodyForNativeCopy, ); } @@ -297,68 +296,61 @@ class Response implements native.Response { native.Response response, native.ResponseInit? init, ) { - if (init?.status == null && response.status == 0) { - return Response._( - NativeResponseHost(response.clone()), - headers: io_headers.Headers(init?.headers ?? response.headers), - redirected: response.redirected, - status: response.status, - statusText: init?.statusText ?? response.statusText, - type: response.type, - url: response.url, - ); - } - - return Response._( - NativeResponseHost(_nativeResponseFromNativeResponse(response, init)), - redirected: response.redirected, - type: response.type, - url: response.url, + return _responseFromNativeCopySource( + response, + init, + cloneHost: () => NativeResponseHost(response.clone()), + body: () => response.body, ); } - static native.Response _nativeResponseFromWrappedResponse( - Response response, - native.ResponseInit? init, - ) { + static Response _responseFromNativeCopySource( + native.Response response, + native.ResponseInit? init, { + required ResponseHost Function() cloneHost, + required Body? Function() body, + }) { + final metadata = _ResponseMetadata.from(response, init); final sourceHeaders = io_headers.Headers(response.headers); - return _nativeResponseFromCopy( - response._bodyForNativeCopy(), - status: init?.status ?? response.status, - statusText: init?.statusText ?? response.statusText, - headers: init?.headers ?? sourceHeaders, - preserveMissingContentType: - init?.headers == null && !sourceHeaders.has('content-type'), + final effectiveHeaders = io_headers.Headers(init?.headers ?? sourceHeaders); + if (init?.status == null && metadata.status == 0) { + return Response._( + cloneHost(), + metadata: metadata, + headers: effectiveHeaders, + ); + } + + final nativeCopy = _nativeResponseFromCopy( + body(), + metadata: metadata, + headers: effectiveHeaders, + preserveMissingContentType: _shouldPreserveMissingContentType( + init, + sourceHeaders, + ), ); + return Response._(NativeResponseHost(nativeCopy), metadata: metadata); } - static native.Response _nativeResponseFromNativeResponse( - native.Response response, + static bool _shouldPreserveMissingContentType( native.ResponseInit? init, + io_headers.Headers sourceHeaders, ) { - final sourceHeaders = io_headers.Headers(response.headers); - return _nativeResponseFromCopy( - response.body, - status: init?.status ?? response.status, - statusText: init?.statusText ?? response.statusText, - headers: init?.headers ?? sourceHeaders, - preserveMissingContentType: - init?.headers == null && !sourceHeaders.has('content-type'), - ); + return init?.headers == null && !sourceHeaders.has('content-type'); } static native.Response _nativeResponseFromCopy( Body? body, { - required int status, - required String statusText, + required _ResponseMetadata metadata, required Object? headers, required bool preserveMissingContentType, }) { final response = native.Response( body, native.ResponseInit( - status: status, - statusText: statusText, + status: metadata.status, + statusText: metadata.statusText, headers: headers, ), ); diff --git a/lib/src/fetch/response.js.dart b/lib/src/fetch/response.js.dart index a593957..8ca73da 100644 --- a/lib/src/fetch/response.js.dart +++ b/lib/src/fetch/response.js.dart @@ -8,6 +8,7 @@ import 'package:web/web.dart' as web; import '../_internal/web_fetch_utils.dart' as web_fetch; import '../_internal/web_stream_bridge.dart'; +import '../core/http_status.dart'; import 'blob.dart'; import 'body.dart'; import 'form_data.native.dart'; @@ -28,21 +29,55 @@ final class NativeResponseHost extends ResponseHost { const NativeResponseHost(super.value); } +final class _ResponseMetadata { + _ResponseMetadata({ + required this.redirected, + required this.status, + required this.statusText, + required this.type, + required this.url, + }); + + factory _ResponseMetadata.from( + native.Response response, [ + native.ResponseInit? init, + ]) { + return _ResponseMetadata( + redirected: response.redirected, + status: init?.status ?? response.status, + statusText: init?.statusText ?? response.statusText, + type: response.type, + url: response.url, + ); + } + + factory _ResponseMetadata.fromWeb( + web.Response response, [ + native.ResponseInit? init, + ]) { + return _ResponseMetadata( + redirected: response.redirected, + status: init?.status ?? response.status, + statusText: init?.statusText ?? response.statusText, + type: Response._responseTypeFromValue(response.type), + url: response.url, + ); + } + + final bool redirected; + final int status; + final String statusText; + final native.ResponseType type; + final String url; +} + class Response implements native.Response { Response._( this._host, { + _ResponseMetadata? metadata, js_headers.Headers? headers, - bool? redirected, - int? status, - String? statusText, - native.ResponseType? type, - String? url, - }) : _headers = headers, - _redirected = redirected, - _status = status, - _statusText = statusText, - _type = type, - _url = url; + }) : _metadata = metadata, + _headers = headers; factory Response([Object? body, native.ResponseInit? init]) { return switch ((body, init)) { @@ -57,11 +92,7 @@ class Response implements native.Response { (final web.Response response, _) => _responseFromWebResponse( response, init, - redirected: response.redirected, - status: response.status, - statusText: response.statusText, - type: _responseTypeFromValue(response.type), - url: response.url, + _ResponseMetadata.fromWeb(response, init), ), (final native.Response response, null) => Response._( NativeResponseHost(response.clone()), @@ -87,13 +118,9 @@ class Response implements native.Response { } final ResponseHost _host; + final _ResponseMetadata? _metadata; js_headers.Headers? _headers; Body? _body; - final bool? _redirected; - final int? _status; - final String? _statusText; - final native.ResponseType? _type; - final String? _url; @override js_headers.Headers get headers { @@ -101,7 +128,9 @@ class Response implements native.Response { if (headers != null) return headers; return _headers = switch (_host) { - final WebResponseHost host => js_headers.Headers(host.value.headers), + final WebResponseHost host => js_headers.headersFromHost( + host.value.headers, + ), final NativeResponseHost host => js_headers.Headers(host.value.headers), }; } @@ -132,8 +161,8 @@ class Response implements native.Response { @override bool get ok { - final status = _status; - if (status != null) return status >= 200 && status <= 299; + final metadata = _metadata; + if (metadata != null) return HttpStatus.isSuccess(metadata.status); return switch (_host) { final WebResponseHost host => host.value.ok, @@ -143,8 +172,8 @@ class Response implements native.Response { @override bool get redirected { - final redirected = _redirected; - if (redirected != null) return redirected; + final metadata = _metadata; + if (metadata != null) return metadata.redirected; return switch (_host) { final WebResponseHost host => host.value.redirected, @@ -154,8 +183,8 @@ class Response implements native.Response { @override int get status { - final status = _status; - if (status != null) return status; + final metadata = _metadata; + if (metadata != null) return metadata.status; return switch (_host) { final WebResponseHost host => host.value.status, @@ -165,8 +194,8 @@ class Response implements native.Response { @override String get statusText { - final statusText = _statusText; - if (statusText != null) return statusText; + final metadata = _metadata; + if (metadata != null) return metadata.statusText; return switch (_host) { final WebResponseHost host => host.value.statusText, @@ -176,8 +205,8 @@ class Response implements native.Response { @override native.ResponseType get type { - final type = _type; - if (type != null) return type; + final metadata = _metadata; + if (metadata != null) return metadata.type; return switch (_host) { final WebResponseHost host => _responseTypeFromValue(host.value.type), @@ -187,8 +216,8 @@ class Response implements native.Response { @override String get url { - final url = _url; - if (url != null) return url; + final metadata = _metadata; + if (metadata != null) return metadata.url; return switch (_host) { final WebResponseHost host => host.value.url, @@ -276,25 +305,18 @@ class Response implements native.Response { @override Response clone() { + final metadata = _ResponseMetadata.from(this); return switch (_host) { final WebResponseHost host => _responseFromWebResponse( host.value, null, + metadata, sourceHeaders: js_headers.Headers(headers), - redirected: redirected, - status: status, - statusText: statusText, - type: type, - url: url, ), final NativeResponseHost host => Response._( NativeResponseHost(host.value.clone()), + metadata: metadata, headers: js_headers.Headers(headers), - redirected: redirected, - status: status, - statusText: statusText, - type: type, - url: url, ), }; } @@ -314,53 +336,40 @@ class Response implements native.Response { Response response, native.ResponseInit? init, ) { + final metadata = _ResponseMetadata.from(response, init); return switch (response._host) { final WebResponseHost host => _responseFromWebResponse( host.value, init, + metadata, sourceHeaders: js_headers.Headers(response.headers), - redirected: response.redirected, - status: response.status, - statusText: response.statusText, - type: response.type, - url: response.url, ), NativeResponseHost() => _responseFromNativeWrappedResponse( response, init, + metadata, ), }; } static Response _responseFromWebResponse( web.Response response, - native.ResponseInit? init, { + native.ResponseInit? init, + _ResponseMetadata metadata, { Object? sourceHeaders, - bool? redirected, - int? status, - String? statusText, - native.ResponseType? type, - String? url, }) { - final sourceStatus = status ?? response.status; final effectiveHeaders = js_headers.Headers( init?.headers ?? sourceHeaders ?? response.headers, ); - final effectiveStatusText = - init?.statusText ?? statusText ?? response.statusText; - if (init?.status == null && sourceStatus == 0) { + if (init?.status == null && metadata.status == 0) { return Response._( WebResponseHost(response.clone()), + metadata: metadata, headers: effectiveHeaders, - redirected: redirected, - status: sourceStatus, - statusText: effectiveStatusText, - type: type, - url: url, ); } - final targetStatus = _validateStatus(init?.status ?? sourceStatus); + final targetStatus = _validateStatus(metadata.status); if (!_statusAllowsBody(targetStatus) && response.body != null) { throw ArgumentError.value( response, @@ -376,43 +385,26 @@ class Response implements native.Response { source.body, web.ResponseInit( status: targetStatus, - statusText: effectiveStatusText, + statusText: metadata.statusText, headers: effectiveHeaders.host, ), ), ), - redirected: redirected, - status: targetStatus, - statusText: effectiveStatusText, - type: type, - url: url, + metadata: metadata, ); } static Response _responseFromNativeWrappedResponse( Response response, native.ResponseInit? init, + _ResponseMetadata metadata, ) { - if (init?.status == null && response.status == 0) { - final clone = response.clone(); - return Response._( - clone._host, - headers: js_headers.Headers(init?.headers ?? response.headers), - redirected: response.redirected, - status: response.status, - statusText: init?.statusText ?? response.statusText, - type: response.type, - url: response.url, - ); - } - - return Response._( - NativeResponseHost( - _nativeResponseFromNativeWrappedResponse(response, init), - ), - redirected: response.redirected, - type: response.type, - url: response.url, + return _responseFromNativeCopySource( + init, + metadata, + js_headers.Headers(response.headers), + cloneHost: () => response.clone()._host, + body: () => response.body, ); } @@ -420,24 +412,42 @@ class Response implements native.Response { native.Response response, native.ResponseInit? init, ) { - if (init?.status == null && response.status == 0) { + final metadata = _ResponseMetadata.from(response, init); + return _responseFromNativeCopySource( + init, + metadata, + js_headers.Headers(response.headers), + cloneHost: () => NativeResponseHost(response.clone()), + body: () => response.body, + ); + } + + static Response _responseFromNativeCopySource( + native.ResponseInit? init, + _ResponseMetadata metadata, + js_headers.Headers sourceHeaders, { + required ResponseHost Function() cloneHost, + required Body? Function() body, + }) { + final effectiveHeaders = js_headers.Headers(init?.headers ?? sourceHeaders); + if (init?.status == null && metadata.status == 0) { return Response._( - NativeResponseHost(response.clone()), - headers: js_headers.Headers(init?.headers ?? response.headers), - redirected: response.redirected, - status: response.status, - statusText: init?.statusText ?? response.statusText, - type: response.type, - url: response.url, + cloneHost(), + metadata: metadata, + headers: effectiveHeaders, ); } - return Response._( - NativeResponseHost(_nativeResponseFromNativeResponse(response, init)), - redirected: response.redirected, - type: response.type, - url: response.url, + final nativeCopy = _nativeResponseFromCopy( + body(), + metadata: metadata, + headers: effectiveHeaders, + preserveMissingContentType: _shouldPreserveMissingContentType( + init, + sourceHeaders, + ), ); + return Response._(NativeResponseHost(nativeCopy), metadata: metadata); } static int _validateStatus(int status) { @@ -451,48 +461,24 @@ class Response implements native.Response { return !const {204, 205, 304}.contains(status); } - static native.Response _nativeResponseFromNativeWrappedResponse( - Response response, + static bool _shouldPreserveMissingContentType( native.ResponseInit? init, + js_headers.Headers sourceHeaders, ) { - final sourceHeaders = js_headers.Headers(response.headers); - return _nativeResponseFromCopy( - response.body, - status: init?.status ?? response.status, - statusText: init?.statusText ?? response.statusText, - headers: init?.headers ?? sourceHeaders, - preserveMissingContentType: - init?.headers == null && !sourceHeaders.has('content-type'), - ); - } - - static native.Response _nativeResponseFromNativeResponse( - native.Response response, - native.ResponseInit? init, - ) { - final sourceHeaders = js_headers.Headers(response.headers); - return _nativeResponseFromCopy( - response.body, - status: init?.status ?? response.status, - statusText: init?.statusText ?? response.statusText, - headers: init?.headers ?? sourceHeaders, - preserveMissingContentType: - init?.headers == null && !sourceHeaders.has('content-type'), - ); + return init?.headers == null && !sourceHeaders.has('content-type'); } static native.Response _nativeResponseFromCopy( Body? body, { - required int status, - required String statusText, + required _ResponseMetadata metadata, required Object? headers, required bool preserveMissingContentType, }) { final response = native.Response( body, native.ResponseInit( - status: status, - statusText: statusText, + status: metadata.status, + statusText: metadata.statusText, headers: headers, ), ); diff --git a/test/response_io_test.dart b/test/response_io_test.dart index c832ebc..53c832f 100644 --- a/test/response_io_test.dart +++ b/test/response_io_test.dart @@ -86,6 +86,7 @@ void main() { expect(response.statusText, 'Created'); expect(response.headers.get('x-source'), isNull); expect(response.headers.get('x-init'), '1'); + expect(response.headers.get('content-type'), 'text/plain;charset=UTF-8'); expect(upstream.bodyUsed, isFalse); expect(await response.text(), 'source body'); expect(upstream.bodyUsed, isFalse); @@ -115,6 +116,7 @@ void main() { expect(response.statusText, 'Created'); expect(response.headers.get('x-source'), isNull); expect(response.headers.get('x-init'), '1'); + expect(response.headers.get('content-type'), 'text/plain;charset=UTF-8'); expect(upstream.bodyUsed, isFalse); expect(await response.text(), 'native body'); expect(upstream.bodyUsed, isFalse); diff --git a/test/response_js_test.dart b/test/response_js_test.dart index fc5db42..214bac1 100644 --- a/test/response_js_test.dart +++ b/test/response_js_test.dart @@ -122,12 +122,35 @@ void main() { expect(response.statusText, 'Created'); expect(response.headers.get('x-source'), isNull); expect(response.headers.get('x-init'), '1'); + expect(response.headers.get('content-type'), 'text/plain;charset=UTF-8'); expect(upstream.bodyUsed, isFalse); expect(await response.text(), 'native body'); expect(upstream.bodyUsed, isFalse); expect(await upstream.text(), 'native body'); }); + test( + 'preserves body-derived content-type for native-backed wrapper copies', + () async { + final upstream = Response('wrapped body'); + + final response = Response( + upstream, + native.ResponseInit(headers: {'x-init': '1'}), + ); + + expect(response.headers.get('x-init'), '1'); + expect( + response.headers.get('content-type'), + 'text/plain;charset=UTF-8', + ); + expect(upstream.bodyUsed, isFalse); + expect(await response.text(), 'wrapped body'); + expect(upstream.bodyUsed, isFalse); + expect(await upstream.text(), 'wrapped body'); + }, + ); + test( 'preserves deleted body-derived content-type when copying responses', () async { @@ -254,6 +277,23 @@ void main() { final wrappedParsed = await wrappedCopy.formData(); expect((wrappedParsed.get('b') as TextMultipart).value, '2'); expect(wrapped.bodyUsed, isFalse); + + final mutableCopy = Response( + web.Response( + 'c=3'.toJS, + web.ResponseInit( + headers: {'content-type': 'text/plain'}.jsify()! as web.HeadersInit, + ), + ), + const native.ResponseInit(statusText: 'OK'), + ); + mutableCopy.headers.set( + 'content-type', + 'application/x-www-form-urlencoded', + ); + + final mutableParsed = await mutableCopy.formData(); + expect((mutableParsed.get('c') as TextMultipart).value, '3'); }); test('preserves zero-status web responses when copying with init', () {