Skip to content

Fix flutter_tools crashing on invalid UTF-8 in log output (fixes #184646)#184685

Merged
auto-submit[bot] merged 12 commits into
flutter:masterfrom
Istiak-Ahmed78:utf8-crash-fix
May 5, 2026
Merged

Fix flutter_tools crashing on invalid UTF-8 in log output (fixes #184646)#184685
auto-submit[bot] merged 12 commits into
flutter:masterfrom
Istiak-Ahmed78:utf8-crash-fix

Conversation

@Istiak-Ahmed78

Copy link
Copy Markdown
Contributor

Summary

Changes the Utf8Decoder in flutter_tools to print a warning instead of calling throwToolExit() when encountering invalid UTF-8 bytes (such as the Unicode replacement character U+FFFD). Previously, this would crash the entire flutter run process, disrupting the developer workflow.

Context

When an app logs data containing invalid UTF-8 (e.g., from external APIs, Bluetooth devices, or network responses), the custom UTF-8 decoder in flutter_tools would detect the replacement character and terminate the tool. This was overly aggressive since debugPrint() and similar logging functions are expected to handle arbitrary data.

Changes

  • packages/flutter_tools/lib/src/convert.dart: Replaced throwToolExit() with a warning print() that reports the issue but allows the tool to continue running.
  • packages/flutter_tools/test/general.shard/convert_test.dart: Updated existing tests and added new tests to verify:
    • Malformed strings return decoded output instead of throwing
    • reportErrors: false decodes silently
    • Empty input, start/end parameters, multiple replacement characters, and valid UTF-8 all work correctly

Before

Bad UTF-8 encoding (U+FFFD; REPLACEMENT CHARACTER) found while decoding string: ...
[flutter run crashes and terminates]

After

Warning: Bad UTF-8 encoding (U+FFFD; REPLACEMENT CHARACTER) found while decoding string: ...
[flutter run continues normally]

Pre-launch Checklist

  • I read the [Contributor Guide] and followed the process outlined there for submitting PRs.
  • I read the [Tree Hygiene] page, which explains my responsibilities.
  • I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement].
  • I signed the [CLA].
  • I listed at least one issue that this PR fixes in the description above.
  • I added/updated tests for this change.
  • I used self-descriptive commit messages.

Fixes #184646

…ter#184646)

The custom Utf8Decoder in flutter_tools was calling throwToolExit()
when encountering invalid UTF-8 bytes (e.g., U+FFFD replacement
character), which crashed the entire 'flutter run' process.

Changed to print a warning instead of crashing, allowing the
development workflow to continue uninterrupted. This is important
because debugPrint() and other logging often contain invalid UTF-8
from external sources (APIs, Bluetooth, network data, etc.).

Updated tests to verify the decoded string is returned instead
of throwing ToolExit.
@github-actions github-actions Bot added the tool Affects the "flutter" command-line tool. See also t: labels. label Apr 6, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request modifies the Utf8Decoder to issue a warning instead of throwing a ToolExit when encountering malformed UTF-8, while also removing the base/common.dart dependency. The test suite is expanded to cover additional decoding edge cases. Review feedback suggests that the validation check may introduce performance overhead and recommends using the logging infrastructure instead of print to prevent console flooding and maintain compatibility with machine-readable output.

Comment thread packages/flutter_tools/lib/src/convert.dart
Comment thread packages/flutter_tools/lib/src/convert.dart Outdated

@justinmc justinmc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure that this shouldn't throw? Have we considered catching this error at a higher level instead?

Also, why does this only happen on web?

@Istiak-Ahmed78

Copy link
Copy Markdown
Contributor Author

@justinmc Thanks for the review! Let me address your questions:

1. Are we sure this shouldn't throw? Have we considered catching this error at a higher level?

We believe printing a warning instead of throwing is the right approach. Here's why:

Why throwing is too aggressive:

  • The invalid UTF-8 comes from the user's running app (Bluetooth devices, network APIs, etc.), not from flutter_tools itself
  • Crashing the entire flutter run process stops the entire development workflow just because of a log message - unlike Android and Windows, where the same invalid UTF-8 in log output doesn't crash the tool
  • The user can still see the output - just as a warning instead of a crash

Why catching at a higher level is more complex:

  • The decode happens in processVmServiceMessage() which is called from multiple places (web in resident_web_runner.dart:855, iOS in ios/devices.dart:1756)
  • Fixing at the Utf8Decoder level is cleaner - it handles all usages across flutter_tools in one place

2. Why does this only happen on web?

This doesn't happen on the web because web exclusively uses VM Service protocol to receive logs from the running app, whereas other platforms use different approaches:

How App Output Works

Platform Compilation Runtime Output Destination
Web Compiles to JavaScript/WASM Runs in browser Browser console → VM Service
Android Compiles to native ARM Runs on device stdout → adb logcat
Windows Compiles to native x64 Runs on desktop stdout → Direct process

How flutter_tools Receives the Output

Platform Log Source Decoder Used Result
Web Via VM Service Protocol (onStdoutEvent) → processVmServiceMessage() Custom Utf8Decoder (throws on U+FFFD) CRASH
Android Via adb logcat (device log reader) Different path No throw
Windows Via direct process stdout (DesktopLogReader) utf8LineDecoder (allows malformed) No throw
iOS Multiple sources (not exclusively VM events) Different path No throw

Root Cause: Web is the only platform that exclusively relies on VM Service protocol to receive stdout/stderr from the running app, which goes through processVmServiceMessage() → our custom Utf8Decoder → throws on U+FFFD.

@Istiak-Ahmed78

Copy link
Copy Markdown
Contributor Author

I ran the example code:

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {

    const String crashString = "�";

    return MaterialApp(
      title: 'App crasher',
      home: Center(
        child: TextButton(onPressed: () => debugPrint(crashString), child: const Text("Crash app")),
      ),
    );
  }
}

And here's I captured behavior in different platforms:
On android

Running Gradle task 'assembleDebug'...                                   474.4s
√ Built build\app\outputs\flutter-apk\app-debug.apk
Installing build\app\outputs\flutter-apk\app-debug.apk...                 42.4s
I/FlutterActivityAndFragmentDelegate(23470): If you are attempting to set --enable-dart-profiling via Intent extras to launch a Flutter component outside of using the Flutter CLI, note that support for setting engine flags on Android via Intent will soon be dropped; see https://github.com/flutter/flutter/issues/180686 for more information on this breaking change. To migrate, set --enable-dart-profiling or any other flags specified via Intent extras on the command line instead or see https://github.com/flutter/flutter/blob/main/docs/engine/Flutter-Android-Engine-Flags.md for alternative methods.
D/FlutterJNI(23470): Beginning load of flutter...
D/FlutterJNI(23470): flutter (null) was loaded normally!
I/flutter (23470): [IMPORTANT:flutter/shell/platform/android/android_context_vk_impeller.cc(62)] Using the Impeller rendering backend (Vulkan).
D/FlutterRenderer(23470): Width is zero. 0,0
D/FlutterRenderer(23470): Width is zero. 0,0
D/FlutterJNI(23470): Sending viewport metrics to the engine.
D/FlutterJNI(23470): Sending viewport metrics to the engine.
D/FlutterJNI(23470): Sending viewport metrics to the engine.
D/BLASTBufferQueue(23470): [VRI[MainActivity]#0](f:0,a:1) acquireNextBufferLocked size=720x1600 mFrameNumber=1 applyTransaction=true mTimestamp=780071801103495(auto) mPendingTransactions.size=0 graphicBufferId=100802882437137 transform=0
D/ProfileInstaller(23470): Installing profile for com.example.basiccappSyncing files to device TECNO KM8n...                                  
D/os.LiceInfo(23470): instance successfully. com.transsion.message.bank.v1.MessageBankLice@f0da6df from com.transsion.message.bank.IMessageBankLice
Syncing files to device TECNO KM8n...                                  
D/TextSelection(23470): call hasMessage cause invalidateCache
Syncing files to device TECNO KM8n...                                  
D/os.SingleLice(23470): instance successfully. com.transsion.dragdrop.v1.DragDropLice@2191218 from com.transsion.dragdrop.DragDropLiceFactory
Syncing files to device TECNO KM8n...                                  
D/os.SingleLiceFactory(23470): instance successfully. com.transsion.dragdrop.v1.DragDropLice@2191218 from android.view.IViewLice
Syncing files to device TECNO KM8n...                                  
D/os.LiceInfo(23470): instance successfully. com.transsion.view.ViewLice@ec03471 from android.view.IViewLice
Syncing files to device TECNO KM8n...                                  
I/flutter (23470): �
Syncing files to device TECNO KM8n...

On Windows

Launching lib\main.dart on Windows in debug mode...
√ Built build\windows\x64\runner\Debug\basiccapp.exe
Connecting to VM Service at ws://127.0.0.1:45363/bS0U2CwLcQU=/ws
Connected to the VM Service.
�

On Web

Launching lib\main.dart on Edge in debug mode...
This app is linked to the debug service: ws://127.0.0.1:45622/FHV3jQ0pi_8=/ws
Debug service listening on ws://127.0.0.1:45622/FHV3jQ0pi_8=/ws
A Dart VM Service on Edge is available at: http://127.0.0.1:45622/FHV3jQ0pi_8=
The Flutter DevTools debugger and profiler on Edge is available at: http://127.0.0.1:9101?uri=ws://127.0.0.1:45622/FHV3jQ0pi_8=/ws
Connecting to VM Service at ws://127.0.0.1:45622/FHV3jQ0pi_8=/ws
Connected to the VM Service.
Starting application from main method in: org-dartlang-app:/web_entrypoint.dart.
Bad UTF-8 encoding (U+FFFD; REPLACEMENT CHARACTER) found while decoding string: �
. The Flutter team would greatly appreciate if you could file a bug explaining exactly what you were doing when this happened:
https://github.com/flutter/flutter/issues/new/choose
The source bytes were:
[239, 191, 189, 10]
3

Exited (1).

@Istiak-Ahmed78 Istiak-Ahmed78 requested a review from justinmc April 9, 2026 16:29
@bkonyi

bkonyi commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

I'm a bit hesitant to change this behavior across the tool since a crash is much more likely to prompt users to file issues than a warning. However, I do agree that print and debugPrint should be able to print strings with invalid UTF-8 characters since they don't need to be parsed specifically by the tool.

I think a better approach would be to update packages/flutter_tools/lib/src/convert.dart to allow for specifying whether or not we should consider an invalid character fatal, and then update the places in the tool which are simply echoing application logs to ignore the invalid characters.

@Istiak-Ahmed78

Copy link
Copy Markdown
Contributor Author

@bkonyi I apologize for the late reply. Whatever, thanks for the thoughtful feedback! I understand your concern about losing crash detection for real errors.

However, I'd like to clarify why I believe the current approach is appropriate:

Why We Don't Need Two Rules Here

1. The crash ONLY happens in app log processing

  • processVmServiceMessage() is exclusively used for echoing application output
  • It's not used for parsing config files, project metadata, or critical tool operations
  • The tool already has separate validation for those critical paths

2. Invalid UTF-8 in app logs is expected

  • Apps receive data from Bluetooth devices, network APIs, external services
  • These sources often have encoding issues beyond the app's control
  • debugPrint() and similar logging functions are designed to handle arbitrary data
  • This is why Android and Windows don't crash on the same data

3. Real tool errors are caught elsewhere

  • If flutter_tools itself corrupts UTF-8 data, it would fail during parsing/validation
  • Those failures happen in different code paths with different error handling
  • The custom Utf8Decoder in this context is specifically for displaying app output, not processing tool data

Proposed Alternative

If you're concerned about future use cases, we could:

  1. Keep the current simple fix (warning instead of crash)
  2. Add a clear comment explaining this is for app log display only
  3. If Utf8Decoder is ever used for critical operations, create a separate validation layer at that point

What do you think? Does this address your concern?

@bkonyi

bkonyi commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

@bkonyi I apologize for the late reply. Whatever, thanks for the thoughtful feedback! I understand your concern about losing crash detection for real errors.

Not a problem! 😄

We actually have places in the tool where we're explicitly disabling this check for invalid characters (here's one example), so this shouldn't be too hard to do elsewhere without disabling this warning. While you're right that the tool failing to parse UTF-8 is a tooling error, we do use the same Utf8Decoder implementation for that and outputting logs from the VM service.

I think the main issue is that we're using the utf8 instance in most places throughout the tool, which does report decoding errors by default. We should identify the locations where we're using this utf8 instance and determine if they should actually be treating UTF-8 encoding errors as failures, in particular when we're processing log output.

@Istiak-Ahmed78

Istiak-Ahmed78 commented Apr 19, 2026

Copy link
Copy Markdown
Contributor Author

@bkonyi Thanks for the thoughtful suggestion! I'll
identify the specific locations where utf8 is used and apply different
handling based on whether it's processing tool data or app logs. This
way we maintain safety for critical operations while fixing the issue
for log output. I appreciate the guidance!

@Istiak-Ahmed78

Istiak-Ahmed78 commented Apr 22, 2026

Copy link
Copy Markdown
Contributor Author

@bkonyi, @justinmc , I conductuced a details investigation about this. Let me document what I've discovered:

2018-2026: The Same Bug Reported 13+ Times

Year Issue Platform Status
2018 #15646 Windows (zh-CN locale) Crash on invalid UTF-8
2018 #16581 Unknown Crash on invalid UTF-8
2018 #15296 Unknown Crash on invalid UTF-8
2019 #33763 macOS (ko-KR locale) Crash on invalid UTF-8
2023 #120447 Unknown Bad UTF-8 encoding
2023 #123850 Unknown Bad UTF-8 encoding
2023 #133102 Unknown Bad UTF-8 encoding
2024 #117952 Unknown Bad UTF-8 encoding
2024 #137025 Web Unicode char throws Bad UTF-8
2024 #160010 Unknown Bad UTF-8 encoding
2024 #169260 Unknown Bad UTF-8 encoding
2024 #171244 Unknown Bad UTF-8 encoding
2026 #184646 Web (Bluetooth data) Bad UTF-8 encoding

Last merged PR on this issue was PR #26650 (2019) - The Incomplete Fix

What jonahwilliams Did:

He wrapped dart:convert's strict utf8 decoder with a custom Utf8Decoder class that:

  1. Accepts allowMalformed: true parameter to the internal system decoder
  2. Detects replacement character (U+FFFD) and throws with context
  3. Added reportErrors parameter to distinguish behavior

The Problem - Incomplete Implementation:

Looking at the code he added:

class Utf8Decoder extends Converter<List<int>, String> {
  const Utf8Decoder({this.reportErrors = true});  //  Parameter exists
  
  static const _systemDecoder = cnv.Utf8Decoder(allowMalformed: true);  //  System decoder is permissive
  
  final bool reportErrors;
  
  @override
  String convert(List<int> input, [int start = 0, int? end]) {
    final String result = _systemDecoder.convert(input, start, end);
    //  If reportErrors: true and result contains U+FFFD  CRASH
    if (reportErrors && result.contains('\u{FFFD}')) {
      throwToolExit('Bad UTF-8 encoding...');  //  THROWS!
    }
    return result;
  }
}

Then he created this global constant:

const Encoding utf8 = Utf8Codec();  //  Always reportErrors: true (STRICT)

He had the infrastructure (reportErrors parameter) but didnot cover all the cases:

@bkonyi Recommended

"Create a separate parameter to distinguish between app logs and tool data"

"We should identify the locations where we're using this utf8 instance and determine if they should actually be treating UTF-8 encoding errors as failures, in particular when we're processing log output."


I separated the two use cases:

1. Created two distinct constants:

const Encoding utf8 = Utf8Codec();  //  Strict (for tool data)
const Encoding utf8AllowMalformed = Utf8Codec(reportErrors: false);  // Permissive (for app logs)

2. Applied permissive decoding to app log streams:

  • vmservice.dart - VM Service events (app logs)
  • compile.dart - Compiler stderr (external tool output)
  • test/runner.dart - Test output (external processes)
  • debug_adapters/flutter_base_adapter.dart - Debugger output
  • base/utils.dart - Helper for line-based streams

3. Kept strict decoding where needed:

  • Config file parsing
  • Build metadata
  • Tool-critical operations

@fluttergithubbot

Copy link
Copy Markdown
Contributor

An existing Git SHA, 4ad67d39aa826202799efb93cbae8c9a2fb860b7, was detected, and no actions were taken.

To re-trigger presubmits after closing or re-opeing a PR, or pushing a HEAD commit (i.e. with --force) that already was pushed before, push a blank commit (git commit --allow-empty -m "Trigger Build") or rebase to continue.

@bkonyi bkonyi added the CICD Run CI/CD label Apr 22, 2026
'https://github.com/flutter/flutter/issues/new/choose\n'
'The source bytes were:\n$input\n\n',
// ignore: avoid_print
print(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should revert this change and continue invoking throwToolExit for the reportErrors case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oww! Exactly. I didnot notice. Right said. I'll do

@bkonyi

bkonyi commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Thanks @Istiak-Ahmed78! This looks much better! I think this will fix most of the common cases where we can encounter invalid UTF-8.

@github-actions github-actions Bot removed the CICD Run CI/CD label Apr 22, 2026
@Istiak-Ahmed78

Copy link
Copy Markdown
Contributor Author

@bkonyi , I reverted the change you suggested. Let me know if more changes needed

@bkonyi bkonyi added the CICD Run CI/CD label Apr 30, 2026
@justinmc

justinmc commented May 1, 2026

Copy link
Copy Markdown
Contributor

Heads up @Istiak-Ahmed78 there are some test failures here.

@Istiak-Ahmed78

Copy link
Copy Markdown
Contributor Author

Heads up @Istiak-Ahmed78 there are some test failures here.

Aww. Okey. I'll check

@github-actions github-actions Bot removed the CICD Run CI/CD label May 3, 2026
@Istiak-Ahmed78

Istiak-Ahmed78 commented May 3, 2026

Copy link
Copy Markdown
Contributor Author

Heads up @Istiak-Ahmed78 there are some test failures here.

@justinmc , I addressed the Linux Analysis failure. Can you please check again. Is everything okey now?

@bkonyi bkonyi added the CICD Run CI/CD label May 4, 2026
@github-actions github-actions Bot removed the CICD Run CI/CD label May 4, 2026
@bkonyi bkonyi added CICD Run CI/CD labels May 4, 2026
@bkonyi

bkonyi commented May 4, 2026

Copy link
Copy Markdown
Contributor

@justinmc can I get you to reapprove this PR please? :)

@justinmc justinmc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍 . One test timed out, I've rerun it.

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

Labels

CICD Run CI/CD tool Affects the "flutter" command-line tool. See also t: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Flutter requested I file a bug about a Bad UTF-8 encoding when running a large application handling radio data of dubious UTF-8-itude

4 participants