Skip to content

Issue with web-launch-url Parameter Handling on Windows in VS Code #5322

Open

Description

@AoEiuV020

Describe the bug
When using web-launch-url in VS Code on Windows, it fails to pass multiple parameters correctly to the Flutter web app. Despite identical configuration files, this only occurs on Windows, while Linux and macOS handle the parameters as expected.
Details
Using the following configuration from my launch.json file:

         {
            "name": "example(livekit)",
            "cwd": "example",
            "request": "launch",
            "type": "dart",
            "env": {
                "DISPLAY": ":0"
            },
            "args": [
                "--web-browser-flag=--disable-web-security",
                "--web-port=9080",
                "--web-launch-url=http://localhost:9080/?autoConnect&serverUrl=https%3A%2F%2Fmeet.livekit.io&room=123456&name=vscode",
                "--dart-entrypoint-args=--autoConnect",
                "--dart-entrypoint-args=--serverUrl=https://meet.livekit.io",
                "--dart-entrypoint-args=--room=123456",
                "--dart-entrypoint-args=--name=vscode",
            ]
        }

https://github.com/AoEiuV020/livekit_meeting/blob/89b0919e4792cfbde3d7f8407733406d30eea1ff/.vscode/launch.json#L30
On Windows, the browser URL launched in the address bar is truncated to:

http://localhost:9080/?autoConnect

All parameters after & are lost, and when targeting Windows, this configuration also causes all subsequent dart-entrypoint-args to be ignored. However, if the web-launch-url is removed, the dart-entrypoint-args function normally.

Running flutter run directly from the command line with parameters works without issue:

flutter run -d chrome "--web-browser-flag=--disable-web-security" "--web-port=9080" "--web-launch-url=http://localhost:9080/?autoConnect&serverUrl=https%3A%2F%2Fmeet.livekit.io&room=123456&name=vscode"

To Reproduce
Steps to reproduce the behavior:

  1. In VS Code, select the Chrome device as the target.
  2. Go to the Run and Debug view.
  3. Select the example (livekit) configuration.
  4. Click on the Debug button to start the Flutter web app.

Expected behavior
The web-launch-url should consistently retain all query parameters across platforms when launching a Flutter web app from VS Code.

Screenshots
image

Please complete the following information:

log-1f1f.md

  • Operating System and version: win11
  • VS Code version: 1.94.2
  • Dart extension version: 3.98.1
  • Dart/Flutter SDK version: 3.24.4
  • Target device (if the issue relates to Flutter debugging): chrome 129.0.6668.101

Activity

  1. DanTup commented on Oct 28, 2024

    @DanTup
    Member

    I can reproduce this, but I also see it from a PowerShell terminal:

    flutter run "-dchrome" "--web-port=9080" "--web-launch-url=http://localhost:9080/?a&b=c"
    
    flutter_cli_encoding.mp4

    However, if I add --% to prevent PowerShell parsing the args, it's good:

    flutter --% run "-dchrome" "--web-port=9080" "--web-launch-url=http://localhost:9080/?a&b=c"
    

    We're just using Process.start() for this, so I'll need to figure out what escaping Dart is (or isn't) doing for shell execution (since Flutter is a .bat, it has to execute via a shell).

  2. DanTup commented on Oct 28, 2024

    @DanTup
    Member

    Looks like this is documented:

    https://api.dart.dev/stable/3.5.4/dart-io/Process/start.html

    NOTE: On Windows, if executable is a batch file ('.bat' or '.cmd'), it may be launched by the operating system in a system shell regardless of the value of runInShell. This could result in arguments being parsed according to shell rules. For example:

    void main() async {
      // Will launch notepad.
      Process.start('test.bat', ['test&notepad.exe']);
    }
    

    Looks like the VM has a private implementation of argument escaping here that perhaps we can use:

    https://github.com/dart-lang/sdk/blob/6d4ebea83b7217b66112df7268ffc5fa6dd2fd36/sdk/lib/_internal/vm/bin/process_patch.dart#L342

    Edit: Apparently that doesn't handle & :-(

  3. DanTup commented on Oct 28, 2024

    @DanTup
    Member

    I'm actually struggling to track down what's happening here. I wrote a small test script that creates .bat file that calls a Dart script and passes the args with %* (which is what Flutter does), then calls it with a load of strings with various characters in. Without any escaping, they all come through correctly. With escaping, the escaped values come through to Dart:

    import 'dart:convert';
    import 'dart:io';
    
    const testCharacters = r'"&:\<>|%=+-_@~#<>?/';
    
    final testArgs = [
      for (var char in testCharacters.split('')) 'one${char}two',
    ];
    
    Future<void> main(List<String> args) async {
      if (args.isNotEmpty && args.first == 'printargs') {
        printArgs(args.skip(1).toList());
        return;
      }
    
      // Create a .bat file that just forwards all args back to us.
      File('tool/print_args.bat').writeAsStringSync('dart tool/test_args.dart %*');
    
      var proc = await Process.start(
        Platform.resolvedExecutable,
        [
          '--enable-asserts',
          Platform.script.toFilePath(),
          'printargs',
          ...testArgs,
        ],
      );
    
      proc.stdout.transform(utf8.decoder).listen(print);
      proc.stderr.transform(utf8.decoder).listen(print);
    
      print('Started process ${proc.pid}.. waiting for exit');
      await proc.exitCode;
    }
    
    void printArgs(List<String> args) {
      assert(args.length == testArgs.length);
      for (var i = 0; i < args.length; i++) {
        var a = testArgs[i];
        var b = args[i];
        print('$a came through as $b    ${a == b ? '' : 'MISMATCH'}');
      }
    }

    image

    It's not clear to me why this works given the docs quoted above (and the actual bug reported here). It's going to need some more digging.

  4. added this to the v3.102.0 milestone on Oct 28, 2024
  5. added
    in flutterRelates to running Flutter apps
    in debuggingRelates to the debug adapter or process of running debug sessions
    on Oct 28, 2024
  6. DanTup commented on Nov 25, 2024

    @DanTup
    Member

    Here's a slightly reduced repro that works with Windows desktop, so there's nothing web-specific about this:

    image

  7. DanTup commented on Nov 25, 2024

    @DanTup
    Member

    I managed to repro this with a Dart script when the executable path contains a space (my Flutter SDK is inside a folder named "Flutter (main)" to catch some issues with escaping like this!). This seems to match some of the description here from the legacy DAP in TS (second line):

    // Spawning processes on Windows with funny symbols in the path requires quoting. However if you quote an
    // executable with a space in its path and an argument also has a space, you have to then quote _all_ of the
    // arguments!
    // https://github.com/nodejs/node/issues/7367
    let escaped = arg.replace(/"/g, `\\"`).replace(/`/g, "\\`");
    // Additionally, on Windows escape redirection symbols with ^ if they come
    // directly after quotes (?).
    // https://ss64.com/nt/syntax-esc.html
    if (isWin)
    escaped = escaped.replace(/"([<>])/g, "\"^$1");
    return `"${escaped}"`;

    However, moving my Flutter SDK to a folder without a space doesn't prevent the original issue from happening, which seems slightly odd.

    In any case, the correct fix here is probably for DAP to escape all of the args and quote them. It's unfortunate because this escaping is probably shell-specific and the rules don't seem complex.

    @bkonyi FYI... it would sure be nice if Flutter didn't require a shell script to invoke 😄 For some similar issues with Pub in the past (when it was .bat), I think I bypassed the shell script.. it's possible we could do similar with Flutter (eg. invoke dart.exe and path to the flutter_tools snapshot), but it could be dangerous if the shell script changes - we'd probably need to agree that this is a supported way to invoke it (when we know there's already a Dart SDK downloaded etc.).

  8. bkonyi commented on Nov 25, 2024

    @bkonyi

    Yeah, I really don't like that flutter.bat and dart.bat (and their .sh equivalents) exist. It'd probably be difficult to migrate away from them and still support shipping Flutter via GitHub, so I doubt it'll change any time soon. It's probably safe to assume those artifacts exist if the plugin already knows it's run something like flutter doctor to initialize the SDK, but poking directly at snapshots is something we discourage since we do change their names from time to time which causes headaches when tooling is reaching into SDK internals.

  9. DanTup commented on Nov 25, 2024

    @DanTup
    Member

    Right, I think I have a good isolated repro for this now...

    The script below fails with:

    Writing scripts to Directory: 'C:\Users\danny\AppData\Local\Temp\flutter dap args escape test1ea07fdc'
    Executing:
      executable: C:\Users\danny\AppData\Local\Temp\flutter dap args escape test1ea07fdc\fake_flutter.bat
      args: beforeaafter beforebafter beforecafter before&after
      runInShell: true
    'after' is not recognized as an internal or external command,
    operable program or batch file.
    Expected: ['beforeaafter', 'beforebafter', 'beforecafter', 'before&after']
      Actual: ['beforeaafter', 'beforebafter', 'beforecafter', 'before']
       Which: at location [3] is 'before' instead of 'before&after'
    package:matcher             expect
    test\escape_test.dart 65:5  main.<fn>
    escape_test.dart:65
    ✖ test escaping
    
    Exited (1).
    

    This seems to match what we're seeing with & in Flutter. I found some rules at https://stackoverflow.com/questions/6427732/how-can-i-escape-an-arbitrary-string-for-use-as-a-command-line-argument-in-windo but in my testing it doesn't seem to work correctly (for ex. doubling up backslashes seems to not work).

    I'll do some more testing and see if I can come up with some escaping rules that pass for all of those characters and then test it in Flutter.

    import 'dart:convert';
    import 'dart:io';
    import 'package:path/path.dart' as path;
    import 'package:test/test.dart';
    
    const testCharacters = r'abc&'; // r' "&:\<>|%=+-_@~#<>?/';
    
    final testArgs = [
      for (var char in testCharacters.split('')) 'before${char}after',
    ];
    
    void main() {
      test('test escaping', () async {
        var tempDir =
            Directory.systemTemp.createTempSync('flutter dap args escape test');
        print('Writing scripts to $tempDir');
    
        // Write a shell script to simulate the Flutter .bat file and a Dart script
        // that just prints the arguments sent to it one-per-line.
        var tempShellScript = File(path.join(tempDir.path, 'fake_flutter.bat'));
        var tempDartScript = File(path.join(tempDir.path, 'print_args.dart'));
        var command = [
          '"${Platform.resolvedExecutable}"',
          '"${tempDartScript.path}"',
          "%*",
        ];
        tempShellScript.writeAsStringSync('@echo off\n${command.join(" ")}');
        tempDartScript.writeAsStringSync(r'''
    void main(List<String> args) {
      print(args.join('\n'));
    }
    ''');
    
        var executable = tempShellScript.path;
        var args = testArgs;
    
        print('''
    Executing:
      executable: $executable
      args: ${args.join(' ')}
      runInShell: true
    ''');
        var proc = await Process.start(
          '"$executable"',
          args,
        );
    
        var stdoutFuture = proc.stdout.transform(utf8.decoder).toList();
        var stderrFuture = proc.stderr.transform(utf8.decoder).toList();
        await proc.exitCode;
    
        var stdout = (await stdoutFuture).join().trim();
        var stderr = (await stderrFuture).join().trim();
    
        if (stderr.isNotEmpty) {
          print(stderr);
        }
    
        var actual = stdout
            .split('\n')
            .map((l) => l.trim())
            .where((l) => l.isNotEmpty)
            .toList();
    
        expect(actual, testArgs);
      });
    }
  10. DanTup commented on Nov 25, 2024

    @DanTup
    Member

    @bkonyi

    but poking directly at snapshots is something we discourage since we do change their names from time to time which causes headaches when tooling is reaching into SDK internals.

    Yeah, that's my fear. I kinda wish we had a dart.exe flutter command that just escaped the args correctly and forwarded them on to flutter.bat so that every editor wanting to call Flutter didn't have to accurately implement all of the weird Windows shell escaping rules themselves (or more likely, have bugs like this one) 😄

    Or similar, when Flutter downloads the Dart SDK, it compiles a tiny flutter.exe that does this forwarding. They're all hacks, but a centralised hack in the SDK might be better than hacks (or bugs) in lots of other places.

    It's annoying because we're only using shell execute because Flutter is a shell script. We don't actually want any difference in how args are treated, it just happens because the command gets squished to a string to be executed.

    Another option could be that when flutter.bat downloads the SDK, it also compiles a simple flutter.exe alongside flutter.bat that escapes and forwards

  11. DanTup commented on Nov 26, 2024

    @DanTup
    Member

    I've not yet found a way to escape & correctly here, so I've filed dart-lang/sdk#59604 to see if someone can advise how this should work (adding ^ which I believe should work, does not seem to).

  12. modified the milestones: v3.102.0, On Deck on Nov 26, 2024
  13. DanTup commented on Nov 26, 2024

    @DanTup
    Member

    Update from dart-lang/sdk#59604... the issue was that flutter.bar uses %* to forward the args, and that results in everything being expanded to a string again, so that needs escaping again too.

    We will have to double-escape these for now.. but it would be nice to avoid assumptions about what's inside flutter.bat here.

  14. DanTup commented on Nov 27, 2024

    @DanTup
    Member

    @bkonyi I'd be interested in your thoughts after reviewing the discussion in dart-lang/sdk#59604 (comment)

    I feel like trying to escape things reliably is going to be tough (it seems like we'd need to either make assumptions about flutter.bat and double-escape in DAP, or we'd need to update flutter.bat to do its own escaping of %* properly) and wonder if we should try to find a good way to instead just bypass the shell script from DAP (and maybe VS Code).

    Some ideas (that I haven't thought through):

    • flutter.bat compiles a native exe instead of just the snapshot
    • we just invoke dart.exe and the snapshot (and try to ensure we pass any additional flags).. we assume that earlier in the IDE session lifecycle, something has run flutter.bat to ensure the versions are consistent and we try to handle anything else that flutter.bat does that is required (I don't think there's much if we know it's initiailized though?)
    • we add a "flutter get-execution-info" command or similar, which just spits out Platform.resolvedExecutable and all of the flags (up to get-execution-info) in a machine-parseable away, so it is a way to find out how to invoke flutter without the shell script, but it forces flutter.bat to run (and perform any initialization) and avoids hard-coding any knowledge of dart.exe or the args that the shell script adds.

    All solutions feel like hacks, but I feel like they're less hacky than trying to handle this escaping. It's bananas (in particular because of %* in the bat file meaning we need to either escape twice in the client, or add escaping to the bat file.. though I haven't been able to make that work right).

  15. Wdestroier commented on Mar 6, 2025

    @Wdestroier

    Hi @DanTup, I'm not sure if the issue is related, but I'm trying to launch Flutter on http://localhost:3000 and opening Chrome on https://localhost:5173/f/ by setting the --web-launch-url flag, but it's launching on http://localhost:3000/f. That's the behavior when I click the "Run" button above the main method to launch the app.

    I want to open Chrome on another URL because I setup a reverse proxy to deal with CORS and Cookies. The following message is printed when I press CTRL+F5 to launch the app:

    Launching lib\main.dart on Chrome in debug mode...
    "https://localhost:5173/f/" is not a valid HTTP URL.
    

    Another issue is clicking "Run" or pressing CTRL+F5 leads to different results. For example: I prefer to click the "Run" button, but then Flutter is launched on a random port (completely ignores launch.json). I applied this workaround to improve the experience: https://stackoverflow.com/a/76259368. Edit: to complement, a system-wide setting is not ideal, but running the app by pressing CTRL+F5 is bad (must remember about the shortcut and breakpoints don't work). I learned VSCode is similar to Visual Studio (F5 runs in Debug mode) and apparently there's no workaround to fix the "Run" button.

  16. DanTup commented on Mar 6, 2025

    @DanTup
    Member

    Hi @DanTup, I'm not sure if the issue is related, but I'm trying to launch Flutter on http://localhost:3000/ and opening Chrome on https://localhost:5173/f/ by setting the --web-launch-url flag, but it's launching on http://localhost:3000/f. That's the behavior when I click the "Run" button above the main method to launch the app.

    No, I don't think it's related - but please file a new issue about this so we can track down where it's being changed.

    Another issue is clicking "Run" or pressing CTRL+F5 leads to different results. For example: I prefer to click the "Run" button, but then Flutter is launched on a random port (completely ignores launch.json)

    This is somewhat expected, because pressing F5 will launch your active debug configuration (which can be selected on the Debug sidebar) whereas the Run/Debug links always run a default launch configuration with the appropriate file (and possibly args, if it's a test/group) set. You can override this behaviour by marking a launch configuration (in launch.json) as a templateFor in which case it can be used for the Run/Debug CodeLens links - see https://dartcode.org/docs/launch-configuration/.

  17. DanTup commented on Mar 6, 2025

    @DanTup
    Member

    "https://localhost:5173/f/" is not a valid HTTP URL.

    I misread your original issue thinking the trailing slash was the issue, but now I see it's HTTPS. I believe this is a result of this regex here:

    https://github.com/flutter/flutter/blob/099e6d39fe38dbda78dd5efb7d8424bf7fd845a2/packages/flutter_tools/lib/src/web/web_device.dart#L119

    I suspect that regex could have s? added to it to fix this if it is only used for launching the browser, but if you're also expecting the server to listen on HTTPS (eg. you're not proxying it yourself) then probably it would require work for the internal web server to use HTTPS. This was noted at flutter/flutter#95002 (comment).

  18. Wdestroier commented on Mar 6, 2025

    @Wdestroier

    You can override this behaviour by marking a launch configuration (in launch.json) as a templateFor in which case it can be used for the Run/Debug CodeLens links - see https://dartcode.org/docs/launch-configuration/.

    Amazing, it works! It's a great QoL improvement, thank you.

    I suspect that regex could have s? added to it to fix this if it is only used for launching the browser, but if you're also expecting the server to listen on HTTPS (eg. you're not proxying it yourself) then probably it would require work for the internal web server to use HTTPS.

    I was searching the codebase and found this code too. The reverse proxy is adding TLS, not throwing the error would be enough.

    Thank you for the answers!

  19. added a commit that references this issue on Sep 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    in debuggingRelates to the debug adapter or process of running debug sessionsin flutterRelates to running Flutter appsis bugon windows

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions