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.