Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
437 changes: 0 additions & 437 deletions .maggus/features/feature_022_completed.md

This file was deleted.

431 changes: 0 additions & 431 deletions .maggus/features/feature_026.md

This file was deleted.

412 changes: 0 additions & 412 deletions .maggus/features/feature_027.md

This file was deleted.

69 changes: 1 addition & 68 deletions .maggus/features/feature_028.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,28 +23,6 @@ These optimizations target 10-20% latency improvement and 30% memory reduction f

## Tasks

### TASK-028-001: Implement Streaming Http10Encoder
**Token Estimate:** ~35k | **Predecessors:** none | **Successors:** TASK-028-004 | **Parallel:** yes (with 002, 003)

**Acceptance Criteria:**
- [ ] Refactor `Http10Encoder.Encode()` to stream headers into `IBufferWriter<byte>` instead of buffering
- [ ] Encode request-line, then headers one-by-one without intermediate buffering
- [ ] Validate no performance regression (benchmark < 50µs for typical request)
- [ ] All existing tests pass

---

### TASK-028-002: Implement Streaming Http11Encoder
**Token Estimate:** ~40k | **Predecessors:** none | **Successors:** TASK-028-004 | **Parallel:** yes (with 001, 003)

**Acceptance Criteria:**
- [ ] Refactor `Http11Encoder.Encode()` to stream headers + body
- [ ] Handle chunked Transfer-Encoding streaming (if body is IAsyncEnumerable)
- [ ] Validate no performance regression (benchmark < 100µs for 50-header request)
- [ ] All existing tests pass

---

### TASK-028-003: Add SIMD CRLF Detection Utility
**Token Estimate:** ~45k | **Predecessors:** none | **Successors:** TASK-028-005 | **Parallel:** yes (with 001, 002)

Expand All @@ -57,17 +35,6 @@ These optimizations target 10-20% latency improvement and 30% memory reduction f

---

### TASK-028-004: Integrate Streaming Encoders into Pipeline
**Token Estimate:** ~25k | **Predecessors:** TASK-028-001, TASK-028-002 | **Successors:** TASK-028-006 | **Parallel:** no

**Acceptance Criteria:**
- [ ] Update `Http10EncoderStage` and `Http11EncoderStage` to use streaming encoders
- [ ] Verify graph construction and backpressure work correctly
- [ ] End-to-end test: send request, verify bytes match old encoder output
- [ ] All stage tests pass

---

### TASK-028-005: Integrate SIMD CRLF Detection into Http11DecoderPipeline
**Token Estimate:** ~30k | **Predecessors:** TASK-028-003 | **Successors:** TASK-028-006 | **Parallel:** no

Expand All @@ -79,58 +46,24 @@ These optimizations target 10-20% latency improvement and 30% memory reduction f

---

### TASK-028-006: Performance Benchmarks (Before/After)
**Token Estimate:** ~40k | **Predecessors:** TASK-028-004, TASK-028-005 | **Successors:** none | **Parallel:** no

**Acceptance Criteria:**
- [ ] Create benchmarks in `src/TurboHttp.Benchmarks/Performance/`:
- [ ] `Http10EncoderStreamingBenchmark` (measure allocation + throughput)
- [ ] `Http11EncoderStreamingBenchmark` (50-header request)
- [ ] `Http11DecoderCrlfBenchmark` (typical response with 20 headers)
- [ ] `SimdCrlfBenchmark` (direct SIMD vs. string.IndexOf)
- [ ] Compare baseline (old) vs. optimized (new)
- [ ] Target: ≥10% latency improvement, ≥30% allocation reduction
- [ ] Report in `docs/PERFORMANCE_RESULTS.md`
- [ ] Run dry: `dotnet run --configuration Release --project src/TurboHttp.Benchmarks -- --filter "*Streaming*"` — results stable

---

## Task Dependency Graph
```
TASK-028-001 ──→ TASK-028-004 ──→ TASK-028-006
TASK-028-002 ──→↗
TASK-028-003 ──→ TASK-028-005 ──→↗
```

### Summary Table

| Task | Estimate | Predecessors | Parallel | Model |
|------|----------|--------------|----------|-------|
| TASK-028-001 | ~35k | none | yes (w/ 002, 003) | — |
| TASK-028-002 | ~40k | none | yes (w/ 001, 003) | — |
| TASK-028-003 | ~45k | none | yes (w/ 001, 002) | opus |
| TASK-028-004 | ~25k | 001, 002 | no | — |
| TASK-028-005 | ~30k | 003 | no | — |
| TASK-028-006 | ~40k | 004, 005 | no | — |

**Total:** ~215k tokens (~5 days solo)

## Functional Requirements

1. **FR-1:** Streaming encoders SHALL encode headers without intermediate buffering
2. **FR-2:** SIMD CRLF detection SHALL be >20% faster than string.IndexOf
3. **FR-3:** All encoder/decoder output SHALL match previous implementation byte-for-byte
4. **FR-4:** Benchmarks SHALL show measurable improvements (latency, allocations)
2. **FR-1:** SIMD CRLF detection SHALL be >20% faster than string.IndexOf

## Non-Goals
- No changes to public API
- No HTTP/2 optimizations (separate phase)
- No changes to header compression (HPACK/QPACK separate)

## Success Metrics
1. Streaming encoders reduce allocations by ≥30%
2. SIMD CRLF detection improves latency by ≥20%
3. P99 latency improved by ≥10% overall
4. All benchmarks pass with stable results
5. Zero regressions in existing tests

569 changes: 0 additions & 569 deletions .maggus/features/feature_035.md

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System.Net;
using TurboHttp.IntegrationTests.Shared;

namespace TurboHttp.IntegrationTests.H11;

[Collection("H11")]
public sealed class RedirectSecurityIntegrationTests
{
private readonly ServerFixture _server;
private readonly ActorSystemFixture _systemFixture;

public RedirectSecurityIntegrationTests(ServerFixture server, ActorSystemFixture systemFixture)
{
_server = server;
_systemFixture = systemFixture;
}

private ClientHelper CreateRedirectClient()
{
return ClientHelper.CreateClient(
_server.HttpPort,
new Version(1, 1),
configure: builder => builder.WithRedirect(),
system: _systemFixture.System);
}

// ── Loop Detection ──────────────────────────────────────────────────────

[Fact(DisplayName = "RFC9110-15.4-RSI-003: Self-redirect loop returns final redirect response")]
public async Task Self_Redirect_Loop_Returns_Final_Response()
{
// RedirectBidiStage catches loop/max-exceeded and forwards the last response.
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
await using var helper = CreateRedirectClient();

var request = new HttpRequestMessage(HttpMethod.Get, "/redirect/loop");
var response = await helper.Client.SendAsync(request, cts.Token);

Assert.Equal(HttpStatusCode.Found, response.StatusCode);
}

// ── Chain Depth ─────────────────────────────────────────────────────────

[Fact(DisplayName = "RFC9110-15.4-RSI-004: Redirect chain of 4 hops succeeds")]
public async Task Redirect_Chain_4_Hops_Succeeds()
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
await using var helper = CreateRedirectClient();

var request = new HttpRequestMessage(HttpMethod.Get, "/redirect/chain/4");
var response = await helper.Client.SendAsync(request, cts.Token);

Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync(cts.Token);
Assert.Equal("Hello World", body);
}

[Fact(DisplayName = "RFC9110-15.4-RSI-005: Redirect chain of 11 hops rejected (exceeds max 10)")]
public async Task Redirect_Chain_11_Hops_Rejected()
{
// Default MaxRedirects = 5. A chain of 6 exceeds this.
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
await using var helper = CreateRedirectClient();

var request = new HttpRequestMessage(HttpMethod.Get, "/redirect/chain/11");
var response = await helper.Client.SendAsync(request, cts.Token);

// The stage forwards the last redirect response when max depth exceeded
Assert.Equal(HttpStatusCode.Found, response.StatusCode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
using System.Net;
using TurboHttp.IntegrationTests.Shared;

namespace TurboHttp.IntegrationTests.H2;

/// <summary>
/// Integration tests for MAX_CONCURRENT_STREAMS enforcement over a real HTTP/2 connection.
/// Uses the shared Kestrel server with H2 h2c endpoint. Kestrel advertises its default
/// MAX_CONCURRENT_STREAMS (100) via SETTINGS. The client must respect this limit.
/// These tests verify that multiple concurrent requests complete successfully through
/// the limiter stage when sent to a real server.
/// </summary>
/// <remarks>
/// RFC 9113 §5.1.2: An endpoint MUST NOT exceed the limit set by its peer.
/// The client reads MAX_CONCURRENT_STREAMS from the server's SETTINGS frame and enforces it.
/// </remarks>
[Collection("H2")]
public sealed class MaxConcurrentStreamsIntegrationTests : IAsyncLifetime
{
private readonly ServerFixture _server;
private readonly ActorSystemFixture _systemFixture;
private ClientHelper? _helper;

public MaxConcurrentStreamsIntegrationTests(ServerFixture server, ActorSystemFixture systemFixture)
{
_server = server;
_systemFixture = systemFixture;
}

public ValueTask InitializeAsync()
{
_helper = ClientHelper.CreateClient(_server.H2Port, new Version(2, 0), system: _systemFixture.System);
return ValueTask.CompletedTask;
}

public async ValueTask DisposeAsync()
{
if (_helper is not null)
{
await _helper.DisposeAsync();
}
}

[Fact(Timeout = 30000, DisplayName = "RFC9113-5.1.2-INT-001: Five concurrent requests complete over H2 with limiter active")]
public async Task Should_CompleteAllRequests_When_FiveConcurrentRequestsSentOverH2()
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20));

// Send 5 concurrent requests — the limiter stage enforces MAX_CONCURRENT_STREAMS
// from the server's SETTINGS. Kestrel defaults to 100, so all 5 should proceed
// immediately. This verifies the limiter stage doesn't block legitimate traffic.
var tasks = Enumerable.Range(0, 5)
.Select(i =>
{
var request = new HttpRequestMessage(HttpMethod.Get, $"/delay/200");
return _helper!.Client.SendAsync(request, cts.Token);
})
.ToArray();

var responses = await Task.WhenAll(tasks);

Assert.Equal(5, responses.Length);
foreach (var response in responses)
{
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
}

[Fact(Timeout = 30000, DisplayName = "RFC9113-5.1.2-INT-002: Sequential requests succeed through limiter on single H2 connection")]
public async Task Should_CompleteSequentially_When_RequestsSentOneByOne()
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20));

// Send 5 requests sequentially — verifies the limiter correctly tracks
// stream opens and closes across the full pipeline lifecycle
for (var i = 0; i < 5; i++)
{
var request = new HttpRequestMessage(HttpMethod.Get, "/hello");
var response = await _helper!.Client.SendAsync(request, cts.Token);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync(cts.Token);
Assert.Equal("Hello World", body);
}
}

[Fact(Timeout = 30000, DisplayName = "RFC9113-5.1.2-INT-003: Ten concurrent requests all complete successfully")]
public async Task Should_CompleteAllTen_When_TenConcurrentRequestsSent()
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20));

// Send 10 concurrent requests — even if queued, all should complete
var tasks = Enumerable.Range(0, 10)
.Select(i =>
{
var request = new HttpRequestMessage(HttpMethod.Get, "/ping");
return _helper!.Client.SendAsync(request, cts.Token);
})
.ToArray();

var responses = await Task.WhenAll(tasks);

Assert.Equal(10, responses.Length);
Assert.All(responses, r => Assert.Equal(HttpStatusCode.OK, r.StatusCode));
}

[Fact(Timeout = 30000, DisplayName = "RFC9113-5.1.2-INT-004: Concurrent delayed requests complete as streams free up")]
public async Task Should_CompleteAsStreamsFreeUp_When_ConcurrentDelayedRequestsSent()
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20));

// Send 5 requests with 300ms server-side delay each
// The limiter should queue any that exceed the server's advertised limit
// and send them as active streams complete
var tasks = Enumerable.Range(0, 5)
.Select(i =>
{
var request = new HttpRequestMessage(HttpMethod.Get, "/delay/300");
return _helper!.Client.SendAsync(request, cts.Token);
})
.ToArray();

var responses = await Task.WhenAll(tasks);

Assert.Equal(5, responses.Length);
foreach (var response in responses)
{
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync(cts.Token);
Assert.Equal("delayed", body);
}
}

[Fact(Timeout = 30000, DisplayName = "RFC9113-5.1.2-INT-005: Mixed GET requests with varying response sizes complete concurrently")]
public async Task Should_CompleteAllMixed_When_ConcurrentMixedRequestsSent()
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20));

// Mix of fast and slow endpoints to exercise limiter under varying stream lifetimes
var endpoints = new[] { "/hello", "/ping", "/delay/100", "/hello", "/ping" };
var tasks = endpoints
.Select(path =>
{
var request = new HttpRequestMessage(HttpMethod.Get, path);
return _helper!.Client.SendAsync(request, cts.Token);
})
.ToArray();

var responses = await Task.WhenAll(tasks);

Assert.Equal(5, responses.Length);
Assert.All(responses, r => Assert.Equal(HttpStatusCode.OK, r.StatusCode));
}
}
9 changes: 9 additions & 0 deletions src/TurboHttp.IntegrationTests/Shared/Routes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,15 @@ internal static void RegisterRedirectRoutes(WebApplication app)
return Results.Empty;
});

// GET /redirect/cross-scheme/{code} → HTTPS→HTTP downgrade with given status code
app.MapGet("/redirect/cross-scheme/{code:int}", (HttpContext ctx, int code) =>
{
var port = ctx.Connection.LocalPort;
ctx.Response.StatusCode = code;
ctx.Response.Headers.Location = $"http://127.0.0.1:{port}/hello";
return Results.Empty;
});

// GET /redirect/cross-origin → 302 to http://127.0.0.1:{port}/headers/echo
// Used to test cross-origin Authorization header stripping
// (client connects via localhost, redirect goes to 127.0.0.1 = different origin)
Expand Down
Loading
Loading