Repository navigation
Description
Steps to reproduce
- Create a project with
flutter create --platforms=web proxy_stream_repro. - Replace/add the four files below, then run
flutter pub get. - In terminal A, run
dart backend.dart. - In terminal B, run
flutter run -d web-serverfrom the project directory. - Open
http://127.0.0.1:51881in a browser. Both requests run automatically.
The backend writes and flushes a small JSON line immediately, then holds the
response open for 30 seconds. The client waits up to 3 seconds for the first line.
response.bufferOutput = false applies to the backend; Flutter's proxy has its
own downstream response buffering.
Expected results
Both the direct request and the request through Flutter's proxy receive
{"type":"connected"} promptly, without waiting for the response to close.
Actual results
GET http://127.0.0.1:51880/api/events
HTTP 200 at 33 ms
First line at 33 ms: {"type":"connected"}
GET http://127.0.0.1:51881/api/events
HTTP 200 at 0 ms
After 3000 ms: TimeoutException after 0:00:03.000000: Future not completed
Related:
dart-lang/shelf#503
Code sample
name: proxy_stream_repro
publish_to: none
environment:
sdk: ^3.10.0
dependencies:
flutter:
sdk: flutter
http: 1.6.0
flutter:
uses-material-design: truebackend.dart
import 'dart:async';
import 'dart:io';
Future<void> main() async {
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 51880);
print('Backend: http://127.0.0.1:51880/api/events');
await for (final request in server) {
unawaited(handle(request));
}
}
Future<void> handle(HttpRequest request) async {
final response = request.response;
response.headers.set('Access-Control-Allow-Origin', '*');
if (request.uri.path != '/api/events') {
response.statusCode = HttpStatus.notFound;
await response.close();
return;
}
response.bufferOutput = false;
response.headers.set('Content-Type', 'application/x-ndjson');
response.headers.set('Cache-Control', 'no-store');
response.headers.set('X-Accel-Buffering', 'no');
try {
response.write('{"type":"connected"}\n');
await response.flush();
// Keep the response open without filling any downstream proxy buffer.
await Future<void>.delayed(const Duration(seconds: 30));
await response.close();
} catch (_) {
// The browser aborts after receiving its first line or timing out.
}
}web_dev_config.yaml
server:
host: "127.0.0.1"
port: 51881
proxy:
- prefix: "/api/"
target: "http://127.0.0.1:51880"lib/main.dart
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/browser_client.dart';
import 'package:http/http.dart' as http;
void main() => runApp(const MaterialApp(home: StreamDemo()));
class StreamDemo extends StatefulWidget {
const StreamDemo({super.key});
@override
State<StreamDemo> createState() => _StreamDemoState();
}
class _StreamDemoState extends State<StreamDemo> {
final _messages = <String>[];
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
await _probe('http://127.0.0.1:51880/api/events');
await _probe('/api/events');
});
}
void _log(String message) {
if (mounted) setState(() => _messages.add(message));
}
Future<void> _probe(String path) async {
final client = BrowserClient();
final abort = Completer<void>();
final uri = Uri.base.resolve(path);
final watch = Stopwatch()..start();
_log('GET $uri');
try {
final firstLine = await (() async {
final response = await client.send(
http.AbortableRequest('GET', uri, abortTrigger: abort.future),
);
_log('HTTP ${response.statusCode} at ${watch.elapsedMilliseconds} ms');
return response.stream
.transform(utf8.decoder)
.transform(const LineSplitter())
.first;
})().timeout(const Duration(seconds: 3));
_log('First line at ${watch.elapsedMilliseconds} ms: $firstLine');
} catch (error) {
_log('After ${watch.elapsedMilliseconds} ms: $error');
} finally {
abort.complete();
client.close();
}
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Flutter proxy streaming reproduction')),
body: Padding(
padding: const EdgeInsets.all(16),
child: SingleChildScrollView(
child: SelectableText(_messages.join('\n')),
),
),
);
}Screenshots or Video
No response
Logs
No response
Flutter Doctor output
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.47.7, on Debian GNU/Linux 13 (trixie)
6.12.111+deb13-amd64, locale zh_CN.UTF-8)
[!] Android toolchain - develop for Android devices (Android SDK version 36.0.0)
✗ cmdline-tools component is missing.
Try installing or updating Android Studio.
Alternatively, download the tools from
https://developer.android.com/studio#command-line-tools-only and make sure
to set the ANDROID_HOME environment variable.
See https://developer.android.com/studio/command-line for more details.
✗ Android license status unknown.
Run `flutter doctor --android-licenses` to accept the SDK licenses.
See https://flutter.dev/to/linux-android-setup for more details.
[✓] Chrome - develop for the web
[✓] Linux toolchain - develop for Linux desktop
[✓] Connected device (2 available)
Activity
saurabh-mirajkar commentedon Oct 14, 2026
@onism0106 Thanks for the report. I am able to reproduce the issue on the latest stable release following the details provided in the report. Labelling for
team-webto investigate further.Scout-commits-dot commentedon Oct 16, 2026
[flutter_tools] Stream web dev server proxy responses instead of buff…