Fix BodyParser to correctly check for json (#1297)

* Fix BodyParser to correctly check for json

* JsonUtils
This commit is contained in:
Stef Heyenrath
2025-05-21 17:28:29 +02:00
committed by GitHub
parent 1e23c58bf2
commit d628ce2270
7 changed files with 76 additions and 59 deletions

View File

@@ -8,18 +8,8 @@ namespace WireMock.Util;
// ReSharper disable once InconsistentNaming // ReSharper disable once InconsistentNaming
public static class IBodyDataExtensions public static class IBodyDataExtensions
{ {
public static BodyType GetBodyType(this IBodyData bodyData) public static BodyType GetDetectedBodyType(this IBodyData bodyData)
{ {
if (bodyData.DetectedBodyTypeFromContentType is not null and not BodyType.None) return bodyData.DetectedBodyType ?? BodyType.None;
{
return bodyData.DetectedBodyTypeFromContentType.Value;
}
if (bodyData.DetectedBodyType is not null and not BodyType.None)
{
return bodyData.DetectedBodyType.Value;
}
return BodyType.None;
} }
} }

View File

@@ -35,7 +35,7 @@ internal static class HttpRequestMessageHelper
} }
var bodyData = requestMessage.BodyData; var bodyData = requestMessage.BodyData;
httpRequestMessage.Content = bodyData?.GetBodyType() switch httpRequestMessage.Content = bodyData?.DetectedBodyType switch
{ {
BodyType.Bytes => ByteArrayContentHelper.Create(bodyData.BodyAsBytes!, contentType), BodyType.Bytes => ByteArrayContentHelper.Create(bodyData.BodyAsBytes!, contentType),
BodyType.Json => StringContentHelper.Create(JsonConvert.SerializeObject(bodyData.BodyAsJson), contentType), BodyType.Json => StringContentHelper.Create(JsonConvert.SerializeObject(bodyData.BodyAsJson), contentType),

View File

@@ -70,7 +70,7 @@ namespace WireMock.Owin.Mappers
} }
var bodyData = responseMessage.BodyData; var bodyData = responseMessage.BodyData;
if (bodyData?.GetBodyType() == BodyType.SseString) if (bodyData?.GetDetectedBodyType() == BodyType.SseString)
{ {
await HandleSseStringAsync(responseMessage, response, bodyData); await HandleSseStringAsync(responseMessage, response, bodyData);
return; return;
@@ -166,7 +166,7 @@ namespace WireMock.Owin.Mappers
private async Task<byte[]?> GetNormalBodyAsync(IResponseMessage responseMessage) private async Task<byte[]?> GetNormalBodyAsync(IResponseMessage responseMessage)
{ {
var bodyData = responseMessage.BodyData; var bodyData = responseMessage.BodyData;
switch (bodyData?.GetBodyType()) switch (bodyData?.GetDetectedBodyType())
{ {
case BodyType.String: case BodyType.String:
case BodyType.FormUrlEncoded: case BodyType.FormUrlEncoded:

View File

@@ -177,7 +177,7 @@ internal static class BodyParser
} }
// If string is not null or empty, try to deserialize the string to a JObject // If string is not null or empty, try to deserialize the string to a JObject
if (settings.DeserializeJson && !string.IsNullOrEmpty(data.BodyAsString)) if (settings.DeserializeJson && JsonUtils.IsJson(data.BodyAsString))
{ {
try try
{ {
@@ -197,7 +197,7 @@ internal static class BodyParser
return data; return data;
} }
private static async Task<(string? ContentType, byte[] Bytes)> ReadBytesAsync(Stream stream, string? contentEncoding = null, bool decompressGZipAndDeflate = true) private static async Task<(string? ContentType, byte[] Bytes)> ReadBytesAsync(Stream stream, string? contentEncoding = null, bool decompressGZipAndDeflate = true)
{ {
using var memoryStream = new MemoryStream(); using var memoryStream = new MemoryStream();

View File

@@ -12,17 +12,23 @@ namespace WireMock.Util;
internal static class JsonUtils internal static class JsonUtils
{ {
public static bool TryParseAsJObject(string? strInput, [NotNullWhen(true)] out JObject? value) public static bool IsJson(string? value)
{ {
value = null; if (string.IsNullOrWhiteSpace(value))
if (strInput == null || string.IsNullOrWhiteSpace(strInput))
{ {
return false; return false;
} }
strInput = strInput.Trim(); value = value!.Trim();
if ((!strInput.StartsWith("{") || !strInput.EndsWith("}")) && (!strInput.StartsWith("[") || !strInput.EndsWith("]")))
return (value.StartsWith("{") && value.EndsWith("}")) || (value.StartsWith("[") && value.EndsWith("]"));
}
public static bool TryParseAsJObject(string? strInput, [NotNullWhen(true)] out JObject? value)
{
value = null;
if (!IsJson(strInput))
{ {
return false; return false;
} }
@@ -30,7 +36,7 @@ internal static class JsonUtils
try try
{ {
// Try to convert this string into a JToken // Try to convert this string into a JToken
value = JObject.Parse(strInput); value = JObject.Parse(strInput!);
return true; return true;
} }
catch catch

View File

@@ -32,7 +32,7 @@ public class HttpRequestMessageHelperTests
} }
[Fact] [Fact]
public async Task HttpRequestMessageHelper_Create_Bytes() public async Task HttpRequestMessageHelper_Create_Bytes_Without_ContentTypeHeader()
{ {
// Assign // Assign
var body = new BodyData var body = new BodyData
@@ -40,27 +40,25 @@ public class HttpRequestMessageHelperTests
BodyAsBytes = Encoding.UTF8.GetBytes("hi"), BodyAsBytes = Encoding.UTF8.GetBytes("hi"),
DetectedBodyType = BodyType.Bytes DetectedBodyType = BodyType.Bytes
}; };
var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", ClientIp, body); var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "POST", ClientIp, body);
// Act // Act
var message = HttpRequestMessageHelper.Create(request, "http://url"); var message = HttpRequestMessageHelper.Create(request, "http://url");
// Assert // Assert
Check.That(await message.Content.ReadAsByteArrayAsync().ConfigureAwait(false)).ContainsExactly(Encoding.UTF8.GetBytes("hi")); Check.That(await message.Content!.ReadAsByteArrayAsync().ConfigureAwait(false)).ContainsExactly(Encoding.UTF8.GetBytes("hi"));
} }
[Fact] [Fact]
public async Task HttpRequestMessageHelper_Create_TextPlain() public async Task HttpRequestMessageHelper_Create_TextPlain_Without_ContentTypeHeader()
{ {
// Assign // Assign
var body = new BodyData var body = new BodyData
{ {
BodyAsString = "0123", // or 83 in decimal BodyAsString = "0123", // or 83 in decimal
BodyAsJson = 83, DetectedBodyType = BodyType.String
DetectedBodyType = BodyType.Json,
DetectedBodyTypeFromContentType = BodyType.String
}; };
var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", ClientIp, body); var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "POST", ClientIp, body);
// Act // Act
var message = HttpRequestMessageHelper.Create(request, "http://url"); var message = HttpRequestMessageHelper.Create(request, "http://url");
@@ -78,7 +76,7 @@ public class HttpRequestMessageHelperTests
BodyAsJson = new { x = 42 }, BodyAsJson = new { x = 42 },
DetectedBodyType = BodyType.Json DetectedBodyType = BodyType.Json
}; };
var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", ClientIp, body); var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "POST", ClientIp, body);
// Act // Act
var message = HttpRequestMessageHelper.Create(request, "http://url"); var message = HttpRequestMessageHelper.Create(request, "http://url");
@@ -97,13 +95,13 @@ public class HttpRequestMessageHelperTests
BodyAsJson = new { x = 42 }, BodyAsJson = new { x = 42 },
DetectedBodyType = BodyType.Json DetectedBodyType = BodyType.Json
}; };
var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", ClientIp, body, headers); var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "POST", ClientIp, body, headers);
// Act // Act
var message = HttpRequestMessageHelper.Create(request, "http://url"); var message = HttpRequestMessageHelper.Create(request, "http://url");
// Assert // Assert
Check.That(await message.Content.ReadAsStringAsync().ConfigureAwait(false)).Equals("{\"x\":42}"); Check.That(await message.Content!.ReadAsStringAsync().ConfigureAwait(false)).Equals("{\"x\":42}");
Check.That(message.Content.Headers.GetValues("Content-Type")).ContainsExactly("application/json"); Check.That(message.Content.Headers.GetValues("Content-Type")).ContainsExactly("application/json");
} }
@@ -117,16 +115,38 @@ public class HttpRequestMessageHelperTests
BodyAsJson = new { x = 42 }, BodyAsJson = new { x = 42 },
DetectedBodyType = BodyType.Json DetectedBodyType = BodyType.Json
}; };
var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", ClientIp, body, headers); var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "POST", ClientIp, body, headers);
// Act // Act
var message = HttpRequestMessageHelper.Create(request, "http://url"); var message = HttpRequestMessageHelper.Create(request, "http://url");
// Assert // Assert
Check.That(await message.Content.ReadAsStringAsync().ConfigureAwait(false)).Equals("{\"x\":42}"); Check.That(await message.Content!.ReadAsStringAsync().ConfigureAwait(false)).Equals("{\"x\":42}");
Check.That(message.Content.Headers.GetValues("Content-Type")).ContainsExactly("application/json; charset=utf-8"); Check.That(message.Content.Headers.GetValues("Content-Type")).ContainsExactly("application/json; charset=utf-8");
} }
[Fact]
public async Task HttpRequestMessageHelper_Create_Json_With_ContentType_MultiPartFormData()
{
// Assign
var headers = new Dictionary<string, string[]> { { "Content-Type", ["multipart/form-data"] } };
var body = new BodyData
{
BodyAsJson = new { x = 42 },
DetectedBodyTypeFromContentType = BodyType.MultiPart,
DetectedBodyType = BodyType.Json
};
var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "POST", ClientIp, body, headers);
// Act
var message = HttpRequestMessageHelper.Create(request, "http://url");
// Assert
Check.That(await message.Content!.ReadAsStringAsync().ConfigureAwait(false)).Equals("{\"x\":42}");
Check.That(message.Content.Headers.GetValues("Content-Type")).ContainsExactly("multipart/form-data");
}
[Fact] [Fact]
public void HttpRequestMessageHelper_Create_String_With_ContentType_ApplicationXml() public void HttpRequestMessageHelper_Create_String_With_ContentType_ApplicationXml()
{ {
@@ -143,7 +163,7 @@ public class HttpRequestMessageHelperTests
var message = HttpRequestMessageHelper.Create(request, "http://url"); var message = HttpRequestMessageHelper.Create(request, "http://url");
// Assert // Assert
Check.That(message.Content.Headers.GetValues("Content-Type")).ContainsExactly("application/xml"); Check.That(message.Content!.Headers.GetValues("Content-Type")).ContainsExactly("application/xml");
} }
[Fact] [Fact]
@@ -162,7 +182,7 @@ public class HttpRequestMessageHelperTests
var message = HttpRequestMessageHelper.Create(request, "http://url"); var message = HttpRequestMessageHelper.Create(request, "http://url");
// Assert // Assert
Check.That(message.Content.Headers.GetValues("Content-Type")).ContainsExactly("application/xml; charset=UTF-8"); Check.That(message.Content!.Headers.GetValues("Content-Type")).ContainsExactly("application/xml; charset=UTF-8");
} }
[Fact] [Fact]
@@ -181,7 +201,7 @@ public class HttpRequestMessageHelperTests
var message = HttpRequestMessageHelper.Create(request, "http://url"); var message = HttpRequestMessageHelper.Create(request, "http://url");
// Assert // Assert
Check.That(message.Content.Headers.GetValues("Content-Type")).ContainsExactly("application/xml; charset=Ascii"); Check.That(message.Content!.Headers.GetValues("Content-Type")).ContainsExactly("application/xml; charset=Ascii");
} }
[Fact] [Fact]
@@ -222,7 +242,7 @@ public class HttpRequestMessageHelperTests
var message = HttpRequestMessageHelper.Create(request, "http://url"); var message = HttpRequestMessageHelper.Create(request, "http://url");
// Assert // Assert
Check.That(await message.Content.ReadAsStringAsync().ConfigureAwait(false)).Equals(body); Check.That(await message.Content!.ReadAsStringAsync().ConfigureAwait(false)).Equals(body);
Check.That(message.Content.Headers.GetValues("Content-Type")).ContainsExactly("multipart/form-data"); Check.That(message.Content.Headers.GetValues("Content-Type")).ContainsExactly("multipart/form-data");
} }

View File

@@ -1,12 +1,11 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System; using System;
using NFluent;
using System.IO; using System.IO;
using System.IO.Compression;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using FluentAssertions; using FluentAssertions;
using NFluent;
using WireMock.Types; using WireMock.Types;
using WireMock.Util; using WireMock.Util;
using Xunit; using Xunit;
@@ -67,7 +66,7 @@ public class BodyParserTests
} }
[Theory] [Theory]
[InlineData(new byte[] { 34, 97, 34 }, BodyType.Json)] [InlineData(new byte[] { 123, 32, 34, 120, 34, 58, 32, 49, 50, 51, 32, 125 }, BodyType.Json)]
[InlineData(new byte[] { 97 }, BodyType.String)] [InlineData(new byte[] { 97 }, BodyType.String)]
[InlineData(new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 }, BodyType.Bytes)] [InlineData(new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 }, BodyType.Bytes)]
public async Task BodyParser_Parse_DetectedBodyType(byte[] content, BodyType detectedBodyType) public async Task BodyParser_Parse_DetectedBodyType(byte[] content, BodyType detectedBodyType)
@@ -88,7 +87,7 @@ public class BodyParserTests
} }
[Theory] [Theory]
[InlineData(new byte[] { 34, 97, 34 }, BodyType.String)] [InlineData(new byte[] { 123, 32, 34, 120, 34, 58, 32, 49, 50, 51, 32, 125 }, BodyType.String)]
[InlineData(new byte[] { 97 }, BodyType.String)] [InlineData(new byte[] { 97 }, BodyType.String)]
[InlineData(new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 }, BodyType.Bytes)] [InlineData(new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 }, BodyType.Bytes)]
public async Task BodyParser_Parse_DetectedBodyTypeNoJsonParsing(byte[] content, BodyType detectedBodyType) public async Task BodyParser_Parse_DetectedBodyTypeNoJsonParsing(byte[] content, BodyType detectedBodyType)
@@ -112,26 +111,28 @@ public class BodyParserTests
public async Task BodyParser_Parse_WithUTF8EncodingAndContentTypeMultipart_DetectedBodyTypeEqualsString() public async Task BodyParser_Parse_WithUTF8EncodingAndContentTypeMultipart_DetectedBodyTypeEqualsString()
{ {
// Arrange // Arrange
string contentType = "multipart/form-data"; var contentType = "multipart/form-data";
string body = @" var body =
"""
-----------------------------9051914041544843365972754266 -----------------------------9051914041544843365972754266
Content-Disposition: form-data; name=""text"" Content-Disposition: form-data; name="text"
text default text default
-----------------------------9051914041544843365972754266 -----------------------------9051914041544843365972754266
Content-Disposition: form-data; name=""file1""; filename=""a.txt"" Content-Disposition: form-data; name="file1"; filename="a.txt"
Content-Type: text/plain Content-Type: text/plain
Content of a txt Content of a txt
-----------------------------9051914041544843365972754266 -----------------------------9051914041544843365972754266
Content-Disposition: form-data; name=""file2""; filename=""a.html"" Content-Disposition: form-data; name="file2"; filename="a.html"
Content-Type: text/html Content-Type: text/html
<!DOCTYPE html><title>Content of a.html.</title> <!DOCTYPE html><title>Content of a.html.</title>
-----------------------------9051914041544843365972754266--"; -----------------------------9051914041544843365972754266--
""";
var bodyParserSettings = new BodyParserSettings var bodyParserSettings = new BodyParserSettings
{ {