From 8af940213b59a6bf2f03f5c5b01c8a5e767172bd Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Mon, 22 Jun 2026 18:13:28 +0800 Subject: [PATCH 1/9] fix(fetch): copy wrapped response inputs --- lib/src/fetch/response.io.dart | 40 ++++++++++- lib/src/fetch/response.js.dart | 57 ++++++++++++++- test/response_io_test.dart | 98 +++++++++++++++++++++++++ test/response_js_test.dart | 126 +++++++++++++++++++++++++++++++++ 4 files changed, 317 insertions(+), 4 deletions(-) diff --git a/lib/src/fetch/response.io.dart b/lib/src/fetch/response.io.dart index cd547b0..f140545 100644 --- a/lib/src/fetch/response.io.dart +++ b/lib/src/fetch/response.io.dart @@ -27,11 +27,19 @@ class Response implements native.Response { factory Response([Object? body, native.ResponseInit? init]) { final host = switch ((body, init)) { - (final Response response, _) => response._host, + (final Response response, null) => response.clone()._host, + (final Response response, _) => NativeResponseHost( + _nativeResponseFromWrappedResponse(response, init), + ), (final io.HttpClientResponse response, null) => HttpClientResponseHost( response, ), - (final native.Response response, _) => NativeResponseHost(response), + (final native.Response response, null) => NativeResponseHost( + response.clone(), + ), + (final native.Response response, _) => NativeResponseHost( + _nativeResponseFromNativeResponse(response, init), + ), _ => NativeResponseHost(native.Response(body, init)), }; @@ -218,4 +226,32 @@ class Response implements native.Response { HttpStatus.notModified, }.contains(status); } + + static native.Response _nativeResponseFromWrappedResponse( + Response response, + native.ResponseInit? init, + ) { + return native.Response( + response.body, + native.ResponseInit( + status: init?.status ?? response.status, + statusText: init?.statusText ?? response.statusText, + headers: init?.headers ?? io_headers.Headers(response.headers), + ), + ); + } + + static native.Response _nativeResponseFromNativeResponse( + native.Response response, + native.ResponseInit? init, + ) { + return native.Response( + response.body, + native.ResponseInit( + status: init?.status ?? response.status, + statusText: init?.statusText ?? response.statusText, + headers: init?.headers ?? io_headers.Headers(response.headers), + ), + ); + } } diff --git a/lib/src/fetch/response.js.dart b/lib/src/fetch/response.js.dart index a3b6897..32548f1 100644 --- a/lib/src/fetch/response.js.dart +++ b/lib/src/fetch/response.js.dart @@ -33,9 +33,20 @@ class Response implements native.Response { factory Response([Object? body, native.ResponseInit? init]) { final host = switch ((body, init)) { - (final Response response, _) => response._host, + (final Response response, null) => response.clone()._host, + (final Response response, _) => NativeResponseHost( + _nativeResponseFromWrappedResponse(response, init), + ), (final web.Response response, null) => WebResponseHost(response), - (final native.Response response, _) => NativeResponseHost(response), + (final web.Response response, _) => NativeResponseHost( + _nativeResponseFromWebResponse(response, init), + ), + (final native.Response response, null) => NativeResponseHost( + response.clone(), + ), + (final native.Response response, _) => NativeResponseHost( + _nativeResponseFromNativeResponse(response, init), + ), _ => NativeResponseHost(native.Response(body, init)), }; @@ -237,4 +248,46 @@ class Response implements native.Response { _ => native.ResponseType.default_, }; } + + static native.Response _nativeResponseFromWrappedResponse( + Response response, + native.ResponseInit? init, + ) { + return native.Response( + _bodyFromWrappedResponse(response), + native.ResponseInit( + status: init?.status ?? response.status, + statusText: init?.statusText ?? response.statusText, + headers: init?.headers ?? js_headers.Headers(response.headers), + ), + ); + } + + static native.Response _nativeResponseFromWebResponse( + web.Response response, + native.ResponseInit? init, + ) { + return _nativeResponseFromWrappedResponse(Response(response), init); + } + + static native.Response _nativeResponseFromNativeResponse( + native.Response response, + native.ResponseInit? init, + ) { + return native.Response( + response.body, + native.ResponseInit( + status: init?.status ?? response.status, + statusText: init?.statusText ?? response.statusText, + headers: init?.headers ?? js_headers.Headers(response.headers), + ), + ); + } + + static Body? _bodyFromWrappedResponse(Response response) { + return switch (response._host) { + final WebResponseHost host => Response(host.value.clone()).body, + NativeResponseHost() => response.body, + }; + } } diff --git a/test/response_io_test.dart b/test/response_io_test.dart index e81ee67..d814243 100644 --- a/test/response_io_test.dart +++ b/test/response_io_test.dart @@ -35,6 +35,104 @@ void main() { ); }); + test('clones wrapped responses without aliasing body state', () async { + final upstream = Response( + native.Response( + 'cloned response', + native.ResponseInit( + status: 202, + statusText: 'Accepted', + headers: {'x-source': '1'}, + ), + ), + ); + final clone = Response(upstream); + + expect(clone.status, 202); + expect(clone.statusText, 'Accepted'); + expect(clone.headers.get('x-source'), '1'); + expect(upstream.bodyUsed, isFalse); + expect(clone.bodyUsed, isFalse); + + expect(await upstream.text(), 'cloned response'); + expect(upstream.bodyUsed, isTrue); + expect(clone.bodyUsed, isFalse); + expect(await clone.text(), 'cloned response'); + expect(clone.bodyUsed, isTrue); + }); + + test('applies init overrides when copying wrapped responses', () async { + final upstream = Response( + native.Response( + 'source body', + native.ResponseInit( + status: 202, + statusText: 'Accepted', + headers: {'x-source': '1'}, + ), + ), + ); + + final response = Response( + upstream, + native.ResponseInit( + status: 201, + statusText: 'Created', + headers: {'x-init': '1'}, + ), + ); + + expect(response.status, io.HttpStatus.created); + expect(response.statusText, 'Created'); + expect(response.headers.get('x-source'), isNull); + expect(response.headers.get('x-init'), '1'); + expect(upstream.bodyUsed, isFalse); + expect(await response.text(), 'source body'); + expect(upstream.bodyUsed, isFalse); + expect(await upstream.text(), 'source body'); + }); + + test('applies init overrides when copying native responses', () async { + final upstream = native.Response( + 'native body', + native.ResponseInit( + status: 202, + statusText: 'Accepted', + headers: {'x-source': '1'}, + ), + ); + + final response = Response( + upstream, + native.ResponseInit( + status: 201, + statusText: 'Created', + headers: {'x-init': '1'}, + ), + ); + + expect(response.status, io.HttpStatus.created); + expect(response.statusText, 'Created'); + expect(response.headers.get('x-source'), isNull); + expect(response.headers.get('x-init'), '1'); + expect(upstream.bodyUsed, isFalse); + expect(await response.text(), 'native body'); + expect(upstream.bodyUsed, isFalse); + expect(await upstream.text(), 'native body'); + }); + + test('rejects copying consumed wrapped responses', () async { + final upstream = Response('used body'); + + expect(await upstream.text(), 'used body'); + expect(upstream.bodyUsed, isTrue); + expect(() => Response(upstream), throwsStateError); + expect( + () => Response(upstream, const native.ResponseInit(status: 201)), + throwsStateError, + ); + }); + test('copies raw HttpHeaders before appending body content-type', () async { final server = await io.HttpServer.bind( io.InternetAddress.loopbackIPv4, diff --git a/test/response_js_test.dart b/test/response_js_test.dart index 6b4d062..4df97f0 100644 --- a/test/response_js_test.dart +++ b/test/response_js_test.dart @@ -42,6 +42,132 @@ void main() { expect(response.headers.get('content-type'), 'text/plain;charset=UTF-8'); }); + test('clones wrapped responses without aliasing body state', () async { + final upstream = Response( + web.Response( + 'cloned response'.toJS, + web.ResponseInit( + status: 202, + statusText: 'Accepted', + headers: {'x-source': '1'}.jsify()! as web.HeadersInit, + ), + ), + ); + final clone = Response(upstream); + + expect(clone.status, 202); + expect(clone.statusText, 'Accepted'); + expect(clone.headers.get('x-source'), '1'); + expect(upstream.bodyUsed, isFalse); + expect(clone.bodyUsed, isFalse); + + expect(await upstream.text(), 'cloned response'); + expect(upstream.bodyUsed, isTrue); + expect(clone.bodyUsed, isFalse); + expect(await clone.text(), 'cloned response'); + expect(clone.bodyUsed, isTrue); + }); + + test('applies init overrides when copying wrapped responses', () async { + final upstream = Response( + web.Response( + 'source body'.toJS, + web.ResponseInit( + status: 202, + statusText: 'Accepted', + headers: {'x-source': '1'}.jsify()! as web.HeadersInit, + ), + ), + ); + + final response = Response( + upstream, + native.ResponseInit( + status: 201, + statusText: 'Created', + headers: {'x-init': '1'}, + ), + ); + + expect(response.status, 201); + expect(response.statusText, 'Created'); + expect(response.headers.get('x-source'), isNull); + expect(response.headers.get('x-init'), '1'); + expect(upstream.bodyUsed, isFalse); + expect(await response.text(), 'source body'); + expect(upstream.bodyUsed, isFalse); + expect(await upstream.text(), 'source body'); + }); + + test('applies init overrides when copying native responses', () async { + final upstream = native.Response( + 'native body', + native.ResponseInit( + status: 202, + statusText: 'Accepted', + headers: {'x-source': '1'}, + ), + ); + + final response = Response( + upstream, + native.ResponseInit( + status: 201, + statusText: 'Created', + headers: {'x-init': '1'}, + ), + ); + + expect(response.status, 201); + expect(response.statusText, 'Created'); + expect(response.headers.get('x-source'), isNull); + expect(response.headers.get('x-init'), '1'); + expect(upstream.bodyUsed, isFalse); + expect(await response.text(), 'native body'); + expect(upstream.bodyUsed, isFalse); + expect(await upstream.text(), 'native body'); + }); + + test('applies init overrides when copying web responses', () async { + final upstream = web.Response( + 'web body'.toJS, + web.ResponseInit( + status: 202, + statusText: 'Accepted', + headers: {'x-source': '1'}.jsify()! as web.HeadersInit, + ), + ); + + final response = Response( + upstream, + native.ResponseInit( + status: 201, + statusText: 'Created', + headers: {'x-init': '1'}, + ), + ); + + expect(response.status, 201); + expect(response.statusText, 'Created'); + expect(response.headers.get('x-source'), isNull); + expect(response.headers.get('x-init'), '1'); + expect(upstream.bodyUsed, isFalse); + expect(await response.text(), 'web body'); + expect(upstream.bodyUsed, isFalse); + }); + + test('rejects copying consumed wrapped responses', () async { + final upstream = Response('used body'); + + expect(await upstream.text(), 'used body'); + expect(upstream.bodyUsed, isTrue); + expect(() => Response(upstream), throwsStateError); + expect( + () => Response(upstream, const native.ResponseInit(status: 201)), + throwsStateError, + ); + }); + test('clone tees a wrapped web.Response body', () async { final upstream = web.Response('cloned body'.toJS); From 21b3f7bbad50f7142b7da8ad3746619829398330 Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Mon, 22 Jun 2026 18:21:24 +0800 Subject: [PATCH 2/9] fix(fetch): preserve null-body response copies --- lib/src/fetch/response.io.dart | 2 +- test/response_io_test.dart | 42 ++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/lib/src/fetch/response.io.dart b/lib/src/fetch/response.io.dart index f140545..d61ac83 100644 --- a/lib/src/fetch/response.io.dart +++ b/lib/src/fetch/response.io.dart @@ -232,7 +232,7 @@ class Response implements native.Response { native.ResponseInit? init, ) { return native.Response( - response.body, + response._bodyForNativeClone(), native.ResponseInit( status: init?.status ?? response.status, statusText: init?.statusText ?? response.statusText, diff --git a/test/response_io_test.dart b/test/response_io_test.dart index d814243..311c634 100644 --- a/test/response_io_test.dart +++ b/test/response_io_test.dart @@ -133,6 +133,48 @@ void main() { ); }); + test( + 'copies null-body HttpClientResponse wrappers with init overrides', + () async { + final server = await io.HttpServer.bind( + io.InternetAddress.loopbackIPv4, + 0, + ); + addTearDown(server.close); + + server.listen((request) { + request.response + ..statusCode = io.HttpStatus.noContent + ..close(); + }); + + final client = io.HttpClient(); + addTearDown(client.close); + + final httpRequest = await client.getUrl( + Uri.parse('http://${server.address.host}:${server.port}/empty'), + ); + final httpResponse = await httpRequest.close(); + + final upstream = Response(httpResponse); + + final response = Response( + upstream, + native.ResponseInit( + statusText: 'No Content', + headers: {'x-init': '1'}, + ), + ); + + expect(response.status, io.HttpStatus.noContent); + expect(response.statusText, 'No Content'); + expect(response.headers.get('x-source'), isNull); + expect(response.headers.get('x-init'), '1'); + expect(response.body, isNull); + expect(await response.text(), ''); + }, + ); + test('copies raw HttpHeaders before appending body content-type', () async { final server = await io.HttpServer.bind( io.InternetAddress.loopbackIPv4, From 6566ca87724c2541868b730675e08850fa46e1bb Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Mon, 22 Jun 2026 18:30:14 +0800 Subject: [PATCH 3/9] fix(fetch): avoid duplicate response body tees --- lib/src/fetch/response.io.dart | 10 +++++++++- lib/src/fetch/response.js.dart | 11 ++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/lib/src/fetch/response.io.dart b/lib/src/fetch/response.io.dart index d61ac83..896bf5d 100644 --- a/lib/src/fetch/response.io.dart +++ b/lib/src/fetch/response.io.dart @@ -232,7 +232,7 @@ class Response implements native.Response { native.ResponseInit? init, ) { return native.Response( - response._bodyForNativeClone(), + response._bodyForNativeCopy(), native.ResponseInit( status: init?.status ?? response.status, statusText: init?.statusText ?? response.statusText, @@ -254,4 +254,12 @@ class Response implements native.Response { ), ); } + + Body? _bodyForNativeCopy() { + if (!_statusAllowsBody(status)) { + return null; + } + + return body; + } } diff --git a/lib/src/fetch/response.js.dart b/lib/src/fetch/response.js.dart index 32548f1..6dd6bcc 100644 --- a/lib/src/fetch/response.js.dart +++ b/lib/src/fetch/response.js.dart @@ -254,7 +254,7 @@ class Response implements native.Response { native.ResponseInit? init, ) { return native.Response( - _bodyFromWrappedResponse(response), + _bodyInitFromWrappedResponse(response), native.ResponseInit( status: init?.status ?? response.status, statusText: init?.statusText ?? response.statusText, @@ -284,9 +284,14 @@ class Response implements native.Response { ); } - static Body? _bodyFromWrappedResponse(Response response) { + static BodyInit? _bodyInitFromWrappedResponse(Response response) { return switch (response._host) { - final WebResponseHost host => Response(host.value.clone()).body, + final WebResponseHost host => switch (host.value.clone().body) { + final web.ReadableStream stream => dartByteStreamFromWebReadableStream( + stream, + ), + null => null, + }, NativeResponseHost() => response.body, }; } From 49e8c1035457d06e07e623a0362eae70196b4863 Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Mon, 22 Jun 2026 18:35:02 +0800 Subject: [PATCH 4/9] fix(fetch): delegate web response copies --- lib/src/fetch/response.js.dart | 59 ++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/lib/src/fetch/response.js.dart b/lib/src/fetch/response.js.dart index 6dd6bcc..9fe8d80 100644 --- a/lib/src/fetch/response.js.dart +++ b/lib/src/fetch/response.js.dart @@ -34,12 +34,13 @@ class Response implements native.Response { factory Response([Object? body, native.ResponseInit? init]) { final host = switch ((body, init)) { (final Response response, null) => response.clone()._host, - (final Response response, _) => NativeResponseHost( - _nativeResponseFromWrappedResponse(response, init), + (final Response response, _) => _responseHostFromWrappedResponse( + response, + init, ), (final web.Response response, null) => WebResponseHost(response), - (final web.Response response, _) => NativeResponseHost( - _nativeResponseFromWebResponse(response, init), + (final web.Response response, _) => WebResponseHost( + _webResponseFromWebResponse(response, init), ), (final native.Response response, null) => NativeResponseHost( response.clone(), @@ -249,25 +250,47 @@ class Response implements native.Response { }; } - static native.Response _nativeResponseFromWrappedResponse( + static ResponseHost _responseHostFromWrappedResponse( Response response, native.ResponseInit? init, ) { - return native.Response( - _bodyInitFromWrappedResponse(response), - native.ResponseInit( + return switch (response._host) { + final WebResponseHost host => WebResponseHost( + _webResponseFromWebResponse(host.value, init), + ), + NativeResponseHost() => NativeResponseHost( + _nativeResponseFromNativeWrappedResponse(response, init), + ), + }; + } + + static web.Response _webResponseFromWebResponse( + web.Response response, + native.ResponseInit? init, + ) { + final source = response.clone(); + return web.Response( + source.body, + web.ResponseInit( status: init?.status ?? response.status, statusText: init?.statusText ?? response.statusText, - headers: init?.headers ?? js_headers.Headers(response.headers), + headers: js_headers.Headers(init?.headers ?? response.headers).host, ), ); } - static native.Response _nativeResponseFromWebResponse( - web.Response response, + static native.Response _nativeResponseFromNativeWrappedResponse( + Response response, native.ResponseInit? init, ) { - return _nativeResponseFromWrappedResponse(Response(response), init); + return native.Response( + response.body, + native.ResponseInit( + status: init?.status ?? response.status, + statusText: init?.statusText ?? response.statusText, + headers: init?.headers ?? js_headers.Headers(response.headers), + ), + ); } static native.Response _nativeResponseFromNativeResponse( @@ -283,16 +306,4 @@ class Response implements native.Response { ), ); } - - static BodyInit? _bodyInitFromWrappedResponse(Response response) { - return switch (response._host) { - final WebResponseHost host => switch (host.value.clone().body) { - final web.ReadableStream stream => dartByteStreamFromWebReadableStream( - stream, - ), - null => null, - }, - NativeResponseHost() => response.body, - }; - } } From f99992ab9b204debb26292619cf019d0e0d03c00 Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Mon, 22 Jun 2026 18:46:38 +0800 Subject: [PATCH 5/9] fix(fetch): avoid duplicate response clone tees --- lib/src/fetch/response.io.dart | 28 ++++++++++++---------------- lib/src/fetch/response.js.dart | 8 ++++++-- test/response_js_test.dart | 4 ++++ 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/lib/src/fetch/response.io.dart b/lib/src/fetch/response.io.dart index 896bf5d..dc2b050 100644 --- a/lib/src/fetch/response.io.dart +++ b/lib/src/fetch/response.io.dart @@ -197,28 +197,24 @@ class Response implements native.Response { @override Response clone() { return switch (_host) { - final NativeResponseHost host => Response(host.value.clone()), - final HttpClientResponseHost _ => Response( - native.Response( - _bodyForNativeClone(), - native.ResponseInit( - status: status, - statusText: statusText, - headers: io_headers.Headers(headers), + final NativeResponseHost host => Response._( + NativeResponseHost(host.value.clone()), + ), + final HttpClientResponseHost _ => Response._( + NativeResponseHost( + native.Response( + _bodyForNativeCopy(), + native.ResponseInit( + status: status, + statusText: statusText, + headers: io_headers.Headers(headers), + ), ), ), ), }; } - Body? _bodyForNativeClone() { - if (!_statusAllowsBody(status)) { - return null; - } - - return body?.clone(); - } - static bool _statusAllowsBody(int status) { return !const { HttpStatus.noContent, diff --git a/lib/src/fetch/response.js.dart b/lib/src/fetch/response.js.dart index 9fe8d80..afb0c5b 100644 --- a/lib/src/fetch/response.js.dart +++ b/lib/src/fetch/response.js.dart @@ -234,8 +234,12 @@ class Response implements native.Response { @override Response clone() { return switch (_host) { - final WebResponseHost host => Response(host.value.clone()), - final NativeResponseHost host => Response(host.value.clone()), + final WebResponseHost host => Response._( + WebResponseHost(host.value.clone()), + ), + final NativeResponseHost host => Response._( + NativeResponseHost(host.value.clone()), + ), }; } diff --git a/test/response_js_test.dart b/test/response_js_test.dart index 4df97f0..1312ef7 100644 --- a/test/response_js_test.dart +++ b/test/response_js_test.dart @@ -154,6 +154,10 @@ void main() { expect(upstream.bodyUsed, isFalse); expect(await response.text(), 'web body'); expect(upstream.bodyUsed, isFalse); + expect( + await upstream.text().toDart.then((text) => text.toDart), + 'web body', + ); }); test('rejects copying consumed wrapped responses', () async { From 025ce1e32885ca252de4251975c61cb66a8ad941 Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Mon, 22 Jun 2026 18:57:13 +0800 Subject: [PATCH 6/9] fix(fetch): preserve copied response headers --- lib/src/fetch/response.io.dart | 43 ++++++++++++++++++++++++++-------- lib/src/fetch/response.js.dart | 43 ++++++++++++++++++++++++++-------- test/response_io_test.dart | 35 +++++++++++++++++++++++++++ test/response_js_test.dart | 35 +++++++++++++++++++++++++++ 4 files changed, 136 insertions(+), 20 deletions(-) diff --git a/lib/src/fetch/response.io.dart b/lib/src/fetch/response.io.dart index dc2b050..0e4800f 100644 --- a/lib/src/fetch/response.io.dart +++ b/lib/src/fetch/response.io.dart @@ -227,13 +227,14 @@ class Response implements native.Response { Response response, native.ResponseInit? init, ) { - return native.Response( + final sourceHeaders = io_headers.Headers(response.headers); + return _nativeResponseFromCopy( response._bodyForNativeCopy(), - native.ResponseInit( - status: init?.status ?? response.status, - statusText: init?.statusText ?? response.statusText, - headers: init?.headers ?? io_headers.Headers(response.headers), - ), + status: init?.status ?? response.status, + statusText: init?.statusText ?? response.statusText, + headers: init?.headers ?? sourceHeaders, + preserveMissingContentType: + init?.headers == null && !sourceHeaders.has('content-type'), ); } @@ -241,14 +242,36 @@ class Response implements native.Response { native.Response response, native.ResponseInit? init, ) { - return native.Response( + 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'), + ); + } + + static native.Response _nativeResponseFromCopy( + Body? body, { + required int status, + required String statusText, + required Object? headers, + required bool preserveMissingContentType, + }) { + final response = native.Response( + body, native.ResponseInit( - status: init?.status ?? response.status, - statusText: init?.statusText ?? response.statusText, - headers: init?.headers ?? io_headers.Headers(response.headers), + status: status, + statusText: statusText, + headers: headers, ), ); + if (preserveMissingContentType) { + response.headers.delete('content-type'); + } + return response; } Body? _bodyForNativeCopy() { diff --git a/lib/src/fetch/response.js.dart b/lib/src/fetch/response.js.dart index afb0c5b..06f9f61 100644 --- a/lib/src/fetch/response.js.dart +++ b/lib/src/fetch/response.js.dart @@ -287,13 +287,14 @@ class Response implements native.Response { Response response, native.ResponseInit? init, ) { - return native.Response( + final sourceHeaders = js_headers.Headers(response.headers); + return _nativeResponseFromCopy( response.body, - native.ResponseInit( - status: init?.status ?? response.status, - statusText: init?.statusText ?? response.statusText, - headers: init?.headers ?? js_headers.Headers(response.headers), - ), + status: init?.status ?? response.status, + statusText: init?.statusText ?? response.statusText, + headers: init?.headers ?? sourceHeaders, + preserveMissingContentType: + init?.headers == null && !sourceHeaders.has('content-type'), ); } @@ -301,13 +302,35 @@ class Response implements native.Response { native.Response response, native.ResponseInit? init, ) { - return native.Response( + 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 _nativeResponseFromCopy( + Body? body, { + required int status, + required String statusText, + required Object? headers, + required bool preserveMissingContentType, + }) { + final response = native.Response( + body, native.ResponseInit( - status: init?.status ?? response.status, - statusText: init?.statusText ?? response.statusText, - headers: init?.headers ?? js_headers.Headers(response.headers), + status: status, + statusText: statusText, + headers: headers, ), ); + if (preserveMissingContentType) { + response.headers.delete('content-type'); + } + return response; } } diff --git a/test/response_io_test.dart b/test/response_io_test.dart index 311c634..bf5bc37 100644 --- a/test/response_io_test.dart +++ b/test/response_io_test.dart @@ -121,6 +121,41 @@ void main() { expect(await upstream.text(), 'native body'); }); + test( + 'preserves deleted body-derived content-type when copying responses', + () async { + final wrapped = Response('wrapped body'); + expect(wrapped.headers.get('content-type'), 'text/plain;charset=UTF-8'); + + wrapped.headers.delete('content-type'); + final wrappedCopy = Response( + wrapped, + const native.ResponseInit(statusText: 'OK'), + ); + + expect(wrappedCopy.statusText, 'OK'); + expect(wrappedCopy.headers.get('content-type'), isNull); + expect(wrapped.bodyUsed, isFalse); + expect(await wrappedCopy.text(), 'wrapped body'); + expect(wrapped.bodyUsed, isFalse); + expect(await wrapped.text(), 'wrapped body'); + + final upstream = native.Response('native body'); + upstream.headers.delete('content-type'); + final nativeCopy = Response( + upstream, + const native.ResponseInit(statusText: 'OK'), + ); + + expect(nativeCopy.statusText, 'OK'); + expect(nativeCopy.headers.get('content-type'), isNull); + expect(upstream.bodyUsed, isFalse); + expect(await nativeCopy.text(), 'native body'); + expect(upstream.bodyUsed, isFalse); + expect(await upstream.text(), 'native body'); + }, + ); + test('rejects copying consumed wrapped responses', () async { final upstream = Response('used body'); diff --git a/test/response_js_test.dart b/test/response_js_test.dart index 1312ef7..e6abcdf 100644 --- a/test/response_js_test.dart +++ b/test/response_js_test.dart @@ -128,6 +128,41 @@ void main() { expect(await upstream.text(), 'native body'); }); + test( + 'preserves deleted body-derived content-type when copying responses', + () async { + final wrapped = Response('wrapped body'); + expect(wrapped.headers.get('content-type'), 'text/plain;charset=UTF-8'); + + wrapped.headers.delete('content-type'); + final wrappedCopy = Response( + wrapped, + const native.ResponseInit(statusText: 'OK'), + ); + + expect(wrappedCopy.statusText, 'OK'); + expect(wrappedCopy.headers.get('content-type'), isNull); + expect(wrapped.bodyUsed, isFalse); + expect(await wrappedCopy.text(), 'wrapped body'); + expect(wrapped.bodyUsed, isFalse); + expect(await wrapped.text(), 'wrapped body'); + + final upstream = native.Response('native body'); + upstream.headers.delete('content-type'); + final nativeCopy = Response( + upstream, + const native.ResponseInit(statusText: 'OK'), + ); + + expect(nativeCopy.statusText, 'OK'); + expect(nativeCopy.headers.get('content-type'), isNull); + expect(upstream.bodyUsed, isFalse); + expect(await nativeCopy.text(), 'native body'); + expect(upstream.bodyUsed, isFalse); + expect(await upstream.text(), 'native body'); + }, + ); + test('applies init overrides when copying web responses', () async { final upstream = web.Response( 'web body'.toJS, From 46f28429a262739da5f8ac99d165b13af6a56b70 Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Mon, 22 Jun 2026 19:35:52 +0800 Subject: [PATCH 7/9] fix(fetch): preserve web wrapper response headers --- lib/src/fetch/response.js.dart | 15 +++++++++++---- test/response_js_test.dart | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/lib/src/fetch/response.js.dart b/lib/src/fetch/response.js.dart index 06f9f61..b124af9 100644 --- a/lib/src/fetch/response.js.dart +++ b/lib/src/fetch/response.js.dart @@ -260,7 +260,11 @@ class Response implements native.Response { ) { return switch (response._host) { final WebResponseHost host => WebResponseHost( - _webResponseFromWebResponse(host.value, init), + _webResponseFromWebResponse( + host.value, + init, + sourceHeaders: js_headers.Headers(response.headers), + ), ), NativeResponseHost() => NativeResponseHost( _nativeResponseFromNativeWrappedResponse(response, init), @@ -270,15 +274,18 @@ class Response implements native.Response { static web.Response _webResponseFromWebResponse( web.Response response, - native.ResponseInit? init, - ) { + native.ResponseInit? init, { + Object? sourceHeaders, + }) { final source = response.clone(); return web.Response( source.body, web.ResponseInit( status: init?.status ?? response.status, statusText: init?.statusText ?? response.statusText, - headers: js_headers.Headers(init?.headers ?? response.headers).host, + headers: js_headers.Headers( + init?.headers ?? sourceHeaders ?? response.headers, + ).host, ), ); } diff --git a/test/response_js_test.dart b/test/response_js_test.dart index e6abcdf..f3d7e2c 100644 --- a/test/response_js_test.dart +++ b/test/response_js_test.dart @@ -163,6 +163,38 @@ void main() { }, ); + test('preserves web wrapper header mutations when copying', () async { + final upstream = Response( + web.Response( + 'web wrapped body'.toJS, + web.ResponseInit( + status: 202, + statusText: 'Accepted', + headers: + {'content-type': 'text/plain', 'x-source': '1'}.jsify()! + as web.HeadersInit, + ), + ), + ); + upstream.headers + ..delete('content-type') + ..set('x-source', '2'); + + final response = Response( + upstream, + const native.ResponseInit(statusText: 'OK'), + ); + + expect(response.status, 202); + expect(response.statusText, 'OK'); + expect(response.headers.get('content-type'), isNull); + expect(response.headers.get('x-source'), '2'); + expect(upstream.bodyUsed, isFalse); + expect(await response.text(), 'web wrapped body'); + expect(upstream.bodyUsed, isFalse); + expect(await upstream.text(), 'web wrapped body'); + }); + test('applies init overrides when copying web responses', () async { final upstream = web.Response( 'web body'.toJS, From adea8f24f17c056c6be0dd0ba2ea9a8603eac4b1 Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Mon, 22 Jun 2026 19:47:37 +0800 Subject: [PATCH 8/9] fix(fetch): preserve copied response metadata --- lib/src/fetch/response.io.dart | 97 +++++++++++++++--- lib/src/fetch/response.js.dart | 173 ++++++++++++++++++++++++++------- test/response_io_test.dart | 8 ++ test/response_js_test.dart | 24 +++++ 4 files changed, 255 insertions(+), 47 deletions(-) diff --git a/lib/src/fetch/response.io.dart b/lib/src/fetch/response.io.dart index 0e4800f..743f25e 100644 --- a/lib/src/fetch/response.io.dart +++ b/lib/src/fetch/response.io.dart @@ -23,27 +23,40 @@ final class NativeResponseHost extends ResponseHost { } class Response implements native.Response { - Response._(this._host); + 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; factory Response([Object? body, native.ResponseInit? init]) { - final host = switch ((body, init)) { - (final Response response, null) => response.clone()._host, - (final Response response, _) => NativeResponseHost( - _nativeResponseFromWrappedResponse(response, init), - ), - (final io.HttpClientResponse response, null) => HttpClientResponseHost( + return switch ((body, init)) { + (final Response response, null) => response.clone(), + (final Response response, _) => _responseFromWrappedResponse( response, + init, + ), + (final io.HttpClientResponse response, null) => Response._( + HttpClientResponseHost(response), ), - (final native.Response response, null) => NativeResponseHost( - response.clone(), + (final native.Response response, null) => Response._( + NativeResponseHost(response.clone()), ), - (final native.Response response, _) => NativeResponseHost( - _nativeResponseFromNativeResponse(response, init), + (final native.Response response, _) => _responseFromNativeResponse( + response, + init, ), - _ => NativeResponseHost(native.Response(body, init)), + _ => Response._(NativeResponseHost(native.Response(body, init))), }; - - return Response._(host); } factory Response.error() => Response(native.Response.error()); @@ -59,6 +72,11 @@ class Response implements native.Response { final ResponseHost _host; 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 { @@ -89,6 +107,9 @@ class Response implements native.Response { @override bool get ok { + final status = _status; + if (status != null) return HttpStatus.isSuccess(status); + return switch (_host) { final HttpClientResponseHost host => HttpStatus.isSuccess( host.value.statusCode, @@ -99,6 +120,9 @@ class Response implements native.Response { @override bool get redirected { + final redirected = _redirected; + if (redirected != null) return redirected; + return switch (_host) { final HttpClientResponseHost host => host.value.redirects.isNotEmpty, final NativeResponseHost host => host.value.redirected, @@ -107,6 +131,9 @@ class Response implements native.Response { @override int get status { + final status = _status; + if (status != null) return status; + return switch (_host) { final HttpClientResponseHost host => host.value.statusCode, final NativeResponseHost host => host.value.status, @@ -115,6 +142,9 @@ class Response implements native.Response { @override String get statusText { + final statusText = _statusText; + if (statusText != null) return statusText; + return switch (_host) { final HttpClientResponseHost host => host.value.reasonPhrase, final NativeResponseHost host => host.value.statusText, @@ -123,6 +153,9 @@ class Response implements native.Response { @override native.ResponseType get type { + final type = _type; + if (type != null) return type; + return switch (_host) { final HttpClientResponseHost _ => native.ResponseType.default_, final NativeResponseHost host => host.value.type, @@ -131,6 +164,9 @@ class Response implements native.Response { @override String get url { + final url = _url; + if (url != null) return url; + return switch (_host) { final HttpClientResponseHost _ => '', final NativeResponseHost host => host.value.url, @@ -199,6 +235,12 @@ class Response implements native.Response { 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, ), final HttpClientResponseHost _ => Response._( NativeResponseHost( @@ -211,6 +253,9 @@ class Response implements native.Response { ), ), ), + redirected: redirected, + type: type, + url: url, ), }; } @@ -223,6 +268,30 @@ class Response implements native.Response { }.contains(status); } + static Response _responseFromWrappedResponse( + Response response, + native.ResponseInit? init, + ) { + return Response._( + NativeResponseHost(_nativeResponseFromWrappedResponse(response, init)), + redirected: response.redirected, + type: response.type, + url: response.url, + ); + } + + static Response _responseFromNativeResponse( + native.Response response, + native.ResponseInit? init, + ) { + return Response._( + NativeResponseHost(_nativeResponseFromNativeResponse(response, init)), + redirected: response.redirected, + type: response.type, + url: response.url, + ); + } + static native.Response _nativeResponseFromWrappedResponse( Response response, native.ResponseInit? init, diff --git a/lib/src/fetch/response.js.dart b/lib/src/fetch/response.js.dart index b124af9..7baafd9 100644 --- a/lib/src/fetch/response.js.dart +++ b/lib/src/fetch/response.js.dart @@ -29,29 +29,44 @@ final class NativeResponseHost extends ResponseHost { } class Response implements native.Response { - Response._(this._host); + 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; factory Response([Object? body, native.ResponseInit? init]) { - final host = switch ((body, init)) { - (final Response response, null) => response.clone()._host, - (final Response response, _) => _responseHostFromWrappedResponse( + return switch ((body, init)) { + (final Response response, null) => response.clone(), + (final Response response, _) => _responseFromWrappedResponse( response, init, ), - (final web.Response response, null) => WebResponseHost(response), - (final web.Response response, _) => WebResponseHost( - _webResponseFromWebResponse(response, init), + (final web.Response response, null) => Response._( + WebResponseHost(response), ), - (final native.Response response, null) => NativeResponseHost( - response.clone(), + (final web.Response response, _) => _responseFromWebResponse( + response, + init, + ), + (final native.Response response, null) => Response._( + NativeResponseHost(response.clone()), ), - (final native.Response response, _) => NativeResponseHost( - _nativeResponseFromNativeResponse(response, init), + (final native.Response response, _) => _responseFromNativeResponse( + response, + init, ), - _ => NativeResponseHost(native.Response(body, init)), + _ => Response._(NativeResponseHost(native.Response(body, init))), }; - - return Response._(host); } factory Response.error() => Response._(WebResponseHost(web.Response.error())); @@ -69,6 +84,11 @@ class Response implements native.Response { final ResponseHost _host; 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 { @@ -107,6 +127,9 @@ class Response implements native.Response { @override bool get ok { + final status = _status; + if (status != null) return status >= 200 && status <= 299; + return switch (_host) { final WebResponseHost host => host.value.ok, final NativeResponseHost host => host.value.ok, @@ -115,6 +138,9 @@ class Response implements native.Response { @override bool get redirected { + final redirected = _redirected; + if (redirected != null) return redirected; + return switch (_host) { final WebResponseHost host => host.value.redirected, final NativeResponseHost host => host.value.redirected, @@ -123,6 +149,9 @@ class Response implements native.Response { @override int get status { + final status = _status; + if (status != null) return status; + return switch (_host) { final WebResponseHost host => host.value.status, final NativeResponseHost host => host.value.status, @@ -131,6 +160,9 @@ class Response implements native.Response { @override String get statusText { + final statusText = _statusText; + if (statusText != null) return statusText; + return switch (_host) { final WebResponseHost host => host.value.statusText, final NativeResponseHost host => host.value.statusText, @@ -139,6 +171,9 @@ class Response implements native.Response { @override native.ResponseType get type { + final type = _type; + if (type != null) return type; + return switch (_host) { final WebResponseHost host => _responseTypeFromValue(host.value.type), final NativeResponseHost host => host.value.type, @@ -147,6 +182,9 @@ class Response implements native.Response { @override String get url { + final url = _url; + if (url != null) return url; + return switch (_host) { final WebResponseHost host => host.value.url, final NativeResponseHost host => host.value.url, @@ -236,9 +274,21 @@ class Response implements native.Response { return switch (_host) { final WebResponseHost host => Response._( WebResponseHost(host.value.clone()), + headers: js_headers.Headers(headers), + redirected: redirected, + status: status, + statusText: statusText, + type: type, + url: url, ), final NativeResponseHost host => Response._( NativeResponseHost(host.value.clone()), + headers: js_headers.Headers(headers), + redirected: redirected, + status: status, + statusText: statusText, + type: type, + url: url, ), }; } @@ -254,42 +304,99 @@ class Response implements native.Response { }; } - static ResponseHost _responseHostFromWrappedResponse( + static Response _responseFromWrappedResponse( Response response, native.ResponseInit? init, ) { return switch (response._host) { - final WebResponseHost host => WebResponseHost( - _webResponseFromWebResponse( - host.value, - init, - sourceHeaders: js_headers.Headers(response.headers), - ), + 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, ), - NativeResponseHost() => NativeResponseHost( - _nativeResponseFromNativeWrappedResponse(response, init), + NativeResponseHost() => _responseFromNativeWrappedResponse( + response, + init, ), }; } - static web.Response _webResponseFromWebResponse( + static Response _responseFromWebResponse( web.Response response, native.ResponseInit? init, { Object? sourceHeaders, + bool? redirected, + int? status, + String? statusText, + native.ResponseType? type, + String? url, }) { - final source = response.clone(); - return web.Response( - source.body, - web.ResponseInit( - status: init?.status ?? response.status, - statusText: init?.statusText ?? response.statusText, - headers: js_headers.Headers( - init?.headers ?? sourceHeaders ?? response.headers, - ).host, + final targetStatus = _validateStatus( + init?.status ?? status ?? response.status, + ); + if (!_statusAllowsBody(targetStatus) && response.body != null) { + throw ArgumentError.value( + response, + 'body', + 'Response status $targetStatus cannot have a body.', + ); + } + + return Response._( + WebResponseHost(response.clone()), + headers: js_headers.Headers( + init?.headers ?? sourceHeaders ?? response.headers, + ), + redirected: redirected, + status: targetStatus, + statusText: init?.statusText ?? statusText ?? response.statusText, + type: type, + url: url, + ); + } + + static Response _responseFromNativeWrappedResponse( + Response response, + native.ResponseInit? init, + ) { + return Response._( + NativeResponseHost( + _nativeResponseFromNativeWrappedResponse(response, init), ), + redirected: response.redirected, + type: response.type, + url: response.url, ); } + static Response _responseFromNativeResponse( + native.Response response, + native.ResponseInit? init, + ) { + return Response._( + NativeResponseHost(_nativeResponseFromNativeResponse(response, init)), + redirected: response.redirected, + type: response.type, + url: response.url, + ); + } + + static int _validateStatus(int status) { + if (status < 200 || status > 599) { + throw RangeError.range(status, 200, 599, 'status'); + } + return status; + } + + static bool _statusAllowsBody(int status) { + return !const {204, 205, 304}.contains(status); + } + static native.Response _nativeResponseFromNativeWrappedResponse( Response response, native.ResponseInit? init, diff --git a/test/response_io_test.dart b/test/response_io_test.dart index bf5bc37..4f26c0c 100644 --- a/test/response_io_test.dart +++ b/test/response_io_test.dart @@ -326,8 +326,16 @@ void main() { final httpResponse = await httpRequest.close(); final response = Response(httpResponse); + final copy = Response( + response, + const native.ResponseInit(statusText: 'OK'), + ); expect(response.redirected, isTrue); + expect(copy.redirected, isTrue); + expect(copy.statusText, 'OK'); + expect(await copy.text(), 'ok'); + expect(response.bodyUsed, isFalse); expect(await response.text(), 'ok'); }); diff --git a/test/response_js_test.dart b/test/response_js_test.dart index f3d7e2c..b4c07ed 100644 --- a/test/response_js_test.dart +++ b/test/response_js_test.dart @@ -195,6 +195,30 @@ void main() { expect(await upstream.text(), 'web wrapped body'); }); + test('preserves fetched web metadata when copying with init', () async { + final upstream = await web.window + .fetch('data:text/plain,fetch%20metadata'.toJS) + .toDart; + final wrapped = Response(upstream); + + expect(wrapped.url, startsWith('data:text/plain')); + + final response = Response( + wrapped, + const native.ResponseInit(statusText: 'OK'), + ); + + expect(response.status, wrapped.status); + expect(response.statusText, 'OK'); + expect(response.type, wrapped.type); + expect(response.url, wrapped.url); + expect(response.redirected, wrapped.redirected); + expect(wrapped.bodyUsed, isFalse); + expect(await response.text(), 'fetch metadata'); + expect(wrapped.bodyUsed, isFalse); + expect(await wrapped.text(), 'fetch metadata'); + }); + test('applies init overrides when copying web responses', () async { final upstream = web.Response( 'web body'.toJS, From 76786f17a6204d1e6764cf3440545f733d671e95 Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Mon, 22 Jun 2026 19:58:18 +0800 Subject: [PATCH 9/9] fix(fetch): preserve zero-status response copies --- lib/src/fetch/response.io.dart | 25 ++++++++++++ lib/src/fetch/response.js.dart | 72 +++++++++++++++++++++++++++++----- test/response_io_test.dart | 32 +++++++++++++++ test/response_js_test.dart | 68 ++++++++++++++++++++++++++++++++ 4 files changed, 188 insertions(+), 9 deletions(-) diff --git a/lib/src/fetch/response.io.dart b/lib/src/fetch/response.io.dart index 743f25e..f86c9a9 100644 --- a/lib/src/fetch/response.io.dart +++ b/lib/src/fetch/response.io.dart @@ -272,6 +272,19 @@ 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, @@ -284,6 +297,18 @@ 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, diff --git a/lib/src/fetch/response.js.dart b/lib/src/fetch/response.js.dart index 7baafd9..a593957 100644 --- a/lib/src/fetch/response.js.dart +++ b/lib/src/fetch/response.js.dart @@ -57,6 +57,11 @@ 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, ), (final native.Response response, null) => Response._( NativeResponseHost(response.clone()), @@ -272,9 +277,10 @@ class Response implements native.Response { @override Response clone() { return switch (_host) { - final WebResponseHost host => Response._( - WebResponseHost(host.value.clone()), - headers: js_headers.Headers(headers), + final WebResponseHost host => _responseFromWebResponse( + host.value, + null, + sourceHeaders: js_headers.Headers(headers), redirected: redirected, status: status, statusText: statusText, @@ -336,9 +342,25 @@ class Response implements native.Response { native.ResponseType? type, String? url, }) { - final targetStatus = _validateStatus( - init?.status ?? status ?? response.status, + 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, + ); + } + + final targetStatus = _validateStatus(init?.status ?? sourceStatus); if (!_statusAllowsBody(targetStatus) && response.body != null) { throw ArgumentError.value( response, @@ -347,14 +369,21 @@ class Response implements native.Response { ); } + final source = response.clone(); return Response._( - WebResponseHost(response.clone()), - headers: js_headers.Headers( - init?.headers ?? sourceHeaders ?? response.headers, + WebResponseHost( + web.Response( + source.body, + web.ResponseInit( + status: targetStatus, + statusText: effectiveStatusText, + headers: effectiveHeaders.host, + ), + ), ), redirected: redirected, status: targetStatus, - statusText: init?.statusText ?? statusText ?? response.statusText, + statusText: effectiveStatusText, type: type, url: url, ); @@ -364,6 +393,19 @@ 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: 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), @@ -378,6 +420,18 @@ 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, + ); + } + return Response._( NativeResponseHost(_nativeResponseFromNativeResponse(response, init)), redirected: response.redirected, diff --git a/test/response_io_test.dart b/test/response_io_test.dart index 4f26c0c..c832ebc 100644 --- a/test/response_io_test.dart +++ b/test/response_io_test.dart @@ -156,6 +156,38 @@ void main() { }, ); + test('preserves zero-status error responses when copying with init', () { + final wrapped = Response.error(); + final wrappedCopy = Response( + wrapped, + native.ResponseInit( + statusText: 'Network Error', + headers: {'x-init': '1'}, + ), + ); + + expect(wrappedCopy.status, 0); + expect(wrappedCopy.statusText, 'Network Error'); + expect(wrappedCopy.ok, isFalse); + expect(wrappedCopy.type, native.ResponseType.error); + expect(wrappedCopy.headers.get('x-init'), '1'); + + final upstream = native.Response.error(); + final nativeCopy = Response( + upstream, + native.ResponseInit( + statusText: 'Network Error', + headers: {'x-init': '1'}, + ), + ); + + expect(nativeCopy.status, 0); + expect(nativeCopy.statusText, 'Network Error'); + expect(nativeCopy.ok, isFalse); + expect(nativeCopy.type, native.ResponseType.error); + expect(nativeCopy.headers.get('x-init'), '1'); + }); + test('rejects copying consumed wrapped responses', () async { final upstream = Response('used body'); diff --git a/test/response_js_test.dart b/test/response_js_test.dart index b4c07ed..fc5db42 100644 --- a/test/response_js_test.dart +++ b/test/response_js_test.dart @@ -219,6 +219,74 @@ void main() { expect(await wrapped.text(), 'fetch metadata'); }); + test('uses effective web copy headers for formData', () async { + final upstream = web.Response( + 'a=1'.toJS, + web.ResponseInit( + headers: {'content-type': 'text/plain'}.jsify()! as web.HeadersInit, + ), + ); + final response = Response( + upstream, + native.ResponseInit( + headers: {'content-type': 'application/x-www-form-urlencoded'}, + ), + ); + + final parsed = await response.formData(); + expect((parsed.get('a') as TextMultipart).value, '1'); + expect(upstream.bodyUsed, isFalse); + + final wrapped = Response( + web.Response( + 'b=2'.toJS, + web.ResponseInit( + headers: {'content-type': 'text/plain'}.jsify()! as web.HeadersInit, + ), + ), + ); + wrapped.headers.set('content-type', 'application/x-www-form-urlencoded'); + final wrappedCopy = Response( + wrapped, + const native.ResponseInit(statusText: 'OK'), + ); + + final wrappedParsed = await wrappedCopy.formData(); + expect((wrappedParsed.get('b') as TextMultipart).value, '2'); + expect(wrapped.bodyUsed, isFalse); + }); + + test('preserves zero-status web responses when copying with init', () { + final rawCopy = Response( + web.Response.error(), + native.ResponseInit( + statusText: 'Network Error', + headers: {'x-init': '1'}, + ), + ); + + expect(rawCopy.status, 0); + expect(rawCopy.statusText, 'Network Error'); + expect(rawCopy.ok, isFalse); + expect(rawCopy.type, native.ResponseType.error); + expect(rawCopy.headers.get('x-init'), '1'); + + final wrapped = Response.error(); + final wrappedCopy = Response( + wrapped, + native.ResponseInit( + statusText: 'Network Error', + headers: {'x-init': '1'}, + ), + ); + + expect(wrappedCopy.status, 0); + expect(wrappedCopy.statusText, 'Network Error'); + expect(wrappedCopy.ok, isFalse); + expect(wrappedCopy.type, native.ResponseType.error); + expect(wrappedCopy.headers.get('x-init'), '1'); + }); + test('applies init overrides when copying web responses', () async { final upstream = web.Response( 'web body'.toJS,