Move client: a 400 is RequestInvalid only with Heimdall's invalid_request body
An nginx HTML 400, an empty 400 or any other 400 is Unknown. The fake answers its schema 400 with Heimdall's error handler body. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
1ffb8886c0
commit
b7b33bc5db
4 changed files with 13 additions and 12 deletions
|
|
@ -33,7 +33,7 @@ public enum HeimdallProofTier
|
|||
public sealed record HeimdallMoveReceipt(
|
||||
string MoveId, string AccountId, IReadOnlyList<string> MovedIdentityIds, HeimdallProofTier ProofFrom, HeimdallProofTier? ProofTo, string? ReversesMoveId);
|
||||
|
||||
/// <summary>Heimdall's fixed refusal codes for a move, plus the one local class: Heimdall's schema 400.</summary>
|
||||
/// <summary>Heimdall's fixed refusal codes for a move, plus invalid_request, Heimdall's own schema 400.</summary>
|
||||
public enum HeimdallMoveRefusal
|
||||
{
|
||||
AppAuthRequired,
|
||||
|
|
@ -128,12 +128,6 @@ public sealed partial class HeimdallIdentityMoveClient(
|
|||
return new HeimdallMoveOutcome.Unknown($"HTTP {status}");
|
||||
}
|
||||
|
||||
// Fastify's schema 400 answers before the route runs, so it carries no Heimdall code and applied nothing.
|
||||
if (status == 400)
|
||||
{
|
||||
return new HeimdallMoveOutcome.Refused(HeimdallMoveRefusal.RequestInvalid);
|
||||
}
|
||||
|
||||
return Refused(await HeimdallRefusal.ReadAsync(response, token), status);
|
||||
}
|
||||
catch (Exception) when (!cancellationToken.IsCancellationRequested)
|
||||
|
|
@ -144,7 +138,7 @@ public sealed partial class HeimdallIdentityMoveClient(
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refused only for the coded refusals Heimdall's move route emits (4fe367a moveIdentitiesRoute). A 4xx without one of those codes
|
||||
/// Refused only for the coded refusals Heimdall's move route emits (4fe367a moveIdentitiesRoute, and its error handler's invalid_request 400). A 4xx without one of those codes
|
||||
/// (a gateway's HTML 404, a 408, a code a later Heimdall adds) proves nothing was applied, so it is Unknown. move_id_conflict is
|
||||
/// Unknown too: a committed move answers it when its proofs no longer verify.
|
||||
/// </summary>
|
||||
|
|
@ -167,6 +161,8 @@ public sealed partial class HeimdallIdentityMoveClient(
|
|||
"already_moved" => HeimdallMoveRefusal.AlreadyMoved,
|
||||
"provider_conflict" => HeimdallMoveRefusal.ProviderConflict,
|
||||
"identity_not_found" => HeimdallMoveRefusal.IdentityNotFound,
|
||||
// Heimdall's error handler gives its own 400 this code; an nginx or other edge 400 has none, so it is not Heimdall's refusal.
|
||||
"invalid_request" when status == 400 => HeimdallMoveRefusal.RequestInvalid,
|
||||
_ => (HeimdallMoveRefusal?)null
|
||||
};
|
||||
if (code is not { } known)
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ public sealed class FakeHeimdallMoverTests
|
|||
};
|
||||
foreach (var json in bad)
|
||||
{
|
||||
Assert.Equal(HttpStatusCode.BadRequest, (await Post(host, json)).Status);
|
||||
Assert.Equal((HttpStatusCode.BadRequest, "invalid_request"), await Post(host, json));
|
||||
}
|
||||
|
||||
Assert.Single(moves.Stored);
|
||||
|
|
|
|||
|
|
@ -292,7 +292,7 @@ public sealed class HeimdallIdentityMoveClientTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_400_is_RequestInvalid_a_definitive_refusal_that_moves_nothing()
|
||||
public async Task A_400_carrying_Heimdalls_invalid_request_code_is_RequestInvalid_a_definitive_refusal_that_moves_nothing()
|
||||
{
|
||||
using var host = new HeimdallHost();
|
||||
var moves = host.Heimdall.Moves;
|
||||
|
|
@ -307,6 +307,11 @@ public sealed class HeimdallIdentityMoveClientTests
|
|||
[Theory]
|
||||
[InlineData(HttpStatusCode.NotFound, "<html><body>404 Not Found</body></html>")]
|
||||
[InlineData(HttpStatusCode.NotFound, "")]
|
||||
[InlineData(HttpStatusCode.BadRequest, "<html><head><title>400 Bad Request</title></head><body>nginx</body></html>")]
|
||||
[InlineData(HttpStatusCode.BadRequest, "")]
|
||||
[InlineData(HttpStatusCode.BadRequest, """{"statusCode":400,"error":"Bad Request","message":"x"}""")]
|
||||
[InlineData(HttpStatusCode.BadRequest, """{"message":"no error field"}""")]
|
||||
[InlineData(HttpStatusCode.Conflict, """{"error":"invalid_request"}""")]
|
||||
[InlineData(HttpStatusCode.RequestTimeout, "")]
|
||||
[InlineData(HttpStatusCode.RequestTimeout, """{"error":"request_timeout"}""")]
|
||||
[InlineData(HttpStatusCode.Gone, "")]
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ public sealed class FakeMoves(Func<DateTimeOffset> now)
|
|||
|
||||
if (Parse(body) is not { } move)
|
||||
{
|
||||
return Json(HttpStatusCode.BadRequest, """{"statusCode":400,"error":"Bad Request","message":"body does not match the schema"}""");
|
||||
return Json(HttpStatusCode.BadRequest, """{"error":"invalid_request","detail":"body: must match the schema"}""");
|
||||
}
|
||||
|
||||
if (MoveStatus is { } forced)
|
||||
|
|
@ -110,7 +110,7 @@ public sealed class FakeMoves(Func<DateTimeOffset> now)
|
|||
return forced == HttpStatusCode.NotFound
|
||||
? new HttpResponseMessage(forced)
|
||||
: Json(forced, forced == HttpStatusCode.BadRequest
|
||||
? """{"statusCode":400,"error":"Bad Request"}"""
|
||||
? """{"error":"invalid_request","detail":"body: must match the schema"}"""
|
||||
: """{"error":"server_error","detail":"HEIMDALL FREE TEXT"}""");
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue