From 532d2b62594510c635bfeeec6d3df2090d7c0662 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 25 Feb 2026 12:02:58 +0000
Subject: [PATCH 1/9] Initial plan
From d6805aa9c77df9ae5ba64c13a7bb65da3e88502d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 25 Feb 2026 12:18:26 +0000
Subject: [PATCH 2/9] Add config validate subcommand for validating Dev Proxy
configuration files
Co-authored-by: waldekmastykarz <11164679+waldekmastykarz@users.noreply.github.com>
---
DevProxy/Commands/ConfigCommand.cs | 441 ++++++++++++++++++++++++++-
DevProxy/Commands/DevProxyCommand.cs | 20 ++
DevProxy/Program.cs | 9 +
3 files changed, 469 insertions(+), 1 deletion(-)
diff --git a/DevProxy/Commands/ConfigCommand.cs b/DevProxy/Commands/ConfigCommand.cs
index a089494b..5957297c 100644
--- a/DevProxy/Commands/ConfigCommand.cs
+++ b/DevProxy/Commands/ConfigCommand.cs
@@ -2,12 +2,15 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using DevProxy.Abstractions.Plugins;
using DevProxy.Abstractions.Proxy;
using DevProxy.Abstractions.Utils;
+using DevProxy.Plugins;
using System.CommandLine;
using System.CommandLine.Parsing;
using System.Diagnostics;
using System.Net.Http.Headers;
+using System.Reflection;
using System.Text.Json;
using System.Text.RegularExpressions;
@@ -45,6 +48,12 @@ sealed class ConfigCommand : Command
private readonly HttpClient _httpClient;
private readonly string snippetsFileUrl = $"https://aka.ms/devproxy/snippets/v{ProxyUtils.NormalizeVersion(ProxyUtils.ProductVersion)}";
private readonly string configFileSnippetName = "ConfigFile";
+ private static readonly JsonSerializerOptions _jsonOutputOptions = new()
+ {
+ WriteIndented = true,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
+ };
public ConfigCommand(
HttpClient httpClient,
@@ -59,6 +68,46 @@ public ConfigCommand(
ConfigureCommand();
}
+ ///
+ /// Runs config validation standalone, without requiring the full DI container.
+ /// Used when the proxy is invoked with 'config validate' to allow validating
+ /// even broken config files.
+ ///
+ internal static async Task RunValidateStandaloneAsync(string[] args)
+ {
+ string? configFile = null;
+ string output = "text";
+
+ for (var i = 0; i < args.Length; i++)
+ {
+ if ((string.Equals(args[i], "--config-file", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(args[i], "-c", StringComparison.OrdinalIgnoreCase)) &&
+ i + 1 < args.Length)
+ {
+ configFile = args[i + 1];
+ i++;
+ }
+ else if (string.Equals(args[i], "--output", StringComparison.OrdinalIgnoreCase) &&
+ i + 1 < args.Length)
+ {
+ output = args[i + 1];
+ i++;
+ }
+ }
+
+ using var httpClient = new HttpClient();
+ httpClient.DefaultRequestHeaders.UserAgent.Add(
+ new ProductInfoHeaderValue("dev-proxy", ProxyUtils.ProductVersion));
+
+ using var loggerFactory = LoggerFactory.Create(builder =>
+ {
+ builder.SetMinimumLevel(LogLevel.Debug);
+ });
+ var logger = loggerFactory.CreateLogger();
+
+ return await ValidateConfigCoreAsync(configFile, output, httpClient, logger, CancellationToken.None);
+ }
+
private void ConfigureCommand()
{
var configGetCommand = new Command("get", "Download the specified config from the Sample Solution Gallery");
@@ -100,11 +149,33 @@ private void ConfigureCommand()
_ = Process.Start(cfgPsi);
});
+ var configValidateCommand = new Command("validate", "Validate a Dev Proxy configuration file");
+ var validateConfigFileOption = new Option("--config-file", "-c")
+ {
+ Description = "The path to the configuration file to validate",
+ HelpName = "config-file"
+ };
+ var outputOption = new Option("--output")
+ {
+ Description = "Output format (text or json)",
+ HelpName = "output",
+ DefaultValueFactory = _ => "text"
+ };
+ configValidateCommand.Add(validateConfigFileOption);
+ configValidateCommand.Add(outputOption);
+ configValidateCommand.SetAction(async (parseResult, cancellationToken) =>
+ {
+ var configFile = parseResult.GetValue(validateConfigFileOption);
+ var output = parseResult.GetValue(outputOption) ?? "text";
+ return await ValidateConfigAsync(configFile, output, cancellationToken);
+ });
+
this.AddCommands(new List
{
configGetCommand,
configNewCommand,
- configOpenCommand
+ configOpenCommand,
+ configValidateCommand
}.OrderByName());
}
@@ -401,4 +472,372 @@ private static string GetTargetFolderPath(string appFolder, string configId)
body = Regex.Replace(body, @"\$[0-9]+", "");
return body;
}
+
+ private static string? ResolveConfigFile(string? configFilePath)
+ {
+ if (!string.IsNullOrEmpty(configFilePath))
+ {
+ var resolved = Path.GetFullPath(ProxyUtils.ReplacePathTokens(configFilePath));
+ return File.Exists(resolved) ? resolved : null;
+ }
+
+ string?[] configFiles = [
+ "devproxyrc.jsonc",
+ "devproxyrc.json",
+ Path.Combine(".devproxy", "devproxyrc.jsonc"),
+ Path.Combine(".devproxy", "devproxyrc.json"),
+ Path.Combine(ProxyUtils.AppFolder ?? "", "devproxyrc.jsonc"),
+ Path.Combine(ProxyUtils.AppFolder ?? "", "devproxyrc.json")
+ ];
+
+ foreach (var configFile in configFiles)
+ {
+ if (!string.IsNullOrEmpty(configFile) && File.Exists(configFile))
+ {
+ return Path.GetFullPath(configFile);
+ }
+ }
+
+ return null;
+ }
+
+ private async Task ValidateConfigAsync(string? configFilePath, string output, CancellationToken cancellationToken)
+ {
+ return await ValidateConfigCoreAsync(configFilePath, output, _httpClient, _logger, cancellationToken);
+ }
+
+ private static async Task ValidateConfigCoreAsync(
+ string? configFilePath,
+ string output,
+ HttpClient httpClient,
+ ILogger logger,
+ CancellationToken cancellationToken)
+ {
+ var errors = new List();
+ var warnings = new List();
+ var pluginNames = new List();
+ var urlPatterns = new List();
+
+ var resolvedConfigFile = ResolveConfigFile(configFilePath);
+ if (resolvedConfigFile is null)
+ {
+ errors.Add(new("configFile", configFilePath is not null
+ ? $"Configuration file '{configFilePath}' not found"
+ : "No configuration file found"));
+ WriteResults(output, null, errors, warnings, pluginNames, urlPatterns);
+ return 1;
+ }
+
+ string configJson;
+ JsonDocument configDoc;
+ try
+ {
+ configJson = await File.ReadAllTextAsync(resolvedConfigFile, cancellationToken);
+ configDoc = JsonDocument.Parse(configJson, new JsonDocumentOptions
+ {
+ CommentHandling = JsonCommentHandling.Skip,
+ AllowTrailingCommas = true
+ });
+ }
+ catch (JsonException ex)
+ {
+ errors.Add(new("configFile", $"Invalid JSON: {ex.Message}"));
+ WriteResults(output, resolvedConfigFile, errors, warnings, pluginNames, urlPatterns);
+ return 1;
+ }
+
+ using (configDoc)
+ {
+ var schemaUrl = configDoc.RootElement.TryGetProperty("$schema", out var schemaProp)
+ ? schemaProp.GetString()
+ : null;
+ if (!string.IsNullOrEmpty(schemaUrl))
+ {
+ try
+ {
+ var (isValid, validationErrors) = await ProxyUtils.ValidateJsonAsync(
+ configJson, schemaUrl, httpClient, logger, cancellationToken);
+ if (!isValid)
+ {
+ foreach (var error in validationErrors)
+ {
+ errors.Add(new("schema", error));
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ warnings.Add(new("$schema", $"Could not validate schema: {ex.Message}"));
+ }
+
+ ValidateSchemaVersion(schemaUrl, warnings);
+ }
+ else
+ {
+ warnings.Add(new("$schema", "No schema URL found, skipping schema validation"));
+ }
+
+ var configFileDirectory = Path.GetDirectoryName(resolvedConfigFile) ?? ".";
+
+ if (configDoc.RootElement.TryGetProperty("plugins", out var pluginsElement) &&
+ pluginsElement.ValueKind == JsonValueKind.Array)
+ {
+ ValidatePlugins(pluginsElement, configFileDirectory, errors, warnings, pluginNames);
+ }
+ else
+ {
+ errors.Add(new("plugins", "No plugins configured"));
+ }
+
+ if (configDoc.RootElement.TryGetProperty("urlsToWatch", out var urlsElement) &&
+ urlsElement.ValueKind == JsonValueKind.Array)
+ {
+ ValidateUrls(urlsElement, errors, warnings, urlPatterns);
+ }
+ else
+ {
+ warnings.Add(new("urlsToWatch", "No URLs to watch configured"));
+ }
+ }
+
+ var isConfigValid = errors.Count == 0;
+ WriteResults(output, resolvedConfigFile, errors, warnings, pluginNames, urlPatterns);
+ return isConfigValid ? 0 : 1;
+ }
+
+ private static void ValidateSchemaVersion(string schemaUrl, List warnings)
+ {
+ try
+ {
+ var uri = new Uri(schemaUrl);
+ if (uri.Segments.Length > 2)
+ {
+ var schemaVersion = uri.Segments[^2]
+ .TrimStart('v')
+ .TrimEnd('/');
+ var currentVersion = ProxyUtils.NormalizeVersion(ProxyUtils.ProductVersion);
+ if (ProxyUtils.CompareSemVer(currentVersion, schemaVersion) != 0)
+ {
+ var currentSchemaUrl = uri.ToString().Replace(
+ $"/v{schemaVersion}/",
+ $"/v{currentVersion}/",
+ StringComparison.OrdinalIgnoreCase);
+ warnings.Add(new("$schema",
+ $"Schema version does not match Dev Proxy version, expected schema: {currentSchemaUrl}"));
+ }
+ }
+ }
+ catch
+ {
+ // Ignore schema version parsing errors
+ }
+ }
+
+ private static void ValidatePlugins(
+ JsonElement pluginsElement,
+ string configFileDirectory,
+ List errors,
+ List warnings,
+ List pluginNames)
+ {
+ var hasEnabledPlugins = false;
+ var i = 0;
+
+ foreach (var plugin in pluginsElement.EnumerateArray())
+ {
+ var name = plugin.TryGetProperty("name", out var nameProp) ? nameProp.GetString() : null;
+ var enabled = !plugin.TryGetProperty("enabled", out var enabledProp) || enabledProp.GetBoolean();
+ var pluginPath = plugin.TryGetProperty("pluginPath", out var pathProp) ? pathProp.GetString() : null;
+
+ if (string.IsNullOrEmpty(name))
+ {
+ errors.Add(new($"plugins[{i}].name", "Plugin name is required"));
+ i++;
+ continue;
+ }
+
+ if (!enabled)
+ {
+ i++;
+ continue;
+ }
+
+ hasEnabledPlugins = true;
+ pluginNames.Add(name);
+
+ if (string.IsNullOrEmpty(pluginPath))
+ {
+ errors.Add(new($"plugins[{i}].pluginPath", $"Plugin path is required for '{name}'"));
+ i++;
+ continue;
+ }
+
+ var resolvedPluginPath = Path.GetFullPath(
+ Path.Combine(
+ configFileDirectory,
+ ProxyUtils.ReplacePathTokens(pluginPath.Replace('\\', Path.DirectorySeparatorChar))));
+
+ if (!File.Exists(resolvedPluginPath))
+ {
+ errors.Add(new($"plugins[{i}].pluginPath",
+ $"Plugin assembly '{resolvedPluginPath}' not found"));
+ i++;
+ continue;
+ }
+
+ try
+ {
+ var pluginLoadContext = new PluginLoadContext(resolvedPluginPath);
+ var assembly = pluginLoadContext.LoadFromAssemblyName(
+ new AssemblyName(Path.GetFileNameWithoutExtension(resolvedPluginPath)));
+ var pluginType = assembly.GetTypes()
+ .FirstOrDefault(t => t.Name == name && typeof(IPlugin).IsAssignableFrom(t));
+ if (pluginType is null)
+ {
+ errors.Add(new($"plugins[{i}].name",
+ $"Plugin '{name}' not found in assembly '{Path.GetFileName(resolvedPluginPath)}'"));
+ }
+ }
+ catch (Exception ex)
+ {
+ errors.Add(new($"plugins[{i}]",
+ $"Failed to load plugin assembly: {ex.Message}"));
+ }
+
+ i++;
+ }
+
+ if (!hasEnabledPlugins)
+ {
+ errors.Add(new("plugins", "No enabled plugins found"));
+ }
+ }
+
+ private static void ValidateUrls(
+ JsonElement urlsElement,
+ List errors,
+ List warnings,
+ List urlPatterns)
+ {
+ var i = 0;
+ foreach (var url in urlsElement.EnumerateArray())
+ {
+ var pattern = url.GetString();
+ if (string.IsNullOrEmpty(pattern))
+ {
+ warnings.Add(new($"urlsToWatch[{i}]", "Empty URL pattern"));
+ }
+ else
+ {
+ urlPatterns.Add(pattern);
+ try
+ {
+ var cleanPattern = pattern.StartsWith('!') ? pattern[1..] : pattern;
+ _ = new Regex(
+ $"^{Regex.Escape(cleanPattern).Replace("\\*", ".*", StringComparison.OrdinalIgnoreCase)}$");
+ }
+ catch (ArgumentException ex)
+ {
+ errors.Add(new($"urlsToWatch[{i}]",
+ $"Invalid URL pattern '{pattern}': {ex.Message}"));
+ }
+ }
+ i++;
+ }
+ }
+
+ private static void WriteResults(
+ string output,
+ string? configFile,
+ List errors,
+ List warnings,
+ List pluginNames,
+ List urlPatterns)
+ {
+ var isValid = errors.Count == 0;
+
+ if (string.Equals(output, "json", StringComparison.OrdinalIgnoreCase))
+ {
+ WriteJsonResults(configFile, errors, warnings, pluginNames, urlPatterns, isValid);
+ }
+ else
+ {
+ WriteTextResults(configFile, errors, warnings, pluginNames, urlPatterns, isValid);
+ }
+ }
+
+ private static void WriteJsonResults(
+ string? configFile,
+ List errors,
+ List warnings,
+ List pluginNames,
+ List urlPatterns,
+ bool isValid)
+ {
+ var result = new
+ {
+ valid = isValid,
+ configFile,
+ plugins = pluginNames,
+ urlsToWatch = urlPatterns,
+ errors = errors.Select(e => new { path = e.Path, message = e.Message }),
+ warnings = warnings.Select(w => new { path = w.Path, message = w.Message })
+ };
+
+ var json = JsonSerializer.Serialize(result, _jsonOutputOptions);
+ Console.WriteLine(json);
+ }
+
+ private static void WriteTextResults(
+ string? configFile,
+ List errors,
+ List warnings,
+ List pluginNames,
+ List urlPatterns,
+ bool isValid)
+ {
+ if (isValid)
+ {
+ Console.WriteLine("\u2713 Configuration is valid");
+ }
+ else
+ {
+ Console.WriteLine("\u2717 Configuration is invalid");
+ }
+
+ if (configFile is not null)
+ {
+ Console.WriteLine($" Config file: {configFile}");
+ }
+ if (pluginNames.Count > 0)
+ {
+ Console.WriteLine($" Plugins: {pluginNames.Count} loaded");
+ }
+ if (urlPatterns.Count > 0)
+ {
+ Console.WriteLine($" URLs to watch: {urlPatterns.Count} pattern{(urlPatterns.Count != 1 ? "s" : "")}");
+ }
+
+ if (errors.Count > 0)
+ {
+ Console.WriteLine();
+ Console.WriteLine("Errors:");
+ foreach (var error in errors)
+ {
+ Console.WriteLine($" - {error.Path}: {error.Message}");
+ }
+ }
+
+ if (warnings.Count > 0)
+ {
+ Console.WriteLine();
+ Console.WriteLine("Warnings:");
+ foreach (var warning in warnings)
+ {
+ Console.WriteLine($" - {warning.Path}: {warning.Message}");
+ }
+ }
+ }
+
+ private sealed record ValidationMessage(string Path, string Message);
}
\ No newline at end of file
diff --git a/DevProxy/Commands/DevProxyCommand.cs b/DevProxy/Commands/DevProxyCommand.cs
index c5c4b8b3..aa8f3fdd 100644
--- a/DevProxy/Commands/DevProxyCommand.cs
+++ b/DevProxy/Commands/DevProxyCommand.cs
@@ -46,6 +46,7 @@ sealed class DevProxyCommand : RootCommand
private static bool _isStdioCommandResolved;
private static bool _isJwtCommandResolved;
private static bool _isRootCommandResolved;
+ private static bool _isConfigValidateCommandResolved;
private static bool _stdioLogFilePathResolved;
public static bool HasGlobalOptions
@@ -97,6 +98,25 @@ public static bool IsJwtCommand
}
}
+ public static bool IsConfigValidateCommand
+ {
+ get
+ {
+ if (_isConfigValidateCommandResolved)
+ {
+ return field;
+ }
+
+ var args = Environment.GetCommandLineArgs();
+ field = args.Length > 2 &&
+ string.Equals(args[1], "config", StringComparison.OrdinalIgnoreCase) &&
+ string.Equals(args[2], "validate", StringComparison.OrdinalIgnoreCase) &&
+ !args.Any(arg => helpOptions.Contains(arg));
+ _isConfigValidateCommandResolved = true;
+ return field;
+ }
+ }
+
///
/// Determines if the root command (proxy itself) is being invoked.
/// Returns true when no subcommand is specified (only options or no args).
diff --git a/DevProxy/Program.cs b/DevProxy/Program.cs
index 435288c9..14deaf7f 100644
--- a/DevProxy/Program.cs
+++ b/DevProxy/Program.cs
@@ -57,6 +57,15 @@ static async Task RunProxyAsync(string[] args, DevProxyConfigOptions option
_ = Announcement.ShowAsync();
+// Handle 'config validate' early to allow validation of broken configs
+// without requiring the full DI container to be built
+if (DevProxyCommand.IsConfigValidateCommand)
+{
+ var validateExitCode = await ConfigCommand.RunValidateStandaloneAsync(args);
+ Environment.Exit(validateExitCode);
+ return;
+}
+
var options = new DevProxyConfigOptions();
options.ParseOptions(args);
From b8793d477d947756118c03e96c2bbf5d49763e07 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 25 Feb 2026 13:14:41 +0000
Subject: [PATCH 3/9] Use ProxyUtils.JsonSerializerOptions and
JsonDocumentOptions instead of custom duplicates
Co-authored-by: waldekmastykarz <11164679+waldekmastykarz@users.noreply.github.com>
---
DevProxy/Commands/ConfigCommand.cs | 15 ++-------------
1 file changed, 2 insertions(+), 13 deletions(-)
diff --git a/DevProxy/Commands/ConfigCommand.cs b/DevProxy/Commands/ConfigCommand.cs
index 5957297c..79eeda14 100644
--- a/DevProxy/Commands/ConfigCommand.cs
+++ b/DevProxy/Commands/ConfigCommand.cs
@@ -48,13 +48,6 @@ sealed class ConfigCommand : Command
private readonly HttpClient _httpClient;
private readonly string snippetsFileUrl = $"https://aka.ms/devproxy/snippets/v{ProxyUtils.NormalizeVersion(ProxyUtils.ProductVersion)}";
private readonly string configFileSnippetName = "ConfigFile";
- private static readonly JsonSerializerOptions _jsonOutputOptions = new()
- {
- WriteIndented = true,
- PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
- Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
- };
-
public ConfigCommand(
HttpClient httpClient,
IProxyConfiguration proxyConfiguration,
@@ -533,11 +526,7 @@ private static async Task ValidateConfigCoreAsync(
try
{
configJson = await File.ReadAllTextAsync(resolvedConfigFile, cancellationToken);
- configDoc = JsonDocument.Parse(configJson, new JsonDocumentOptions
- {
- CommentHandling = JsonCommentHandling.Skip,
- AllowTrailingCommas = true
- });
+ configDoc = JsonDocument.Parse(configJson, ProxyUtils.JsonDocumentOptions);
}
catch (JsonException ex)
{
@@ -784,7 +773,7 @@ private static void WriteJsonResults(
warnings = warnings.Select(w => new { path = w.Path, message = w.Message })
};
- var json = JsonSerializer.Serialize(result, _jsonOutputOptions);
+ var json = JsonSerializer.Serialize(result, ProxyUtils.JsonSerializerOptions);
Console.WriteLine(json);
}
From 4e809cdbea0f2af98ae4f33478cf253b7a08efd1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 25 Feb 2026 14:20:46 +0000
Subject: [PATCH 4/9] Switch from Console.WriteLine to ILogger in config
validate output
Co-authored-by: waldekmastykarz <11164679+waldekmastykarz@users.noreply.github.com>
---
DevProxy/Commands/ConfigCommand.cs | 56 +++++++++++++++++++-----------
1 file changed, 35 insertions(+), 21 deletions(-)
diff --git a/DevProxy/Commands/ConfigCommand.cs b/DevProxy/Commands/ConfigCommand.cs
index 79eeda14..5885b12e 100644
--- a/DevProxy/Commands/ConfigCommand.cs
+++ b/DevProxy/Commands/ConfigCommand.cs
@@ -5,6 +5,7 @@
using DevProxy.Abstractions.Plugins;
using DevProxy.Abstractions.Proxy;
using DevProxy.Abstractions.Utils;
+using DevProxy.Logging;
using DevProxy.Plugins;
using System.CommandLine;
using System.CommandLine.Parsing;
@@ -94,7 +95,19 @@ internal static async Task RunValidateStandaloneAsync(string[] args)
using var loggerFactory = LoggerFactory.Create(builder =>
{
- builder.SetMinimumLevel(LogLevel.Debug);
+ builder
+ .SetMinimumLevel(LogLevel.Information)
+ .AddConsole(consoleOptions =>
+ {
+ consoleOptions.FormatterName = ProxyConsoleFormatter.DefaultCategoryName;
+ consoleOptions.LogToStandardErrorThreshold = LogLevel.Warning;
+ })
+ .AddConsoleFormatter(formatterOptions =>
+ {
+ formatterOptions.IncludeScopes = false;
+ formatterOptions.ShowSkipMessages = true;
+ formatterOptions.ShowTimestamps = false;
+ });
});
var logger = loggerFactory.CreateLogger();
@@ -517,7 +530,7 @@ private static async Task ValidateConfigCoreAsync(
errors.Add(new("configFile", configFilePath is not null
? $"Configuration file '{configFilePath}' not found"
: "No configuration file found"));
- WriteResults(output, null, errors, warnings, pluginNames, urlPatterns);
+ WriteResults(output, null, errors, warnings, pluginNames, urlPatterns, logger);
return 1;
}
@@ -531,7 +544,7 @@ private static async Task ValidateConfigCoreAsync(
catch (JsonException ex)
{
errors.Add(new("configFile", $"Invalid JSON: {ex.Message}"));
- WriteResults(output, resolvedConfigFile, errors, warnings, pluginNames, urlPatterns);
+ WriteResults(output, resolvedConfigFile, errors, warnings, pluginNames, urlPatterns, logger);
return 1;
}
@@ -590,7 +603,7 @@ private static async Task ValidateConfigCoreAsync(
}
var isConfigValid = errors.Count == 0;
- WriteResults(output, resolvedConfigFile, errors, warnings, pluginNames, urlPatterns);
+ WriteResults(output, resolvedConfigFile, errors, warnings, pluginNames, urlPatterns, logger);
return isConfigValid ? 0 : 1;
}
@@ -741,17 +754,18 @@ private static void WriteResults(
List errors,
List warnings,
List pluginNames,
- List urlPatterns)
+ List urlPatterns,
+ ILogger logger)
{
var isValid = errors.Count == 0;
if (string.Equals(output, "json", StringComparison.OrdinalIgnoreCase))
{
- WriteJsonResults(configFile, errors, warnings, pluginNames, urlPatterns, isValid);
+ WriteJsonResults(configFile, errors, warnings, pluginNames, urlPatterns, isValid, logger);
}
else
{
- WriteTextResults(configFile, errors, warnings, pluginNames, urlPatterns, isValid);
+ WriteTextResults(configFile, errors, warnings, pluginNames, urlPatterns, isValid, logger);
}
}
@@ -761,7 +775,8 @@ private static void WriteJsonResults(
List warnings,
List pluginNames,
List urlPatterns,
- bool isValid)
+ bool isValid,
+ ILogger logger)
{
var result = new
{
@@ -774,7 +789,7 @@ private static void WriteJsonResults(
};
var json = JsonSerializer.Serialize(result, ProxyUtils.JsonSerializerOptions);
- Console.WriteLine(json);
+ logger.LogInformation("{Result}", json);
}
private static void WriteTextResults(
@@ -783,47 +798,46 @@ private static void WriteTextResults(
List warnings,
List pluginNames,
List urlPatterns,
- bool isValid)
+ bool isValid,
+ ILogger logger)
{
if (isValid)
{
- Console.WriteLine("\u2713 Configuration is valid");
+ logger.LogInformation("\u2713 Configuration is valid");
}
else
{
- Console.WriteLine("\u2717 Configuration is invalid");
+ logger.LogError("\u2717 Configuration is invalid");
}
if (configFile is not null)
{
- Console.WriteLine($" Config file: {configFile}");
+ logger.LogInformation(" Config file: {ConfigFile}", configFile);
}
if (pluginNames.Count > 0)
{
- Console.WriteLine($" Plugins: {pluginNames.Count} loaded");
+ logger.LogInformation(" Plugins: {Count} loaded", pluginNames.Count);
}
if (urlPatterns.Count > 0)
{
- Console.WriteLine($" URLs to watch: {urlPatterns.Count} pattern{(urlPatterns.Count != 1 ? "s" : "")}");
+ logger.LogInformation(" URLs to watch: {Count} pattern{Plural}", urlPatterns.Count, urlPatterns.Count != 1 ? "s" : "");
}
if (errors.Count > 0)
{
- Console.WriteLine();
- Console.WriteLine("Errors:");
+ logger.LogInformation("");
foreach (var error in errors)
{
- Console.WriteLine($" - {error.Path}: {error.Message}");
+ logger.LogError(" - {Path}: {Message}", error.Path, error.Message);
}
}
if (warnings.Count > 0)
{
- Console.WriteLine();
- Console.WriteLine("Warnings:");
+ logger.LogInformation("");
foreach (var warning in warnings)
{
- Console.WriteLine($" - {warning.Path}: {warning.Message}");
+ logger.LogWarning(" - {Path}: {Message}", warning.Path, warning.Message);
}
}
}
From fb4b5d283e7f9d42110f84eb4ee14c0c6b831e9f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 25 Feb 2026 14:29:11 +0000
Subject: [PATCH 5/9] Centralize config file search and schema version
validation to avoid duplication
Co-authored-by: waldekmastykarz <11164679+waldekmastykarz@users.noreply.github.com>
---
DevProxy.Abstractions/Utils/ProxyUtils.cs | 62 ++++++++++++++++---
DevProxy/Commands/ConfigCommand.cs | 36 ++---------
.../ConfigurationManagerExtensions.cs | 26 +-------
3 files changed, 60 insertions(+), 64 deletions(-)
diff --git a/DevProxy.Abstractions/Utils/ProxyUtils.cs b/DevProxy.Abstractions/Utils/ProxyUtils.cs
index ec21a389..c9a40402 100644
--- a/DevProxy.Abstractions/Utils/ProxyUtils.cs
+++ b/DevProxy.Abstractions/Utils/ProxyUtils.cs
@@ -273,6 +273,25 @@ public static void ValidateSchemaVersion(string schemaUrl, ILogger logger)
return;
}
+ var warning = GetSchemaVersionMismatchWarning(schemaUrl);
+ if (warning is not null)
+ {
+ logger.LogWarning("{Warning}", warning);
+ }
+ }
+
+ ///
+ /// Checks if the schema URL version matches the current Dev Proxy version.
+ /// Returns a warning message if versions don't match, or null if they match
+ /// or the schema URL cannot be parsed.
+ ///
+ public static string? GetSchemaVersionMismatchWarning(string schemaUrl)
+ {
+ if (string.IsNullOrWhiteSpace(schemaUrl))
+ {
+ return null;
+ }
+
try
{
var uri = new Uri(schemaUrl);
@@ -285,18 +304,47 @@ public static void ValidateSchemaVersion(string schemaUrl, ILogger logger)
if (CompareSemVer(currentVersion, schemaVersion) != 0)
{
var currentSchemaUrl = uri.ToString().Replace($"/v{schemaVersion}/", $"/v{currentVersion}/", StringComparison.OrdinalIgnoreCase);
- logger.LogWarning("The version of schema does not match the installed Dev Proxy version, the expected schema is {Schema}", currentSchemaUrl);
+ return $"The version of schema does not match the installed Dev Proxy version, the expected schema is {currentSchemaUrl}";
}
}
- else
- {
- logger.LogDebug("Invalid schema {SchemaUrl}, skipping schema version validation.", schemaUrl);
- }
}
- catch (Exception ex)
+ catch
{
- logger.LogWarning("Invalid schema {SchemaUrl}, skipping schema version validation. Error: {Error}", schemaUrl, ex.Message);
+ // Ignore schema URL parsing errors
}
+
+ return null;
+ }
+
+ ///
+ /// Returns the ordered list of config file paths to search.
+ /// The first existing file in the list should be used.
+ ///
+ public static IEnumerable GetConfigFileCandidates(string? userConfigFile)
+ {
+ return [
+ // config file specified by the user takes precedence
+ // null if not specified
+ userConfigFile,
+ // current directory - JSON/JSONC files
+ "devproxyrc.jsonc",
+ "devproxyrc.json",
+ // current directory - YAML files
+ "devproxyrc.yaml",
+ "devproxyrc.yml",
+ // .devproxy subdirectory - JSON/JSONC files
+ Path.Combine(".devproxy", "devproxyrc.jsonc"),
+ Path.Combine(".devproxy", "devproxyrc.json"),
+ // .devproxy subdirectory - YAML files
+ Path.Combine(".devproxy", "devproxyrc.yaml"),
+ Path.Combine(".devproxy", "devproxyrc.yml"),
+ // app folder - JSON/JSONC files
+ Path.Combine(AppFolder ?? "", "devproxyrc.jsonc"),
+ Path.Combine(AppFolder ?? "", "devproxyrc.json"),
+ // app folder - YAML files
+ Path.Combine(AppFolder ?? "", "devproxyrc.yaml"),
+ Path.Combine(AppFolder ?? "", "devproxyrc.yml")
+ ];
}
///
diff --git a/DevProxy/Commands/ConfigCommand.cs b/DevProxy/Commands/ConfigCommand.cs
index 332ce890..e5a9d8f4 100644
--- a/DevProxy/Commands/ConfigCommand.cs
+++ b/DevProxy/Commands/ConfigCommand.cs
@@ -504,16 +504,7 @@ private static string GetTargetFolderPath(string appFolder, string configId)
return File.Exists(resolved) ? resolved : null;
}
- string?[] configFiles = [
- "devproxyrc.jsonc",
- "devproxyrc.json",
- Path.Combine(".devproxy", "devproxyrc.jsonc"),
- Path.Combine(".devproxy", "devproxyrc.json"),
- Path.Combine(ProxyUtils.AppFolder ?? "", "devproxyrc.jsonc"),
- Path.Combine(ProxyUtils.AppFolder ?? "", "devproxyrc.json")
- ];
-
- foreach (var configFile in configFiles)
+ foreach (var configFile in ProxyUtils.GetConfigFileCandidates(null))
{
if (!string.IsNullOrEmpty(configFile) && File.Exists(configFile))
{
@@ -626,29 +617,10 @@ private static async Task ValidateConfigCoreAsync(
private static void ValidateSchemaVersion(string schemaUrl, List warnings)
{
- try
- {
- var uri = new Uri(schemaUrl);
- if (uri.Segments.Length > 2)
- {
- var schemaVersion = uri.Segments[^2]
- .TrimStart('v')
- .TrimEnd('/');
- var currentVersion = ProxyUtils.NormalizeVersion(ProxyUtils.ProductVersion);
- if (ProxyUtils.CompareSemVer(currentVersion, schemaVersion) != 0)
- {
- var currentSchemaUrl = uri.ToString().Replace(
- $"/v{schemaVersion}/",
- $"/v{currentVersion}/",
- StringComparison.OrdinalIgnoreCase);
- warnings.Add(new("$schema",
- $"Schema version does not match Dev Proxy version, expected schema: {currentSchemaUrl}"));
- }
- }
- }
- catch
+ var warning = ProxyUtils.GetSchemaVersionMismatchWarning(schemaUrl);
+ if (warning is not null)
{
- // Ignore schema version parsing errors
+ warnings.Add(new("$schema", warning));
}
}
diff --git a/DevProxy/Extensions/ConfigurationManagerExtensions.cs b/DevProxy/Extensions/ConfigurationManagerExtensions.cs
index cac373b9..d37e514c 100644
--- a/DevProxy/Extensions/ConfigurationManagerExtensions.cs
+++ b/DevProxy/Extensions/ConfigurationManagerExtensions.cs
@@ -16,31 +16,7 @@ public static ConfigurationManager ConfigureDevProxyConfig(this ConfigurationMan
configuration.Sources.Clear();
_ = configuration.SetBasePath(Directory.GetCurrentDirectory());
- string?[] configFiles = [
- // config file specified by the user takes precedence
- // null if not specified
- options.ConfigFile,
- // current directory - JSON/JSONC files
- "devproxyrc.jsonc",
- "devproxyrc.json",
- // current directory - YAML files
- "devproxyrc.yaml",
- "devproxyrc.yml",
- // .devproxy subdirectory - JSON/JSONC files
- Path.Combine(".devproxy", "devproxyrc.jsonc"),
- Path.Combine(".devproxy", "devproxyrc.json"),
- // .devproxy subdirectory - YAML files
- Path.Combine(".devproxy", "devproxyrc.yaml"),
- Path.Combine(".devproxy", "devproxyrc.yml"),
- // app folder - JSON/JSONC files
- Path.Combine(ProxyUtils.AppFolder ?? "", "devproxyrc.jsonc"),
- Path.Combine(ProxyUtils.AppFolder ?? "", "devproxyrc.json"),
- // app folder - YAML files
- Path.Combine(ProxyUtils.AppFolder ?? "", "devproxyrc.yaml"),
- Path.Combine(ProxyUtils.AppFolder ?? "", "devproxyrc.yml")
- ];
-
- foreach (var configFile in configFiles)
+ foreach (var configFile in ProxyUtils.GetConfigFileCandidates(options.ConfigFile))
{
if (!string.IsNullOrEmpty(configFile) && File.Exists(configFile))
{
From 60d4acafedb38ff9d32aa10c86375e1d64bd057e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 25 Feb 2026 14:44:43 +0000
Subject: [PATCH 6/9] Remove local --output option from validate command, use
global --log-for instead
Co-authored-by: waldekmastykarz <11164679+waldekmastykarz@users.noreply.github.com>
---
DevProxy/Commands/ConfigCommand.cs | 36 +++++++++++++-----------------
1 file changed, 15 insertions(+), 21 deletions(-)
diff --git a/DevProxy/Commands/ConfigCommand.cs b/DevProxy/Commands/ConfigCommand.cs
index e5a9d8f4..21360303 100644
--- a/DevProxy/Commands/ConfigCommand.cs
+++ b/DevProxy/Commands/ConfigCommand.cs
@@ -70,7 +70,7 @@ public ConfigCommand(
internal static async Task RunValidateStandaloneAsync(string[] args)
{
string? configFile = null;
- string output = "text";
+ var isJsonOutput = false;
for (var i = 0; i < args.Length; i++)
{
@@ -81,10 +81,10 @@ internal static async Task RunValidateStandaloneAsync(string[] args)
configFile = args[i + 1];
i++;
}
- else if (string.Equals(args[i], "--output", StringComparison.OrdinalIgnoreCase) &&
+ else if (string.Equals(args[i], DevProxyCommand.LogForOptionName, StringComparison.OrdinalIgnoreCase) &&
i + 1 < args.Length)
{
- output = args[i + 1];
+ isJsonOutput = string.Equals(args[i + 1], "machine", StringComparison.OrdinalIgnoreCase);
i++;
}
}
@@ -111,7 +111,7 @@ internal static async Task RunValidateStandaloneAsync(string[] args)
});
var logger = loggerFactory.CreateLogger();
- return await ValidateConfigCoreAsync(configFile, output, httpClient, logger, CancellationToken.None);
+ return await ValidateConfigCoreAsync(configFile, isJsonOutput, httpClient, logger, CancellationToken.None);
}
private void ConfigureCommand()
@@ -161,19 +161,13 @@ private void ConfigureCommand()
Description = "The path to the configuration file to validate",
HelpName = "config-file"
};
- var outputOption = new Option("--output")
- {
- Description = "Output format (text or json)",
- HelpName = "output",
- DefaultValueFactory = _ => "text"
- };
configValidateCommand.Add(validateConfigFileOption);
- configValidateCommand.Add(outputOption);
configValidateCommand.SetAction(async (parseResult, cancellationToken) =>
{
var configFile = parseResult.GetValue(validateConfigFileOption);
- var output = parseResult.GetValue(outputOption) ?? "text";
- return await ValidateConfigAsync(configFile, output, cancellationToken);
+ var logFor = parseResult.GetValueOrDefault(DevProxyCommand.LogForOptionName);
+ var isJsonOutput = logFor == LogFor.Machine;
+ return await ValidateConfigAsync(configFile, isJsonOutput, cancellationToken);
});
this.AddCommands(new List
@@ -515,14 +509,14 @@ private static string GetTargetFolderPath(string appFolder, string configId)
return null;
}
- private async Task ValidateConfigAsync(string? configFilePath, string output, CancellationToken cancellationToken)
+ private async Task ValidateConfigAsync(string? configFilePath, bool isJsonOutput, CancellationToken cancellationToken)
{
- return await ValidateConfigCoreAsync(configFilePath, output, _httpClient, _logger, cancellationToken);
+ return await ValidateConfigCoreAsync(configFilePath, isJsonOutput, _httpClient, _logger, cancellationToken);
}
private static async Task ValidateConfigCoreAsync(
string? configFilePath,
- string output,
+ bool isJsonOutput,
HttpClient httpClient,
ILogger logger,
CancellationToken cancellationToken)
@@ -538,7 +532,7 @@ private static async Task ValidateConfigCoreAsync(
errors.Add(new("configFile", configFilePath is not null
? $"Configuration file '{configFilePath}' not found"
: "No configuration file found"));
- WriteResults(output, null, errors, warnings, pluginNames, urlPatterns, logger);
+ WriteResults(isJsonOutput, null, errors, warnings, pluginNames, urlPatterns, logger);
return 1;
}
@@ -552,7 +546,7 @@ private static async Task ValidateConfigCoreAsync(
catch (JsonException ex)
{
errors.Add(new("configFile", $"Invalid JSON: {ex.Message}"));
- WriteResults(output, resolvedConfigFile, errors, warnings, pluginNames, urlPatterns, logger);
+ WriteResults(isJsonOutput, resolvedConfigFile, errors, warnings, pluginNames, urlPatterns, logger);
return 1;
}
@@ -611,7 +605,7 @@ private static async Task ValidateConfigCoreAsync(
}
var isConfigValid = errors.Count == 0;
- WriteResults(output, resolvedConfigFile, errors, warnings, pluginNames, urlPatterns, logger);
+ WriteResults(isJsonOutput, resolvedConfigFile, errors, warnings, pluginNames, urlPatterns, logger);
return isConfigValid ? 0 : 1;
}
@@ -738,7 +732,7 @@ private static void ValidateUrls(
}
private static void WriteResults(
- string output,
+ bool isJsonOutput,
string? configFile,
List errors,
List warnings,
@@ -748,7 +742,7 @@ private static void WriteResults(
{
var isValid = errors.Count == 0;
- if (string.Equals(output, "json", StringComparison.OrdinalIgnoreCase))
+ if (isJsonOutput)
{
WriteJsonResults(configFile, errors, warnings, pluginNames, urlPatterns, isValid, logger);
}
From 57f99375c3525840b61a148394a4b0abedf0848e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 25 Feb 2026 14:51:28 +0000
Subject: [PATCH 7/9] Remove ValidateConfigAsync wrapper; use exit code 2 for
validation errors
Co-authored-by: waldekmastykarz <11164679+waldekmastykarz@users.noreply.github.com>
---
DevProxy/Commands/ConfigCommand.cs | 13 ++++---------
1 file changed, 4 insertions(+), 9 deletions(-)
diff --git a/DevProxy/Commands/ConfigCommand.cs b/DevProxy/Commands/ConfigCommand.cs
index 21360303..ba448924 100644
--- a/DevProxy/Commands/ConfigCommand.cs
+++ b/DevProxy/Commands/ConfigCommand.cs
@@ -167,7 +167,7 @@ private void ConfigureCommand()
var configFile = parseResult.GetValue(validateConfigFileOption);
var logFor = parseResult.GetValueOrDefault(DevProxyCommand.LogForOptionName);
var isJsonOutput = logFor == LogFor.Machine;
- return await ValidateConfigAsync(configFile, isJsonOutput, cancellationToken);
+ return await ValidateConfigCoreAsync(configFile, isJsonOutput, _httpClient, _logger, cancellationToken);
});
this.AddCommands(new List
@@ -509,11 +509,6 @@ private static string GetTargetFolderPath(string appFolder, string configId)
return null;
}
- private async Task ValidateConfigAsync(string? configFilePath, bool isJsonOutput, CancellationToken cancellationToken)
- {
- return await ValidateConfigCoreAsync(configFilePath, isJsonOutput, _httpClient, _logger, cancellationToken);
- }
-
private static async Task ValidateConfigCoreAsync(
string? configFilePath,
bool isJsonOutput,
@@ -533,7 +528,7 @@ private static async Task ValidateConfigCoreAsync(
? $"Configuration file '{configFilePath}' not found"
: "No configuration file found"));
WriteResults(isJsonOutput, null, errors, warnings, pluginNames, urlPatterns, logger);
- return 1;
+ return 2;
}
string configJson;
@@ -547,7 +542,7 @@ private static async Task ValidateConfigCoreAsync(
{
errors.Add(new("configFile", $"Invalid JSON: {ex.Message}"));
WriteResults(isJsonOutput, resolvedConfigFile, errors, warnings, pluginNames, urlPatterns, logger);
- return 1;
+ return 2;
}
using (configDoc)
@@ -606,7 +601,7 @@ private static async Task ValidateConfigCoreAsync(
var isConfigValid = errors.Count == 0;
WriteResults(isJsonOutput, resolvedConfigFile, errors, warnings, pluginNames, urlPatterns, logger);
- return isConfigValid ? 0 : 1;
+ return isConfigValid ? 0 : 2;
}
private static void ValidateSchemaVersion(string schemaUrl, List warnings)
From fe35faeebc60e4e51715e18fc02430ca4f1e24f4 Mon Sep 17 00:00:00 2001
From: Waldek Mastykarz
Date: Thu, 26 Feb 2026 08:53:48 +0100
Subject: [PATCH 8/9] Address review feedback: YAML validation, type safety,
error handling, machine output formatting
- Add YAML-to-JSON conversion in ValidateConfigCoreAsync so YAML configs can be validated
- Wrap plugin property reads in try-catch for malformed type handling
- Check url.ValueKind before calling GetString() in ValidateUrls
- Catch IOException/UnauthorizedAccessException when reading config files
- Register MachineConsoleFormatter and select formatter based on isJsonOutput
- Return warning message for unparseable schema URLs instead of silently ignoring
---
DevProxy.Abstractions/Utils/ProxyUtils.cs | 2 +-
DevProxy/Commands/ConfigCommand.cs | 68 +++++++++++++++++++++--
2 files changed, 63 insertions(+), 7 deletions(-)
diff --git a/DevProxy.Abstractions/Utils/ProxyUtils.cs b/DevProxy.Abstractions/Utils/ProxyUtils.cs
index c9a40402..536f5a45 100644
--- a/DevProxy.Abstractions/Utils/ProxyUtils.cs
+++ b/DevProxy.Abstractions/Utils/ProxyUtils.cs
@@ -310,7 +310,7 @@ public static void ValidateSchemaVersion(string schemaUrl, ILogger logger)
}
catch
{
- // Ignore schema URL parsing errors
+ return $"The $schema value '{schemaUrl}' is not a valid URL. Schema version could not be validated.";
}
return null;
diff --git a/DevProxy/Commands/ConfigCommand.cs b/DevProxy/Commands/ConfigCommand.cs
index ba448924..5c482e9e 100644
--- a/DevProxy/Commands/ConfigCommand.cs
+++ b/DevProxy/Commands/ConfigCommand.cs
@@ -14,6 +14,7 @@
using System.Reflection;
using System.Text.Json;
using System.Text.RegularExpressions;
+using YamlDotNet.Core;
namespace DevProxy.Commands;
@@ -93,16 +94,26 @@ internal static async Task RunValidateStandaloneAsync(string[] args)
httpClient.DefaultRequestHeaders.UserAgent.Add(
new ProductInfoHeaderValue("dev-proxy", ProxyUtils.ProductVersion));
+ var formatterName = isJsonOutput
+ ? MachineConsoleFormatter.FormatterName
+ : ProxyConsoleFormatter.DefaultCategoryName;
+
using var loggerFactory = LoggerFactory.Create(builder =>
{
builder
.SetMinimumLevel(LogLevel.Information)
.AddConsole(consoleOptions =>
{
- consoleOptions.FormatterName = ProxyConsoleFormatter.DefaultCategoryName;
+ consoleOptions.FormatterName = formatterName;
consoleOptions.LogToStandardErrorThreshold = LogLevel.Warning;
})
.AddConsoleFormatter(formatterOptions =>
+ {
+ formatterOptions.IncludeScopes = false;
+ formatterOptions.ShowSkipMessages = true;
+ formatterOptions.ShowTimestamps = false;
+ })
+ .AddConsoleFormatter(formatterOptions =>
{
formatterOptions.IncludeScopes = false;
formatterOptions.ShowSkipMessages = true;
@@ -535,12 +546,38 @@ private static async Task ValidateConfigCoreAsync(
JsonDocument configDoc;
try
{
- configJson = await File.ReadAllTextAsync(resolvedConfigFile, cancellationToken);
+ var configText = await File.ReadAllTextAsync(resolvedConfigFile, cancellationToken);
+
+ if (ProxyYaml.IsYamlFile(resolvedConfigFile))
+ {
+ if (!ProxyYaml.TryConvertYamlToJson(configText, out var converted, out var yamlError))
+ {
+ throw new JsonException($"Could not convert YAML configuration to JSON: {yamlError}");
+ }
+ configJson = converted!;
+ }
+ else
+ {
+ configJson = configText;
+ }
+
configDoc = JsonDocument.Parse(configJson, ProxyUtils.JsonDocumentOptions);
}
catch (JsonException ex)
{
- errors.Add(new("configFile", $"Invalid JSON: {ex.Message}"));
+ errors.Add(new("configFile", $"Invalid configuration: {ex.Message}"));
+ WriteResults(isJsonOutput, resolvedConfigFile, errors, warnings, pluginNames, urlPatterns, logger);
+ return 2;
+ }
+ catch (YamlException ex)
+ {
+ errors.Add(new("configFile", $"Invalid YAML: {ex.Message}"));
+ WriteResults(isJsonOutput, resolvedConfigFile, errors, warnings, pluginNames, urlPatterns, logger);
+ return 2;
+ }
+ catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
+ {
+ errors.Add(new("configFile", $"Could not read configuration file: {ex.Message}"));
WriteResults(isJsonOutput, resolvedConfigFile, errors, warnings, pluginNames, urlPatterns, logger);
return 2;
}
@@ -625,9 +662,21 @@ private static void ValidatePlugins(
foreach (var plugin in pluginsElement.EnumerateArray())
{
- var name = plugin.TryGetProperty("name", out var nameProp) ? nameProp.GetString() : null;
- var enabled = !plugin.TryGetProperty("enabled", out var enabledProp) || enabledProp.GetBoolean();
- var pluginPath = plugin.TryGetProperty("pluginPath", out var pathProp) ? pathProp.GetString() : null;
+ string? name;
+ bool enabled;
+ string? pluginPath;
+ try
+ {
+ name = plugin.TryGetProperty("name", out var nameProp) ? nameProp.GetString() : null;
+ enabled = !plugin.TryGetProperty("enabled", out var enabledProp) || enabledProp.GetBoolean();
+ pluginPath = plugin.TryGetProperty("pluginPath", out var pathProp) ? pathProp.GetString() : null;
+ }
+ catch (InvalidOperationException ex)
+ {
+ errors.Add(new($"plugins[{i}]", $"Invalid plugin definition: {ex.Message}"));
+ i++;
+ continue;
+ }
if (string.IsNullOrEmpty(name))
{
@@ -702,6 +751,13 @@ private static void ValidateUrls(
var i = 0;
foreach (var url in urlsElement.EnumerateArray())
{
+ if (url.ValueKind != JsonValueKind.String)
+ {
+ errors.Add(new($"urlsToWatch[{i}]", $"Expected a string but got {url.ValueKind}"));
+ i++;
+ continue;
+ }
+
var pattern = url.GetString();
if (string.IsNullOrEmpty(pattern))
{
From 1071b1fa1d5dbf077100d029dea3128fb2e175d9 Mon Sep 17 00:00:00 2001
From: Garry Trinder
Date: Thu, 26 Feb 2026 12:15:25 +0000
Subject: [PATCH 9/9] Fix references to renamed
OutputFormat/JsonConsoleFormatter after merge with main
---
DevProxy/Commands/ConfigCommand.cs | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/DevProxy/Commands/ConfigCommand.cs b/DevProxy/Commands/ConfigCommand.cs
index a3ea15c1..976294b8 100644
--- a/DevProxy/Commands/ConfigCommand.cs
+++ b/DevProxy/Commands/ConfigCommand.cs
@@ -82,10 +82,10 @@ internal static async Task RunValidateStandaloneAsync(string[] args)
configFile = args[i + 1];
i++;
}
- else if (string.Equals(args[i], DevProxyCommand.LogForOptionName, StringComparison.OrdinalIgnoreCase) &&
+ else if (string.Equals(args[i], DevProxyCommand.OutputOptionName, StringComparison.OrdinalIgnoreCase) &&
i + 1 < args.Length)
{
- isJsonOutput = string.Equals(args[i + 1], "machine", StringComparison.OrdinalIgnoreCase);
+ isJsonOutput = string.Equals(args[i + 1], "json", StringComparison.OrdinalIgnoreCase);
i++;
}
}
@@ -95,7 +95,7 @@ internal static async Task RunValidateStandaloneAsync(string[] args)
new ProductInfoHeaderValue("dev-proxy", ProxyUtils.ProductVersion));
var formatterName = isJsonOutput
- ? MachineConsoleFormatter.FormatterName
+ ? JsonConsoleFormatter.FormatterName
: ProxyConsoleFormatter.DefaultCategoryName;
using var loggerFactory = LoggerFactory.Create(builder =>
@@ -113,7 +113,7 @@ internal static async Task RunValidateStandaloneAsync(string[] args)
formatterOptions.ShowSkipMessages = true;
formatterOptions.ShowTimestamps = false;
})
- .AddConsoleFormatter(formatterOptions =>
+ .AddConsoleFormatter(formatterOptions =>
{
formatterOptions.IncludeScopes = false;
formatterOptions.ShowSkipMessages = true;
@@ -178,8 +178,8 @@ private void ConfigureCommand()
configValidateCommand.SetAction(async (parseResult, cancellationToken) =>
{
var configFile = parseResult.GetValue(validateConfigFileOption);
- var logFor = parseResult.GetValueOrDefault(DevProxyCommand.LogForOptionName);
- var isJsonOutput = logFor == LogFor.Machine;
+ var output = parseResult.GetValueOrDefault(DevProxyCommand.OutputOptionName);
+ var isJsonOutput = output == OutputFormat.Json;
return await ValidateConfigCoreAsync(configFile, isJsonOutput, _httpClient, _logger, cancellationToken);
});