Skip to content

Regression handling deep links with the app closed in iOS #170665

Description

@js2702

Steps to reproduce

It looks like in 3.32.x there has been a regression with deep links handling with the app closed in iOS. We did some git bisect and it looks to be related to this commit #164247 @hellohuanlin .

Steps:

  1. Install an app with https deep links managed by Flutter.
  2. Kill the app.
  3. Trigger the deep link (for example with xcrun simctl openurl booted )
  4. App is started but instead of opening the link in the app it opens in Safari.

Our app takes a couple seconds to load and deep links can only be handled after that. So what we do when onGenerateRoute is called and the app didn't load yet we save the link for later handling.

In the example we do a mock of this behavior that can trigger the error. This only seems to happen with https links because they are handled by the phone browser. After some debugging it looks like it has this behavior because we return null in generateRoute when the app didn't load. This didn't happen before the linked commit.

Expected results

Link is handled in app.

Actual results

Link is opened in Safari.

Code sample

import 'dart:async';

import 'package:flutter/material.dart';

// When a deep link is triggered while the app is closed we first need to load the app so we save it here to handle later.
String? globalPendingDeepLink;

Future<void> main() async {
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  bool loading = true;

  @override
  void initState() {
    super.initState();
    // Mock initial app loading. Deep links would be handled after the app loads.
    Future.delayed(Duration(seconds: 3)).then((_) {
      if (!mounted) return;
      setState(() {
        loading = false;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      onGenerateRoute: createOnGenerateRoute(),
      onGenerateInitialRoutes: (route) {
        print("FLUTTER: onGenerateInitialRoutes: $route");

        return [
          MaterialPageRoute(
            builder: (context) {
              return HomeWidget(appLoading: loading);
            },
          ),
        ];
      },
    );
  }

  RouteFactory createOnGenerateRoute() {
    return (RouteSettings settings) {
      final pathOrUri = settings.name;
      print("FLUTTER: onGenerateRoute: $pathOrUri initial app loading: $loading");
      if (loading) {
        // Save link until the app is loaded to handle later.
        globalPendingDeepLink = pathOrUri;
        return null;
      }

      final route = _handleRouteSettings(pathOrUri);
      if (route != null) {
        return route;
      }

      return null;
    };
  }
}

class HomeWidget extends StatefulWidget {
  final bool appLoading;
  const HomeWidget({super.key, required this.appLoading});

  @override
  State<HomeWidget> createState() => HomeWidgetState();
}

class HomeWidgetState extends State<HomeWidget> {
  @override
  void didUpdateWidget(covariant HomeWidget oldWidget) {
    if (widget.appLoading != oldWidget.appLoading && !widget.appLoading) {
      print("FLUTTER: app has been loaded. Pending link $globalPendingDeepLink");
      WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
        if (globalPendingDeepLink != null) {
          final route = _handleRouteSettings(globalPendingDeepLink);
          if (route != null) {
            globalPendingDeepLink = null;
            Navigator.of(context, rootNavigator: true).push<void>(route);
          }
        }
      });
    }
    ;
    super.didUpdateWidget(oldWidget);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Home")),
      body: Center(child: widget.appLoading ? CircularProgressIndicator() : Text("Home")),
    );
  }
}

Route<Object?>? _handleRouteSettings(String? pathOrUri) {
  print("HANDLE ROUTE SETTINGS: $pathOrUri");
  if (pathOrUri == null || pathOrUri == "/") {
    return null;
  }

  return MaterialPageRoute(
    builder: (context) => Scaffold(
      appBar: AppBar(),
      body: Center(child: Text("Deep link: $pathOrUri")),
    ),
  );
}

Screenshots or Video

No response

Logs

Since the app needs to be closed when opening through the deep link we don't have logs.

Flutter Doctor output

Doctor output
[✓] Flutter (Channel stable, 3.32.4, on macOS 15.5 24F74 darwin-arm64, locale es-ES) [387ms]
    • Flutter version 3.32.4 on channel stable at /Users/jorge/.local/share/mise/installs/flutter/3.32.4-stable
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 6fba2447e9 (hace 3 días), 2025-06-12 19:03:56 -0700
    • Engine revision 8cd19e509d
    • Dart version 3.8.1
    • DevTools version 2.45.1

[✗] Android toolchain - develop for Android devices [368ms]
    • Android SDK at /opt/homebrew/Caskroom/android-platform-tools/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.

[✓] Xcode - develop for iOS and macOS (Xcode 16.2) [835ms]
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Build 16C5032a
    • CocoaPods version 1.16.2

[✓] Chrome - develop for the web [11ms]
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2022.3) [10ms]
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 17.0.6+0-17.0.6b829.9-10027231)

[✓] VS Code (version 1.101.0) [9ms]
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.112.0

[✓] Connected device (4 available) [6,2s]
    • iphone J (wireless) (mobile) • 00008130-00016C4A0E90001C            • ios            • iOS 18.4.1 22E252
    • iPhone 15 Pro (mobile)       • 52288C1D-7C4B-4E41-8F79-FC26DEEF0EC3 • ios            • com.apple.CoreSimulator.SimRuntime.iOS-18-3 (simulator)
    • macOS (desktop)              • macos                                • darwin-arm64   • macOS 15.5 24F74 darwin-arm64
    • Chrome (web)                 • chrome                               • web-javascript • Google Chrome 137.0.7151.104
    ! Error: Browsing on the local area network for iPad J. Ensure the device is unlocked and attached with a cable or associated with the same local area network as this Mac.
      The device must be opted into Developer Mode to connect wirelessly. (code -27)

[✓] Network resources [287ms]
    • All expected network resources are available.

! Doctor found issues in 1 category.

Activity

  1. darshankawar commented on Jun 17, 2025

    @darshankawar
    Member

    @js2702 Thanks for the report. I tried a sample deeplinks app from this issue and ran it in latest stable 3.32.4 with which I didn't observe the reported behavior. Upon running the command xcrun simctl openurl booted 'sampleiosdeeplinks:///details' after killing the app from recent screen, it opened the app properly.

    Can you provide us the same sample demo that we can use to verify this further ?

  2. added
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Jun 17, 2025
  3. js2702 commented on Jun 17, 2025

    @js2702
    Author

    @darshankawar I updated the issue description with a sample. After further testing we found the issue only happens with https links and it's because we return null in the onGenerateRoute, because we handle the link later in the initial app start flow. It does not seem to trigger with custom url schemes. Our theory is because only the app would handle the link, with https also the browser can handle the link.

    One thing that we found months ago when doing this deep link handling is that on Android the deep link from a closed app comes through the function onGenerateInitialRoutes whereas on iOS it comes through onGenerateRoute. Maybe this difference could be related?

  4. removed
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Jun 17, 2025
  5. darshankawar commented on Jun 18, 2025

    @darshankawar
    Member

    Android the deep link from a closed app comes through the function onGenerateInitialRoutes whereas on iOS it comes through onGenerateRoute. Maybe this difference could be related?

    I believe this is WAI. Please see #161867.

    re: the issue reported, I used the provided code sample by you in same deeplinks sample I used earlier and after killing the app from recent apps screen, ran xcrun simctl openurl booted 'sampleiosdeeplinks:///details' which took me to the details screen correctly.
    Did I use the deeplink url correctly ? If not, please provide the link to append to the command, or you may try the same by using the code you provided in the issue I linked earlier and run the above provided command.

  6. added
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Jun 18, 2025
  7. js2702 commented on Jun 19, 2025

    @js2702
    Author

    @darshankawar I understand if that difference works as intended. I was just trying to find some explanation to the regression from the commit linked in the description.
    The other issue you mentioned with an example is not valid. The problem only occurs when handling https urls. And for that there needs to be a registered domain. Maybe the Flutter team has a package id to use that can be used in debug to test https deep links.
    The command is the same, just changing the url to a https one.

    Our dart code example can be used in conjunction with the https configuration for iOS to associate a domain with the app.
    https://docs.flutter.dev/cookbook/navigation/set-up-universal-links#associate-your-app-with-your-web-domain

  8. removed
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Jun 19, 2025
  9. darshankawar commented on Jun 20, 2025

    @darshankawar
    Member

    Thanks for the update.

    The problem only occurs when handling https urls. And for that there needs to be a registered domain. Maybe the Flutter team has a package id to use that can be used in debug to test https deep links. The command is the same, just changing the url to a https one.

    Our dart code example can be used in conjunction with the https configuration for iOS to associate a domain with the app. https://docs.flutter.dev/cookbook/navigation/set-up-universal-links#associate-your-app-with-your-web-domain

    I'll try to set this up and verify if I can replicate / validate this or not. Meantime, I'll keep the issue open for team's tracking on behavior reported.

  10. added
    platform-iosiOS applications specifically
    team-iosOwned by iOS platform team
    c: regressionIt was better in the past than it is now
    and removed
    in triagePresently being triaged by the triage team
    on Jun 20, 2025
  11. added
    P1High-priority issues at the top of the work list
    on Jun 20, 2025
  12. 40 remaining items

  13. self-assigned this
    on Dec 8, 2025
  14. added
    ⏳Bot is counting down the days until it unassigns the issue
    on Apr 13, 2026
  15. flutter-triage-bot commented on Apr 13, 2026

    @flutter-triage-bot

    This issue is assigned to @okorohelijah but has had no recent status updates. Please consider unassigning this issue if it is not going to be addressed in the near future. This allows people to have a clearer picture of what work is actually planned. Thanks!

  16. okorohelijah commented on Apr 21, 2026

    @okorohelijah
    Contributor

    Here is my friction log/ observation:

    I completely closed the app from recent apps, it opened on safari with the content {'error': 'Not found: /details'} but then there is a notification to open the app at the top of the website which i can confirm opens. I don't think this should be the expected behavior?? @hellohuanlin @vashworth

    I tried using the custom url in debug mode cold start but i got error: "in Ios 14 debug mode Flutter apps can only be lunched from flutter tooling, ides with flutter plugins, or form xcode. Alternatively build in profile or release mode"

    So it seems like on physical devices, custom shemes and https can only be cold started when in profile or release modes and the cold start works on the simulator in debug mode but https links doesnt seem to work on simulators because of the AASA maybe? and release/profile modes cant be run on simulators so if you want to test an https, it has to be on physical devices?

    NB: You also have to make sure you're using the right bundle identifier that has the Associaited Domains Capability

  17. okorohelijah commented on Apr 21, 2026

    @okorohelijah
    Contributor

    Analysis by Gemini which seems to make sense:

    The throwBack parameter is YES when the link comes from continueUserActivity (universal links). 
    This is by design for the case where the app genuinely can't handle a link. 
    But it doesn't account for apps that want to defer handling until initialization is complete.
    

    as seen in

    [engine sendDeepLinkToFramework:url
    completionHandler:^(BOOL success) {
    if (!success && throwBack) {
    // throw it back to iOS
    [FlutterSharedApplication.application openURL:url
    options:@{}
    completionHandler:nil];
    }
    }];
    [flutterViewController.engine sendDeepLinkToFramework:url
    completionHandler:^(BOOL success) {
    if (!success && throwBack) {
    // throw it back to iOS
    [flutterApplication openURL:url
    options:@{}
    completionHandler:nil];

  18. vashworth commented on Apr 22, 2026

    @vashworth
    Contributor

    I don't think this should be the expected behavior??

    No that's not the expected behavior. It should open the app directly

  19. vashworth commented on Apr 22, 2026

    @vashworth
    Contributor

    @okorohelijah Were you able to recreate the issue with profile/release mode?

  20. okorohelijah commented on Apr 22, 2026

    @okorohelijah
    Contributor

    @vashworth yes, I was able to recreate the issue in profile mode

  21. hellohuanlin commented on Apr 27, 2026

    @hellohuanlin
    Contributor

    @okorohelijah does this issue turn out to be a regression from #164247?

  22. removed
    ⏳Bot is counting down the days until it unassigns the issue
    on Apr 28, 2026
  23. okorohelijah commented on Jun 15, 2026

    @okorohelijah
    Contributor

    @js2702 If you want, I add a comment in #185430 (comment) explaining the cause

  24. added a commit that references this issue on Aug 11, 2026
    f6d405c
  25. github-actions commented on Aug 25, 2026

    @github-actions

    This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.

  26. locked as resolved and limited conversation to collaborators on Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

P1High-priority issues at the top of the work listc: regressionIt was better in the past than it is nowfound in release: 3.32Found to occur in 3.32needs repro infoAutomated crash report whose cause isn't yet knownplatform-iosiOS applications specificallyteam-iosOwned by iOS platform teamtriaged-iosTriaged by iOS platform team

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions