Skip to content

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

Closed
@nielsenko

Description

@nielsenko

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              |

Activity

added
area-vmUse area-vm for VM related issues, including code coverage, and the AOT and JIT backends.
on Mar 10, 2025
brianquinlan

brianquinlan commented on Mar 13, 2025

@brianquinlan
Contributor

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.

I think that this is working as intended.

If you don't read the body at all then calling HttpResponse.write or HttpResponse.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.

self-assigned this
on Mar 13, 2025
nielsenko

nielsenko commented on Mar 14, 2025

@nielsenko
Author

@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 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area-vmUse area-vm for VM related issues, including code coverage, and the AOT and JIT backends.library-io

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

    Development

    No branches or pull requests

      Participants

      @lrhn@brianquinlan@nielsenko

      Issue actions

        Expected response.statusCode == 500, but got HttpException: Connection closed · Issue #60271 · dart-lang/sdk