Closed
Description
The following code simulates an error while processing a HttpRequest server side.
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
extension on HttpResponse {
Future<void> flushAndClose() async {
await flush();
await close();
}
}
Future<void> main(List<String> arguments) async {
const port = 8080;
await Isolate.spawn((port) async {
final server = await HttpServer.bind(InternetAddress.loopbackIPv6, port);
print('[Server] Started on port $port');
await for (var request in server) {
print('[Server] Request received: $request');
final response = request.response;
response.headers.contentType = ContentType(
'text',
'plain',
charset: 'utf-8',
);
try {
// INNER TRY BLOCK
await for (var data in request) {
print('[Server] Data received: $data');
// throw without draining the request data stream
throw Exception('This is an internal server error');
request.response.add(data);
}
await request.response.flushAndClose();
} catch (e) {
print('Error: $e');
response.statusCode = HttpStatus.internalServerError;
response.write('Internal server error: $e');
await request.response.flushAndClose();
}
}
}, port);
final request = await HttpClient().post('::1', port, '/');
request.headers.contentType = ContentType('text', 'plain', charset: 'utf-8');
request.add(utf8.encode('Hello, world!'));
final response = await request.close();
print('[Client] Response: ${await response.transform(utf8.decoder).join()}');
print('[Client] Response status: ${response.statusCode}');
exit(response.statusCode == HttpStatus.ok ? 0 : response.statusCode);
}
However this will not report a 500 on the client as expected, instead we get connection closed:
[Server] Started on port 8080
[Server] Request received: Instance of '_HttpRequest'
[Server] Data received: [72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100, 33]
Error: Exception: This is an internal server error
Unhandled exception:
HttpException: Connection closed while receiving data, uri = http://[::1]:8080/
#0 _HttpIncoming.listen.<anonymous closure> (dart:_http/http_impl.dart:433:11)
#1 Stream.handleError.<anonymous closure> (dart:async/stream.dart:959:16)
#2 _HandleErrorStream._handleError (dart:async/stream_pipe.dart:303:17)
#3 _ForwardingStreamSubscription._handleError (dart:async/stream_pipe.dart:188:13)
#4 _RootZone.runBinaryGuarded (dart:async/zone.dart:1790:10)
...
I found a workaround, which is to ensure that the request
stream is drained before raising the exception,
ie. replace the INNER TRY BLOCK with:
var error = false;
await for (var data in request) {
print('[Server] Data received: $data');
error = true;
if (error) continue; // always drain request stream
request.response.add(data);
}
if (error) throw Exception('This is an internal server error');
await request.response.flushAndClose();
which will give the expected flow:
[Server] Started on port 8080
[Server] Request received: Instance of '_HttpRequest'
[Server] Data received: [72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100, 33]
Error: Exception: This is an internal server error
[Client] Response: Internal server error: Exception: This is an internal server error
[Client] Response status: 500
Exited (244).
But the code feels clumsy, and I don't like that I have to drain the request stream first. Why would we waste time traversing the full stream, when we have already decided to fault.
- The Dart version and tooling diagnostic info (
dart info
)
bin/dart info
If providing this information as part of reporting a bug, please review the information
below to ensure it only contains things you're comfortable posting publicly.
#### General info
- Dart 3.7.0 (stable) (Wed Feb 5 04:53:58 2025 -0800) on "macos_x64"
- on macos / Version 15.3.1 (Build 24D70)
- locale is en-DK
#### Process info
| Memory | CPU | Elapsed time | Command line |
| -----: | ---: | -----------: | -------------------------------------------------------------------------------- |
| 435 MB | 0.0% | 01:05:26 | dart language-server --protocol=lsp --client-id=VS-Code --client-version=3.106.0 |
| 507 MB | 0.0% | 03:01:58 | dart language-server --protocol=lsp --client-id=VS-Code --client-version=3.106.0 |
| 75 MB | 0.0% | 01:05:26 | dart tooling-daemon --machine |
| 27 MB | 0.0% | 03:01:58 | dart tooling-daemon --machine |
| 41 MB | 0.0% | 02:17:14 | serverpod_cli.dart-3.7.0-323.0.dev.snapshot language-server --stdio |
Metadata
Metadata
Assignees
Type
Projects
Milestone
Relationships
Development
No branches or pull requests
Activity
fix:Ensure we drain the request data stream completely, even if we re…
fix: Always drain the request data stream completely
fix: Always drain the request data stream completely
fix: Always drain the request data stream completely
brianquinlan commentedon Mar 13, 2025
I think that this is working as intended.
If you don't read the body at all then calling
HttpResponse.write
orHttpResponse.close
will drain it for you. But I think that the contents must be drained because the connection may be persisted i.e. if the same socket is used for the next HTTP request by the client.Please reopen if you disagree.
nielsenko commentedon Mar 14, 2025
@brianquinlan Thank you for taking a look at this issue.
I agree that the request must be drained as the socket is still in use, but I think it should be handled further down the stack, or at least documented better. The work-around is not natural, as I'm sure you can agree. I'm just thinking of the next guy/girl 😄