Skip to content

Commit 68173ed

Browse files
committed
[cupertino_http] Fix a bug where content-length was not set for multipart messages
Fixes dart-lang#1236
1 parent 0bbd166 commit 68173ed

File tree

7 files changed

+129
-5
lines changed

7 files changed

+129
-5
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_client_conformance_tests/lib/http_client_conformance_tests.dart

+3
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;
@@ -97,6 +99,7 @@ void testAll(
9799
testServerErrors(clientFactory());
98100
testCompressedResponseBody(clientFactory());
99101
testMultipleClients(clientFactory);
102+
testMultipartRequests(clientFactory());
100103
testClose(clientFactory);
101104
testIsolate(clientFactory, canWorkInIsolates: canWorkInIsolates);
102105
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,44 @@
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+
void testMultipartRequests(Client client) async {
15+
group('multipart requests', () {
16+
late final String host;
17+
late final StreamChannel<Object?> httpServerChannel;
18+
late final StreamQueue<Object?> httpServerQueue;
19+
20+
setUpAll(() async {
21+
httpServerChannel = await startServer();
22+
httpServerQueue = StreamQueue(httpServerChannel.stream);
23+
host = 'localhost:${await httpServerQueue.nextAsInt}';
24+
});
25+
tearDownAll(() => httpServerChannel.sink.add(null));
26+
27+
test('attached file', () async {
28+
final request = MultipartRequest('POST', Uri.http(host, ''));
29+
30+
request.files.add(MultipartFile.fromString('file1', 'Hello World'));
31+
32+
await client.send(request);
33+
final (headers, body) =
34+
await httpServerQueue.next as (Map<String, List<String>>, String);
35+
expect(headers['content-length']!.single, '${request.contentLength}');
36+
expect(headers['content-type']!.single,
37+
startsWith('multipart/form-data; boundary='));
38+
expect(body, contains('''content-type: text/plain; charset=utf-8\r
39+
content-disposition: form-data; name="file1"\r
40+
\r
41+
Hello World'''));
42+
});
43+
});
44+
}

0 commit comments

Comments
 (0)