[io]: Fix a bug where HttpResponse.writeln did not honor the charset.

Bug:https://github.com/dart-lang/sdk/issues/59719
Change-Id: Ie2606b557b1f9f50d52de23c56d36b45e52efda1
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/402281
Reviewed-by: Alexander Aprelev <aam@google.com>
Commit-Queue: Brian Quinlan <bquinlan@google.com>
diff --git a/sdk/lib/_http/http.dart b/sdk/lib/_http/http.dart
index d873524..e3adac0 100644
--- a/sdk/lib/_http/http.dart
+++ b/sdk/lib/_http/http.dart
@@ -1040,6 +1040,10 @@
 /// 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
@@ -1060,8 +1064,9 @@
 ///     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
diff --git a/sdk/lib/_http/http_impl.dart b/sdk/lib/_http/http_impl.dart
index 2585110..46cee63 100644
--- a/sdk/lib/_http/http_impl.dart
+++ b/sdk/lib/_http/http_impl.dart
@@ -1129,7 +1129,7 @@
   }
 
   void writeln([Object? object = ""]) {
-    _writeString('$object\n');
+    write('$object\n');
   }
 
   void writeCharCode(int charCode) {
diff --git a/tests/standalone/io/http_server_encoding_test.dart b/tests/standalone/io/http_server_encoding_test.dart
new file mode 100644
index 0000000..1df75de
--- /dev/null
+++ b/tests/standalone/io/http_server_encoding_test.dart
@@ -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();
+}