diff --git a/server/PlanShare/Program.cs b/server/PlanShare/Program.cs index d70657b..f77e5da 100644 --- a/server/PlanShare/Program.cs +++ b/server/PlanShare/Program.cs @@ -92,7 +92,10 @@ created_at TEXT NOT NULL builder.Services.AddSingleton(uploadBudget); builder.Services.AddHostedService(); -// Request size limit (10 MB) +// Request size limit (10 MB). nginx on the server sets the same limit for /api/ +// (client_max_body_size 10m); without it, nginx's 1 MB default refuses larger shares +// first. The web client's too-large message (PlanShareService) also names 10 MB. +// Change all three together. builder.WebHost.ConfigureKestrel(o => o.Limits.MaxRequestBodySize = 10 * 1024 * 1024); var app = builder.Build(); diff --git a/src/PlanViewer.Web/Services/PlanShareService.cs b/src/PlanViewer.Web/Services/PlanShareService.cs index 9e5012d..e301e33 100644 --- a/src/PlanViewer.Web/Services/PlanShareService.cs +++ b/src/PlanViewer.Web/Services/PlanShareService.cs @@ -82,7 +82,9 @@ public async Task ShareAsync(AnalysisResult result, string text /* The server explains a refusal (store full, daily limit, bad request) as {"error": "..."} in a message meant for the user, so show that text. A reply without it, such as an HTML error - page from the proxy in front of the server, gets the generic message with the status code. */ + page from the proxy in front of the server, gets the generic message with the status code. + A 413 is the exception: nginx and Kestrel both refuse an upload over 10 MB without JSON (see + the limit in server/PlanShare/Program.cs), and "server returned 413" explains nothing. */ private static async Task ShareFailureMessageAsync(HttpResponseMessage response) { try @@ -101,6 +103,8 @@ private static async Task ShareFailureMessageAsync(HttpResponseMessage r { // Not JSON, so there is no server text to show } + if (response.StatusCode == HttpStatusCode.RequestEntityTooLarge) + return "This plan is too large to share. The limit is 10 MB."; return $"Share failed: server returned {(int)response.StatusCode}"; } diff --git a/tests/PlanViewer.Core.Tests/PlanShareServiceTests.cs b/tests/PlanViewer.Core.Tests/PlanShareServiceTests.cs index 267d7df..86da650 100644 --- a/tests/PlanViewer.Core.Tests/PlanShareServiceTests.cs +++ b/tests/PlanViewer.Core.Tests/PlanShareServiceTests.cs @@ -61,6 +61,8 @@ public async Task Share_ReturnsTheIdAndDeleteToken() [InlineData(HttpStatusCode.InsufficientStorage, "Plan sharing is full right now. Please try again later.")] [InlineData(HttpStatusCode.TooManyRequests, "Daily sharing limit reached for your network. Please try again tomorrow.")] [InlineData(HttpStatusCode.BadRequest, "The request body must be a JSON object.")] + // Server text wins over the built-in 413 message below + [InlineData(HttpStatusCode.RequestEntityTooLarge, "Plans this large can't be shared.")] public async Task Share_ShowsTheErrorTextTheServerSent(HttpStatusCode status, string error) { var handler = new StubHandler(() => Reply(status, $$"""{"error":"{{error}}"}""")); @@ -86,6 +88,19 @@ public async Task Share_FallsBackToTheStatusCode_WhenTheReplyHasNoErrorText(Http Assert.Equal($"Share failed: server returned {(int)status}", ex.Message); } + // nginx refuses an oversized upload with its own HTML page, and Kestrel with an empty body + [Theory] + [InlineData("413 Request Entity Too Large

413 Request Entity Too Large


nginx
", "text/html")] + [InlineData("", "application/json")] + public async Task Share_ExplainsTheSizeLimit_WhenTheUploadIsTooLarge(string body, string contentType) + { + var handler = new StubHandler(() => Reply(HttpStatusCode.RequestEntityTooLarge, body, contentType)); + + var ex = await Assert.ThrowsAsync(() => Share(handler)); + + Assert.Equal("This plan is too large to share. The limit is 10 MB.", ex.Message); + } + [Fact] public async Task Delete_SendsTheTokenInAHeader_NotInTheUrl() {