diff --git a/DevProxy.Abstractions/Models/MockStdioResponse.cs b/DevProxy.Abstractions/Models/MockStdioResponse.cs
index 4a39a24e..47e01132 100644
--- a/DevProxy.Abstractions/Models/MockStdioResponse.cs
+++ b/DevProxy.Abstractions/Models/MockStdioResponse.cs
@@ -34,11 +34,21 @@ public object Clone()
public class MockStdioRequest
{
///
- /// A fragment of the stdin body to match.
- /// If null or empty, the mock matches any stdin (or is applied immediately on startup).
+ /// A fragment of the stdin body to match (case-insensitive contains).
+ /// Prefer using either or , not both.
+ /// If both are specified, takes precedence and is ignored.
+ /// If neither is specified, the mock matches any stdin (or is applied immediately on startup).
///
public string? BodyFragment { get; set; }
+ ///
+ /// A regular expression pattern to match against the stdin body (case-insensitive).
+ /// Prefer using either or , not both.
+ /// If both are specified, takes precedence and is ignored.
+ /// If neither is specified, the mock matches any stdin (or is applied immediately on startup).
+ ///
+ public string? BodyRegex { get; set; }
+
///
/// The Nth occurrence to match. If null, matches every occurrence.
///
diff --git a/DevProxy.Plugins/Mocking/MockStdioResponsePlugin.cs b/DevProxy.Plugins/Mocking/MockStdioResponsePlugin.cs
index 09a38322..885b2207 100644
--- a/DevProxy.Plugins/Mocking/MockStdioResponsePlugin.cs
+++ b/DevProxy.Plugins/Mocking/MockStdioResponsePlugin.cs
@@ -202,7 +202,8 @@ private void ApplyStartupMocks(StdioRequestArgs e)
// Find mocks without stdin patterns (startup mocks)
var startupMocks = Configuration.Mocks
- .Where(m => string.IsNullOrEmpty(m.Request?.BodyFragment))
+ .Where(m => string.IsNullOrEmpty(m.Request?.BodyFragment) &&
+ string.IsNullOrEmpty(m.Request?.BodyRegex))
.ToList();
foreach (var mock in startupMocks)
@@ -249,16 +250,42 @@ Configuration.Mocks is null ||
return false;
}
- if (string.IsNullOrEmpty(mockResponse.Request.BodyFragment))
+ var hasBodyFragment = !string.IsNullOrEmpty(mockResponse.Request.BodyFragment);
+ var hasBodyRegex = !string.IsNullOrEmpty(mockResponse.Request.BodyRegex);
+
+ if (!hasBodyFragment && !hasBodyRegex)
{
- // No body fragment means startup mock, handled separately
+ // No body fragment or regex means startup mock, handled separately
return false;
}
- // Check if stdin contains the body fragment
- if (!stdinBody.Contains(mockResponse.Request.BodyFragment, StringComparison.OrdinalIgnoreCase))
+ // Use bodyRegex if specified, otherwise use bodyFragment (either/or)
+ if (hasBodyRegex)
{
- return false;
+ try
+ {
+ if (!Regex.IsMatch(stdinBody, mockResponse.Request.BodyRegex!, RegexOptions.IgnoreCase, TimeSpan.FromSeconds(5)))
+ {
+ return false;
+ }
+ }
+ catch (RegexMatchTimeoutException ex)
+ {
+ Logger.LogError(ex, "Regex match timed out for body regex pattern '{Pattern}'. Treating as non-matching mock.", mockResponse.Request.BodyRegex);
+ return false;
+ }
+ catch (ArgumentException ex)
+ {
+ Logger.LogError(ex, "Invalid body regex pattern '{Pattern}'. Treating as non-matching mock.", mockResponse.Request.BodyRegex);
+ return false;
+ }
+ }
+ else if (hasBodyFragment)
+ {
+ if (!stdinBody.Contains(mockResponse.Request.BodyFragment!, StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
}
// Check Nth condition
@@ -267,7 +294,7 @@ Configuration.Mocks is null ||
if (mockResponse?.Request is not null)
{
- var mockKey = mockResponse.Request.BodyFragment ?? "default";
+ var mockKey = GetMockKey(mockResponse);
_ = _appliedMocks.AddOrUpdate(mockKey, 1, (_, value) => ++value);
}
@@ -282,13 +309,20 @@ private bool IsNthRequest(MockStdioResponse mockResponse)
return true;
}
- var mockKey = mockResponse.Request.BodyFragment ?? "default";
+ var mockKey = GetMockKey(mockResponse);
_ = _appliedMocks.TryGetValue(mockKey, out var nth);
nth++;
return mockResponse.Request.Nth == nth;
}
+ private static string GetMockKey(MockStdioResponse mockResponse)
+ {
+ return mockResponse.Request?.BodyRegex
+ ?? mockResponse.Request?.BodyFragment
+ ?? "default";
+ }
+
private void ProcessMockResponse(StdioRequestArgs e, MockStdioResponse matchingResponse)
{
// Replace placeholders in the response with values from stdin
diff --git a/schemas/v2.2.0/mockstdioresponseplugin.mocksfile.schema.json b/schemas/v2.2.0/mockstdioresponseplugin.mocksfile.schema.json
index 23582644..3579f028 100644
--- a/schemas/v2.2.0/mockstdioresponseplugin.mocksfile.schema.json
+++ b/schemas/v2.2.0/mockstdioresponseplugin.mocksfile.schema.json
@@ -20,7 +20,11 @@
"properties": {
"bodyFragment": {
"type": "string",
- "description": "(Optional) A fragment of the stdin body to match. If null or empty, the mock matches any stdin or is applied immediately on startup."
+ "description": "(Optional) A fragment of the stdin body to match (case-insensitive contains). You can specify bodyFragment, bodyRegex, or both; if both are specified, bodyRegex takes precedence. If neither is specified, the mock matches any stdin or is applied immediately on startup."
+ },
+ "bodyRegex": {
+ "type": "string",
+ "description": "(Optional) A regular expression pattern to match against the stdin body (case-insensitive). You can specify bodyRegex, bodyFragment, or both; if both are specified, bodyRegex takes precedence. If neither is specified, the mock matches any stdin or is applied immediately on startup."
},
"nth": {
"type": "integer",