Skip to content

Commit 70e73db

Browse files
authored
fix!: validate HTTP message statusCode and store it as int (#52)
1 parent c57a8d6 commit 70e73db

2 files changed

Lines changed: 97 additions & 14 deletions

File tree

‎src/ByteBard.AsyncAPI.Bindings/Http/HttpMessageBinding.cs‎

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
namespace ByteBard.AsyncAPI.Bindings.Http
22
{
33
using System;
4-
using System.Net;
54
using ByteBard.AsyncAPI.Models;
65
using ByteBard.AsyncAPI.Readers;
76
using ByteBard.AsyncAPI.Readers.ParseNodes;
@@ -25,26 +24,37 @@ public class HttpMessageBinding : MessageBinding<HttpMessageBinding>
2524

2625
/// <summary>
2726
/// The HTTP response status code according to RFC 9110. `statusCode` is only relevant for messages referenced by the Operation Reply Object.
27+
/// Stored as an integer so that codes without a <see cref="System.Net.HttpStatusCode"/> member (e.g. 425, 499, 520) are represented as-is.
2828
/// Note: This field is only serialized in AsyncAPI V3.
2929
/// </summary>
30-
public HttpStatusCode? StatusCode { get; set; }
30+
public int? StatusCode { get; set; }
3131

3232
public override string BindingKey => "http";
3333

3434
protected override FixedFieldMap<HttpMessageBinding> FixedFieldMap => new()
3535
{
3636
{ "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } },
3737
{ "headers", (a, n) => { a.Headers = AsyncApiJsonSchemaDeserializer.LoadSchema(n); } },
38-
{ "statusCode", (a, n) =>
39-
{
40-
if (int.TryParse(n.GetScalarValue(), out var code))
41-
{
42-
a.StatusCode = (HttpStatusCode)code;
43-
}
44-
}
45-
},
38+
{ "statusCode", (a, n) => { a.StatusCode = LoadStatusCode(n); } },
4639
};
4740

41+
/// <summary>
42+
/// Reads the status code, reporting values outside the RFC 9110 range (100-599) as errors.
43+
/// </summary>
44+
/// <remarks>An out-of-range value is still kept so the document can be written back unchanged.</remarks>
45+
private static int LoadStatusCode(ParseNode node)
46+
{
47+
var code = node.GetIntegerValue();
48+
if (code < 100 || code > 599)
49+
{
50+
node.Context.Diagnostic.Errors.Add(new AsyncApiError(
51+
node.Context.GetLocation(),
52+
$"statusCode {code} is not a valid HTTP status code. It must be between 100 and 599."));
53+
}
54+
55+
return code;
56+
}
57+
4858
public override void SerializeV2(IAsyncApiWriter writer)
4959
{
5060
if (writer is null)
@@ -71,7 +81,7 @@ public override void SerializeV3(IAsyncApiWriter writer)
7181

7282
if (this.StatusCode.HasValue)
7383
{
74-
writer.WriteRequiredProperty(AsyncApiConstants.StatusCode, (int)this.StatusCode.Value);
84+
writer.WriteRequiredProperty(AsyncApiConstants.StatusCode, this.StatusCode.Value);
7585
}
7686

7787
writer.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion ?? V3BindingVersion);

‎test/ByteBard.AsyncAPI.Tests/Bindings/Http/HttpBindings_Should.cs‎

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
namespace ByteBard.AsyncAPI.Tests.Bindings.Http
22
{
33
using System.Linq;
4-
using System.Net;
54
using FluentAssertions;
65
using ByteBard.AsyncAPI.Bindings;
76
using ByteBard.AsyncAPI.Bindings.Http;
@@ -156,7 +155,7 @@ public void V3_HttpMessageBinding_IncludesStatusCode()
156155
{
157156
Description = "response headers",
158157
},
159-
StatusCode = HttpStatusCode.OK,
158+
StatusCode = 200,
160159
});
161160

162161
// Act
@@ -179,7 +178,7 @@ public void V2_HttpMessageBinding_OmitsStatusCode()
179178
{
180179
Description = "response headers",
181180
},
182-
StatusCode = HttpStatusCode.OK,
181+
StatusCode = 200,
183182
});
184183

185184
// Act
@@ -189,5 +188,79 @@ public void V2_HttpMessageBinding_OmitsStatusCode()
189188
actual.Should().NotContain("statusCode");
190189
actual.Should().Contain("bindingVersion: 0.2.0");
191190
}
191+
192+
[Test]
193+
public void V3_HttpMessageBinding_ReadsStatusCode()
194+
{
195+
var yaml =
196+
"""
197+
bindings:
198+
http:
199+
statusCode: 404
200+
""";
201+
202+
var settings = new AsyncApiReaderSettings { Bindings = BindingsCollection.Http };
203+
var message = new AsyncApiStringReader(settings).ReadFragment<AsyncApiMessage>(yaml, AsyncApiVersion.AsyncApi3_0, out var diagnostics);
204+
205+
diagnostics.Errors.Should().BeEmpty();
206+
((HttpMessageBinding)message.Bindings["http"]).StatusCode.Should().Be(404);
207+
}
208+
209+
[TestCase("not-found")]
210+
[TestCase("200.5")]
211+
public void V3_HttpMessageBinding_WithInvalidStatusCode_ReportsError(string statusCode)
212+
{
213+
var yaml =
214+
"bindings:\n" +
215+
" http:\n" +
216+
" statusCode: " + statusCode + "\n";
217+
218+
var settings = new AsyncApiReaderSettings { Bindings = BindingsCollection.Http };
219+
new AsyncApiStringReader(settings).ReadFragment<AsyncApiMessage>(yaml, AsyncApiVersion.AsyncApi3_0, out var diagnostics);
220+
221+
diagnostics.Errors.Should().ContainSingle().Which.Message.Should().Contain("could not parse to integer");
222+
}
223+
224+
[TestCase(100)]
225+
[TestCase(599)]
226+
[TestCase(425)] // Too Early (RFC 8470), not a System.Net.HttpStatusCode member
227+
[TestCase(499)] // nginx client closed request
228+
[TestCase(520)] // Cloudflare unknown error
229+
public void V3_HttpMessageBinding_WithValidStatusCode_ReportsNoErrorAndRoundTrips(int statusCode)
230+
{
231+
var yaml =
232+
"bindings:\n" +
233+
" http:\n" +
234+
" statusCode: " + statusCode + "\n";
235+
236+
var settings = new AsyncApiReaderSettings { Bindings = BindingsCollection.Http };
237+
var message = new AsyncApiStringReader(settings).ReadFragment<AsyncApiMessage>(yaml, AsyncApiVersion.AsyncApi3_0, out var diagnostics);
238+
239+
diagnostics.Errors.Should().BeEmpty();
240+
((HttpMessageBinding)message.Bindings["http"]).StatusCode.Should().Be(statusCode);
241+
message.SerializeAsYaml(AsyncApiVersion.AsyncApi3_0).Should().Contain("statusCode: " + statusCode);
242+
}
243+
244+
[TestCase(0)]
245+
[TestCase(42)]
246+
[TestCase(99)]
247+
[TestCase(600)]
248+
[TestCase(-200)]
249+
public void V3_HttpMessageBinding_WithOutOfRangeStatusCode_ReportsErrorAndKeepsValue(int statusCode)
250+
{
251+
var yaml =
252+
"bindings:\n" +
253+
" http:\n" +
254+
" statusCode: " + statusCode + "\n";
255+
256+
var settings = new AsyncApiReaderSettings { Bindings = BindingsCollection.Http };
257+
var message = new AsyncApiStringReader(settings).ReadFragment<AsyncApiMessage>(yaml, AsyncApiVersion.AsyncApi3_0, out var diagnostics);
258+
259+
var error = diagnostics.Errors.Should().ContainSingle().Subject;
260+
error.Message.Should().Contain("must be between 100 and 599");
261+
error.Pointer.Should().EndWith("statusCode");
262+
((HttpMessageBinding)message.Bindings["http"]).StatusCode.Should().Be(statusCode);
263+
message.SerializeAsYaml(AsyncApiVersion.AsyncApi3_0).Should().Contain("statusCode: " + statusCode);
264+
}
192265
}
193266
}

0 commit comments

Comments
 (0)