From a0144337ee0f9af2a3a79e9ad898c7c04f671f76 Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Mon, 22 Jun 2026 22:14:51 +0800 Subject: [PATCH 1/2] fix: align request method and priority semantics --- README.md | 4 +- example/main.dart | 2 +- lib/src/fetch/request.dart | 3 +- lib/src/fetch/request.io.dart | 27 +++++-- lib/src/fetch/request.js.dart | 16 +++- lib/src/fetch/request.native.dart | 63 ++++++++++++--- test/public_api_surface_test.dart | 13 +++- test/request_io_test.dart | 52 +++++++------ test/request_js_test.dart | 44 ++++++++--- test/request_native_test.dart | 125 ++++++++++++++++++++++-------- 10 files changed, 258 insertions(+), 91 deletions(-) diff --git a/README.md b/README.md index 5049764..67b2529 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Future main() async { final request = Request( Uri.parse('https://api.example.com/tasks'), RequestInit( - method: HttpMethod.post, + method: 'POST', headers: Headers({'content-type': 'application/json; charset=utf-8'}), body: '{"title":"rewrite ht"}', ), @@ -119,7 +119,7 @@ Future main() async { final body = block.Block(['hello'], type: 'text/plain'); final request = Request( Uri.parse('https://example.com'), - RequestInit(method: HttpMethod.post, body: body), + RequestInit(method: 'POST', body: body), ); print(await request.text()); // hello diff --git a/example/main.dart b/example/main.dart index 03fd8c3..4ed6513 100644 --- a/example/main.dart +++ b/example/main.dart @@ -6,7 +6,7 @@ Future main() async { final request = Request( Uri.parse('https://api.example.com/tasks'), RequestInit( - method: HttpMethod.post, + method: 'POST', headers: Headers({'content-type': 'application/json; charset=utf-8'}), body: jsonEncode({'title': 'Ship ht', 'priority': 'high'}), ), diff --git a/lib/src/fetch/request.dart b/lib/src/fetch/request.dart index 27a1a3a..cfe25a5 100644 --- a/lib/src/fetch/request.dart +++ b/lib/src/fetch/request.dart @@ -6,7 +6,8 @@ export 'request.native.dart' RequestCache, RequestRedirect, RequestReferrerPolicy, - RequestDuplex; + RequestDuplex, + RequestPriority; export 'request.native.dart' if (dart.library.js_interop) 'request.js.dart' if (dart.library.io) 'request.io.dart' diff --git a/lib/src/fetch/request.io.dart b/lib/src/fetch/request.io.dart index 8fed6e2..6aca011 100644 --- a/lib/src/fetch/request.io.dart +++ b/lib/src/fetch/request.io.dart @@ -1,7 +1,6 @@ import 'dart:io' as io; import 'dart:typed_data'; -import '../core/http_method.dart'; import 'body.dart'; import 'blob.dart'; import 'form_data.native.dart'; @@ -122,9 +121,9 @@ class Request implements native.Request { } @override - HttpMethod get method { + String get method { return switch (_host) { - final HttpRequestHost host => HttpMethod.parse(host.value.method), + final HttpRequestHost host => host.value.method, final NativeRequestHost host => host.value.method, }; } @@ -137,6 +136,14 @@ class Request implements native.Request { }; } + @override + native.RequestPriority get priority { + return switch (_host) { + final HttpRequestHost _ => native.RequestPriority.auto, + final NativeRequestHost host => host.value.priority, + }; + } + @override native.RequestRedirect get redirect { return switch (_host) { @@ -243,6 +250,7 @@ class Request implements native.Request { integrity: integrity, keepalive: keepalive, duplex: duplex, + priority: priority, ); } @@ -253,7 +261,7 @@ class Request implements native.Request { HttpRequestHost() => Request( native.Request( url, - init(body: method.allowsRequestBody ? body?.clone() : null), + init(body: _allowsRequestBody(method) ? body?.clone() : null), ), ), }; @@ -295,6 +303,7 @@ class Request implements native.Request { integrity: init?.integrity ?? request.integrity, keepalive: init?.keepalive ?? request.keepalive, duplex: init?.duplex ?? request.duplex, + priority: init?.priority ?? request.priority, ); } @@ -314,13 +323,17 @@ class Request implements native.Request { return native.Request(request.url, requestInit(body: init?.body ?? body)); } - static Body? _bodyFromWrappedRequest(Request request, HttpMethod method) { - if (!method.allowsRequestBody && + static Body? _bodyFromWrappedRequest(Request request, String method) { + if (!_allowsRequestBody(method) && request._host is HttpRequestHost && - !request.method.allowsRequestBody) { + !_allowsRequestBody(request.method)) { return null; } return request.body; } + + static bool _allowsRequestBody(String method) { + return method != 'GET' && method != 'HEAD'; + } } diff --git a/lib/src/fetch/request.js.dart b/lib/src/fetch/request.js.dart index ba89d6d..deb7d8c 100644 --- a/lib/src/fetch/request.js.dart +++ b/lib/src/fetch/request.js.dart @@ -8,7 +8,6 @@ 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_method.dart'; import 'body.dart'; import 'blob.dart'; import 'form_data.native.dart'; @@ -141,9 +140,9 @@ class Request implements native.Request { } @override - HttpMethod get method { + String get method { return switch (_host) { - final WebRequestHost host => HttpMethod.parse(host.value.method), + final WebRequestHost host => host.value.method, final NativeRequestHost host => host.value.method, }; } @@ -156,6 +155,14 @@ class Request implements native.Request { }; } + @override + native.RequestPriority get priority { + return switch (_host) { + final WebRequestHost _ => native.RequestPriority.auto, + final NativeRequestHost host => host.value.priority, + }; + } + @override native.RequestRedirect get redirect { return switch (_host) { @@ -286,6 +293,7 @@ class Request implements native.Request { integrity: integrity, keepalive: keepalive, duplex: duplex, + priority: priority, ); } @@ -332,6 +340,7 @@ class Request implements native.Request { integrity: init?.integrity ?? request.integrity, keepalive: init?.keepalive ?? request.keepalive, duplex: init?.duplex ?? request.duplex, + priority: init?.priority ?? request.priority, ); } @@ -374,6 +383,7 @@ class Request implements native.Request { integrity: init?.integrity ?? wrapped.integrity, keepalive: init?.keepalive ?? wrapped.keepalive, duplex: init?.duplex ?? wrapped.duplex, + priority: init?.priority ?? wrapped.priority, ), ); } diff --git a/lib/src/fetch/request.native.dart b/lib/src/fetch/request.native.dart index 8288ed7..51cebc9 100644 --- a/lib/src/fetch/request.native.dart +++ b/lib/src/fetch/request.native.dart @@ -1,6 +1,5 @@ import 'dart:typed_data'; -import '../core/http_method.dart'; import 'body.dart'; import 'blob.dart'; import 'form_data.native.dart'; @@ -73,6 +72,16 @@ enum RequestDuplex { final String value; } +enum RequestPriority { + auto('auto'), + high('high'), + low('low'); + + const RequestPriority(this.value); + + final String value; +} + sealed class _RequestInput { const _RequestInput(); } @@ -111,9 +120,10 @@ class RequestInit { this.integrity, this.keepalive, this.duplex, + this.priority, }); - final HttpMethod? method; + final String? method; final HeadersInit? headers; final BodyInit? body; final String? referrer; @@ -125,6 +135,7 @@ class RequestInit { final String? integrity; final bool? keepalive; final RequestDuplex? duplex; + final RequestPriority? priority; } /// Native request contract shell aligned with the MDN `Request` surface. @@ -142,6 +153,7 @@ class Request { isHistoryNavigation = _isHistoryNavigationFromInput(input), keepalive = _keepaliveFromInput(input, init?.keepalive), mode = _modeFromInput(input, init?.mode), + priority = _priorityFromInput(input, init?.priority), redirect = _redirectFromInput(input, init?.redirect), referrer = _referrerFromInput(input, init?.referrer), referrerPolicy = _referrerPolicyFromInput(input, init?.referrerPolicy), @@ -167,8 +179,9 @@ class Request { final String integrity; final bool isHistoryNavigation; final bool keepalive; - final HttpMethod method; + final String method; final RequestMode mode; + final RequestPriority priority; final RequestRedirect redirect; final String referrer; final RequestReferrerPolicy? referrerPolicy; @@ -242,6 +255,7 @@ class Request { integrity: integrity, keepalive: keepalive, duplex: duplex, + priority: priority, ), ); } @@ -257,7 +271,7 @@ class Request { static Body? _bodyFromInput( _RequestInput input, BodyInit? init, - HttpMethod method, + String method, ) { if (init != null) { _validateRequestBodyMethod(method); @@ -343,26 +357,44 @@ class Request { }; } - static HttpMethod _methodFromInput(_RequestInput input, HttpMethod? init) { - if (init != null) return init; + static String _methodFromInput(_RequestInput input, String? init) { + if (init != null) return _normalizeMethod(init); return switch (input) { _RequestRequestInput(:final value) => value.method, - _ => HttpMethod.get, + _ => 'GET', }; } - static void _validateRequestBodyMethod(HttpMethod method) { - if (method.allowsRequestBody) { + static void _validateRequestBodyMethod(String method) { + if (method != 'GET' && method != 'HEAD') { return; } throw ArgumentError.value( method, 'method', - '${method.value} requests cannot have a body.', + '$method requests cannot have a body.', ); } + static String _normalizeMethod(String method) { + if (!_methodPattern.hasMatch(method)) { + throw ArgumentError.value(method, 'method', 'Invalid HTTP method'); + } + + final upper = method.toUpperCase(); + if (upper == 'CONNECT' || upper == 'TRACE' || upper == 'TRACK') { + throw ArgumentError.value(method, 'method', 'Forbidden HTTP method'); + } + + return switch (upper) { + 'DELETE' || 'GET' || 'HEAD' || 'OPTIONS' || 'POST' || 'PUT' => upper, + _ => method, + }; + } + + static final _methodPattern = RegExp(r"^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$"); + static RequestMode _modeFromInput(_RequestInput input, RequestMode? init) { if (init != null) return init; return switch (input) { @@ -382,6 +414,17 @@ class Request { }; } + static RequestPriority _priorityFromInput( + _RequestInput input, + RequestPriority? init, + ) { + if (init != null) return init; + return switch (input) { + _RequestRequestInput(:final value) => value.priority, + _ => RequestPriority.auto, + }; + } + static String _referrerFromInput(_RequestInput input, String? init) { if (init != null) return init; return switch (input) { diff --git a/test/public_api_surface_test.dart b/test/public_api_surface_test.dart index e01d62b..e547a0e 100644 --- a/test/public_api_surface_test.dart +++ b/test/public_api_surface_test.dart @@ -4,11 +4,15 @@ import 'package:test/test.dart'; void main() { test('public API symbols are importable and usable', () async { - const method = HttpMethod.post; + const method = 'POST'; + const protocolMethod = HttpMethod.post; const status = HttpStatus.ok; const version = HttpVersion.http11; final mime = MimeType.json; - final requestInit = RequestInit(method: method); + final requestInit = RequestInit( + method: method, + priority: RequestPriority.high, + ); final responseInit = ResponseInit(status: status); final headers = Headers({'content-type': mime.toString()}); @@ -28,13 +32,14 @@ void main() { final Object init = 'x'; - expect(method.toString(), 'POST'); + expect(protocolMethod.toString(), 'POST'); expect(version.value, 'HTTP/1.1'); expect(mime.essence, 'application/json'); expect(params.get('a'), '1'); expect(await blob.text(), 'hello'); expect(file.name, 'hello.txt'); - expect(requestInit.method, HttpMethod.post); + expect(requestInit.method, 'POST'); + expect(requestInit.priority, RequestPriority.high); expect(responseInit.status, 200); expect(request.headers.has('content-type'), isTrue); expect(await multipart.bytes(), isNotEmpty); diff --git a/test/request_io_test.dart b/test/request_io_test.dart index bf9d06c..2099422 100644 --- a/test/request_io_test.dart +++ b/test/request_io_test.dart @@ -3,7 +3,6 @@ library; import 'dart:io'; -import 'package:ht/src/core/http_method.dart'; import 'package:ht/src/fetch/request.io.dart' as io_request; import 'package:ht/src/fetch/request.native.dart' as native; import 'package:ht/src/fetch/url_search_params.dart'; @@ -15,7 +14,7 @@ void main() { final request = io_request.Request( native.Request( 'https://example.com', - native.RequestInit(method: HttpMethod.post, body: 'payload'), + native.RequestInit(method: 'POST', body: 'payload'), ), ); @@ -25,7 +24,7 @@ void main() { test('sets default content-type for native construction body init', () { final textRequest = io_request.Request( 'https://example.com/text', - native.RequestInit(method: HttpMethod.post, body: 'hello'), + native.RequestInit(method: 'POST', body: 'hello'), ); expect( textRequest.headers.get('content-type'), @@ -34,10 +33,7 @@ void main() { final paramsRequest = io_request.Request( 'https://example.com/form', - native.RequestInit( - method: HttpMethod.post, - body: URLSearchParams({'a': '1'}), - ), + native.RequestInit(method: 'POST', body: URLSearchParams({'a': '1'})), ); expect( paramsRequest.headers.get('content-type'), @@ -66,7 +62,7 @@ void main() { final request = native.Request( 'https://example.com/text', native.RequestInit( - method: HttpMethod.post, + method: 'POST', headers: httpRequest.headers, body: 'hello', ), @@ -86,7 +82,7 @@ void main() { test('clone preserves deleted body-derived content-type', () async { final request = io_request.Request( 'https://example.com/clone', - native.RequestInit(method: HttpMethod.post, body: 'hello'), + native.RequestInit(method: 'POST', body: 'hello'), ); expect(request.headers.get('content-type'), 'text/plain;charset=UTF-8'); @@ -102,7 +98,7 @@ void main() { test('init override preserves deleted body-derived content-type', () async { final request = io_request.Request( 'https://example.com/rebuild', - native.RequestInit(method: HttpMethod.post, body: 'hello'), + native.RequestInit(method: 'POST', body: 'hello'), ); request.headers.delete('content-type'); @@ -123,10 +119,11 @@ void main() { native.Request( 'https://example.com/base', native.RequestInit( - method: HttpMethod.post, + method: 'POST', headers: {'x-upstream': '1'}, body: 'payload', cache: native.RequestCache.reload, + priority: native.RequestPriority.high, ), ), ); @@ -134,20 +131,33 @@ void main() { final request = io_request.Request( upstream, native.RequestInit( - method: HttpMethod.put, + method: 'PUT', headers: {'x-override': '2'}, cache: native.RequestCache.noStore, + priority: native.RequestPriority.low, ), ); expect(request.url, 'https://example.com/base'); - expect(request.method, HttpMethod.put); + expect(request.method, 'PUT'); expect(request.headers.get('x-upstream'), isNull); expect(request.headers.get('x-override'), '2'); expect(request.cache, native.RequestCache.noStore); + expect(request.priority, native.RequestPriority.low); expect(await request.text(), 'payload'); }); + test('preserves native request priority through clone', () { + final request = io_request.Request( + 'https://example.com/priority', + native.RequestInit(priority: native.RequestPriority.high), + ); + final clone = request.clone(); + + expect(request.priority, native.RequestPriority.high); + expect(clone.priority, native.RequestPriority.high); + }); + test('clones wrapped requests without init by teeing the body', () async { final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); addTearDown(server.close); @@ -209,7 +219,7 @@ void main() { final upstream = io_request.Request(httpRequest); final clone = io_request.Request(upstream); - expect(clone.method, HttpMethod.get); + expect(clone.method, 'GET'); expect(clone.body, isNull); expect(await clone.text(), ''); @@ -246,10 +256,7 @@ void main() { final upstream = io_request.Request(httpRequest); expect( - () => io_request.Request( - upstream, - native.RequestInit(method: HttpMethod.get), - ), + () => io_request.Request(upstream, native.RequestInit(method: 'GET')), throwsArgumentError, ); @@ -272,7 +279,7 @@ void main() { native.Request( 'https://example.com/base', native.RequestInit( - method: HttpMethod.post, + method: 'POST', headers: {'x-upstream': '1'}, body: 'payload', ), @@ -288,7 +295,7 @@ void main() { ); expect(rebuilt.url, 'https://example.com/base'); - expect(rebuilt.method, HttpMethod.post); + expect(rebuilt.method, 'POST'); expect(rebuilt.headers.get('x-upstream'), isNull); expect(rebuilt.headers.get('x-override'), '2'); expect(rebuilt.bodyUsed, isFalse); @@ -301,7 +308,7 @@ void main() { expect( () => io_request.Request( 'https://example.com', - native.RequestInit(method: HttpMethod.head, body: 'payload'), + native.RequestInit(method: 'HEAD', body: 'payload'), ), throwsArgumentError, ); @@ -330,10 +337,11 @@ void main() { final httpRequest = await requestFuture; final request = io_request.Request(httpRequest); - expect(request.method, HttpMethod.post); + expect(request.method, 'POST'); expect(request.url, 'http://127.0.0.1:$port/upload?q=1'); expect(request.keepalive, isTrue); expect(request.cache, native.RequestCache.default_); + expect(request.priority, native.RequestPriority.auto); expect(request.headers.get('content-type'), 'text/plain;charset=utf-8'); expect(request.headers.get('x-id'), '1'); expect(request.bodyUsed, isFalse); diff --git a/test/request_js_test.dart b/test/request_js_test.dart index 8b4a598..84ec7d5 100644 --- a/test/request_js_test.dart +++ b/test/request_js_test.dart @@ -5,7 +5,6 @@ import 'dart:convert'; import 'dart:js_interop'; import 'dart:typed_data'; -import 'package:ht/src/core/http_method.dart'; import 'package:ht/src/fetch/file.dart'; import 'package:ht/src/fetch/form_data.native.dart'; import 'package:ht/src/fetch/request.js.dart'; @@ -34,11 +33,12 @@ void main() { final request = Request(upstream); - expect(request.method, HttpMethod.post); + expect(request.method, 'POST'); expect(request.url, 'https://example.com/upload?x=1'); expect(request.keepalive, isTrue); expect(request.cache, native.RequestCache.reload); expect(request.credentials, native.RequestCredentials.include); + expect(request.priority, native.RequestPriority.auto); expect(request.redirect, native.RequestRedirect.manual); expect(request.referrer, 'about:client'); expect(request.referrerPolicy, native.RequestReferrerPolicy.origin); @@ -48,10 +48,22 @@ void main() { expect(request.bodyUsed, isTrue); }); + test('preserves custom methods from web.Request hosts', () { + final request = Request( + web.Request( + 'https://example.com/custom'.toJS, + web.RequestInit(method: 'propfind'), + ), + ); + + expect(request.method, 'propfind'); + expect(request.priority, native.RequestPriority.auto); + }); + test('sets default content-type for native construction body init', () { final request = Request( 'https://example.com/text', - native.RequestInit(method: HttpMethod.post, body: 'hello'), + native.RequestInit(method: 'POST', body: 'hello'), ); expect(request.headers.get('content-type'), 'text/plain;charset=UTF-8'); @@ -60,7 +72,7 @@ void main() { test('clone preserves deleted body-derived content-type', () async { final request = Request( 'https://example.com/clone', - native.RequestInit(method: HttpMethod.post, body: 'hello'), + native.RequestInit(method: 'POST', body: 'hello'), ); expect(request.headers.get('content-type'), 'text/plain;charset=UTF-8'); @@ -103,7 +115,7 @@ void main() { test('init override preserves deleted body-derived content-type', () async { final request = Request( 'https://example.com/rebuild', - native.RequestInit(method: HttpMethod.post, body: 'hello'), + native.RequestInit(method: 'POST', body: 'hello'), ); request.headers.delete('content-type'); @@ -128,6 +140,7 @@ void main() { headers: {'x-upstream': '1'}.jsify()! as web.HeadersInit, body: 'payload'.toJS, cache: 'reload', + priority: 'high', ), ), ); @@ -135,20 +148,33 @@ void main() { final request = Request( upstream, native.RequestInit( - method: HttpMethod.put, + method: 'PUT', headers: {'x-override': '2'}, cache: native.RequestCache.noStore, + priority: native.RequestPriority.low, ), ); expect(request.url, 'https://example.com/base'); - expect(request.method, HttpMethod.put); + expect(request.method, 'PUT'); expect(request.headers.get('x-upstream'), isNull); expect(request.headers.get('x-override'), '2'); expect(request.cache, native.RequestCache.noStore); + expect(request.priority, native.RequestPriority.low); expect(await request.text(), 'payload'); }); + test('preserves native request priority through clone', () { + final request = Request( + 'https://example.com/priority', + native.RequestInit(priority: native.RequestPriority.high), + ); + final clone = request.clone(); + + expect(request.priority, native.RequestPriority.high); + expect(clone.priority, native.RequestPriority.high); + }); + test('clones wrapped requests without init by teeing the body', () async { final upstream = Request( web.Request( @@ -186,7 +212,7 @@ void main() { ); expect(rebuilt.url, 'https://example.com/base'); - expect(rebuilt.method, HttpMethod.post); + expect(rebuilt.method, 'POST'); expect(rebuilt.headers.get('x-override'), '2'); expect(rebuilt.bodyUsed, isFalse); expect(await rebuilt.text(), 'replacement'); @@ -198,7 +224,7 @@ void main() { expect( () => Request( 'https://example.com', - native.RequestInit(method: HttpMethod.head, body: 'payload'), + native.RequestInit(method: 'HEAD', body: 'payload'), ), throwsArgumentError, ); diff --git a/test/request_native_test.dart b/test/request_native_test.dart index 299be3c..05dc70f 100644 --- a/test/request_native_test.dart +++ b/test/request_native_test.dart @@ -1,6 +1,5 @@ import 'dart:convert'; -import 'package:ht/src/core/http_method.dart'; import 'package:ht/src/fetch/blob.dart'; import 'package:ht/src/fetch/form_data.native.dart'; import 'package:ht/src/fetch/headers.dart'; @@ -14,7 +13,7 @@ void main() { final request = Request('https://example.com'); expect(request.url, 'https://example.com'); - expect(request.method, HttpMethod.get); + expect(request.method, 'GET'); expect(request.headers.entries(), isEmpty); expect(request.body, isNull); expect(request.cache, RequestCache.default_); @@ -25,6 +24,7 @@ void main() { expect(request.isHistoryNavigation, isFalse); expect(request.keepalive, isFalse); expect(request.mode, RequestMode.cors); + expect(request.priority, RequestPriority.auto); expect(request.redirect, RequestRedirect.follow); expect(request.referrer, 'about:client'); expect(request.referrerPolicy, isNull); @@ -34,7 +34,7 @@ void main() { final upstream = Request( Uri.parse('https://example.com/base'), RequestInit( - method: HttpMethod.post, + method: 'POST', headers: Headers({'x-upstream': '1'}), body: 'payload', cache: RequestCache.reload, @@ -43,6 +43,7 @@ void main() { integrity: 'sha256-abc', keepalive: true, mode: RequestMode.sameOrigin, + priority: RequestPriority.high, redirect: RequestRedirect.manual, referrer: 'https://referrer.example', referrerPolicy: RequestReferrerPolicy.origin, @@ -52,15 +53,16 @@ void main() { final request = Request( upstream, RequestInit( - method: HttpMethod.put, + method: 'PUT', headers: Headers({'x-override': '2'}), cache: RequestCache.noStore, + priority: RequestPriority.low, referrer: 'https://override.example', ), ); expect(request.url, 'https://example.com/base'); - expect(request.method, HttpMethod.put); + expect(request.method, 'PUT'); expect(request.headers.get('x-upstream'), isNull); expect(request.headers.get('x-override'), '2'); expect(request.cache, RequestCache.noStore); @@ -69,17 +71,80 @@ void main() { expect(request.integrity, 'sha256-abc'); expect(request.keepalive, isTrue); expect(request.mode, RequestMode.sameOrigin); + expect(request.priority, RequestPriority.low); expect(request.redirect, RequestRedirect.manual); expect(request.referrer, 'https://override.example'); expect(request.referrerPolicy, RequestReferrerPolicy.origin); expect(await request.text(), 'payload'); }); + test('normalizes standard methods and preserves custom method casing', () { + expect( + Request('https://example.com', RequestInit(method: 'post')).method, + 'POST', + ); + expect( + Request('https://example.com', RequestInit(method: 'patch')).method, + 'patch', + ); + expect( + Request('https://example.com', RequestInit(method: 'PROPFIND')).method, + 'PROPFIND', + ); + expect( + Request('https://example.com', RequestInit(method: 'propfind')).method, + 'propfind', + ); + expect( + Request('https://example.com', RequestInit(method: 'X-Custom')).method, + 'X-Custom', + ); + }); + + test('rejects invalid and forbidden methods', () { + expect( + () => Request('https://example.com', RequestInit(method: 'BAD METHOD')), + throwsArgumentError, + ); + expect( + () => Request('https://example.com', RequestInit(method: '')), + throwsArgumentError, + ); + expect( + () => Request('https://example.com', RequestInit(method: 'CONNECT')), + throwsArgumentError, + ); + expect( + () => Request('https://example.com', RequestInit(method: 'trace')), + throwsArgumentError, + ); + expect( + () => Request('https://example.com', RequestInit(method: 'TRACK')), + throwsArgumentError, + ); + }); + + test('copies and overrides request priority', () { + final upstream = Request( + 'https://example.com', + RequestInit(priority: RequestPriority.high), + ); + final copy = Request(upstream); + final override = Request( + upstream, + RequestInit(priority: RequestPriority.low), + ); + + expect(copy.priority, RequestPriority.high); + expect(override.priority, RequestPriority.low); + expect(override.clone().priority, RequestPriority.low); + }); + test('bytes, text, json and arrayBuffer delegate to body', () async { final textRequest = Request( 'https://example.com/text', RequestInit( - method: HttpMethod.post, + method: 'POST', headers: Headers({'content-type': 'application/json'}), body: '{"ok":true}', ), @@ -89,19 +154,19 @@ void main() { final bytesRequest = Request( 'https://example.com/bytes', - RequestInit(method: HttpMethod.post, body: utf8.encode('hello')), + RequestInit(method: 'POST', body: utf8.encode('hello')), ); expect(utf8.decode(await bytesRequest.bytes()), 'hello'); final arrayBufferRequest = Request( 'https://example.com/array-buffer', - RequestInit(method: HttpMethod.post, body: utf8.encode('hello')), + RequestInit(method: 'POST', body: utf8.encode('hello')), ); expect(utf8.decode(await arrayBufferRequest.arrayBuffer()), 'hello'); final parsedRequest = Request( 'https://example.com/parsed', - RequestInit(method: HttpMethod.post, body: '{"ok":true}'), + RequestInit(method: 'POST', body: '{"ok":true}'), ); expect(await parsedRequest.json>(), {'ok': true}); @@ -115,7 +180,7 @@ void main() { final request = Request( 'https://example.com/blob', RequestInit( - method: HttpMethod.post, + method: 'POST', headers: Headers({'content-type': 'application/custom'}), body: 'hello', ), @@ -129,7 +194,7 @@ void main() { test('sets default content-type from body init', () async { final textRequest = Request( 'https://example.com/text', - RequestInit(method: HttpMethod.post, body: 'hello'), + RequestInit(method: 'POST', body: 'hello'), ); expect( textRequest.headers.get('content-type'), @@ -139,7 +204,7 @@ void main() { final paramsRequest = Request( 'https://example.com/form', RequestInit( - method: HttpMethod.post, + method: 'POST', body: URLSearchParams({'a': '1', 'b': '2'}), ), ); @@ -151,7 +216,7 @@ void main() { final blobRequest = Request( 'https://example.com/blob', RequestInit( - method: HttpMethod.post, + method: 'POST', body: Blob(['hello'], 'text/plain'), ), ); @@ -159,14 +224,14 @@ void main() { final emptyBlobRequest = Request( 'https://example.com/blob', - RequestInit(method: HttpMethod.post, body: Blob(['hello'])), + RequestInit(method: 'POST', body: Blob(['hello'])), ); expect(emptyBlobRequest.headers.get('content-type'), isNull); final formRequest = Request( 'https://example.com/multipart', RequestInit( - method: HttpMethod.post, + method: 'POST', body: FormData()..append('name', Multipart.text('alice')), ), ); @@ -179,14 +244,14 @@ void main() { final bytesRequest = Request( 'https://example.com/bytes', - RequestInit(method: HttpMethod.post, body: utf8.encode('hello')), + RequestInit(method: 'POST', body: utf8.encode('hello')), ); expect(bytesRequest.headers.get('content-type'), isNull); final streamRequest = Request( 'https://example.com/stream', RequestInit( - method: HttpMethod.post, + method: 'POST', body: Stream>.value(utf8.encode('hello')), ), ); @@ -197,7 +262,7 @@ void main() { final request = Request( 'https://example.com/text', RequestInit( - method: HttpMethod.post, + method: 'POST', headers: Headers({'content-type': 'application/custom'}), body: 'hello', ), @@ -212,7 +277,7 @@ void main() { final request = Request( 'https://example.com/form', RequestInit( - method: HttpMethod.post, + method: 'POST', headers: Headers({ 'content-type': 'application/x-www-form-urlencoded;charset=utf-8', }), @@ -247,11 +312,7 @@ void main() { final headers = Headers()..set('content-type', encoded.contentType); final request = Request( 'https://example.com/upload', - RequestInit( - method: HttpMethod.post, - headers: headers, - body: encoded.stream, - ), + RequestInit(method: 'POST', headers: headers, body: encoded.stream), ); final formData = await request.formData(); @@ -269,7 +330,7 @@ void main() { final request = Request( 'https://example.com/clone', RequestInit( - method: HttpMethod.post, + method: 'POST', headers: Headers({'x-id': '1'}), body: Stream>.fromIterable(>[ utf8.encode('hello '), @@ -300,7 +361,7 @@ void main() { test('clone preserves deleted body-derived content-type', () async { final request = Request( 'https://example.com/clone', - RequestInit(method: HttpMethod.post, body: 'hello'), + RequestInit(method: 'POST', body: 'hello'), ); expect(request.headers.get('content-type'), 'text/plain;charset=UTF-8'); @@ -316,7 +377,7 @@ void main() { test('clone fails after body has been consumed', () async { final request = Request( 'https://example.com/clone', - RequestInit(method: HttpMethod.post, body: 'used'), + RequestInit(method: 'POST', body: 'used'), ); expect(await request.text(), 'used'); @@ -331,7 +392,7 @@ void main() { expect( () => Request( 'https://example.com', - RequestInit(method: HttpMethod.head, body: 'payload'), + RequestInit(method: 'HEAD', body: 'payload'), ), throwsArgumentError, ); @@ -340,11 +401,11 @@ void main() { test('rejects inherited bodies when overriding to bodyless methods', () { final upstream = Request( 'https://example.com', - RequestInit(method: HttpMethod.post, body: 'payload'), + RequestInit(method: 'POST', body: 'payload'), ); expect( - () => Request(upstream, RequestInit(method: HttpMethod.get)), + () => Request(upstream, RequestInit(method: 'GET')), throwsArgumentError, ); }); @@ -355,7 +416,7 @@ void main() { final upstream = Request( 'https://example.com', RequestInit( - method: HttpMethod.post, + method: 'POST', body: Stream>.fromIterable(>[ utf8.encode('pay'), utf8.encode('load'), @@ -364,7 +425,7 @@ void main() { ); expect( - () => Request(upstream, RequestInit(method: HttpMethod.get)), + () => Request(upstream, RequestInit(method: 'GET')), throwsArgumentError, ); From 919a180f612cfd9abe4ddc7ebe4c47f8c9c7efd1 Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Mon, 22 Jun 2026 22:28:00 +0800 Subject: [PATCH 2/2] fix: handle lowercase bodyless method overrides --- lib/src/fetch/request.io.dart | 3 ++- test/request_io_test.dart | 39 +++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/lib/src/fetch/request.io.dart b/lib/src/fetch/request.io.dart index 6aca011..508c660 100644 --- a/lib/src/fetch/request.io.dart +++ b/lib/src/fetch/request.io.dart @@ -334,6 +334,7 @@ class Request implements native.Request { } static bool _allowsRequestBody(String method) { - return method != 'GET' && method != 'HEAD'; + final upper = method.toUpperCase(); + return upper != 'GET' && upper != 'HEAD'; } } diff --git a/test/request_io_test.dart b/test/request_io_test.dart index 2099422..4f6df30 100644 --- a/test/request_io_test.dart +++ b/test/request_io_test.dart @@ -232,6 +232,45 @@ void main() { }, ); + test( + 'rebuilds wrapped GET requests with lowercase method override', + () 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.get( + InternetAddress.loopbackIPv4.host, + port, + '/bodyless-override', + ); + final clientResponseFuture = clientRequest.close(); + + final httpRequest = await requestFuture; + final upstream = io_request.Request(httpRequest); + final rebuilt = io_request.Request( + upstream, + native.RequestInit(method: 'get'), + ); + + expect(rebuilt.method, 'GET'); + expect(rebuilt.body, isNull); + expect(await rebuilt.text(), ''); + + httpRequest.response + ..statusCode = HttpStatus.noContent + ..close(); + + final clientResponse = await clientResponseFuture; + await clientResponse.drain(); + }, + ); + test( 'rejects overriding wrapped bodyful requests to bodyless methods', () async {