diff --git a/src/Bifrost.Web/Features/Desk/DeskDocuments.cs b/src/Bifrost.Web/Features/Desk/DeskDocuments.cs index aaa652f..61237b7 100644 --- a/src/Bifrost.Web/Features/Desk/DeskDocuments.cs +++ b/src/Bifrost.Web/Features/Desk/DeskDocuments.cs @@ -151,7 +151,7 @@ public static class DeskRefusals [RequestIdConflict] = (409, "request_id_conflict: this request_id was used for a different body"), }; - // fork_too_large (Libby's ruling) is list-only and names its fork, so it is built here, not read from the table. + // fork_too_large (Libby's ruling) names its fork, so it is built here, not read from the table. // Not a literal const: the pinned vectors' doorRefusals do not name it yet, and the schema test pins literals to them. public static string ForkTooLarge => "fork_too_large"; @@ -164,6 +164,10 @@ public static class DeskRefusals ? $"fork_too_large: fork {forkId} (order {order}) is too large for a page; continue with cursor c{order}" : $"fork_too_large: fork {forkId} (order {order}) is too large for a page; the list has reached its end"); + // A read of an oversize fork: the same permanent refusal, naming the fork and nothing to page. + public static DeskRefusal ForkTooLargeDocument(string forkId) => new( + DeskSchemas.Refusal, ForkTooLarge, $"fork_too_large: fork {forkId} is too large to return"); + public static bool IsKnown(string code) => Table.ContainsKey(code); public static int StatusOf(string code) => Table[code].Status; diff --git a/src/Bifrost.Web/Features/Desk/DeskEndpoints.cs b/src/Bifrost.Web/Features/Desk/DeskEndpoints.cs index 7f5cfff..f97a949 100644 --- a/src/Bifrost.Web/Features/Desk/DeskEndpoints.cs +++ b/src/Bifrost.Web/Features/Desk/DeskEndpoints.cs @@ -189,6 +189,10 @@ public static class DeskEndpoints { await WriteJsonAsync(http, 200, IsDecider(context) ? entry : DeskNotes.Strip(entry)); } + else if (found is DeskReadOutcome.ForkTooLarge(var largeFork)) + { + await WriteJsonAsync(http, DeskRefusals.ForkTooLargeStatus, DeskRefusals.ForkTooLargeDocument(largeFork)); + } else if (found is DeskReadOutcome.NotFound(var readCode) && DoorMayRefuse(readCode) && readCode != DeskRefusals.RequestIdConflict) { await WriteRefusalAsync(http, ReadListCode(readCode)); diff --git a/src/Bifrost.Web/Features/Desk/DeskPorts.cs b/src/Bifrost.Web/Features/Desk/DeskPorts.cs index 84ddba4..385f1bb 100644 --- a/src/Bifrost.Web/Features/Desk/DeskPorts.cs +++ b/src/Bifrost.Web/Features/Desk/DeskPorts.cs @@ -35,6 +35,9 @@ public abstract record DeskReadOutcome public sealed record Found(ForkEntry Entry) : DeskReadOutcome; public sealed record NotFound(string Code) : DeskReadOutcome; + + // The fork is too large to return: permanent, and nothing to page, so no order or cursor. + public sealed record ForkTooLarge(string ForkId) : DeskReadOutcome; } // The list request the desk's query string stands for. Mind is the route's, resolved against the configured list. @@ -47,7 +50,7 @@ public abstract record DeskListOutcome public sealed record NotListed(string Code) : DeskListOutcome; // The next fork is too large for any page: the page holds nothing past the cursor. The client continues with - // cursor c; at Order 0 it has reached the end. List only: no other call can be refused this way. + // cursor c; at Order 0 it has reached the end. A read of such a fork has its own outcome (no order). public sealed record ForkTooLarge(string ForkId, int Order) : DeskListOutcome; } diff --git a/tests/Bifrost.Web.Tests/DeskReadTests.cs b/tests/Bifrost.Web.Tests/DeskReadTests.cs index a713433..3587c3d 100644 --- a/tests/Bifrost.Web.Tests/DeskReadTests.cs +++ b/tests/Bifrost.Web.Tests/DeskReadTests.cs @@ -150,7 +150,7 @@ public sealed class DeskReadTests } [Fact] - public async Task fork_too_large_is_list_only_elsewhere_the_door_saying_it_is_unavailable() + public async Task fork_too_large_as_a_bare_door_code_is_unavailable_only_the_typed_outcomes_are_the_refusal() { using var factory = new DeskWebApplicationFactory(); await factory.EnsureAccountAsync(DeskWebApplicationFactory.Alice, true); @@ -164,6 +164,24 @@ public sealed class DeskReadTests } } + [Fact] + public async Task A_read_of_an_oversize_fork_is_the_same_422_naming_the_fork_with_no_order_or_cursor() + { + using var factory = new DeskWebApplicationFactory(); + await factory.EnsureAccountAsync(DeskWebApplicationFactory.Alice, true); + factory.Door.ReadScript = (_, id) => new DeskReadOutcome.ForkTooLarge(id); + + var reply = await factory.SendAsync("GET", DeskReplay.Target("read", "nibu", "nibu:question:huge"), null, ReadToken(factory)); + + var doc = JsonNode.Parse(reply.Body)!; + Assert.Equal(422, reply.Status); + Assert.Equal("fork_too_large", (string?)doc["code"]); + var error = (string)doc["error"]!; + Assert.Contains("nibu:question:huge", error); + Assert.DoesNotContain("order", error); + Assert.DoesNotContain("cursor", error); + } + [Fact] public async Task A_short_page_with_a_cursor_below_its_last_entry_passes_through_unchanged() {