Skip to content

Commit

Permalink
[stable] [io]: Fix a bug where HttpResponse.writeln did not honor the…
Browse files Browse the repository at this point in the history
… charset.

Bug:#59719
Change-Id: Ief2f79d98a400f90d2098cd2f3cd8312325c91cb
Cherry-pick: https://dart-review.googlesource.com/c/sdk/+/402281
Cherry-pick-request: #59813
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/402422
Reviewed-by: Kevin Moore <[email protected]>
Reviewed-by: Alexander Aprelev <[email protected]>
Commit-Queue: Brian Quinlan <[email protected]>
  • Loading branch information
brianquinlan authored and Commit Queue committed Jan 17, 2025
1 parent 49d7d8f commit bbd8d97
Show file tree
Hide file tree
Showing 4 changed files with 244 additions and 3 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 3.6.2

- Fixes a bug where `HttpServer` responses were not correctly encoded
if a "Content-Type" header was set (issue [#59719][]).

[#59719]: https://github.com/dart-lang/sdk/issues/59719

## 3.6.1 - 2025-01-08

- When inside a pub workspace, `pub get` will now delete stray
Expand Down
9 changes: 7 additions & 2 deletions sdk/lib/_http/http.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1011,6 +1011,10 @@ abstract interface class HttpRequest implements Stream<Uint8List> {
/// first time, the request header is sent. Calling any methods that
/// change the header after it is sent throws an exception.
///
/// If no "Content-Type" header is set then a default of
/// "text/plain; charset=utf-8" is used and string data written to the IOSink
/// will be encoded using UTF-8.
///
/// ## Setting the headers
///
/// The HttpResponse object has a number of properties for setting up
Expand All @@ -1031,8 +1035,9 @@ abstract interface class HttpRequest implements Stream<Uint8List> {
/// response.headers.add(HttpHeaders.contentTypeHeader, "text/plain");
/// response.write(...); // Strings written will be ISO-8859-1 encoded.
///
/// An exception is thrown if you use the `write()` method
/// while an unsupported content-type is set.
/// If a charset is provided but it is not recognized, then the "Content-Type"
/// header will include that charset but string data will be encoded using
/// ISO-8859-1 (Latin 1).
abstract interface class HttpResponse implements IOSink {
// TODO(ajohnsen): Add documentation of how to pipe a file to the response.
/// Gets and sets the content length of the response. If the size of
Expand Down
2 changes: 1 addition & 1 deletion sdk/lib/_http/http_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1087,7 +1087,7 @@ class _IOSinkImpl extends _StreamSinkImpl<List<int>> implements IOSink {
}

void writeln([Object? object = ""]) {
_writeString('$object\n');
write('$object\n');
}

void writeCharCode(int charCode) {
Expand Down
229 changes: 229 additions & 0 deletions tests/standalone/io/http_server_encoding_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
// Copyright (c) 2024, 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.

// Tests that the server response body is returned according to defaults or the
// charset set in the "Content-Type" header.

import 'dart:convert';
import 'dart:io';

import "package:expect/expect.dart";

Future<void> testWriteWithoutContentTypeJapanese() async {
final server = await HttpServer.bind(InternetAddress.anyIPv4, 0);

server.first.then((request) {
request.response
..write('日本語')
..close();
});
final request = await HttpClient().get('localhost', server.port, '/');
final response = await request.close();
Expect.listEquals([
'text/plain; charset=utf-8',
], response.headers[HttpHeaders.contentTypeHeader] ?? []);
final body = utf8.decode(await response.fold([], (o, n) => o + n));
Expect.equals('日本語', body);
}

Future<void> testWritelnWithoutContentTypeJapanese() async {
final server = await HttpServer.bind(InternetAddress.anyIPv4, 0);

server.first.then((request) {
request.response
..writeln('日本語')
..close();
});
final request = await HttpClient().get('localhost', server.port, '/');
final response = await request.close();
Expect.listEquals([
'text/plain; charset=utf-8',
], response.headers[HttpHeaders.contentTypeHeader] ?? []);
final body = utf8.decode(await response.fold([], (o, n) => o + n));
Expect.equals('日本語\n', body);
}

Future<void> testWriteAllWithoutContentTypeJapanese() async {
final server = await HttpServer.bind(InternetAddress.anyIPv4, 0);

server.first.then((request) {
request.response
..writeAll(['日', '本', '語'])
..close();
});
final request = await HttpClient().get('localhost', server.port, '/');
final response = await request.close();
Expect.listEquals([
'text/plain; charset=utf-8',
], response.headers[HttpHeaders.contentTypeHeader] ?? []);
final body = utf8.decode(await response.fold([], (o, n) => o + n));
Expect.equals('日本語', body);
}

Future<void> testWriteCharCodeWithoutContentTypeJapanese() async {
final server = await HttpServer.bind(InternetAddress.anyIPv4, 0);

server.first.then((request) {
request.response
..writeCharCode(0x65E5)
..close();
});
final request = await HttpClient().get('localhost', server.port, '/');
final response = await request.close();
Expect.listEquals([
'text/plain; charset=utf-8',
], response.headers[HttpHeaders.contentTypeHeader] ?? []);
final body = utf8.decode(await response.fold([], (o, n) => o + n));
Expect.equals('日', body);
}

Future<void> testWriteWithCharsetJapanese() async {
final server = await HttpServer.bind(InternetAddress.anyIPv4, 0);

server.first.then((request) {
request.response
..headers.contentType = ContentType('text', 'plain', charset: 'utf-8')
..write('日本語')
..close();
});
final request = await HttpClient().get('localhost', server.port, '/');
final response = await request.close();
Expect.listEquals([
'text/plain; charset=utf-8',
], response.headers[HttpHeaders.contentTypeHeader] ?? []);
final body = utf8.decode(await response.fold([], (o, n) => o + n));
Expect.equals('日本語', body);
}

/// Tests for regression: https://github.com/dart-lang/sdk/issues/59719
Future<void> testWritelnWithCharsetJapanese() async {
final server = await HttpServer.bind(InternetAddress.anyIPv4, 0);

server.first.then((request) {
request.response
..headers.contentType = ContentType('text', 'plain', charset: 'utf-8')
..writeln('日本語')
..close();
});
final request = await HttpClient().get('localhost', server.port, '/');
final response = await request.close();
Expect.listEquals([
'text/plain; charset=utf-8',
], response.headers[HttpHeaders.contentTypeHeader] ?? []);
final body = utf8.decode(await response.fold([], (o, n) => o + n));
Expect.equals('日本語\n', body);
}

Future<void> testWriteAllWithCharsetJapanese() async {
final server = await HttpServer.bind(InternetAddress.anyIPv4, 0);

server.first.then((request) {
request.response
..headers.contentType = ContentType('text', 'plain', charset: 'utf-8')
..writeAll(['日', '本', '語'])
..close();
});
final request = await HttpClient().get('localhost', server.port, '/');
final response = await request.close();
Expect.listEquals([
'text/plain; charset=utf-8',
], response.headers[HttpHeaders.contentTypeHeader] ?? []);
final body = utf8.decode(await response.fold([], (o, n) => o + n));
Expect.equals('日本語', body);
}

Future<void> testWriteCharCodeWithCharsetJapanese() async {
final server = await HttpServer.bind(InternetAddress.anyIPv4, 0);

server.first.then((request) {
request.response
..headers.contentType = ContentType('text', 'plain', charset: 'utf-8')
..writeCharCode(0x65E5)
..close();
});
final request = await HttpClient().get('localhost', server.port, '/');
final response = await request.close();
Expect.listEquals([
'text/plain; charset=utf-8',
], response.headers[HttpHeaders.contentTypeHeader] ?? []);
final body = utf8.decode(await response.fold([], (o, n) => o + n));
Expect.equals('日', body);
}

Future<void> testWriteWithoutCharsetGerman() async {
final server = await HttpServer.bind(InternetAddress.anyIPv4, 0);

server.first.then((request) {
request.response
..headers.contentType = ContentType('text', 'plain')
..write('Löscherstraße')
..close();
});
final request = await HttpClient().get('localhost', server.port, '/');
final response = await request.close();
Expect.listEquals([
'text/plain',
], response.headers[HttpHeaders.contentTypeHeader] ?? []);
final body = latin1.decode(await response.fold([], (o, n) => o + n));
Expect.equals('Löscherstraße', body);
}

/// If the charset is not recognized then the text is encoded using ISO-8859-1.
///
/// NOTE: If you change this behavior, make sure that you change the
/// documentation for [HttpResponse].
Future<void> testWriteWithUnrecognizedCharsetGerman() async {
final server = await HttpServer.bind(InternetAddress.anyIPv4, 0);

server.first.then((request) {
request.response
..headers.contentType = ContentType('text', 'plain', charset: '123')
..write('Löscherstraße')
..close();
});
final request = await HttpClient().get('localhost', server.port, '/');
final response = await request.close();
Expect.listEquals([
'text/plain; charset=123',
], response.headers[HttpHeaders.contentTypeHeader] ?? []);
final body = latin1.decode(await response.fold([], (o, n) => o + n));
Expect.equals('Löscherstraße', body);
}

Future<void> testWriteWithoutContentTypeGerman() async {
final server = await HttpServer.bind(InternetAddress.anyIPv4, 0);

server.first.then((request) {
request.response
..write('Löscherstraße')
..close();
});
final request = await HttpClient().get('localhost', server.port, '/');
final response = await request.close();
Expect.listEquals([
'text/plain; charset=utf-8',
], response.headers[HttpHeaders.contentTypeHeader] ?? []);
final body = utf8.decode(await response.fold([], (o, n) => o + n));
Expect.equals('Löscherstraße', body);
}

main() async {
// Japanese, utf-8 (only built-in encoding that supports Japanese)
await testWriteWithoutContentTypeJapanese();
await testWritelnWithoutContentTypeJapanese();
await testWriteAllWithoutContentTypeJapanese();
await testWriteCharCodeWithoutContentTypeJapanese();

await testWriteWithCharsetJapanese();
await testWritelnWithCharsetJapanese();
await testWriteAllWithCharsetJapanese();
await testWriteCharCodeWithCharsetJapanese();

// Write using an invalid or non-utf-8 charset will fail for Japanese.

// German
await testWriteWithoutCharsetGerman();
await testWriteWithUnrecognizedCharsetGerman();
await testWriteWithoutContentTypeGerman();
}

0 comments on commit bbd8d97

Please sign in to comment.