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
61 changes: 61 additions & 0 deletions GitLfsCache.Tests/Integration/ProxyFlowTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
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]
Expand Down Expand Up @@ -332,6 +334,65 @@
Assert.IsTrue(fixture.Store.Exists("github", oid), "A pushed object should be cached for the next fetch.");
}

/// <summary>
/// Stands in for Kestrel's body size limit, which <see cref="Microsoft.AspNetCore.TestHost.TestServer"/> does not enforce.
/// </summary>
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<IHttpMaxRequestBodySizeFeature>(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;
});

Check warning on line 365 in GitLfsCache.Tests/Integration/ProxyFlowTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Consider using the overload that accepts a CancellationToken and pass 'TestContext.CancellationToken'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitLfsCache&issues=AaEaNq1qItWydQfGXM7i&open=AaEaNq1qItWydQfGXM7i&pullRequest=101

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]);

Check warning on line 369 in GitLfsCache.Tests/Integration/ProxyFlowTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.AreSequenceEqual' instead of 'CollectionAssert.AreEqual'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitLfsCache&issues=AaEaNq1qItWydQfGXM7h&open=AaEaNq1qItWydQfGXM7h&pullRequest=101
}

[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();

Check warning on line 379 in GitLfsCache.Tests/Integration/ProxyFlowTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Consider using the overload that accepts a CancellationToken and pass 'TestContext.CancellationToken'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitLfsCache&issues=AaEaNq1qItWydQfGXM7j&open=AaEaNq1qItWydQfGXM7j&pullRequest=101

HttpContext context = await fixture.Server.SendAsync(context =>
{
context.Features.Set<IHttpMaxRequestBodySizeFeature>(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;
});

Check warning on line 390 in GitLfsCache.Tests/Integration/ProxyFlowTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Consider using the overload that accepts a CancellationToken and pass 'TestContext.CancellationToken'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitLfsCache&issues=AaEaNq1qItWydQfGXM7k&open=AaEaNq1qItWydQfGXM7k&pullRequest=101

Assert.AreEqual(StatusCodes.Status200OK, context.Response.StatusCode);
Assert.AreEqual(30_000_000, limit.MaxRequestBodySize);
}

/// <summary>
/// Puts a file where the upstream's staging directory belongs, so no staging file can be opened.
/// </summary>
Expand Down
23 changes: 23 additions & 0 deletions GitLfsCache/Endpoints/ObjectRouteHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
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;

Expand All @@ -34,7 +35,7 @@
/// <param name="metrics">Cache counters.</param>
/// <param name="options">The configured options.</param>
/// <param name="logger">Logger.</param>
internal sealed class ObjectRouteHandler(

Check warning on line 38 in GitLfsCache/Endpoints/ObjectRouteHandler.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Constructor has 9 parameters, which is greater than the 7 authorized.

Check warning on line 38 in GitLfsCache/Endpoints/ObjectRouteHandler.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Constructor has 9 parameters, which is greater than the 7 authorized.

Check warning on line 38 in GitLfsCache/Endpoints/ObjectRouteHandler.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Constructor has 9 parameters, which is greater than the 7 authorized.

Check warning on line 38 in GitLfsCache/Endpoints/ObjectRouteHandler.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Constructor has 9 parameters, which is greater than the 7 authorized.
IUpstreamClient upstreamClient,
IHrefTokenCodec codec,
BatchRewriter rewriter,
Expand Down Expand Up @@ -206,6 +207,26 @@
}
}

/// <summary>
/// Removes the server's request body size limit for this request.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="context">The request context.</param>
private static void LiftRequestBodyLimit(HttpContext context)
{
IHttpMaxRequestBodySizeFeature? limit = context.Features.Get<IHttpMaxRequestBodySizeFeature>();

if (limit is { IsReadOnly: false })
{
limit.MaxRequestBodySize = null;
}
}

/// <summary>
/// Sends an upload upstream, keeping a copy on the way through.
/// </summary>
Expand All @@ -223,6 +244,8 @@
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.
Expand Down
Loading