{"record":{"id":"ee8687be4df4e4ff","repo":"microsoft/aspire","slug":"cli-path-must-be-provided","errorCode":null,"errorMessage":"CLI path must be provided","messagePattern":"CLI path must be provided","errorType":"validation","errorClass":"ArgumentException","httpStatus":null,"severity":"error","filePath":"src/Aspire.Hosting.DevTunnels/DevTunnelCli.cs","lineNumber":29,"sourceCode":"\nnamespace Aspire.Hosting.DevTunnels;\n\ninternal class DevTunnelCli\n{\n    public const int ResourceConflictsWithExistingExitCode = 1;\n    public const int ResourceNotFoundExitCode = 2;\n\n    public static readonly Version MinimumSupportedVersion = new(1, 0, 1435);\n\n    private readonly string _cliPath;\n\n    public static string GetCliPath(IConfiguration configuration) => configuration[\"ASPIRE_DEVTUNNEL_CLI_PATH\"] ?? \"devtunnel\";\n\n    public DevTunnelCli(string filePath)\n    {\n        if (string.IsNullOrWhiteSpace(filePath))\n        {\n            throw new ArgumentException(\"CLI path must be provided\", nameof(filePath));\n        }\n\n        _cliPath = filePath;\n    }\n\n    public Task<int> GetVersionAsync(TextWriter? outputWriter = null, TextWriter? errorWriter = null, ILogger? logger = default, CancellationToken cancellationToken = default)\n        => RunAsync([\"--version\", \"--nologo\"], outputWriter, errorWriter, logger, cancellationToken);\n\n    public Task<int> UserLoginMicrosoftAsync(ILogger? logger = default, CancellationToken cancellationToken = default)\n        => RunAsync([\"user\", \"login\", \"--entra\", \"--json\", \"--nologo\"], null, null, useShellExecute: true, logger, cancellationToken);\n\n    public Task<int> UserLoginGitHubAsync(ILogger? logger = default, CancellationToken cancellationToken = default)\n        => RunAsync([\"user\", \"login\", \"--github\", \"--json\", \"--nologo\"], null, null, useShellExecute: true, logger, cancellationToken);\n\n    public Task<int> UserLogoutAsync(TextWriter? outputWriter = null, TextWriter? errorWriter = null, ILogger? logger = default, CancellationToken cancellationToken = default)\n        => RunAsync(new ArgsBuilder([\"user\", \"logout\", \"--json\", \"--nologo\"])\n        , outputWriter, errorWriter, logger, cancellationToken);\n","sourceCodeStart":11,"sourceCodeEnd":47,"githubUrl":"https://github.com/microsoft/aspire/blob/25830f84bd145686607ad00c057b3f84e2e51d43/src/Aspire.Hosting.DevTunnels/DevTunnelCli.cs#L11-L47","documentation":"The DevTunnelCli constructor validates the path to the devtunnel CLI executable before storing it. If the constructor receives an empty, null, or whitespace-only path, it refuses to construct and throws this ArgumentException so that downstream process launches do not fail obscurely.","triggerScenarios":"Calling new DevTunnelCli(null), new DevTunnelCli(\"\"), or new DevTunnelCli(\"   \"), or wiring the value of ASPIRE_DEVTUNNEL_CLI_PATH (or the fallback from GetCliPath) into the constructor when it is null/whitespace.","commonSituations":"Configuration returns null because the ASPIRE_DEVTUNNEL_CLI_PATH environment variable is unset and a caller bypasses GetCliPath; a config string is trimmed to empty; a DI-registered factory resolves an unset option property.","solutions":["Set the ASPIRE_DEVTUNNEL_CLI_PATH environment variable to the full path of the devtunnel executable","Use DevTunnelCli.GetCliPath(configuration) to obtain a path (it falls back to 'devtunnel') instead of constructing with a raw config value","Verify the devtunnel CLI is installed and on PATH so a valid fallback path exists","Guard the constructor input: pass configuration[\"ASPIRE_DEVTUNNEL_CLI_PATH\"] ?? \"devtunnel\""],"exampleFix":"// before\nvar cli = new DevTunnelCli(configuration[\"ASPIRE_DEVTUNNEL_CLI_PATH\"]); // null if unset\n// after\nvar cli = new DevTunnelCli(DevTunnelCli.GetCliPath(configuration)); // falls back to \"devtunnel\"","handlingStrategy":"validation","validationCode":"var path = configuration[\"ASPIRE_DEVTUNNEL_CLI_PATH\"];\nif (string.IsNullOrWhiteSpace(path))\n    path = \"devtunnel\"; // or fail fast with a clear message before constructing DevTunnelCli","typeGuard":null,"tryCatchPattern":"try\n{\n    var cli = new DevTunnelCli(path);\n}\ncatch (ArgumentException ex)\n{\n    logger.LogError(ex, \"devtunnel CLI path is not configured; set ASPIRE_DEVTUNNEL_CLI_PATH.\");\n    return;\n}","preventionTips":["Always resolve the CLI path via DevTunnelCli.GetCliPath(configuration)","Set ASPIRE_DEVTUNNEL_CLI_PATH in CI/dev environments where devtunnel is not on PATH","Assert the path is non-empty in startup configuration validation"],"tags":["devtunnels","argument-validation","configuration","cli"],"backgroundTag":"invalid-constructor-argument","analyzedSha":"25830f84bd145686607ad00c057b3f84e2e51d43","analyzedAt":"2026-09-16T11:10:06.193Z","contentChangedAt":"2026-09-16T11:10:06.193Z","schemaVersion":2},"datasetVersion":"2026-09-21T04:17:39.646Z"}