From bbda0a67484843b50ddd9746152eff98adb6db8d Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Mon, 22 Jun 2026 20:41:19 +0800 Subject: [PATCH 1/3] refactor(fetch): centralize response copy state --- lib/src/fetch/response.io.dart | 196 +++++++++++------------ lib/src/fetch/response.js.dart | 274 +++++++++++++++------------------ 2 files changed, 214 insertions(+), 256 deletions(-) diff --git a/lib/src/fetch/response.io.dart b/lib/src/fetch/response.io.dart index f86c9a9..3c060cc 100644 --- a/lib/src/fetch/response.io.dart +++ b/lib/src/fetch/response.io.dart @@ -22,21 +22,40 @@ final class NativeResponseHost extends ResponseHost { const NativeResponseHost(super.value); } +final class _ResponseSnapshot { + _ResponseSnapshot({ + required this.headers, + required this.redirected, + required this.status, + required this.statusText, + required this.type, + required this.url, + }); + + factory _ResponseSnapshot.from( + native.Response response, [ + native.ResponseInit? init, + ]) { + return _ResponseSnapshot( + headers: io_headers.Headers(init?.headers ?? response.headers), + redirected: response.redirected, + status: init?.status ?? response.status, + statusText: init?.statusText ?? response.statusText, + type: response.type, + url: response.url, + ); + } + + final io_headers.Headers headers; + final bool redirected; + final int status; + final String statusText; + final native.ResponseType type; + final String url; +} + class Response implements native.Response { - Response._( - this._host, { - 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; + Response._(this._host, [this._snapshot]); factory Response([Object? body, native.ResponseInit? init]) { return switch ((body, init)) { @@ -70,19 +89,18 @@ class Response implements native.Response { } final ResponseHost _host; + final _ResponseSnapshot? _snapshot; 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 { final headers = _headers; if (headers != null) return headers; + final snapshotHeaders = _snapshot?.headers; + if (snapshotHeaders != null) return _headers = snapshotHeaders; + return _headers = switch (_host) { final HttpClientResponseHost host => io_headers.Headers( host.value.headers, @@ -107,8 +125,8 @@ class Response implements native.Response { @override bool get ok { - final status = _status; - if (status != null) return HttpStatus.isSuccess(status); + final snapshot = _snapshot; + if (snapshot != null) return HttpStatus.isSuccess(snapshot.status); return switch (_host) { final HttpClientResponseHost host => HttpStatus.isSuccess( @@ -120,8 +138,8 @@ class Response implements native.Response { @override bool get redirected { - final redirected = _redirected; - if (redirected != null) return redirected; + final snapshot = _snapshot; + if (snapshot != null) return snapshot.redirected; return switch (_host) { final HttpClientResponseHost host => host.value.redirects.isNotEmpty, @@ -131,8 +149,8 @@ class Response implements native.Response { @override int get status { - final status = _status; - if (status != null) return status; + final snapshot = _snapshot; + if (snapshot != null) return snapshot.status; return switch (_host) { final HttpClientResponseHost host => host.value.statusCode, @@ -142,8 +160,8 @@ class Response implements native.Response { @override String get statusText { - final statusText = _statusText; - if (statusText != null) return statusText; + final snapshot = _snapshot; + if (snapshot != null) return snapshot.statusText; return switch (_host) { final HttpClientResponseHost host => host.value.reasonPhrase, @@ -153,8 +171,8 @@ class Response implements native.Response { @override native.ResponseType get type { - final type = _type; - if (type != null) return type; + final snapshot = _snapshot; + if (snapshot != null) return snapshot.type; return switch (_host) { final HttpClientResponseHost _ => native.ResponseType.default_, @@ -164,8 +182,8 @@ class Response implements native.Response { @override String get url { - final url = _url; - if (url != null) return url; + final snapshot = _snapshot; + if (snapshot != null) return snapshot.url; return switch (_host) { final HttpClientResponseHost _ => '', @@ -232,15 +250,11 @@ class Response implements native.Response { @override Response clone() { + final snapshot = _ResponseSnapshot.from(this); return switch (_host) { final NativeResponseHost host => Response._( NativeResponseHost(host.value.clone()), - headers: io_headers.Headers(headers), - redirected: redirected, - status: status, - statusText: statusText, - type: type, - url: url, + snapshot, ), final HttpClientResponseHost _ => Response._( NativeResponseHost( @@ -253,9 +267,7 @@ class Response implements native.Response { ), ), ), - redirected: redirected, - type: type, - url: url, + snapshot, ), }; } @@ -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,69 +296,58 @@ 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, - ) { - 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'), + static Response _responseFromNativeCopySource( + native.Response response, + native.ResponseInit? init, { + required ResponseHost Function() cloneHost, + required Body? Function() body, + }) { + final snapshot = _ResponseSnapshot.from(response, init); + if (init?.status == null && snapshot.status == 0) { + return Response._(cloneHost(), snapshot); + } + + return Response._( + NativeResponseHost( + _nativeResponseFromCopy( + body(), + snapshot: snapshot, + preserveMissingContentType: _shouldPreserveMissingContentType( + init, + snapshot, + ), + ), + ), + snapshot, ); } - static native.Response _nativeResponseFromNativeResponse( - native.Response response, + static bool _shouldPreserveMissingContentType( native.ResponseInit? init, + _ResponseSnapshot snapshot, ) { - 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 && !snapshot.headers.has('content-type'); } static native.Response _nativeResponseFromCopy( Body? body, { - required int status, - required String statusText, - required Object? headers, + required _ResponseSnapshot snapshot, required bool preserveMissingContentType, }) { final response = native.Response( body, native.ResponseInit( - status: status, - statusText: statusText, - headers: headers, + status: snapshot.status, + statusText: snapshot.statusText, + headers: snapshot.headers, ), ); if (preserveMissingContentType) { diff --git a/lib/src/fetch/response.js.dart b/lib/src/fetch/response.js.dart index a593957..b533268 100644 --- a/lib/src/fetch/response.js.dart +++ b/lib/src/fetch/response.js.dart @@ -28,21 +28,54 @@ final class NativeResponseHost extends ResponseHost { const NativeResponseHost(super.value); } +final class _ResponseSnapshot { + _ResponseSnapshot({ + required this.headers, + required this.redirected, + required this.status, + required this.statusText, + required this.type, + required this.url, + }); + + factory _ResponseSnapshot.from( + native.Response response, [ + native.ResponseInit? init, + ]) { + return _ResponseSnapshot( + headers: js_headers.Headers(init?.headers ?? response.headers), + redirected: response.redirected, + status: init?.status ?? response.status, + statusText: init?.statusText ?? response.statusText, + type: response.type, + url: response.url, + ); + } + + factory _ResponseSnapshot.fromWeb( + web.Response response, [ + native.ResponseInit? init, + ]) { + return _ResponseSnapshot( + headers: js_headers.Headers(init?.headers ?? response.headers), + redirected: response.redirected, + status: init?.status ?? response.status, + statusText: init?.statusText ?? response.statusText, + type: Response._responseTypeFromValue(response.type), + url: response.url, + ); + } + + final js_headers.Headers headers; + final bool redirected; + final int status; + final String statusText; + final native.ResponseType type; + final String url; +} + class Response implements native.Response { - Response._( - this._host, { - 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; + Response._(this._host, [this._snapshot]); factory Response([Object? body, native.ResponseInit? init]) { return switch ((body, init)) { @@ -57,11 +90,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, + _ResponseSnapshot.fromWeb(response, init), ), (final native.Response response, null) => Response._( NativeResponseHost(response.clone()), @@ -87,19 +116,18 @@ class Response implements native.Response { } final ResponseHost _host; + final _ResponseSnapshot? _snapshot; 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 { final headers = _headers; if (headers != null) return headers; + final snapshotHeaders = _snapshot?.headers; + if (snapshotHeaders != null) return _headers = snapshotHeaders; + return _headers = switch (_host) { final WebResponseHost host => js_headers.Headers(host.value.headers), final NativeResponseHost host => js_headers.Headers(host.value.headers), @@ -132,8 +160,10 @@ class Response implements native.Response { @override bool get ok { - final status = _status; - if (status != null) return status >= 200 && status <= 299; + final snapshot = _snapshot; + if (snapshot != null) { + return snapshot.status >= 200 && snapshot.status <= 299; + } return switch (_host) { final WebResponseHost host => host.value.ok, @@ -143,8 +173,8 @@ class Response implements native.Response { @override bool get redirected { - final redirected = _redirected; - if (redirected != null) return redirected; + final snapshot = _snapshot; + if (snapshot != null) return snapshot.redirected; return switch (_host) { final WebResponseHost host => host.value.redirected, @@ -154,8 +184,8 @@ class Response implements native.Response { @override int get status { - final status = _status; - if (status != null) return status; + final snapshot = _snapshot; + if (snapshot != null) return snapshot.status; return switch (_host) { final WebResponseHost host => host.value.status, @@ -165,8 +195,8 @@ class Response implements native.Response { @override String get statusText { - final statusText = _statusText; - if (statusText != null) return statusText; + final snapshot = _snapshot; + if (snapshot != null) return snapshot.statusText; return switch (_host) { final WebResponseHost host => host.value.statusText, @@ -176,8 +206,8 @@ class Response implements native.Response { @override native.ResponseType get type { - final type = _type; - if (type != null) return type; + final snapshot = _snapshot; + if (snapshot != null) return snapshot.type; return switch (_host) { final WebResponseHost host => _responseTypeFromValue(host.value.type), @@ -187,8 +217,8 @@ class Response implements native.Response { @override String get url { - final url = _url; - if (url != null) return url; + final snapshot = _snapshot; + if (snapshot != null) return snapshot.url; return switch (_host) { final WebResponseHost host => host.value.url, @@ -276,25 +306,16 @@ class Response implements native.Response { @override Response clone() { + final snapshot = _ResponseSnapshot.from(this); return switch (_host) { final WebResponseHost host => _responseFromWebResponse( host.value, null, - sourceHeaders: js_headers.Headers(headers), - redirected: redirected, - status: status, - statusText: statusText, - type: type, - url: url, + snapshot, ), final NativeResponseHost host => Response._( NativeResponseHost(host.value.clone()), - headers: js_headers.Headers(headers), - redirected: redirected, - status: status, - statusText: statusText, - type: type, - url: url, + snapshot, ), }; } @@ -314,53 +335,31 @@ class Response implements native.Response { Response response, native.ResponseInit? init, ) { + final snapshot = _ResponseSnapshot.from(response, init); return switch (response._host) { final WebResponseHost host => _responseFromWebResponse( host.value, init, - sourceHeaders: js_headers.Headers(response.headers), - redirected: response.redirected, - status: response.status, - statusText: response.statusText, - type: response.type, - url: response.url, + snapshot, ), NativeResponseHost() => _responseFromNativeWrappedResponse( response, init, + snapshot, ), }; } static Response _responseFromWebResponse( web.Response response, - native.ResponseInit? init, { - 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) { - return Response._( - WebResponseHost(response.clone()), - headers: effectiveHeaders, - redirected: redirected, - status: sourceStatus, - statusText: effectiveStatusText, - type: type, - url: url, - ); + native.ResponseInit? init, + _ResponseSnapshot snapshot, + ) { + if (init?.status == null && snapshot.status == 0) { + return Response._(WebResponseHost(response.clone()), snapshot); } - final targetStatus = _validateStatus(init?.status ?? sourceStatus); + final targetStatus = _validateStatus(snapshot.status); if (!_statusAllowsBody(targetStatus) && response.body != null) { throw ArgumentError.value( response, @@ -376,43 +375,25 @@ class Response implements native.Response { source.body, web.ResponseInit( status: targetStatus, - statusText: effectiveStatusText, - headers: effectiveHeaders.host, + statusText: snapshot.statusText, + headers: snapshot.headers.host, ), ), ), - redirected: redirected, - status: targetStatus, - statusText: effectiveStatusText, - type: type, - url: url, + snapshot, ); } static Response _responseFromNativeWrappedResponse( Response response, native.ResponseInit? init, + _ResponseSnapshot snapshot, ) { - 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, + snapshot, + cloneHost: () => response.clone()._host, + body: () => response.body, ); } @@ -420,23 +401,37 @@ class Response implements native.Response { native.Response response, native.ResponseInit? init, ) { - if (init?.status == null && response.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, - ); + final snapshot = _ResponseSnapshot.from(response, init); + return _responseFromNativeCopySource( + init, + snapshot, + cloneHost: () => NativeResponseHost(response.clone()), + body: () => response.body, + ); + } + + static Response _responseFromNativeCopySource( + native.ResponseInit? init, + _ResponseSnapshot snapshot, { + required ResponseHost Function() cloneHost, + required Body? Function() body, + }) { + if (init?.status == null && snapshot.status == 0) { + return Response._(cloneHost(), snapshot); } return Response._( - NativeResponseHost(_nativeResponseFromNativeResponse(response, init)), - redirected: response.redirected, - type: response.type, - url: response.url, + NativeResponseHost( + _nativeResponseFromCopy( + body(), + snapshot: snapshot, + preserveMissingContentType: _shouldPreserveMissingContentType( + init, + snapshot, + ), + ), + ), + snapshot, ); } @@ -451,49 +446,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, + _ResponseSnapshot snapshot, ) { - 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 && !snapshot.headers.has('content-type'); } static native.Response _nativeResponseFromCopy( Body? body, { - required int status, - required String statusText, - required Object? headers, + required _ResponseSnapshot snapshot, required bool preserveMissingContentType, }) { final response = native.Response( body, native.ResponseInit( - status: status, - statusText: statusText, - headers: headers, + status: snapshot.status, + statusText: snapshot.statusText, + headers: snapshot.headers, ), ); if (preserveMissingContentType) { From 143183ce06bc10e959690de71cd8baf6c8a75a15 Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Mon, 22 Jun 2026 21:02:28 +0800 Subject: [PATCH 2/3] fix(fetch): preserve native copy content type --- lib/src/fetch/response.io.dart | 32 +++++++++++++++++++---------- lib/src/fetch/response.js.dart | 37 +++++++++++++++++++++------------- test/response_io_test.dart | 2 ++ test/response_js_test.dart | 23 +++++++++++++++++++++ 4 files changed, 69 insertions(+), 25 deletions(-) diff --git a/lib/src/fetch/response.io.dart b/lib/src/fetch/response.io.dart index 3c060cc..b3b933d 100644 --- a/lib/src/fetch/response.io.dart +++ b/lib/src/fetch/response.io.dart @@ -52,6 +52,17 @@ final class _ResponseSnapshot { final String statusText; final native.ResponseType type; final String url; + + _ResponseSnapshot withHeaders(io_headers.Headers headers) { + return _ResponseSnapshot( + headers: headers, + redirected: redirected, + status: status, + statusText: statusText, + type: type, + url: url, + ); + } } class Response implements native.Response { @@ -315,18 +326,17 @@ class Response implements native.Response { return Response._(cloneHost(), snapshot); } - return Response._( - NativeResponseHost( - _nativeResponseFromCopy( - body(), - snapshot: snapshot, - preserveMissingContentType: _shouldPreserveMissingContentType( - init, - snapshot, - ), - ), + final nativeCopy = _nativeResponseFromCopy( + body(), + snapshot: snapshot, + preserveMissingContentType: _shouldPreserveMissingContentType( + init, + snapshot, ), - snapshot, + ); + return Response._( + NativeResponseHost(nativeCopy), + snapshot.withHeaders(io_headers.Headers(nativeCopy.headers)), ); } diff --git a/lib/src/fetch/response.js.dart b/lib/src/fetch/response.js.dart index b533268..f36238f 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'; @@ -72,6 +73,17 @@ final class _ResponseSnapshot { final String statusText; final native.ResponseType type; final String url; + + _ResponseSnapshot withHeaders(js_headers.Headers headers) { + return _ResponseSnapshot( + headers: headers, + redirected: redirected, + status: status, + statusText: statusText, + type: type, + url: url, + ); + } } class Response implements native.Response { @@ -161,9 +173,7 @@ class Response implements native.Response { @override bool get ok { final snapshot = _snapshot; - if (snapshot != null) { - return snapshot.status >= 200 && snapshot.status <= 299; - } + if (snapshot != null) return HttpStatus.isSuccess(snapshot.status); return switch (_host) { final WebResponseHost host => host.value.ok, @@ -420,18 +430,17 @@ class Response implements native.Response { return Response._(cloneHost(), snapshot); } - return Response._( - NativeResponseHost( - _nativeResponseFromCopy( - body(), - snapshot: snapshot, - preserveMissingContentType: _shouldPreserveMissingContentType( - init, - snapshot, - ), - ), + final nativeCopy = _nativeResponseFromCopy( + body(), + snapshot: snapshot, + preserveMissingContentType: _shouldPreserveMissingContentType( + init, + snapshot, ), - snapshot, + ); + return Response._( + NativeResponseHost(nativeCopy), + snapshot.withHeaders(js_headers.Headers(nativeCopy.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..0ac537b 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 { From 0cf98bed0c66a8b0b7184a4dd2783eaf89833085 Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Mon, 22 Jun 2026 21:29:10 +0800 Subject: [PATCH 3/3] refactor(fetch): keep response headers host-bound --- lib/src/_internal/web_utils.dart | 2 + lib/src/fetch/headers.js.dart | 3 + lib/src/fetch/response.io.dart | 100 ++++++++++---------- lib/src/fetch/response.js.dart | 153 ++++++++++++++++--------------- test/response_js_test.dart | 17 ++++ 5 files changed, 149 insertions(+), 126 deletions(-) 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 b3b933d..7ffd1cc 100644 --- a/lib/src/fetch/response.io.dart +++ b/lib/src/fetch/response.io.dart @@ -22,9 +22,8 @@ final class NativeResponseHost extends ResponseHost { const NativeResponseHost(super.value); } -final class _ResponseSnapshot { - _ResponseSnapshot({ - required this.headers, +final class _ResponseMetadata { + _ResponseMetadata({ required this.redirected, required this.status, required this.statusText, @@ -32,12 +31,11 @@ final class _ResponseSnapshot { required this.url, }); - factory _ResponseSnapshot.from( + factory _ResponseMetadata.from( native.Response response, [ native.ResponseInit? init, ]) { - return _ResponseSnapshot( - headers: io_headers.Headers(init?.headers ?? response.headers), + return _ResponseMetadata( redirected: response.redirected, status: init?.status ?? response.status, statusText: init?.statusText ?? response.statusText, @@ -46,27 +44,20 @@ final class _ResponseSnapshot { ); } - final io_headers.Headers headers; final bool redirected; final int status; final String statusText; final native.ResponseType type; final String url; - - _ResponseSnapshot withHeaders(io_headers.Headers headers) { - return _ResponseSnapshot( - headers: headers, - redirected: redirected, - status: status, - statusText: statusText, - type: type, - url: url, - ); - } } class Response implements native.Response { - Response._(this._host, [this._snapshot]); + Response._( + this._host, { + _ResponseMetadata? metadata, + io_headers.Headers? headers, + }) : _metadata = metadata, + _headers = headers; factory Response([Object? body, native.ResponseInit? init]) { return switch ((body, init)) { @@ -100,7 +91,7 @@ class Response implements native.Response { } final ResponseHost _host; - final _ResponseSnapshot? _snapshot; + final _ResponseMetadata? _metadata; io_headers.Headers? _headers; Body? _body; @@ -109,9 +100,6 @@ class Response implements native.Response { final headers = _headers; if (headers != null) return headers; - final snapshotHeaders = _snapshot?.headers; - if (snapshotHeaders != null) return _headers = snapshotHeaders; - return _headers = switch (_host) { final HttpClientResponseHost host => io_headers.Headers( host.value.headers, @@ -136,8 +124,8 @@ class Response implements native.Response { @override bool get ok { - final snapshot = _snapshot; - if (snapshot != null) return HttpStatus.isSuccess(snapshot.status); + final metadata = _metadata; + if (metadata != null) return HttpStatus.isSuccess(metadata.status); return switch (_host) { final HttpClientResponseHost host => HttpStatus.isSuccess( @@ -149,8 +137,8 @@ class Response implements native.Response { @override bool get redirected { - final snapshot = _snapshot; - if (snapshot != null) return snapshot.redirected; + final metadata = _metadata; + if (metadata != null) return metadata.redirected; return switch (_host) { final HttpClientResponseHost host => host.value.redirects.isNotEmpty, @@ -160,8 +148,8 @@ class Response implements native.Response { @override int get status { - final snapshot = _snapshot; - if (snapshot != null) return snapshot.status; + final metadata = _metadata; + if (metadata != null) return metadata.status; return switch (_host) { final HttpClientResponseHost host => host.value.statusCode, @@ -171,8 +159,8 @@ class Response implements native.Response { @override String get statusText { - final snapshot = _snapshot; - if (snapshot != null) return snapshot.statusText; + final metadata = _metadata; + if (metadata != null) return metadata.statusText; return switch (_host) { final HttpClientResponseHost host => host.value.reasonPhrase, @@ -182,8 +170,8 @@ class Response implements native.Response { @override native.ResponseType get type { - final snapshot = _snapshot; - if (snapshot != null) return snapshot.type; + final metadata = _metadata; + if (metadata != null) return metadata.type; return switch (_host) { final HttpClientResponseHost _ => native.ResponseType.default_, @@ -193,8 +181,8 @@ class Response implements native.Response { @override String get url { - final snapshot = _snapshot; - if (snapshot != null) return snapshot.url; + final metadata = _metadata; + if (metadata != null) return metadata.url; return switch (_host) { final HttpClientResponseHost _ => '', @@ -261,11 +249,12 @@ class Response implements native.Response { @override Response clone() { - final snapshot = _ResponseSnapshot.from(this); + final metadata = _ResponseMetadata.from(this); return switch (_host) { final NativeResponseHost host => Response._( NativeResponseHost(host.value.clone()), - snapshot, + metadata: metadata, + headers: io_headers.Headers(headers), ), final HttpClientResponseHost _ => Response._( NativeResponseHost( @@ -278,7 +267,7 @@ class Response implements native.Response { ), ), ), - snapshot, + metadata: metadata, ), }; } @@ -321,43 +310,48 @@ class Response implements native.Response { required ResponseHost Function() cloneHost, required Body? Function() body, }) { - final snapshot = _ResponseSnapshot.from(response, init); - if (init?.status == null && snapshot.status == 0) { - return Response._(cloneHost(), snapshot); + final metadata = _ResponseMetadata.from(response, init); + final sourceHeaders = io_headers.Headers(response.headers); + 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(), - snapshot: snapshot, + metadata: metadata, + headers: effectiveHeaders, preserveMissingContentType: _shouldPreserveMissingContentType( init, - snapshot, + sourceHeaders, ), ); - return Response._( - NativeResponseHost(nativeCopy), - snapshot.withHeaders(io_headers.Headers(nativeCopy.headers)), - ); + return Response._(NativeResponseHost(nativeCopy), metadata: metadata); } static bool _shouldPreserveMissingContentType( native.ResponseInit? init, - _ResponseSnapshot snapshot, + io_headers.Headers sourceHeaders, ) { - return init?.headers == null && !snapshot.headers.has('content-type'); + return init?.headers == null && !sourceHeaders.has('content-type'); } static native.Response _nativeResponseFromCopy( Body? body, { - required _ResponseSnapshot snapshot, + required _ResponseMetadata metadata, + required Object? headers, required bool preserveMissingContentType, }) { final response = native.Response( body, native.ResponseInit( - status: snapshot.status, - statusText: snapshot.statusText, - headers: snapshot.headers, + status: metadata.status, + statusText: metadata.statusText, + headers: headers, ), ); if (preserveMissingContentType) { diff --git a/lib/src/fetch/response.js.dart b/lib/src/fetch/response.js.dart index f36238f..8ca73da 100644 --- a/lib/src/fetch/response.js.dart +++ b/lib/src/fetch/response.js.dart @@ -29,9 +29,8 @@ final class NativeResponseHost extends ResponseHost { const NativeResponseHost(super.value); } -final class _ResponseSnapshot { - _ResponseSnapshot({ - required this.headers, +final class _ResponseMetadata { + _ResponseMetadata({ required this.redirected, required this.status, required this.statusText, @@ -39,12 +38,11 @@ final class _ResponseSnapshot { required this.url, }); - factory _ResponseSnapshot.from( + factory _ResponseMetadata.from( native.Response response, [ native.ResponseInit? init, ]) { - return _ResponseSnapshot( - headers: js_headers.Headers(init?.headers ?? response.headers), + return _ResponseMetadata( redirected: response.redirected, status: init?.status ?? response.status, statusText: init?.statusText ?? response.statusText, @@ -53,12 +51,11 @@ final class _ResponseSnapshot { ); } - factory _ResponseSnapshot.fromWeb( + factory _ResponseMetadata.fromWeb( web.Response response, [ native.ResponseInit? init, ]) { - return _ResponseSnapshot( - headers: js_headers.Headers(init?.headers ?? response.headers), + return _ResponseMetadata( redirected: response.redirected, status: init?.status ?? response.status, statusText: init?.statusText ?? response.statusText, @@ -67,27 +64,20 @@ final class _ResponseSnapshot { ); } - final js_headers.Headers headers; final bool redirected; final int status; final String statusText; final native.ResponseType type; final String url; - - _ResponseSnapshot withHeaders(js_headers.Headers headers) { - return _ResponseSnapshot( - headers: headers, - redirected: redirected, - status: status, - statusText: statusText, - type: type, - url: url, - ); - } } class Response implements native.Response { - Response._(this._host, [this._snapshot]); + Response._( + this._host, { + _ResponseMetadata? metadata, + js_headers.Headers? headers, + }) : _metadata = metadata, + _headers = headers; factory Response([Object? body, native.ResponseInit? init]) { return switch ((body, init)) { @@ -102,7 +92,7 @@ class Response implements native.Response { (final web.Response response, _) => _responseFromWebResponse( response, init, - _ResponseSnapshot.fromWeb(response, init), + _ResponseMetadata.fromWeb(response, init), ), (final native.Response response, null) => Response._( NativeResponseHost(response.clone()), @@ -128,7 +118,7 @@ class Response implements native.Response { } final ResponseHost _host; - final _ResponseSnapshot? _snapshot; + final _ResponseMetadata? _metadata; js_headers.Headers? _headers; Body? _body; @@ -137,11 +127,10 @@ class Response implements native.Response { final headers = _headers; if (headers != null) return headers; - final snapshotHeaders = _snapshot?.headers; - if (snapshotHeaders != null) return _headers = snapshotHeaders; - 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), }; } @@ -172,8 +161,8 @@ class Response implements native.Response { @override bool get ok { - final snapshot = _snapshot; - if (snapshot != null) return HttpStatus.isSuccess(snapshot.status); + final metadata = _metadata; + if (metadata != null) return HttpStatus.isSuccess(metadata.status); return switch (_host) { final WebResponseHost host => host.value.ok, @@ -183,8 +172,8 @@ class Response implements native.Response { @override bool get redirected { - final snapshot = _snapshot; - if (snapshot != null) return snapshot.redirected; + final metadata = _metadata; + if (metadata != null) return metadata.redirected; return switch (_host) { final WebResponseHost host => host.value.redirected, @@ -194,8 +183,8 @@ class Response implements native.Response { @override int get status { - final snapshot = _snapshot; - if (snapshot != null) return snapshot.status; + final metadata = _metadata; + if (metadata != null) return metadata.status; return switch (_host) { final WebResponseHost host => host.value.status, @@ -205,8 +194,8 @@ class Response implements native.Response { @override String get statusText { - final snapshot = _snapshot; - if (snapshot != null) return snapshot.statusText; + final metadata = _metadata; + if (metadata != null) return metadata.statusText; return switch (_host) { final WebResponseHost host => host.value.statusText, @@ -216,8 +205,8 @@ class Response implements native.Response { @override native.ResponseType get type { - final snapshot = _snapshot; - if (snapshot != null) return snapshot.type; + final metadata = _metadata; + if (metadata != null) return metadata.type; return switch (_host) { final WebResponseHost host => _responseTypeFromValue(host.value.type), @@ -227,8 +216,8 @@ class Response implements native.Response { @override String get url { - final snapshot = _snapshot; - if (snapshot != null) return snapshot.url; + final metadata = _metadata; + if (metadata != null) return metadata.url; return switch (_host) { final WebResponseHost host => host.value.url, @@ -316,16 +305,18 @@ class Response implements native.Response { @override Response clone() { - final snapshot = _ResponseSnapshot.from(this); + final metadata = _ResponseMetadata.from(this); return switch (_host) { final WebResponseHost host => _responseFromWebResponse( host.value, null, - snapshot, + metadata, + sourceHeaders: js_headers.Headers(headers), ), final NativeResponseHost host => Response._( NativeResponseHost(host.value.clone()), - snapshot, + metadata: metadata, + headers: js_headers.Headers(headers), ), }; } @@ -345,17 +336,18 @@ class Response implements native.Response { Response response, native.ResponseInit? init, ) { - final snapshot = _ResponseSnapshot.from(response, init); + final metadata = _ResponseMetadata.from(response, init); return switch (response._host) { final WebResponseHost host => _responseFromWebResponse( host.value, init, - snapshot, + metadata, + sourceHeaders: js_headers.Headers(response.headers), ), NativeResponseHost() => _responseFromNativeWrappedResponse( response, init, - snapshot, + metadata, ), }; } @@ -363,13 +355,21 @@ class Response implements native.Response { static Response _responseFromWebResponse( web.Response response, native.ResponseInit? init, - _ResponseSnapshot snapshot, - ) { - if (init?.status == null && snapshot.status == 0) { - return Response._(WebResponseHost(response.clone()), snapshot); + _ResponseMetadata metadata, { + Object? sourceHeaders, + }) { + final effectiveHeaders = js_headers.Headers( + init?.headers ?? sourceHeaders ?? response.headers, + ); + if (init?.status == null && metadata.status == 0) { + return Response._( + WebResponseHost(response.clone()), + metadata: metadata, + headers: effectiveHeaders, + ); } - final targetStatus = _validateStatus(snapshot.status); + final targetStatus = _validateStatus(metadata.status); if (!_statusAllowsBody(targetStatus) && response.body != null) { throw ArgumentError.value( response, @@ -385,23 +385,24 @@ class Response implements native.Response { source.body, web.ResponseInit( status: targetStatus, - statusText: snapshot.statusText, - headers: snapshot.headers.host, + statusText: metadata.statusText, + headers: effectiveHeaders.host, ), ), ), - snapshot, + metadata: metadata, ); } static Response _responseFromNativeWrappedResponse( Response response, native.ResponseInit? init, - _ResponseSnapshot snapshot, + _ResponseMetadata metadata, ) { return _responseFromNativeCopySource( init, - snapshot, + metadata, + js_headers.Headers(response.headers), cloneHost: () => response.clone()._host, body: () => response.body, ); @@ -411,10 +412,11 @@ class Response implements native.Response { native.Response response, native.ResponseInit? init, ) { - final snapshot = _ResponseSnapshot.from(response, init); + final metadata = _ResponseMetadata.from(response, init); return _responseFromNativeCopySource( init, - snapshot, + metadata, + js_headers.Headers(response.headers), cloneHost: () => NativeResponseHost(response.clone()), body: () => response.body, ); @@ -422,26 +424,30 @@ class Response implements native.Response { static Response _responseFromNativeCopySource( native.ResponseInit? init, - _ResponseSnapshot snapshot, { + _ResponseMetadata metadata, + js_headers.Headers sourceHeaders, { required ResponseHost Function() cloneHost, required Body? Function() body, }) { - if (init?.status == null && snapshot.status == 0) { - return Response._(cloneHost(), snapshot); + final effectiveHeaders = js_headers.Headers(init?.headers ?? sourceHeaders); + if (init?.status == null && metadata.status == 0) { + return Response._( + cloneHost(), + metadata: metadata, + headers: effectiveHeaders, + ); } final nativeCopy = _nativeResponseFromCopy( body(), - snapshot: snapshot, + metadata: metadata, + headers: effectiveHeaders, preserveMissingContentType: _shouldPreserveMissingContentType( init, - snapshot, + sourceHeaders, ), ); - return Response._( - NativeResponseHost(nativeCopy), - snapshot.withHeaders(js_headers.Headers(nativeCopy.headers)), - ); + return Response._(NativeResponseHost(nativeCopy), metadata: metadata); } static int _validateStatus(int status) { @@ -457,22 +463,23 @@ class Response implements native.Response { static bool _shouldPreserveMissingContentType( native.ResponseInit? init, - _ResponseSnapshot snapshot, + js_headers.Headers sourceHeaders, ) { - return init?.headers == null && !snapshot.headers.has('content-type'); + return init?.headers == null && !sourceHeaders.has('content-type'); } static native.Response _nativeResponseFromCopy( Body? body, { - required _ResponseSnapshot snapshot, + required _ResponseMetadata metadata, + required Object? headers, required bool preserveMissingContentType, }) { final response = native.Response( body, native.ResponseInit( - status: snapshot.status, - statusText: snapshot.statusText, - headers: snapshot.headers, + status: metadata.status, + statusText: metadata.statusText, + headers: headers, ), ); if (preserveMissingContentType) { diff --git a/test/response_js_test.dart b/test/response_js_test.dart index 0ac537b..214bac1 100644 --- a/test/response_js_test.dart +++ b/test/response_js_test.dart @@ -277,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', () {