fix(phase-16): forward X-Forwarded-For from EventProxyController so the API rate limiter partitions per client IP
Proxy chains any inbound XFF with the connection IP before relaying upstream; UseForwardedHeaders resolves it to the limiter's partition key. Documents the EventRepository first-play counter race (unique index is the backstop).
This commit is contained in:
@@ -102,6 +102,12 @@ public class EventRepository
|
||||
// Bump the matching bucket column on the track's counter row, creating the row on first play. The
|
||||
// row is added to the change tracker but not saved here — the caller's SaveChanges/commit persists
|
||||
// it inside the same transaction as the event append.
|
||||
//
|
||||
// Race note: two concurrent first-plays of the same track can both reach this method, find no
|
||||
// counter row, and both Add a new PlayCounter. The second SaveChanges will hit the unique index on
|
||||
// (track_id) and throw, causing the outer transaction to roll back and the event to be dropped —
|
||||
// no crash, no counter corruption. At the expected play volume this is an acceptable loss; the
|
||||
// unique index is the integrity backstop.
|
||||
private async Task BumpCounterAsync(long trackId, PlayBucket bucket, CancellationToken ct)
|
||||
{
|
||||
var counter = await _context.PlayCounters.FirstOrDefaultAsync(c => c.TrackId == trackId, ct);
|
||||
|
||||
@@ -47,12 +47,29 @@ public class EventProxyController : ControllerBase
|
||||
body = await reader.ReadToEndAsync(ct);
|
||||
}
|
||||
|
||||
using var content = new StringContent(body, Encoding.UTF8, "application/json");
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, upstreamPath)
|
||||
{
|
||||
Content = new StringContent(body, Encoding.UTF8, "application/json")
|
||||
};
|
||||
|
||||
// Forward the real client IP so DeepDrftAPI's per-IP rate limiter (Program.cs "events" policy)
|
||||
// partitions on individual listeners rather than the proxy host. Standard XFF chaining: relay
|
||||
// any inbound X-Forwarded-For from an upstream proxy (nginx), then append the connection IP
|
||||
// of the current hop (the browser → public host connection). DeepDrftAPI calls
|
||||
// UseForwardedHeaders() in production, which resolves the leftmost untrusted value in the
|
||||
// chain into Connection.RemoteIpAddress — which the rate limiter then keys on.
|
||||
var clientIp = HttpContext.Connection.RemoteIpAddress?.ToString();
|
||||
if (clientIp is not null)
|
||||
{
|
||||
var existing = Request.Headers["X-Forwarded-For"].ToString();
|
||||
var xff = string.IsNullOrEmpty(existing) ? clientIp : $"{existing}, {clientIp}";
|
||||
request.Headers.TryAddWithoutValidation("X-Forwarded-For", xff);
|
||||
}
|
||||
|
||||
HttpResponseMessage upstream;
|
||||
try
|
||||
{
|
||||
upstream = await _upstream.PostAsync(upstreamPath, content, ct);
|
||||
upstream = await _upstream.SendAsync(request, ct);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user