From 3489b9c9d0be144f4bba287b4404255bf44808a3 Mon Sep 17 00:00:00 2001 From: Arkadiusz Biel - bielu <2244074+bielu@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:45:46 +0100 Subject: [PATCH 1/3] fix: report invalid HTTP message statusCode as an error HttpMessageBinding parsed `statusCode` with int.TryParse and ignored the value when parsing failed, so e.g. `statusCode: not-found` was read with no diagnostics and dropped on write. It now uses GetIntegerValue(), which records "Value could not parse to integer." like the other bindings. Co-Authored-By: Claude Opus 5.5 --- .../Http/HttpMessageBinding.cs | 9 +----- .../Bindings/Http/HttpBindings_Should.cs | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/ByteBard.AsyncAPI.Bindings/Http/HttpMessageBinding.cs b/src/ByteBard.AsyncAPI.Bindings/Http/HttpMessageBinding.cs index 5a76874..3de0266 100644 --- a/src/ByteBard.AsyncAPI.Bindings/Http/HttpMessageBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/Http/HttpMessageBinding.cs @@ -35,14 +35,7 @@ public class HttpMessageBinding : MessageBinding { { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, { "headers", (a, n) => { a.Headers = AsyncApiJsonSchemaDeserializer.LoadSchema(n); } }, - { "statusCode", (a, n) => - { - if (int.TryParse(n.GetScalarValue(), out var code)) - { - a.StatusCode = (HttpStatusCode)code; - } - } - }, + { "statusCode", (a, n) => { a.StatusCode = (HttpStatusCode)n.GetIntegerValue(); } }, }; public override void SerializeV2(IAsyncApiWriter writer) diff --git a/test/ByteBard.AsyncAPI.Tests/Bindings/Http/HttpBindings_Should.cs b/test/ByteBard.AsyncAPI.Tests/Bindings/Http/HttpBindings_Should.cs index b66eb67..0bff0ed 100644 --- a/test/ByteBard.AsyncAPI.Tests/Bindings/Http/HttpBindings_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/Http/HttpBindings_Should.cs @@ -189,5 +189,37 @@ public void V2_HttpMessageBinding_OmitsStatusCode() actual.Should().NotContain("statusCode"); actual.Should().Contain("bindingVersion: 0.2.0"); } + + [Test] + public void V3_HttpMessageBinding_ReadsStatusCode() + { + var yaml = + """ + bindings: + http: + statusCode: 404 + """; + + var settings = new AsyncApiReaderSettings { Bindings = BindingsCollection.Http }; + var message = new AsyncApiStringReader(settings).ReadFragment(yaml, AsyncApiVersion.AsyncApi3_0, out var diagnostics); + + diagnostics.Errors.Should().BeEmpty(); + ((HttpMessageBinding)message.Bindings["http"]).StatusCode.Should().Be(HttpStatusCode.NotFound); + } + + [TestCase("not-found")] + [TestCase("200.5")] + public void V3_HttpMessageBinding_WithInvalidStatusCode_ReportsError(string statusCode) + { + var yaml = + "bindings:\n" + + " http:\n" + + " statusCode: " + statusCode + "\n"; + + var settings = new AsyncApiReaderSettings { Bindings = BindingsCollection.Http }; + new AsyncApiStringReader(settings).ReadFragment(yaml, AsyncApiVersion.AsyncApi3_0, out var diagnostics); + + diagnostics.Errors.Should().ContainSingle().Which.Message.Should().Contain("could not parse to integer"); + } } } From 94bdcd8bddd6554a6382756938a64152b1f64be2 Mon Sep 17 00:00:00 2001 From: Arkadiusz Biel - bielu <2244074+bielu@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:48:20 +0100 Subject: [PATCH 2/3] fix: report HTTP statusCode outside 100-599 as an error RFC 9110 defines status codes as three-digit integers from 100 to 599. Values outside that range (e.g. `statusCode: 42`) were accepted without any diagnostic. They are now reported in diagnostic.Errors with a pointer to the field. The value is still kept on the binding so the document can be written back unchanged. The check lives in the binding parser because the core validation rules cannot reference the bindings assembly. Co-Authored-By: Claude Opus 5.5 --- .../Http/HttpMessageBinding.cs | 19 +++++++++- .../Bindings/Http/HttpBindings_Should.cs | 38 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/ByteBard.AsyncAPI.Bindings/Http/HttpMessageBinding.cs b/src/ByteBard.AsyncAPI.Bindings/Http/HttpMessageBinding.cs index 3de0266..0a53585 100644 --- a/src/ByteBard.AsyncAPI.Bindings/Http/HttpMessageBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/Http/HttpMessageBinding.cs @@ -35,9 +35,26 @@ public class HttpMessageBinding : MessageBinding { { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, { "headers", (a, n) => { a.Headers = AsyncApiJsonSchemaDeserializer.LoadSchema(n); } }, - { "statusCode", (a, n) => { a.StatusCode = (HttpStatusCode)n.GetIntegerValue(); } }, + { "statusCode", (a, n) => { a.StatusCode = LoadStatusCode(n); } }, }; + /// + /// Reads the status code, reporting values outside the RFC 9110 range (100-599) as errors. + /// + /// An out-of-range value is still kept so the document can be written back unchanged. + private static HttpStatusCode LoadStatusCode(ParseNode node) + { + var code = node.GetIntegerValue(); + if (code < 100 || code > 599) + { + node.Context.Diagnostic.Errors.Add(new AsyncApiError( + node.Context.GetLocation(), + $"statusCode {code} is not a valid HTTP status code. It must be between 100 and 599.")); + } + + return (HttpStatusCode)code; + } + public override void SerializeV2(IAsyncApiWriter writer) { if (writer is null) diff --git a/test/ByteBard.AsyncAPI.Tests/Bindings/Http/HttpBindings_Should.cs b/test/ByteBard.AsyncAPI.Tests/Bindings/Http/HttpBindings_Should.cs index 0bff0ed..cefaf68 100644 --- a/test/ByteBard.AsyncAPI.Tests/Bindings/Http/HttpBindings_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/Http/HttpBindings_Should.cs @@ -221,5 +221,43 @@ public void V3_HttpMessageBinding_WithInvalidStatusCode_ReportsError(string stat diagnostics.Errors.Should().ContainSingle().Which.Message.Should().Contain("could not parse to integer"); } + + [TestCase(100)] + [TestCase(599)] + public void V3_HttpMessageBinding_WithBoundaryStatusCode_ReportsNoError(int statusCode) + { + var yaml = + "bindings:\n" + + " http:\n" + + " statusCode: " + statusCode + "\n"; + + var settings = new AsyncApiReaderSettings { Bindings = BindingsCollection.Http }; + var message = new AsyncApiStringReader(settings).ReadFragment(yaml, AsyncApiVersion.AsyncApi3_0, out var diagnostics); + + diagnostics.Errors.Should().BeEmpty(); + ((HttpMessageBinding)message.Bindings["http"]).StatusCode.Should().Be((HttpStatusCode)statusCode); + } + + [TestCase(0)] + [TestCase(42)] + [TestCase(99)] + [TestCase(600)] + [TestCase(-200)] + public void V3_HttpMessageBinding_WithOutOfRangeStatusCode_ReportsErrorAndKeepsValue(int statusCode) + { + var yaml = + "bindings:\n" + + " http:\n" + + " statusCode: " + statusCode + "\n"; + + var settings = new AsyncApiReaderSettings { Bindings = BindingsCollection.Http }; + var message = new AsyncApiStringReader(settings).ReadFragment(yaml, AsyncApiVersion.AsyncApi3_0, out var diagnostics); + + var error = diagnostics.Errors.Should().ContainSingle().Subject; + error.Message.Should().Contain("must be between 100 and 599"); + error.Pointer.Should().EndWith("statusCode"); + ((HttpMessageBinding)message.Bindings["http"]).StatusCode.Should().Be((HttpStatusCode)statusCode); + message.SerializeAsYaml(AsyncApiVersion.AsyncApi3_0).Should().Contain("statusCode: " + statusCode); + } } } From a27d8222054080564219824cc57e74c029c4668d Mon Sep 17 00:00:00 2001 From: Arkadiusz Biel - bielu <2244074+bielu@users.noreply.github.com> Date: Sat, 17 Oct 2026 19:04:15 +0100 Subject: [PATCH 3/3] refactor(bindings)!: store HttpMessageBinding.StatusCode as int? System.Net.HttpStatusCode only defines 61 of the 500 codes in 100-599. Codes such as 425 Too Early (RFC 8470), nginx 499 or Cloudflare 520 round-tripped through the enum cast, but could not be named, printed as bare numbers and failed Enum.IsDefined, so the type implied they were invalid. StatusCode is now a plain int?, validated by the 100-599 range check. BREAKING CHANGE: `HttpMessageBinding.StatusCode` is now `int?` instead of `HttpStatusCode?`. Cast when assigning from the enum, e.g. `StatusCode = (int)HttpStatusCode.OK`. Co-Authored-By: Claude Opus 5.5 --- .../Http/HttpMessageBinding.cs | 10 +++++----- .../Bindings/Http/HttpBindings_Should.cs | 17 ++++++++++------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/ByteBard.AsyncAPI.Bindings/Http/HttpMessageBinding.cs b/src/ByteBard.AsyncAPI.Bindings/Http/HttpMessageBinding.cs index 0a53585..0a8fc0f 100644 --- a/src/ByteBard.AsyncAPI.Bindings/Http/HttpMessageBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/Http/HttpMessageBinding.cs @@ -1,7 +1,6 @@ namespace ByteBard.AsyncAPI.Bindings.Http { using System; - using System.Net; using ByteBard.AsyncAPI.Models; using ByteBard.AsyncAPI.Readers; using ByteBard.AsyncAPI.Readers.ParseNodes; @@ -25,9 +24,10 @@ public class HttpMessageBinding : MessageBinding /// /// The HTTP response status code according to RFC 9110. `statusCode` is only relevant for messages referenced by the Operation Reply Object. + /// Stored as an integer so that codes without a member (e.g. 425, 499, 520) are represented as-is. /// Note: This field is only serialized in AsyncAPI V3. /// - public HttpStatusCode? StatusCode { get; set; } + public int? StatusCode { get; set; } public override string BindingKey => "http"; @@ -42,7 +42,7 @@ public class HttpMessageBinding : MessageBinding /// Reads the status code, reporting values outside the RFC 9110 range (100-599) as errors. /// /// An out-of-range value is still kept so the document can be written back unchanged. - private static HttpStatusCode LoadStatusCode(ParseNode node) + private static int LoadStatusCode(ParseNode node) { var code = node.GetIntegerValue(); if (code < 100 || code > 599) @@ -52,7 +52,7 @@ private static HttpStatusCode LoadStatusCode(ParseNode node) $"statusCode {code} is not a valid HTTP status code. It must be between 100 and 599.")); } - return (HttpStatusCode)code; + return code; } public override void SerializeV2(IAsyncApiWriter writer) @@ -81,7 +81,7 @@ public override void SerializeV3(IAsyncApiWriter writer) if (this.StatusCode.HasValue) { - writer.WriteRequiredProperty(AsyncApiConstants.StatusCode, (int)this.StatusCode.Value); + writer.WriteRequiredProperty(AsyncApiConstants.StatusCode, this.StatusCode.Value); } writer.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion ?? V3BindingVersion); diff --git a/test/ByteBard.AsyncAPI.Tests/Bindings/Http/HttpBindings_Should.cs b/test/ByteBard.AsyncAPI.Tests/Bindings/Http/HttpBindings_Should.cs index cefaf68..f1b25ef 100644 --- a/test/ByteBard.AsyncAPI.Tests/Bindings/Http/HttpBindings_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/Http/HttpBindings_Should.cs @@ -1,7 +1,6 @@ namespace ByteBard.AsyncAPI.Tests.Bindings.Http { using System.Linq; - using System.Net; using FluentAssertions; using ByteBard.AsyncAPI.Bindings; using ByteBard.AsyncAPI.Bindings.Http; @@ -156,7 +155,7 @@ public void V3_HttpMessageBinding_IncludesStatusCode() { Description = "response headers", }, - StatusCode = HttpStatusCode.OK, + StatusCode = 200, }); // Act @@ -179,7 +178,7 @@ public void V2_HttpMessageBinding_OmitsStatusCode() { Description = "response headers", }, - StatusCode = HttpStatusCode.OK, + StatusCode = 200, }); // Act @@ -204,7 +203,7 @@ public void V3_HttpMessageBinding_ReadsStatusCode() var message = new AsyncApiStringReader(settings).ReadFragment(yaml, AsyncApiVersion.AsyncApi3_0, out var diagnostics); diagnostics.Errors.Should().BeEmpty(); - ((HttpMessageBinding)message.Bindings["http"]).StatusCode.Should().Be(HttpStatusCode.NotFound); + ((HttpMessageBinding)message.Bindings["http"]).StatusCode.Should().Be(404); } [TestCase("not-found")] @@ -224,7 +223,10 @@ public void V3_HttpMessageBinding_WithInvalidStatusCode_ReportsError(string stat [TestCase(100)] [TestCase(599)] - public void V3_HttpMessageBinding_WithBoundaryStatusCode_ReportsNoError(int statusCode) + [TestCase(425)] // Too Early (RFC 8470), not a System.Net.HttpStatusCode member + [TestCase(499)] // nginx client closed request + [TestCase(520)] // Cloudflare unknown error + public void V3_HttpMessageBinding_WithValidStatusCode_ReportsNoErrorAndRoundTrips(int statusCode) { var yaml = "bindings:\n" + @@ -235,7 +237,8 @@ public void V3_HttpMessageBinding_WithBoundaryStatusCode_ReportsNoError(int stat var message = new AsyncApiStringReader(settings).ReadFragment(yaml, AsyncApiVersion.AsyncApi3_0, out var diagnostics); diagnostics.Errors.Should().BeEmpty(); - ((HttpMessageBinding)message.Bindings["http"]).StatusCode.Should().Be((HttpStatusCode)statusCode); + ((HttpMessageBinding)message.Bindings["http"]).StatusCode.Should().Be(statusCode); + message.SerializeAsYaml(AsyncApiVersion.AsyncApi3_0).Should().Contain("statusCode: " + statusCode); } [TestCase(0)] @@ -256,7 +259,7 @@ public void V3_HttpMessageBinding_WithOutOfRangeStatusCode_ReportsErrorAndKeepsV var error = diagnostics.Errors.Should().ContainSingle().Subject; error.Message.Should().Contain("must be between 100 and 599"); error.Pointer.Should().EndWith("statusCode"); - ((HttpMessageBinding)message.Bindings["http"]).StatusCode.Should().Be((HttpStatusCode)statusCode); + ((HttpMessageBinding)message.Bindings["http"]).StatusCode.Should().Be(statusCode); message.SerializeAsYaml(AsyncApiVersion.AsyncApi3_0).Should().Contain("statusCode: " + statusCode); } }