diff --git a/GitLfsCache.Tests/Integration/ProxyFlowTests.cs b/GitLfsCache.Tests/Integration/ProxyFlowTests.cs
index 76c30d8..971466d 100644
--- a/GitLfsCache.Tests/Integration/ProxyFlowTests.cs
+++ b/GitLfsCache.Tests/Integration/ProxyFlowTests.cs
@@ -9,6 +9,8 @@ namespace ktsu.GitLfsCache.Tests.Integration;
using System.Text;
using System.Text.Json.Nodes;
using ktsu.GitLfsCache.Observability;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
@@ -332,6 +334,65 @@ public async Task Upload_IsRelayedUpstreamAndStoredOnTheWayThrough()
Assert.IsTrue(fixture.Store.Exists("github", oid), "A pushed object should be cached for the next fetch.");
}
+ ///
+ /// Stands in for Kestrel's body size limit, which does not enforce.
+ ///
+ private sealed class BodySizeLimit : IHttpMaxRequestBodySizeFeature
+ {
+ public bool IsReadOnly => false;
+
+ public long? MaxRequestBodySize { get; set; } = 30_000_000;
+ }
+
+ [TestMethod]
+ public async Task Upload_LiftsTheServersRequestBodyLimit()
+ {
+ await using ProxyFixture fixture = await ProxyFixture.StartAsync();
+ (byte[] content, string oid) = Object("pushed past the server's default body limit");
+ BodySizeLimit limit = new();
+
+ JsonNode batch = await PostBatchAsync(fixture, "upload", oid, content.Length);
+ Uri href = new(HrefOf(batch, "upload"));
+
+ HttpContext context = await fixture.Server.SendAsync(context =>
+ {
+ context.Features.Set(limit);
+ context.Request.Method = HttpMethods.Put;
+ context.Request.Path = href.AbsolutePath;
+ context.Request.QueryString = new QueryString(href.Query);
+ context.Request.Body = new MemoryStream(content);
+ context.Request.ContentLength = content.Length;
+ });
+
+ Assert.AreEqual(StatusCodes.Status200OK, context.Response.StatusCode);
+ Assert.IsNull(limit.MaxRequestBodySize, "An LFS object routinely exceeds Kestrel's 30,000,000-byte default.");
+ CollectionAssert.AreEqual(content, fixture.Upstream.Uploaded[oid]);
+ }
+
+ [TestMethod]
+ public async Task Batch_KeepsTheServersRequestBodyLimit()
+ {
+ await using ProxyFixture fixture = await ProxyFixture.StartAsync();
+ (byte[] content, string oid) = Object("only the transfer route is unbounded");
+ BodySizeLimit limit = new();
+ using StringContent request = BatchRequest("upload", oid, content.Length);
+ byte[] body = await request.ReadAsByteArrayAsync();
+
+ HttpContext context = await fixture.Server.SendAsync(context =>
+ {
+ context.Features.Set(limit);
+ context.Request.Method = HttpMethods.Post;
+ context.Request.Path = $"{LfsPath}/objects/batch";
+ context.Request.Headers.Authorization = "Basic dXNlcjp0b2tlbg==";
+ context.Request.ContentType = "application/vnd.git-lfs+json";
+ context.Request.Body = new MemoryStream(body);
+ context.Request.ContentLength = body.Length;
+ });
+
+ Assert.AreEqual(StatusCodes.Status200OK, context.Response.StatusCode);
+ Assert.AreEqual(30_000_000, limit.MaxRequestBodySize);
+ }
+
///
/// Puts a file where the upstream's staging directory belongs, so no staging file can be opened.
///
diff --git a/GitLfsCache/Endpoints/ObjectRouteHandler.cs b/GitLfsCache/Endpoints/ObjectRouteHandler.cs
index 873d389..c9fe8c1 100644
--- a/GitLfsCache/Endpoints/ObjectRouteHandler.cs
+++ b/GitLfsCache/Endpoints/ObjectRouteHandler.cs
@@ -13,6 +13,7 @@ namespace ktsu.GitLfsCache.Endpoints;
using ktsu.GitLfsCache.Tokens;
using ktsu.GitLfsCache.Upstreams;
using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -206,6 +207,26 @@ await ServeFromStoreAsync(context, nowCached, nowLength, cancellationToken)
}
}
+ ///
+ /// Removes the server's request body size limit for this request.
+ ///
+ ///
+ /// LFS objects are routinely larger than Kestrel's default 30,000,000-byte limit, and a body the
+ /// server refuses part way through surfaces as a 500 from the upstream relay rather than a 413.
+ /// The limit is lifted here, in the library, so every host is covered, and only for an upload
+ /// whose token has already been checked, so batch and lock bodies keep the server's bound.
+ ///
+ /// The request context.
+ private static void LiftRequestBodyLimit(HttpContext context)
+ {
+ IHttpMaxRequestBodySizeFeature? limit = context.Features.Get();
+
+ if (limit is { IsReadOnly: false })
+ {
+ limit.MaxRequestBodySize = null;
+ }
+ }
+
///
/// Sends an upload upstream, keeping a copy on the way through.
///
@@ -223,6 +244,8 @@ public async Task UploadAsync(HttpContext context, LfsRoute route, CancellationT
return;
}
+ LiftRequestBodyLimit(context);
+
// Without a staging file the upload is still relayed, through a tee into nothing so the relayed
// byte count is kept. Failing the push because the cache cannot take a copy would make the
// cache the reason a push failed.