diff --git a/example/main.dart b/example/main.dart index 92c4c49..03fd8c3 100644 --- a/example/main.dart +++ b/example/main.dart @@ -4,7 +4,7 @@ import 'package:ht/ht.dart'; Future main() async { final request = Request( - RequestInput.uri(Uri.parse('https://api.example.com/tasks')), + Uri.parse('https://api.example.com/tasks'), RequestInit( method: HttpMethod.post, headers: Headers({'content-type': 'application/json; charset=utf-8'}), diff --git a/lib/src/fetch/request.dart b/lib/src/fetch/request.dart index a69e291..27a1a3a 100644 --- a/lib/src/fetch/request.dart +++ b/lib/src/fetch/request.dart @@ -1,10 +1,6 @@ export 'request.native.dart' show RequestInit, - RequestInput, - RequestRequestInput, - StringRequestInput, - UriRequestInput, RequestMode, RequestCredentials, RequestCache, diff --git a/lib/src/fetch/request.io.dart b/lib/src/fetch/request.io.dart index 2841965..a78dad3 100644 --- a/lib/src/fetch/request.io.dart +++ b/lib/src/fetch/request.io.dart @@ -26,14 +26,14 @@ class Request implements native.Request { Request._(this._host); factory Request(Object? input, [native.RequestInit? init]) { - final host = switch ((input, init)) { - (final Request request, _) => request._host, + return Request._(switch ((input, init)) { + (final Request request, null) => request.clone()._host, (final io.HttpRequest request, null) => HttpRequestHost(request), - (final native.Request request, _) => NativeRequestHost(request), + (final native.Request request, null) => NativeRequestHost( + request.clone(), + ), _ => NativeRequestHost(_toNativeRequest(input, init)), - }; - - return Request._(host); + }); } final RequestHost _host; @@ -231,7 +231,7 @@ class Request implements native.Request { final body = this.body; return Request( native.Request( - native.RequestInput.string(url), + url, native.RequestInit( method: method, headers: io_headers.Headers(headers), @@ -255,16 +255,10 @@ class Request implements native.Request { native.RequestInit? init, ) { return switch (input) { - final native.Request request => request, - final native.RequestInput requestInput => native.Request( - requestInput, - init, - ), - final String value => native.Request( - native.RequestInput.string(value), - init, - ), - final Uri value => native.Request(native.RequestInput.uri(value), init), + final Request request => _nativeRequestFromWrappedRequest(request, init), + final native.Request request => native.Request(request, init), + final String value => native.Request(value, init), + final Uri value => native.Request(value, init), _ => throw ArgumentError.value( input, 'input', @@ -272,4 +266,29 @@ class Request implements native.Request { ), }; } + + static native.Request _nativeRequestFromWrappedRequest( + Request request, + native.RequestInit? init, + ) { + final body = init?.body == null ? request.body : null; + + return native.Request( + request.url, + native.RequestInit( + method: init?.method ?? request.method, + headers: init?.headers ?? io_headers.Headers(request.headers), + body: init?.body ?? body?.clone(), + referrer: init?.referrer ?? request.referrer, + referrerPolicy: init?.referrerPolicy ?? request.referrerPolicy, + mode: init?.mode ?? request.mode, + credentials: init?.credentials ?? request.credentials, + cache: init?.cache ?? request.cache, + redirect: init?.redirect ?? request.redirect, + integrity: init?.integrity ?? request.integrity, + keepalive: init?.keepalive ?? request.keepalive, + duplex: init?.duplex ?? request.duplex, + ), + ); + } } diff --git a/lib/src/fetch/request.js.dart b/lib/src/fetch/request.js.dart index 1955e02..b4e78b3 100644 --- a/lib/src/fetch/request.js.dart +++ b/lib/src/fetch/request.js.dart @@ -33,14 +33,14 @@ class Request implements native.Request { Request._(this._host); factory Request(Object? input, [native.RequestInit? init]) { - final host = switch ((input, init)) { - (final Request request, _) => request._host, + return Request._(switch ((input, init)) { + (final Request request, null) => request.clone()._host, (final web.Request request, null) => WebRequestHost(request), - (final native.Request request, _) => NativeRequestHost(request), + (final native.Request request, null) => NativeRequestHost( + request.clone(), + ), _ => NativeRequestHost(_toNativeRequest(input, init)), - }; - - return Request._(host); + }); } final RequestHost _host; @@ -283,18 +283,40 @@ class Request implements native.Request { native.RequestInit? init, ) { return switch (input) { - final native.Request request => request, - final native.RequestInput requestInput => native.Request( - requestInput, - init, - ), - final String url => native.Request(native.RequestInput.string(url), init), - final Uri url => native.Request(native.RequestInput.uri(url), init), + final Request request => _nativeRequestFromWrappedRequest(request, init), + final native.Request request => native.Request(request, init), + final String _ => native.Request(input, init), + final Uri _ => native.Request(input, init), final web.Request request => _nativeRequestFromWebRequest(request, init), _ => throw ArgumentError.value(input, 'input'), }; } + static native.Request _nativeRequestFromWrappedRequest( + Request request, + native.RequestInit? init, + ) { + final body = init?.body == null ? request.body : null; + + return native.Request( + request.url, + native.RequestInit( + method: init?.method ?? request.method, + headers: init?.headers ?? js_headers.Headers(request.headers), + body: init?.body ?? body?.clone(), + referrer: init?.referrer ?? request.referrer, + referrerPolicy: init?.referrerPolicy ?? request.referrerPolicy, + mode: init?.mode ?? request.mode, + credentials: init?.credentials ?? request.credentials, + cache: init?.cache ?? request.cache, + redirect: init?.redirect ?? request.redirect, + integrity: init?.integrity ?? request.integrity, + keepalive: init?.keepalive ?? request.keepalive, + duplex: init?.duplex ?? request.duplex, + ), + ); + } + static native.Request _nativeRequestFromWebRequest( web.Request request, native.RequestInit? init, @@ -303,7 +325,7 @@ class Request implements native.Request { final body = wrapped.body; return native.Request( - native.RequestInput.string(wrapped.url), + wrapped.url, native.RequestInit( method: init?.method ?? wrapped.method, headers: init?.headers ?? js_headers.Headers(wrapped.headers), diff --git a/lib/src/fetch/request.native.dart b/lib/src/fetch/request.native.dart index b1ec004..2336b3f 100644 --- a/lib/src/fetch/request.native.dart +++ b/lib/src/fetch/request.native.dart @@ -73,28 +73,24 @@ enum RequestDuplex { final String value; } -sealed class RequestInput { - const RequestInput(); - - const factory RequestInput.request(Request value) = RequestRequestInput; - const factory RequestInput.string(String value) = StringRequestInput; - const factory RequestInput.uri(Uri value) = UriRequestInput; +sealed class _RequestInput { + const _RequestInput(); } -final class RequestRequestInput extends RequestInput { - const RequestRequestInput(this.value); +final class _RequestRequestInput extends _RequestInput { + const _RequestRequestInput(this.value); final Request value; } -final class StringRequestInput extends RequestInput { - const StringRequestInput(this.value); +final class _StringRequestInput extends _RequestInput { + const _StringRequestInput(this.value); final String value; } -final class UriRequestInput extends RequestInput { - const UriRequestInput(this.value); +final class _UriRequestInput extends _RequestInput { + const _UriRequestInput(this.value); final Uri value; } @@ -133,7 +129,10 @@ class RequestInit { /// Native request contract shell aligned with the MDN `Request` surface. class Request { - Request(RequestInput input, [RequestInit? init]) + Request(Object? input, [RequestInit? init]) + : this._(_coerceInput(_requireInput(input)), init); + + Request._(_RequestInput input, [RequestInit? init]) : headers = _headersFromInput(input, init?.headers), body = _bodyFromInput(input, init?.body), cache = _cacheFromInput(input, init?.cache), @@ -149,7 +148,6 @@ class Request { referrer = _referrerFromInput(input, init?.referrer), referrerPolicy = _referrerPolicyFromInput(input, init?.referrerPolicy), url = _urlFromInput(input); - final Headers headers; final Body? body; final RequestCache cache; @@ -221,7 +219,7 @@ class Request { Request clone() { return Request( - RequestInput.string(url), + url, RequestInit( method: method, headers: Headers(headers), @@ -239,133 +237,158 @@ class Request { ); } - static Headers _headersFromInput(RequestInput input, HeadersInit? init) { + static Headers _headersFromInput(_RequestInput input, HeadersInit? init) { if (init != null) return Headers(init); return switch (input) { - RequestRequestInput(:final value) => Headers(value.headers), + _RequestRequestInput(:final value) => Headers(value.headers), _ => Headers(), }; } - static Body? _bodyFromInput(RequestInput input, BodyInit? init) { + static Body? _bodyFromInput(_RequestInput input, BodyInit? init) { if (init != null) return Body(init); return switch (input) { - RequestRequestInput(:final value) => value.body?.clone(), + _RequestRequestInput(:final value) => value.body?.clone(), _ => null, }; } - static RequestCache _cacheFromInput(RequestInput input, RequestCache? init) { + static RequestCache _cacheFromInput(_RequestInput input, RequestCache? init) { if (init != null) return init; return switch (input) { - RequestRequestInput(:final value) => value.cache, + _RequestRequestInput(:final value) => value.cache, _ => RequestCache.default_, }; } static RequestCredentials _credentialsFromInput( - RequestInput input, + _RequestInput input, RequestCredentials? init, ) { if (init != null) return init; return switch (input) { - RequestRequestInput(:final value) => value.credentials, + _RequestRequestInput(:final value) => value.credentials, _ => RequestCredentials.sameOrigin, }; } - static String _destinationFromInput(RequestInput input) { + static String _destinationFromInput(_RequestInput input) { return switch (input) { - RequestRequestInput(:final value) => value.destination, + _RequestRequestInput(:final value) => value.destination, _ => '', }; } static RequestDuplex _duplexFromInput( - RequestInput input, + _RequestInput input, RequestDuplex? init, ) { if (init != null) return init; return switch (input) { - RequestRequestInput(:final value) => value.duplex, + _RequestRequestInput(:final value) => value.duplex, _ => RequestDuplex.half, }; } - static String _integrityFromInput(RequestInput input, String? init) { + static String _integrityFromInput(_RequestInput input, String? init) { if (init != null) return init; return switch (input) { - RequestRequestInput(:final value) => value.integrity, + _RequestRequestInput(:final value) => value.integrity, _ => '', }; } - static bool _isHistoryNavigationFromInput(RequestInput input) { + static bool _isHistoryNavigationFromInput(_RequestInput input) { return switch (input) { - RequestRequestInput(:final value) => value.isHistoryNavigation, + _RequestRequestInput(:final value) => value.isHistoryNavigation, _ => false, }; } - static bool _keepaliveFromInput(RequestInput input, bool? init) { + static bool _keepaliveFromInput(_RequestInput input, bool? init) { if (init != null) return init; return switch (input) { - RequestRequestInput(:final value) => value.keepalive, + _RequestRequestInput(:final value) => value.keepalive, _ => false, }; } - static HttpMethod _methodFromInput(RequestInput input, HttpMethod? init) { + static HttpMethod _methodFromInput(_RequestInput input, HttpMethod? init) { if (init != null) return init; return switch (input) { - RequestRequestInput(:final value) => value.method, + _RequestRequestInput(:final value) => value.method, _ => HttpMethod.get, }; } - static RequestMode _modeFromInput(RequestInput input, RequestMode? init) { + static RequestMode _modeFromInput(_RequestInput input, RequestMode? init) { if (init != null) return init; return switch (input) { - RequestRequestInput(:final value) => value.mode, + _RequestRequestInput(:final value) => value.mode, _ => RequestMode.cors, }; } static RequestRedirect _redirectFromInput( - RequestInput input, + _RequestInput input, RequestRedirect? init, ) { if (init != null) return init; return switch (input) { - RequestRequestInput(:final value) => value.redirect, + _RequestRequestInput(:final value) => value.redirect, _ => RequestRedirect.follow, }; } - static String _referrerFromInput(RequestInput input, String? init) { + static String _referrerFromInput(_RequestInput input, String? init) { if (init != null) return init; return switch (input) { - RequestRequestInput(:final value) => value.referrer, + _RequestRequestInput(:final value) => value.referrer, _ => 'about:client', }; } static RequestReferrerPolicy? _referrerPolicyFromInput( - RequestInput input, + _RequestInput input, RequestReferrerPolicy? init, ) { if (init != null) return init; return switch (input) { - RequestRequestInput(:final value) => value.referrerPolicy, + _RequestRequestInput(:final value) => value.referrerPolicy, _ => null, }; } - static String _urlFromInput(RequestInput input) { + static String _urlFromInput(_RequestInput input) { + return switch (input) { + _RequestRequestInput(:final value) => value.url, + _StringRequestInput(:final value) => Uri.parse(value).toString(), + _UriRequestInput(:final value) => value.toString(), + }; + } + + static _RequestInput _coerceInput(Object input) { return switch (input) { - RequestRequestInput(:final value) => value.url, - StringRequestInput(:final value) => Uri.parse(value).toString(), - UriRequestInput(:final value) => value.toString(), + final Request value => _RequestRequestInput(value), + final String value => _StringRequestInput(value), + final Uri value => _UriRequestInput(value), + _ => throw ArgumentError.value( + input, + 'input', + 'Unsupported request input: ${input.runtimeType}', + ), }; } + + static Object _requireInput(Object? input) { + if (input == null) { + throw ArgumentError.value( + input, + 'input', + 'Unsupported request input: ${input.runtimeType}', + ); + } + + return input; + } } diff --git a/test/headers_test.dart b/test/headers_test.dart index 3f62611..dfe9d01 100644 --- a/test/headers_test.dart +++ b/test/headers_test.dart @@ -1,3 +1,6 @@ +@TestOn('vm') +library; + import 'package:ht/ht.dart'; import 'package:test/test.dart'; diff --git a/test/public_api_surface_test.dart b/test/public_api_surface_test.dart index e076ed2..e01d62b 100644 --- a/test/public_api_surface_test.dart +++ b/test/public_api_surface_test.dart @@ -20,7 +20,7 @@ void main() { final blockBody = block.Block(['block-body'], type: 'text/plain'); final request = Request( - RequestInput.uri(Uri.parse('https://example.com/upload')), + Uri.parse('https://example.com/upload'), RequestInit(method: requestInit.method, headers: headers, body: form), ); diff --git a/test/request_io_test.dart b/test/request_io_test.dart index 68194a7..04bdfe5 100644 --- a/test/request_io_test.dart +++ b/test/request_io_test.dart @@ -1,3 +1,6 @@ +@TestOn('vm') +library; + import 'dart:io'; import 'package:ht/src/core/http_method.dart'; @@ -10,7 +13,7 @@ void main() { test('caches body for wrapped native requests', () { final request = io_request.Request( native.Request( - const native.RequestInput.string('https://example.com'), + 'https://example.com', native.RequestInit(body: 'payload'), ), ); @@ -18,6 +21,106 @@ void main() { expect(identical(request.body, request.body), isTrue); }); + test('applies init overrides when cloning from wrapped requests', () async { + final upstream = io_request.Request( + native.Request( + 'https://example.com/base', + native.RequestInit( + method: HttpMethod.post, + headers: {'x-upstream': '1'}, + body: 'payload', + cache: native.RequestCache.reload, + ), + ), + ); + + final request = io_request.Request( + upstream, + native.RequestInit( + method: HttpMethod.put, + headers: {'x-override': '2'}, + cache: native.RequestCache.noStore, + ), + ); + + expect(request.url, 'https://example.com/base'); + expect(request.method, HttpMethod.put); + expect(request.headers.get('x-upstream'), isNull); + expect(request.headers.get('x-override'), '2'); + expect(request.cache, native.RequestCache.noStore); + expect(await request.text(), 'payload'); + }); + + test('clones wrapped requests without init by teeing the body', () async { + final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); + addTearDown(server.close); + final port = server.port; + + final requestFuture = server.first; + + final client = HttpClient(); + addTearDown(client.close); + + final clientRequest = await client.post( + InternetAddress.loopbackIPv4.host, + port, + '/upstream-clone', + ); + clientRequest.write('hello world'); + final clientResponseFuture = clientRequest.close(); + + final httpRequest = await requestFuture; + final upstream = io_request.Request(httpRequest); + final clone = io_request.Request(upstream); + + expect(upstream.bodyUsed, isFalse); + expect(clone.bodyUsed, isFalse); + expect(await upstream.text(), 'hello world'); + expect(upstream.bodyUsed, isTrue); + expect(clone.bodyUsed, isFalse); + expect(await clone.text(), 'hello world'); + expect(clone.bodyUsed, isTrue); + + httpRequest.response + ..statusCode = HttpStatus.noContent + ..close(); + + final clientResponse = await clientResponseFuture; + await clientResponse.drain(); + }); + + test( + 'rebuilds consumed wrapped requests when init provides a replacement body', + () async { + final upstream = io_request.Request( + native.Request( + 'https://example.com/base', + native.RequestInit( + method: HttpMethod.post, + headers: {'x-upstream': '1'}, + body: 'payload', + ), + ), + ); + + expect(await upstream.text(), 'payload'); + expect(upstream.bodyUsed, isTrue); + + final rebuilt = io_request.Request( + upstream, + native.RequestInit(body: 'replacement', headers: {'x-override': '2'}), + ); + + expect(rebuilt.url, 'https://example.com/base'); + expect(rebuilt.method, HttpMethod.post); + expect(rebuilt.headers.get('x-upstream'), isNull); + expect(rebuilt.headers.get('x-override'), '2'); + expect(rebuilt.bodyUsed, isFalse); + expect(await rebuilt.text(), 'replacement'); + expect(rebuilt.bodyUsed, isTrue); + }, + ); + test('wraps HttpRequest without copying headers or body eagerly', () async { final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); addTearDown(server.close); diff --git a/test/request_js_test.dart b/test/request_js_test.dart index d89e9df..99002be 100644 --- a/test/request_js_test.dart +++ b/test/request_js_test.dart @@ -48,6 +48,81 @@ void main() { expect(request.bodyUsed, isTrue); }); + test('applies init overrides when cloning from wrapped requests', () async { + final upstream = Request( + web.Request( + 'https://example.com/base'.toJS, + web.RequestInit( + method: 'POST', + headers: {'x-upstream': '1'}.jsify()! as web.HeadersInit, + body: 'payload'.toJS, + cache: 'reload', + ), + ), + ); + + final request = Request( + upstream, + native.RequestInit( + method: HttpMethod.put, + headers: {'x-override': '2'}, + cache: native.RequestCache.noStore, + ), + ); + + expect(request.url, 'https://example.com/base'); + expect(request.method, HttpMethod.put); + expect(request.headers.get('x-upstream'), isNull); + expect(request.headers.get('x-override'), '2'); + expect(request.cache, native.RequestCache.noStore); + expect(await request.text(), 'payload'); + }); + + test('clones wrapped requests without init by teeing the body', () async { + final upstream = Request( + web.Request( + 'https://example.com/upstream-clone'.toJS, + web.RequestInit(method: 'POST', body: 'cloned body'.toJS), + ), + ); + final clone = Request(upstream); + + expect(upstream.bodyUsed, isFalse); + expect(clone.bodyUsed, isFalse); + expect(await upstream.text(), 'cloned body'); + expect(upstream.bodyUsed, isTrue); + expect(clone.bodyUsed, isFalse); + expect(await clone.text(), 'cloned body'); + expect(clone.bodyUsed, isTrue); + }); + + test( + 'rebuilds consumed wrapped requests when init provides a replacement body', + () async { + final upstream = Request( + web.Request( + 'https://example.com/base'.toJS, + web.RequestInit(method: 'POST', body: 'payload'.toJS), + ), + ); + + expect(await upstream.text(), 'payload'); + expect(upstream.bodyUsed, isTrue); + + final rebuilt = Request( + upstream, + native.RequestInit(body: 'replacement', headers: {'x-override': '2'}), + ); + + expect(rebuilt.url, 'https://example.com/base'); + expect(rebuilt.method, HttpMethod.post); + expect(rebuilt.headers.get('x-override'), '2'); + expect(rebuilt.bodyUsed, isFalse); + expect(await rebuilt.text(), 'replacement'); + expect(rebuilt.bodyUsed, isTrue); + }, + ); + test('clone tees a wrapped web.Request body', () async { final upstream = web.Request( 'https://example.com/clone'.toJS, diff --git a/test/request_native_test.dart b/test/request_native_test.dart index 233c502..d3e1a46 100644 --- a/test/request_native_test.dart +++ b/test/request_native_test.dart @@ -10,7 +10,7 @@ import 'package:test/test.dart'; void main() { group('Request (native)', () { test('defaults metadata for string input', () { - final request = Request(RequestInput.string('https://example.com')); + final request = Request('https://example.com'); expect(request.url, 'https://example.com'); expect(request.method, HttpMethod.get); @@ -31,7 +31,7 @@ void main() { test('inherits from input request and allows init overrides', () async { final upstream = Request( - RequestInput.uri(Uri.parse('https://example.com/base')), + Uri.parse('https://example.com/base'), RequestInit( method: HttpMethod.post, headers: Headers({'x-upstream': '1'}), @@ -49,7 +49,7 @@ void main() { ); final request = Request( - RequestInput.request(upstream), + upstream, RequestInit( method: HttpMethod.put, headers: Headers({'x-override': '2'}), @@ -76,7 +76,7 @@ void main() { test('bytes, text, json and arrayBuffer delegate to body', () async { final textRequest = Request( - RequestInput.string('https://example.com/text'), + 'https://example.com/text', RequestInit( method: HttpMethod.post, headers: Headers({'content-type': 'application/json'}), @@ -87,24 +87,24 @@ void main() { expect(await textRequest.text(), '{"ok":true}'); final bytesRequest = Request( - RequestInput.string('https://example.com/bytes'), + 'https://example.com/bytes', RequestInit(body: utf8.encode('hello')), ); expect(utf8.decode(await bytesRequest.bytes()), 'hello'); final arrayBufferRequest = Request( - RequestInput.string('https://example.com/array-buffer'), + 'https://example.com/array-buffer', RequestInit(body: utf8.encode('hello')), ); expect(utf8.decode(await arrayBufferRequest.arrayBuffer()), 'hello'); final parsedRequest = Request( - RequestInput.string('https://example.com/parsed'), + 'https://example.com/parsed', RequestInit(body: '{"ok":true}'), ); expect(await parsedRequest.json>(), {'ok': true}); - final emptyRequest = Request(RequestInput.string('https://example.com')); + final emptyRequest = Request('https://example.com'); expect(await emptyRequest.text(), ''); expect(await emptyRequest.bytes(), isEmpty); await expectLater(emptyRequest.json(), throwsFormatException); @@ -112,7 +112,7 @@ void main() { test('blob prefers explicit content-type header', () async { final request = Request( - RequestInput.string('https://example.com/blob'), + 'https://example.com/blob', RequestInit( headers: Headers({'content-type': 'application/custom'}), body: 'hello', @@ -128,7 +128,7 @@ void main() { 'formData parses application/x-www-form-urlencoded request bodies', () async { final request = Request( - RequestInput.string('https://example.com/form'), + 'https://example.com/form', RequestInit( method: HttpMethod.post, headers: Headers({ @@ -164,7 +164,7 @@ void main() { final headers = Headers()..set('content-type', encoded.contentType); final request = Request( - RequestInput.string('https://example.com/upload'), + 'https://example.com/upload', RequestInit( method: HttpMethod.post, headers: headers, @@ -185,7 +185,7 @@ void main() { test('clone duplicates unread stream bodies and metadata', () async { final request = Request( - RequestInput.string('https://example.com/clone'), + 'https://example.com/clone', RequestInit( method: HttpMethod.post, headers: Headers({'x-id': '1'}), @@ -217,7 +217,7 @@ void main() { test('clone fails after body has been consumed', () async { final request = Request( - RequestInput.string('https://example.com/clone'), + 'https://example.com/clone', RequestInit(body: 'used'), ); diff --git a/test/response_io_test.dart b/test/response_io_test.dart index e1b5f80..bb57fba 100644 --- a/test/response_io_test.dart +++ b/test/response_io_test.dart @@ -1,3 +1,6 @@ +@TestOn('vm') +library; + import 'dart:io' as io; import 'package:ht/src/fetch/response.io.dart'; diff --git a/test/url_search_params_test.dart b/test/url_search_params_test.dart index 1464d46..42d4159 100644 --- a/test/url_search_params_test.dart +++ b/test/url_search_params_test.dart @@ -1,3 +1,6 @@ +@TestOn('vm') +library; + import 'package:ht/src/fetch/url_search_params.dart'; import 'package:test/test.dart';