Fix a bug where cronet_http sends incorrect HTTP request methods (#1058)
diff --git a/pkgs/cronet_http/CHANGELOG.md b/pkgs/cronet_http/CHANGELOG.md
index 7eecd29..59ef74f 100644
--- a/pkgs/cronet_http/CHANGELOG.md
+++ b/pkgs/cronet_http/CHANGELOG.md
@@ -1,8 +1,7 @@
-## 0.4.3-wip
-
## 0.4.2
* Require `package:jni >= 0.7.2` to remove a potential buffer overflow.
+* Fix a bug where incorrect HTTP request methods were sent.
## 0.4.1
diff --git a/pkgs/cronet_http/lib/src/cronet_client.dart b/pkgs/cronet_http/lib/src/cronet_client.dart
index c4d0d19..272b602 100644
--- a/pkgs/cronet_http/lib/src/cronet_client.dart
+++ b/pkgs/cronet_http/lib/src/cronet_client.dart
@@ -317,7 +317,7 @@
jb.UrlRequestCallbackProxy.new1(
_urlRequestCallbacks(request, responseCompleter)),
_executor,
- );
+ )..setHttpMethod(request.method.toJString());
var headers = request.headers;
if (body.isNotEmpty &&
diff --git a/pkgs/http/test/io/client_conformance_test.dart b/pkgs/http/test/io/client_conformance_test.dart
index 5d8f7f5..20bf39f 100644
--- a/pkgs/http/test/io/client_conformance_test.dart
+++ b/pkgs/http/test/io/client_conformance_test.dart
@@ -10,5 +10,6 @@
import 'package:test/test.dart';
void main() {
- testAll(IOClient.new);
+ testAll(IOClient.new, preservesMethodCase: false // https://dartbug.com/54187
+ );
}
diff --git a/pkgs/http_client_conformance_tests/lib/http_client_conformance_tests.dart b/pkgs/http_client_conformance_tests/lib/http_client_conformance_tests.dart
index 3d00319..bd83c02 100644
--- a/pkgs/http_client_conformance_tests/lib/http_client_conformance_tests.dart
+++ b/pkgs/http_client_conformance_tests/lib/http_client_conformance_tests.dart
@@ -12,6 +12,7 @@
import 'src/request_body_streamed_tests.dart';
import 'src/request_body_tests.dart';
import 'src/request_headers_tests.dart';
+import 'src/request_methods_tests.dart';
import 'src/response_body_streamed_test.dart';
import 'src/response_body_tests.dart';
import 'src/response_headers_tests.dart';
@@ -27,6 +28,7 @@
export 'src/request_body_streamed_tests.dart' show testRequestBodyStreamed;
export 'src/request_body_tests.dart' show testRequestBody;
export 'src/request_headers_tests.dart' show testRequestHeaders;
+export 'src/request_methods_tests.dart' show testRequestMethods;
export 'src/response_body_streamed_test.dart' show testResponseBodyStreamed;
export 'src/response_body_tests.dart' show testResponseBody;
export 'src/response_headers_tests.dart' show testResponseHeaders;
@@ -49,14 +51,20 @@
/// If [canWorkInIsolates] is `false` then tests that require that the [Client]
/// work in Isolates other than the main isolate will be skipped.
///
+/// If [preservesMethodCase] is `false` then tests that assume that the
+/// [Client] preserves custom request method casing will be skipped.
+///
/// The tests are run against a series of HTTP servers that are started by the
/// tests. If the tests are run in the browser, then the test servers are
/// started in another process. Otherwise, the test servers are run in-process.
-void testAll(Client Function() clientFactory,
- {bool canStreamRequestBody = true,
- bool canStreamResponseBody = true,
- bool redirectAlwaysAllowed = false,
- bool canWorkInIsolates = true}) {
+void testAll(
+ Client Function() clientFactory, {
+ bool canStreamRequestBody = true,
+ bool canStreamResponseBody = true,
+ bool redirectAlwaysAllowed = false,
+ bool canWorkInIsolates = true,
+ bool preservesMethodCase = false,
+}) {
testRequestBody(clientFactory());
testRequestBodyStreamed(clientFactory(),
canStreamRequestBody: canStreamRequestBody);
@@ -65,6 +73,7 @@
testResponseBodyStreamed(clientFactory(),
canStreamResponseBody: canStreamResponseBody);
testRequestHeaders(clientFactory());
+ testRequestMethods(clientFactory(), preservesMethodCase: preservesMethodCase);
testResponseHeaders(clientFactory());
testResponseStatusLine(clientFactory());
testRedirect(clientFactory(), redirectAlwaysAllowed: redirectAlwaysAllowed);
diff --git a/pkgs/http_client_conformance_tests/lib/src/request_methods_server.dart b/pkgs/http_client_conformance_tests/lib/src/request_methods_server.dart
new file mode 100644
index 0000000..bf05ec0
--- /dev/null
+++ b/pkgs/http_client_conformance_tests/lib/src/request_methods_server.dart
@@ -0,0 +1,41 @@
+// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:stream_channel/stream_channel.dart';
+
+/// Starts an HTTP server that captures the request headers.
+///
+/// Channel protocol:
+/// On Startup:
+/// - send port
+/// On Request Received:
+/// - send the received request method (e.g. GET) as a String
+/// When Receive Anything:
+/// - exit
+void hybridMain(StreamChannel<Object?> channel) async {
+ late HttpServer server;
+
+ server = (await HttpServer.bind('localhost', 0))
+ ..listen((request) async {
+ request.response.headers.set('Access-Control-Allow-Origin', '*');
+ if (request.method == 'OPTIONS') {
+ // Handle a CORS preflight request:
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#preflighted_requests
+ request.response.headers
+ ..set('Access-Control-Allow-Methods', '*')
+ ..set('Access-Control-Allow-Headers', '*');
+ } else {
+ channel.sink.add(request.method);
+ }
+ unawaited(request.response.close());
+ });
+
+ channel.sink.add(server.port);
+ await channel
+ .stream.first; // Any writes indicates that the server should exit.
+ unawaited(server.close());
+}
diff --git a/pkgs/http_client_conformance_tests/lib/src/request_methods_server_vm.dart b/pkgs/http_client_conformance_tests/lib/src/request_methods_server_vm.dart
new file mode 100644
index 0000000..6ce6627
--- /dev/null
+++ b/pkgs/http_client_conformance_tests/lib/src/request_methods_server_vm.dart
@@ -0,0 +1,12 @@
+// Generated by generate_server_wrappers.dart. Do not edit.
+
+import 'package:stream_channel/stream_channel.dart';
+
+import 'request_methods_server.dart';
+
+/// Starts the redirect test HTTP server in the same process.
+Future<StreamChannel<Object?>> startServer() async {
+ final controller = StreamChannelController<Object?>(sync: true);
+ hybridMain(controller.foreign);
+ return controller.local;
+}
diff --git a/pkgs/http_client_conformance_tests/lib/src/request_methods_server_web.dart b/pkgs/http_client_conformance_tests/lib/src/request_methods_server_web.dart
new file mode 100644
index 0000000..8cddf5a
--- /dev/null
+++ b/pkgs/http_client_conformance_tests/lib/src/request_methods_server_web.dart
@@ -0,0 +1,9 @@
+// Generated by generate_server_wrappers.dart. Do not edit.
+
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test/test.dart';
+
+/// Starts the redirect test HTTP server out-of-process.
+Future<StreamChannel<Object?>> startServer() async => spawnHybridUri(Uri(
+ scheme: 'package',
+ path: 'http_client_conformance_tests/src/request_methods_server.dart'));
diff --git a/pkgs/http_client_conformance_tests/lib/src/request_methods_tests.dart b/pkgs/http_client_conformance_tests/lib/src/request_methods_tests.dart
new file mode 100644
index 0000000..ec11387
--- /dev/null
+++ b/pkgs/http_client_conformance_tests/lib/src/request_methods_tests.dart
@@ -0,0 +1,88 @@
+// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:async/async.dart';
+import 'package:http/http.dart';
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test/test.dart';
+
+import 'request_methods_server_vm.dart'
+ if (dart.library.html) 'request_methods_server_web.dart';
+
+/// Tests that the [Client] correctly sends HTTP request methods
+/// (e.g. GET, HEAD).
+///
+/// If [preservesMethodCase] is `false` then tests that assume that the
+/// [Client] preserves custom request method casing will be skipped.
+void testRequestMethods(Client client,
+ {bool preservesMethodCase = true}) async {
+ group('request methods', () {
+ late final String host;
+ late final StreamChannel<Object?> httpServerChannel;
+ late final StreamQueue<Object?> httpServerQueue;
+
+ setUpAll(() async {
+ httpServerChannel = await startServer();
+ httpServerQueue = StreamQueue(httpServerChannel.stream);
+ host = 'localhost:${await httpServerQueue.next}';
+ });
+ tearDownAll(() => httpServerChannel.sink.add(null));
+
+ test('custom method - not case preserving', () async {
+ await client.send(Request(
+ 'CuStOm',
+ Uri.http(host, ''),
+ ));
+ final method = await httpServerQueue.next as String;
+ expect('CUSTOM', method.toUpperCase());
+ });
+
+ test('custom method case preserving', () async {
+ await client.send(Request(
+ 'CuStOm',
+ Uri.http(host, ''),
+ ));
+ final method = await httpServerQueue.next as String;
+ expect('CuStOm', method);
+ },
+ skip: preservesMethodCase
+ ? false
+ : 'does not preserve HTTP request method case');
+
+ test('delete', () async {
+ await client.delete(Uri.http(host, ''));
+ final method = await httpServerQueue.next as String;
+ expect('DELETE', method);
+ });
+
+ test('get', () async {
+ await client.get(Uri.http(host, ''));
+ final method = await httpServerQueue.next as String;
+ expect('GET', method);
+ });
+ test('head', () async {
+ await client.head(Uri.http(host, ''));
+ final method = await httpServerQueue.next as String;
+ expect('HEAD', method);
+ });
+
+ test('patch', () async {
+ await client.patch(Uri.http(host, ''));
+ final method = await httpServerQueue.next as String;
+ expect('PATCH', method);
+ });
+
+ test('post', () async {
+ await client.post(Uri.http(host, ''));
+ final method = await httpServerQueue.next as String;
+ expect('POST', method);
+ });
+
+ test('put', () async {
+ await client.put(Uri.http(host, ''));
+ final method = await httpServerQueue.next as String;
+ expect('PUT', method);
+ });
+ });
+}