Skip to content

Commit eb189e1

Browse files
authored
[cupertino_http] Fix a bug where content-length was not set for multipart messages (#1247)
1 parent 0bbd166 commit eb189e1

8 files changed

+143
-6
lines changed

pkgs/cupertino_http/CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
## 1.5.1-wip
22

33
* Allow `1000` as a `code` argument in `CupertinoWebSocket.close`.
4+
* Fix a bug where the `Content-Length` header would not be set under certain
5+
circumstances.
46

57
## 1.5.0
68

pkgs/cupertino_http/lib/src/cupertino_client.dart

+7-5
Original file line numberDiff line numberDiff line change
@@ -308,16 +308,18 @@ class CupertinoClient extends BaseClient {
308308
..headersCommaValues = request.headers
309309
..maxRedirects = request.maxRedirects;
310310

311-
if (profile != null && request.contentLength != null) {
312-
profile.requestData.headersListValues = {
311+
final urlRequest = MutableURLRequest.fromUrl(request.url)
312+
..httpMethod = request.method;
313+
314+
if (request.contentLength != null) {
315+
profile?.requestData.headersListValues = {
313316
'Content-Length': ['${request.contentLength}'],
314317
...profile.requestData.headers!
315318
};
319+
urlRequest.setValueForHttpHeaderField(
320+
'Content-Length', '${request.contentLength}');
316321
}
317322

318-
final urlRequest = MutableURLRequest.fromUrl(request.url)
319-
..httpMethod = request.method;
320-
321323
if (request is Request) {
322324
// Optimize the (typical) `Request` case since assigning to
323325
// `httpBodyStream` requires a lot of expensive setup and data passing.

pkgs/http/test/html/client_conformance_test.dart

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@ void main() {
1414
redirectAlwaysAllowed: true,
1515
canStreamRequestBody: false,
1616
canStreamResponseBody: false,
17-
canWorkInIsolates: false);
17+
canWorkInIsolates: false,
18+
supportsMultipartRequest: false);
1819
}

pkgs/http_client_conformance_tests/lib/http_client_conformance_tests.dart

+8
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:http/http.dart';
77
import 'src/close_tests.dart';
88
import 'src/compressed_response_body_tests.dart';
99
import 'src/isolate_test.dart';
10+
import 'src/multipart_tests.dart';
1011
import 'src/multiple_clients_tests.dart';
1112
import 'src/redirect_tests.dart';
1213
import 'src/request_body_streamed_tests.dart';
@@ -25,6 +26,7 @@ export 'src/close_tests.dart' show testClose;
2526
export 'src/compressed_response_body_tests.dart'
2627
show testCompressedResponseBody;
2728
export 'src/isolate_test.dart' show testIsolate;
29+
export 'src/multipart_tests.dart' show testMultipartRequests;
2830
export 'src/multiple_clients_tests.dart' show testMultipleClients;
2931
export 'src/redirect_tests.dart' show testRedirect;
3032
export 'src/request_body_streamed_tests.dart' show testRequestBodyStreamed;
@@ -67,6 +69,9 @@ export 'src/server_errors_test.dart' show testServerErrors;
6769
/// If [supportsFoldedHeaders] is `false` then the tests that assume that the
6870
/// [Client] can parse folded headers will be skipped.
6971
///
72+
/// If [supportsMultipartRequest] is `false` then tests that assume that
73+
/// multipart requests can be sent will be skipped.
74+
///
7075
/// The tests are run against a series of HTTP servers that are started by the
7176
/// tests. If the tests are run in the browser, then the test servers are
7277
/// started in another process. Otherwise, the test servers are run in-process.
@@ -80,6 +85,7 @@ void testAll(
8085
bool supportsFoldedHeaders = true,
8186
bool canSendCookieHeaders = false,
8287
bool canReceiveSetCookieHeaders = false,
88+
bool supportsMultipartRequest = true,
8389
}) {
8490
testRequestBody(clientFactory());
8591
testRequestBodyStreamed(clientFactory(),
@@ -97,6 +103,8 @@ void testAll(
97103
testServerErrors(clientFactory());
98104
testCompressedResponseBody(clientFactory());
99105
testMultipleClients(clientFactory);
106+
testMultipartRequests(clientFactory(),
107+
supportsMultipartRequest: supportsMultipartRequest);
100108
testClose(clientFactory);
101109
testIsolate(clientFactory, canWorkInIsolates: canWorkInIsolates);
102110
testRequestCookies(clientFactory(),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:async';
6+
import 'dart:convert';
7+
import 'dart:io';
8+
9+
import 'package:stream_channel/stream_channel.dart';
10+
11+
/// Starts an HTTP server that captures the request headers and body.
12+
///
13+
/// Channel protocol:
14+
/// On Startup:
15+
/// - send port
16+
/// On Request Received:
17+
/// - send the received headers and request body
18+
/// When Receive Anything:
19+
/// - exit
20+
void hybridMain(StreamChannel<Object?> channel) async {
21+
late HttpServer server;
22+
23+
server = (await HttpServer.bind('localhost', 0))
24+
..listen((request) async {
25+
request.response.headers.set('Access-Control-Allow-Origin', '*');
26+
if (request.method == 'OPTIONS') {
27+
// Handle a CORS preflight request:
28+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#preflighted_requests
29+
request.response.headers
30+
..set('Access-Control-Allow-Methods', '*')
31+
..set('Access-Control-Allow-Headers', '*');
32+
} else {
33+
final headers = <String, List<String>>{};
34+
request.headers.forEach((field, value) {
35+
headers[field] = value;
36+
});
37+
final body =
38+
await const Utf8Decoder().bind(request).fold('', (x, y) => '$x$y');
39+
channel.sink.add((headers, body));
40+
}
41+
unawaited(request.response.close());
42+
});
43+
44+
channel.sink.add(server.port);
45+
await channel
46+
.stream.first; // Any writes indicates that the server should exit.
47+
unawaited(server.close());
48+
}

pkgs/http_client_conformance_tests/lib/src/multipart_server_vm.dart

+14
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkgs/http_client_conformance_tests/lib/src/multipart_server_web.dart

+11
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:async/async.dart';
6+
import 'package:http/http.dart';
7+
import 'package:stream_channel/stream_channel.dart';
8+
import 'package:test/test.dart';
9+
10+
import 'multipart_server_vm.dart'
11+
if (dart.library.js_interop) 'multipart_server_web.dart';
12+
13+
/// Tests that the [Client] correctly sends [MultipartRequest].
14+
///
15+
/// If [supportsMultipartRequest] is `false` then tests that assume that
16+
/// multipart requests can be sent will be skipped.
17+
void testMultipartRequests(Client client,
18+
{required bool supportsMultipartRequest}) async {
19+
group('multipart requests', () {
20+
late final String host;
21+
late final StreamChannel<Object?> httpServerChannel;
22+
late final StreamQueue<Object?> httpServerQueue;
23+
24+
setUpAll(() async {
25+
httpServerChannel = await startServer();
26+
httpServerQueue = StreamQueue(httpServerChannel.stream);
27+
host = 'localhost:${await httpServerQueue.nextAsInt}';
28+
});
29+
tearDownAll(() => httpServerChannel.sink.add(null));
30+
31+
test('attached file', () async {
32+
final request = MultipartRequest('POST', Uri.http(host, ''));
33+
34+
request.files.add(MultipartFile.fromString('file1', 'Hello World'));
35+
36+
await client.send(request);
37+
final (headers, body) =
38+
await httpServerQueue.next as (Map<String, List<String>>, String);
39+
expect(headers['content-length']!.single, '${request.contentLength}');
40+
expect(headers['content-type']!.single,
41+
startsWith('multipart/form-data; boundary='));
42+
expect(body, contains('''content-type: text/plain; charset=utf-8\r
43+
content-disposition: form-data; name="file1"\r
44+
\r
45+
Hello World'''));
46+
});
47+
},
48+
skip: supportsMultipartRequest
49+
? false
50+
: 'does not support multipart requests');
51+
}

0 commit comments

Comments
 (0)