Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expected response.statusCode == 500, but got HttpException: Connection closed #60271

Open
nielsenko opened this issue Mar 7, 2025 · 0 comments
Labels
area-vm Use area-vm for VM related issues, including code coverage, and the AOT and JIT backends. library-io

Comments

@nielsenko
Copy link

nielsenko commented Mar 7, 2025

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              |
nielsenko added a commit to nielsenko/serverpod that referenced this issue Mar 7, 2025
nielsenko added a commit to nielsenko/serverpod that referenced this issue Mar 8, 2025
- Ensure we drain the request data stream completely, even if we realize it is too large
- Add e2e test for request too large

This is a workaround to dart-lang/sdk#60271
nielsenko added a commit to nielsenko/serverpod that referenced this issue Mar 10, 2025
performance:
- Don't start reading, if contentLenght is too large
- Use BytesBuffer copy flag to avoid copying data
- Use takeBytes to avoid copying data

- Ensure we drain the request data stream completely, even if we realize it is too large
- Add e2e test for request too large

This is a workaround to dart-lang/sdk#60271
nielsenko added a commit to nielsenko/serverpod that referenced this issue Mar 10, 2025
performance:
- Don't start reading, if contentLenght is too large
- Use BytesBuffer copy flag to avoid copying data
- Use takeBytes to avoid copying data

- Ensure we drain the request data stream completely, even if we realize it is too large
- Add e2e test for request too large

This is a workaround to dart-lang/sdk#60271
@lrhn lrhn added area-vm Use area-vm for VM related issues, including code coverage, and the AOT and JIT backends. library-io labels Mar 10, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-vm Use area-vm for VM related issues, including code coverage, and the AOT and JIT backends. library-io
Projects
None yet
Development

No branches or pull requests

2 participants