Skip to content

Commit d2e013f

Browse files
st0o0claude
andcommitted
feat(h11): add Http11Profile, Options, ConnectionReuseEvaluator, ChunkExtensionParser, RequestValidator
Phase 1 of HTTP/1.1 Redesign. New types in Protocol/Syntax/Http11/. Old Protocol/Http11/ untouched — cutover in Phase 3. 29 new tests green, 262 total Http11 tests green. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8c7dfb8 commit d2e013f

13 files changed

Lines changed: 890 additions & 0 deletions
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
using System.Text;
2+
using TurboHTTP.Protocol.Syntax.Http11;
3+
using Xunit;
4+
5+
namespace TurboHTTP.Tests.Http11;
6+
7+
[Trait("RFC", "RFC9112")]
8+
public sealed class ChunkExtensionParserV2Spec
9+
{
10+
[Fact(Timeout = 5000)]
11+
public void EmptyExtensions_ShouldParse()
12+
{
13+
var result = ChunkExtensionParser.TryParse([]);
14+
15+
Assert.True(result);
16+
}
17+
18+
[Fact(Timeout = 5000)]
19+
public void NameOnlyExtension_ShouldParse()
20+
{
21+
var bytes = Encoding.UTF8.GetBytes("name");
22+
var result = ChunkExtensionParser.TryParse(bytes);
23+
24+
Assert.True(result);
25+
}
26+
27+
[Fact(Timeout = 5000)]
28+
public void NameValueExtension_ShouldParse()
29+
{
30+
var bytes = Encoding.UTF8.GetBytes("name=value");
31+
var result = ChunkExtensionParser.TryParse(bytes);
32+
33+
Assert.True(result);
34+
}
35+
36+
[Fact(Timeout = 5000)]
37+
public void QuotedStringValue_ShouldParse()
38+
{
39+
var bytes = Encoding.UTF8.GetBytes("name=\"quoted value\"");
40+
var result = ChunkExtensionParser.TryParse(bytes);
41+
42+
Assert.True(result);
43+
}
44+
45+
[Fact(Timeout = 5000)]
46+
public void MissingSemicolon_ShouldFail()
47+
{
48+
// Multiple extensions without semicolon separator should fail
49+
var bytes = Encoding.UTF8.GetBytes("name1 name2");
50+
var result = ChunkExtensionParser.TryParse(bytes);
51+
52+
Assert.False(result);
53+
}
54+
55+
[Fact(Timeout = 5000)]
56+
public void MultipleValidExtensions_ShouldParse()
57+
{
58+
var bytes = Encoding.UTF8.GetBytes("name1=value1;name2=value2");
59+
var result = ChunkExtensionParser.TryParse(bytes);
60+
61+
Assert.True(result);
62+
}
63+
64+
[Fact(Timeout = 5000)]
65+
public void ExtensionWithWhitespace_ShouldParse()
66+
{
67+
var bytes = Encoding.UTF8.GetBytes("name = value ");
68+
var result = ChunkExtensionParser.TryParse(bytes);
69+
70+
Assert.True(result);
71+
}
72+
73+
[Fact(Timeout = 5000)]
74+
public void EscapedCharacterInQuotedString_ShouldParse()
75+
{
76+
var bytes = Encoding.UTF8.GetBytes("name=\"value\\\"with\\\"quotes\"");
77+
var result = ChunkExtensionParser.TryParse(bytes);
78+
79+
Assert.True(result);
80+
}
81+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
using System.Net;
2+
using TurboHTTP.Protocol.Syntax.Http11;
3+
using Xunit;
4+
5+
namespace TurboHTTP.Tests.Http11;
6+
7+
[Trait("RFC", "RFC9112")]
8+
public sealed class ConnectionReuseEvaluatorV2Spec
9+
{
10+
[Fact(Timeout = 5000)]
11+
public void ProtocolError_ShouldClose()
12+
{
13+
var response = new HttpResponseMessage(HttpStatusCode.OK);
14+
var decision = ConnectionReuseEvaluator.Evaluate(response, HttpVersion.Version11, protocolErrorOccurred: true);
15+
16+
Assert.False(decision.CanReuse);
17+
Assert.Contains("Protocol error", decision.Reason);
18+
}
19+
20+
[Fact(Timeout = 5000)]
21+
public void BodyNotConsumed_ShouldClose()
22+
{
23+
var response = new HttpResponseMessage(HttpStatusCode.OK);
24+
var decision = ConnectionReuseEvaluator.Evaluate(response, HttpVersion.Version11, bodyFullyConsumed: false);
25+
26+
Assert.False(decision.CanReuse);
27+
Assert.Contains("not fully consumed", decision.Reason);
28+
}
29+
30+
[Fact(Timeout = 5000)]
31+
public void SwitchingProtocols_ShouldClose()
32+
{
33+
var response = new HttpResponseMessage((HttpStatusCode)101);
34+
var decision = ConnectionReuseEvaluator.Evaluate(response, HttpVersion.Version11);
35+
36+
Assert.False(decision.CanReuse);
37+
Assert.Contains("101 Switching Protocols", decision.Reason);
38+
}
39+
40+
[Fact(Timeout = 5000)]
41+
public void ConnectionClose_ShouldClose()
42+
{
43+
var response = new HttpResponseMessage(HttpStatusCode.OK);
44+
response.Headers.Connection.Add("close");
45+
var decision = ConnectionReuseEvaluator.Evaluate(response, HttpVersion.Version11);
46+
47+
Assert.False(decision.CanReuse);
48+
Assert.Contains("Connection: close", decision.Reason);
49+
}
50+
51+
[Fact(Timeout = 5000)]
52+
public void Http10WithoutKeepAlive_ShouldClose()
53+
{
54+
var response = new HttpResponseMessage(HttpStatusCode.OK);
55+
var decision = ConnectionReuseEvaluator.Evaluate(response, HttpVersion.Version10);
56+
57+
Assert.False(decision.CanReuse);
58+
Assert.Contains("HTTP/1.0", decision.Reason);
59+
}
60+
61+
[Fact(Timeout = 5000)]
62+
public void Http10WithKeepAlive_ShouldKeepAlive()
63+
{
64+
var response = new HttpResponseMessage(HttpStatusCode.OK);
65+
response.Headers.Connection.Add("Keep-Alive");
66+
var decision = ConnectionReuseEvaluator.Evaluate(response, HttpVersion.Version10);
67+
68+
Assert.True(decision.CanReuse);
69+
Assert.Contains("HTTP/1.0", decision.Reason);
70+
}
71+
72+
[Fact(Timeout = 5000)]
73+
public void Http11Default_ShouldKeepAlive()
74+
{
75+
var response = new HttpResponseMessage(HttpStatusCode.OK);
76+
var decision = ConnectionReuseEvaluator.Evaluate(response, HttpVersion.Version11);
77+
78+
Assert.True(decision.CanReuse);
79+
Assert.Contains("HTTP/1.1", decision.Reason);
80+
}
81+
82+
[Fact(Timeout = 5000)]
83+
public void Http11WithClose_ShouldClose()
84+
{
85+
var response = new HttpResponseMessage(HttpStatusCode.OK);
86+
response.Headers.Connection.Add("close");
87+
var decision = ConnectionReuseEvaluator.Evaluate(response, HttpVersion.Version11);
88+
89+
Assert.False(decision.CanReuse);
90+
Assert.Contains("Connection: close", decision.Reason);
91+
}
92+
93+
[Fact(Timeout = 5000)]
94+
public void Http20_ShouldAlwaysKeepAlive()
95+
{
96+
var response = new HttpResponseMessage(HttpStatusCode.OK);
97+
var decision = ConnectionReuseEvaluator.Evaluate(response, HttpVersion.Version20);
98+
99+
Assert.True(decision.CanReuse);
100+
Assert.Contains("HTTP/2", decision.Reason);
101+
}
102+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using System.Net;
2+
using TurboHTTP.Protocol.Syntax.Http11;
3+
4+
namespace TurboHTTP.Tests.Http11;
5+
6+
public sealed class Http11ProfileSpec
7+
{
8+
[Fact(Timeout = 5000)]
9+
public void SupportsChunked_should_be_true()
10+
{
11+
Assert.True(Http11Profile.SupportsChunked);
12+
}
13+
14+
[Fact(Timeout = 5000)]
15+
public void DefaultPersistent_should_be_true()
16+
{
17+
Assert.True(Http11Profile.DefaultPersistent);
18+
}
19+
20+
[Fact(Timeout = 5000)]
21+
public void RequiresHost_should_be_true()
22+
{
23+
Assert.True(Http11Profile.RequiresHost);
24+
}
25+
26+
[Fact(Timeout = 5000)]
27+
public void Version_should_be_11()
28+
{
29+
Assert.Equal(HttpVersion.Version11, Http11Profile.Version);
30+
}
31+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
using System.Net.Http.Headers;
2+
using TurboHTTP.Protocol.Syntax.Http11.Client;
3+
using Xunit;
4+
5+
namespace TurboHTTP.Tests.Http11;
6+
7+
[Trait("RFC", "RFC9112")]
8+
public sealed class RequestValidatorV2Spec
9+
{
10+
[Fact(Timeout = 5000)]
11+
public void ValidGet_ShouldPass()
12+
{
13+
var request = new HttpRequestMessage(HttpMethod.Get, "http://example.com");
14+
request.Headers.Add("User-Agent", "Test");
15+
16+
RequestValidator.Validate(request);
17+
}
18+
19+
[Fact(Timeout = 5000)]
20+
public void ValidPostWithBody_ShouldPass()
21+
{
22+
var request = new HttpRequestMessage(HttpMethod.Post, "http://example.com")
23+
{
24+
Content = new StringContent("test body")
25+
};
26+
request.Headers.Add("User-Agent", "Test");
27+
28+
RequestValidator.Validate(request);
29+
}
30+
31+
[Fact(Timeout = 5000)]
32+
public void LowercaseMethod_ShouldThrow()
33+
{
34+
var request = new HttpRequestMessage(new HttpMethod("get"), "http://example.com");
35+
36+
var exception = Assert.Throws<ArgumentException>(() => RequestValidator.Validate(request));
37+
Assert.Contains("uppercase", exception.Message);
38+
}
39+
40+
[Fact(Timeout = 5000)]
41+
public void ValidRangeHeader_ShouldPass()
42+
{
43+
var request = new HttpRequestMessage(HttpMethod.Get, "http://example.com");
44+
request.Headers.Add("Range", "bytes=0-99");
45+
46+
RequestValidator.Validate(request);
47+
}
48+
49+
[Fact(Timeout = 5000)]
50+
public void ValidMultipleRanges_ShouldPass()
51+
{
52+
var request = new HttpRequestMessage(HttpMethod.Get, "http://example.com");
53+
request.Headers.Add("Range", "bytes=0-100,200-300");
54+
55+
RequestValidator.Validate(request);
56+
}
57+
58+
[Fact(Timeout = 5000)]
59+
public void ValidSuffixRange_ShouldPass()
60+
{
61+
var request = new HttpRequestMessage(HttpMethod.Get, "http://example.com");
62+
request.Headers.Add("Range", "bytes=-100");
63+
64+
RequestValidator.Validate(request);
65+
}
66+
67+
[Fact(Timeout = 5000)]
68+
public void ValidPostWithContentHeaders_ShouldPass()
69+
{
70+
var request = new HttpRequestMessage(HttpMethod.Post, "http://example.com")
71+
{
72+
Content = new StringContent("test body")
73+
};
74+
request.Content.Headers.ContentType = new MediaTypeHeaderValue("text/plain");
75+
76+
RequestValidator.Validate(request);
77+
}
78+
79+
[Fact(Timeout = 5000)]
80+
public void ValidMixedCaseMethod_ShouldThrow()
81+
{
82+
var request = new HttpRequestMessage(new HttpMethod("Get"), "http://example.com");
83+
84+
var exception = Assert.Throws<ArgumentException>(() => RequestValidator.Validate(request));
85+
Assert.Contains("uppercase", exception.Message);
86+
}
87+
}

0 commit comments

Comments
 (0)