From 3bd6e509feeca5f8bffa4179ffb30ddef9f33ff6 Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Mon, 6 Jul 2026 15:58:45 +0200 Subject: [PATCH] Fix empty JSON/XML requests being rejected by endpoints which allow empty bodies --- .../Endpoints/Middlewares/MainMiddleware.cs | 3 +- .../Endpoints/BodyEndpoints.cs | 20 +++++ BunkumTests.HttpServer/Tests/BodyTests.cs | 82 +++++++++++++++++++ 3 files changed, 104 insertions(+), 1 deletion(-) diff --git a/Bunkum.Core/Endpoints/Middlewares/MainMiddleware.cs b/Bunkum.Core/Endpoints/Middlewares/MainMiddleware.cs index 1114917..95057f7 100644 --- a/Bunkum.Core/Endpoints/Middlewares/MainMiddleware.cs +++ b/Bunkum.Core/Endpoints/Middlewares/MainMiddleware.cs @@ -215,7 +215,8 @@ private Response GenerateResponseFromEndpoint(object? val, EndpointAttribute att return new Response([], ContentType.Plaintext, HttpStatusCode.BadRequest); } - if (!this.TryGetBodyParameter(attribute, paramType, body, out arg)) + // Don't try to deserialize/inject empty bodies if the endpoint explicitly allows them. + if (context.HasBody && !this.TryGetBodyParameter(attribute, paramType, body, out arg)) { return new Response([], ContentType.Plaintext, HttpStatusCode.BadRequest); } diff --git a/BunkumTests.HttpServer/Endpoints/BodyEndpoints.cs b/BunkumTests.HttpServer/Endpoints/BodyEndpoints.cs index e51f682..cbecae5 100644 --- a/BunkumTests.HttpServer/Endpoints/BodyEndpoints.cs +++ b/BunkumTests.HttpServer/Endpoints/BodyEndpoints.cs @@ -29,12 +29,32 @@ public Serializable Json(RequestContext context, Serializable body) return body; } + [AllowEmptyBody] + [HttpEndpoint("/body/json/optional", HttpMethods.Post, ContentType.Json)] + public Serializable OptionalJson(RequestContext context, Serializable? body) + { + return body ?? new() + { + Field = "had no body", + }; + } + [HttpEndpoint("/body/xml", HttpMethods.Post, ContentType.Xml)] public Serializable Xml(RequestContext context, Serializable body) { return body; } + [AllowEmptyBody] + [HttpEndpoint("/body/xml/optional", HttpMethods.Post, ContentType.Xml)] + public Serializable OptionalXml(RequestContext context, Serializable? body) + { + return body ?? new() + { + Field = "had no body", + }; + } + [HttpEndpoint("/body/byteArray", HttpMethods.Post)] public string ByteArray(RequestContext context, byte[] body) { diff --git a/BunkumTests.HttpServer/Tests/BodyTests.cs b/BunkumTests.HttpServer/Tests/BodyTests.cs index 06b4735..75487af 100644 --- a/BunkumTests.HttpServer/Tests/BodyTests.cs +++ b/BunkumTests.HttpServer/Tests/BodyTests.cs @@ -32,4 +32,86 @@ public async Task ReturnsBadRequestOnNoData() HttpResponseMessage msg = await client.PostAsync("/body/string", null); Assert.That(msg.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest)); } + + [Test] + public async Task RejectsEmptyJsonBodyIfRequired() + { + (BunkumServer server, HttpClient client) = this.Setup(); + server.AddEndpointGroup(); + + // empty bodies are rejected + HttpResponseMessage msg = await client.PostAsync("/body/json", null); + Assert.That(msg.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest)); + + // but valid bodies are accepted + string requestBody = "{\"Field\":\"has body\"}"; + msg = await client.PostAsync("/body/json", new StringContent(requestBody)); + Assert.That(msg.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + + string text = await msg.Content.ReadAsStringAsync(); + Assert.That(text, Is.EqualTo(requestBody)); + } + + [Test] + public async Task AcceptsEmptyJsonBodyIfOptional() + { + (BunkumServer server, HttpClient client) = this.Setup(); + server.AddEndpointGroup(); + + // empty bodies are accepted + HttpResponseMessage msg = await client.PostAsync("/body/json/optional", null); + Assert.That(msg.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + + string text = await msg.Content.ReadAsStringAsync(); + Assert.That(text, Is.EqualTo("{\"Field\":\"had no body\"}")); + + // and valid bodies are also accepted + string requestBody = "{\"Field\":\"has body\"}"; + msg = await client.PostAsync("/body/json/optional", new StringContent(requestBody)); + Assert.That(msg.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + + text = await msg.Content.ReadAsStringAsync(); + Assert.That(text, Is.EqualTo(requestBody)); + } + + [Test] + public async Task RejectsEmptyXmlBodyIfRequired() + { + (BunkumServer server, HttpClient client) = this.Setup(); + server.AddEndpointGroup(); + + // empty bodies are rejected + HttpResponseMessage msg = await client.PostAsync("/body/xml", null); + Assert.That(msg.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest)); + + // but valid bodies are accepted + string requestBody = "has body"; + msg = await client.PostAsync("/body/xml", new StringContent(requestBody)); + Assert.That(msg.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + + string text = await msg.Content.ReadAsStringAsync(); + Assert.That(text, Is.EqualTo(requestBody)); + } + + [Test] + public async Task AcceptsEmptyXmlBodyIfOptional() + { + (BunkumServer server, HttpClient client) = this.Setup(); + server.AddEndpointGroup(); + + // empty bodies are accepted + HttpResponseMessage msg = await client.PostAsync("/body/xml/optional", null); + Assert.That(msg.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + + string text = await msg.Content.ReadAsStringAsync(); + Assert.That(text, Is.EqualTo("had no body")); + + // and valid bodies are also accepted + string requestBody = "has body"; + msg = await client.PostAsync("/body/xml/optional", new StringContent(requestBody)); + Assert.That(msg.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + + text = await msg.Content.ReadAsStringAsync(); + Assert.That(text, Is.EqualTo(requestBody)); + } } \ No newline at end of file