Skip to content

[vector_graphics] useHtmlRenderObject() only catches UnsupportedError, leaks StateError from CanvasKit on iOS Safari #186333

Description

@smihica

Steps to reproduce

  1. Build any Flutter web app that renders SVGs via flutter_svg / vector_graphics.
  2. Build with the CanvasKit renderer (the default for Flutter web on Safari).
  3. Open the page in Mobile Safari on iOS 18.7 (observed on iPhone with Safari 26.3).

Expected results

useHtmlRenderObject() is supposed to detect that the current web renderer cannot perform a synchronous picture-to-image conversion and fall back to the HTML render object. The error should be swallowed and a fallback path taken.

Actual results

The fallback only triggers for UnsupportedError. On affected Safari builds, CanvasKit's SkImage throws a StateError ("Unable to convert read pixels from SkImage") instead, which escapes the try/catch and propagates as an unhandled FlutterError,
crashing the widget subtree.

  StateError: Bad state: Unable to convert read pixels from SkImage.
    at CkPicture.toImageSync (org-dartlang-sdk:///lib/_engine/engine/canvaskit/picture.dart:129:7)
    at useHtmlRenderObject (.../vector_graphics-1.1.20/lib/src/render_object_selection.dart:28:13)
    at _VectorGraphicWidgetState.build (.../vector_graphics-1.1.20/lib/src/vector_graphics.dart:471:11)
    ...

Code sample

main.dart
  import 'package:flutter/material.dart';
  import 'package:flutter_svg/flutter_svg.dart';

  void main() => runApp(const ReproApp());

  class ReproApp extends StatelessWidget {
    const ReproApp({super.key});

    @override
    Widget build(BuildContext context) {
      return MaterialApp(
        home: Scaffold(
          body: Center(
            // Any SvgPicture is enough — the crash happens inside
            // vector_graphics' useHtmlRenderObject() probe, before this
            // widget even paints, when CanvasKit's toImageSync fails.
            child: SvgPicture.asset('assets/example.svg', width: 64, height: 64),
          ),
        ),
      );
    }
  }

Screenshots or Video

No response

Logs

No response

Flutter Doctor output

Flutter doctor -v

[✓] Flutter (Channel stable, 3.41.5, on <host OS>, locale en_US.UTF-8)
    • Framework revision 2c9eb20739, 2026-03-17 16:14:01 -0700
    • Engine revision 052f31d115
    • Dart version 3.11.3
    • DevTools version 2.54.2

[✓] Chrome - develop for the web
    • CHROME_EXECUTABLE = <path>

[✓] Connected device (2 available)
    • Linux (desktop)
    • Chrome (web)

[✓] Network resources

Affected client device (not the build host): iPhone, iOS 18.7, Mobile Safari 26.3 — this is where the StateError is thrown. Any Flutter web build using CanvasKit reaches the same code path; the crash is browser/engine specific, not build-host specific.

Root cause

In packages/vector_graphics/lib/src/render_object_selection.dart:

try {
  image = picture.toImageSync(1, 1);
  _cachedUseHtmlRenderObject = false;
} on UnsupportedError catch (_) {
  _cachedUseHtmlRenderObject = true;
} finally {
  image?.dispose();
  picture.dispose();
}

The on UnsupportedError clause was added in commit 5c729eb (Sep 2022, "HTML backend compatibility (#138)") to detect the original HTML renderer which threw UnsupportedError for toImageSync. It does not cover the runtime StateError that the current CanvasKit engine throws on certain Safari versions when readPixels fails. The same code is present in vector_graphics 1.1.20, 1.1.21, and 1.2.0 (latest published), as well as the main branch.

Suggested fix

Treat any failure of the probe call as "the current renderer cannot do this, use the HTML fallback":

try {
  image = picture.toImageSync(1, 1);
  _cachedUseHtmlRenderObject = false;
} on UnsupportedError catch (_) {
  _cachedUseHtmlRenderObject = true;
} on StateError catch (_) {
  _cachedUseHtmlRenderObject = true;
} finally {
  image?.dispose();
  picture.dispose();
}

(Or collapse to a single catch (_) — this function's only job is to probe and pick a fallback, so any failure should mean "use the HTML path".)

Happy to send a PR with this change + a test if the maintainers agree on the direction.

Update (2026-05-11): second exception type observed from the same probe

After filing this issue I went looking through older Sentry data on the same app and found a second flavor of failure originating from the same useHtmlRenderObject() probe call on the same browser/OS combination (CanvasKit, iPhone Mobile Safari, iOS 18.7):

NoSuchMethodError: Null check operator used on a null value
  at wb (canvaskit.js:163:45)
  at <wasm-function>[697]
  at <wasm-function>[11400]
  at <wasm-function>[1994]
  at Surface.createOrUpdateSurface (org-dartlang-sdk:///dart-sdk/lib/_internal/js_shared/lib/js_util_patch.dart:148:10)
  at CkPicture.toImageSync (org-dartlang-sdk:///lib/_engine/engine/canvaskit/picture.dart:94:41)
  at useHtmlRenderObject (.../vector_graphics-1.1.19/lib/src/render_object_selection.dart:28:13)

Same probe, same renderer/browser, different exception type — surfaced via canvaskit.js wasm internals on the way down from CkPicture.toImageSync.

The PR (flutter/packages#11685) has been updated to add NoSuchMethodError alongside StateError:

try {
  image = picture.toImageSync(1, 1);
  _cachedUseHtmlRenderObject = false;
} on UnsupportedError catch (_) {
  _cachedUseHtmlRenderObject = true;
} on StateError catch (_) {
  _cachedUseHtmlRenderObject = true;
} on NoSuchMethodError catch (_) {
  _cachedUseHtmlRenderObject = true;
} finally {
  image?.dispose();
  picture.dispose();
}

Two independently observed exception types from the same probe is the practical evidence that this function should treat any failure as a feature-detection negative. Enumeration keeps the catch surface explicit while covering both observed cases.

Metadata

Metadata

Assignees

No one assigned

    Labels

    engineflutter/engine related. See also e: labels.has reproducible stepsThe issue has been confirmed reproducible and is ready to work onp: vector_graphicsThe vector_graphics package setpackageflutter/packages repository. See also p: labels.platform-webWeb applications specificallyteam-engineOwned by Engine team

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions