Our @context stays JSON-LD 1.0, and a 500 no longer takes a host out
The proof term added with integrity proofs had a @graph container, which only JSON-LD 1.1 knows: Smithereen's reader refused every document carrying our context with a 500. Those 500s, retried, then put Smithereen's host in quarantine for an hour, and every delivery to it waited. Proofs are canonicalised as JSON (JCS), so the term loses nothing as a plain id; a test keeps every term within 1.0. Only an unreachable host, a timeout or a gateway's 502, 503 or 504 now counts against a host: a 500 is the server failing on that one activity, retried on its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
1 parent
b66f0acf4e
commit
b478049303
5 files changed
+62
-3
No files matched your search
+2
-1
@@ -391,7 +391,8 @@ Posts with a location (shown to nearby users of this server) never leave the ser
|
|||||||
- **Delivery.** An activity goes once to each server, to its shared inbox when the server has one, whoever there it
|
- **Delivery.** An activity goes once to each server, to its shared inbox when the server has one, whoever there it
|
||||||
follows, names or answers (a circle post excepted: each member's copy names that member). Failed deliveries are
|
follows, names or answers (a circle post excepted: each member's copy names that member). Failed deliveries are
|
||||||
retried with Mastodon's backoff (16 attempts). A host that keeps failing is paused,
|
retried with Mastodon's backoff (16 attempts). A host that keeps failing is paused,
|
||||||
starting at an hour and growing to a week. A server can also take a Follow (202) and drop it afterwards, as Pleroma
|
starting at an hour and growing to a week: unreachable, timing out, or a gateway answering 502, 503 or 504. A 500 is
|
||||||
|
the server failing on that one activity, retried on its own while its other deliveries go on. A server can also take a Follow (202) and drop it afterwards, as Pleroma
|
||||||
does when it cannot yet fetch our actor, so a follow request still unanswered is sent again, same activity, when the
|
does when it cannot yet fetch our actor, so a follow request still unanswered is sent again, same activity, when the
|
||||||
persona follows once more, at most once an hour.
|
persona follows once more, at most once an hour.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using PrivaPub.Federation.Rendering;
|
||||||
|
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
|
||||||
|
namespace PrivaPub.Tests.Federation
|
||||||
|
{
|
||||||
|
// The @context on everything we send. Readers that expand JSON-LD 1.0 (Smithereen) refuse a whole document over one
|
||||||
|
// term only 1.1 knows, so every term stays within 1.0
|
||||||
|
public class ContextTests
|
||||||
|
{
|
||||||
|
static readonly string[] Containers10 = ["@list", "@set", "@index", "@language"];
|
||||||
|
static readonly string[] Keywords11 = ["@version", "@protected", "@propagate", "@import", "@nest", "@prefix", "@context", "@direction", "@json"];
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Every_term_is_json_ld_1_0()
|
||||||
|
{
|
||||||
|
var terms = ActivityPubRenderer.Context().OfType<JsonObject>().Single();
|
||||||
|
|
||||||
|
foreach (var (name, definition) in terms)
|
||||||
|
{
|
||||||
|
if (definition is not JsonObject term)
|
||||||
|
continue;
|
||||||
|
if (term["@container"] is JsonValue container)
|
||||||
|
Assert.True(Containers10.Contains(container.GetValue<string>()), $"{name}: @container {container} is JSON-LD 1.1");
|
||||||
|
Assert.True(term["@container"] is null or JsonValue, $"{name}: an array of containers is JSON-LD 1.1");
|
||||||
|
Assert.DoesNotContain(term.Select(p => p.Key), Keywords11.Contains);
|
||||||
|
Assert.NotEqual("@json", term["@type"]?.GetValue<string>());
|
||||||
|
}
|
||||||
|
Assert.DoesNotContain(terms.Select(p => p.Key), Keywords11.Contains);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -666,6 +666,27 @@ namespace PrivaPub.Tests.Federation
|
|||||||
Assert.StartsWith("500", broken.Error);
|
Assert.StartsWith("500", broken.Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// a server that fails on one activity is up: its 500s are that delivery's to retry, and only a gateway's 502, 503 or
|
||||||
|
// 504 counts towards taking the host out (Smithereen's 500s on documents it could not read once held every delivery
|
||||||
|
// to it back for an hour)
|
||||||
|
[Fact]
|
||||||
|
public async Task Only_a_gateways_failures_take_the_host_out()
|
||||||
|
{
|
||||||
|
var (_, alice) = await _harness.Persona("alice");
|
||||||
|
_harness.Peer.Answer("/broken/inbox", 500);
|
||||||
|
_harness.Peer.Answer("/gateway/inbox", 502);
|
||||||
|
|
||||||
|
for (var i = 0; i < HostCircuitBreaker.Threshold; i++)
|
||||||
|
await Attempt(alice, _harness.Peer.A + "/broken/inbox");
|
||||||
|
var stillTried = await Attempt(alice, _harness.Peer.A + "/broken/inbox");
|
||||||
|
for (var i = 0; i < HostCircuitBreaker.Threshold; i++)
|
||||||
|
await Attempt(alice, _harness.Peer.A + "/gateway/inbox");
|
||||||
|
var heldBack = await Attempt(alice, _harness.Peer.A + "/broken/inbox");
|
||||||
|
|
||||||
|
Assert.Equal((JobResult.Retry, "500"), (stillTried.Result, stillTried.Error[..3]));
|
||||||
|
Assert.Equal((JobResult.Defer, "the host is unavailable"), (heldBack.Result, heldBack.Error));
|
||||||
|
}
|
||||||
|
|
||||||
// Mastodon's 422 for two first contacts racing to create one account, and its 409 for a held lock, are retried a
|
// Mastodon's 422 for two first contacts racing to create one account, and its 409 for a held lock, are retried a
|
||||||
// few times; the third refusal is final like any other
|
// few times; the third refusal is final like any other
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -258,6 +258,10 @@ namespace PrivaPub.Federation.Outbox
|
|||||||
_logger.LogInformation("Delivery to {Inbox} refused with {Status}", payload.Inbox, status);
|
_logger.LogInformation("Delivery to {Inbox} refused with {Status}", payload.Inbox, status);
|
||||||
return JobOutcome.Dead($"{status} {response.ReasonPhrase}");
|
return JobOutcome.Dead($"{status} {response.ReasonPhrase}");
|
||||||
}
|
}
|
||||||
|
// a gateway's 502, 503 or 504 says the server behind it is down, which counts against the host; any other 5xx
|
||||||
|
// says the server is up and failed on this activity (Smithereen's 500 on a document its JSON-LD reader
|
||||||
|
// refused), retried on its own while the host's other deliveries go on
|
||||||
|
if (status is 502 or 503 or 504)
|
||||||
await _breaker.Failed(job.Host, $"{status}", token);
|
await _breaker.Failed(job.Host, $"{status}", token);
|
||||||
return JobOutcome.Retry($"{status} {response.ReasonPhrase}");
|
return JobOutcome.Retry($"{status} {response.ReasonPhrase}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,13 +81,14 @@ namespace PrivaPub.Federation.Rendering
|
|||||||
["wallPostVisibility"] = "sm:wallPostVisibility",
|
["wallPostVisibility"] = "sm:wallPostVisibility",
|
||||||
["allowedTo"] = "sm:allowedTo",
|
["allowedTo"] = "sm:allowedTo",
|
||||||
// FEP-521a keys and FEP-8b32 proofs, defined here rather than by the data-integrity and multikey contexts,
|
// FEP-521a keys and FEP-8b32 proofs, defined here rather than by the data-integrity and multikey contexts,
|
||||||
// which strict JSON-LD readers would have to fetch
|
// which strict JSON-LD readers would have to fetch. JSON-LD 1.0 only: Smithereen's reader refuses the whole
|
||||||
|
// document over a 1.1 term (proof's @graph container there; our proofs are canonicalised as JSON, by JCS)
|
||||||
["assertionMethod"] = new JsonObject { ["@id"] = "sec:assertionMethod", ["@type"] = "@id", ["@container"] = "@set" },
|
["assertionMethod"] = new JsonObject { ["@id"] = "sec:assertionMethod", ["@type"] = "@id", ["@container"] = "@set" },
|
||||||
["Multikey"] = "sec:Multikey",
|
["Multikey"] = "sec:Multikey",
|
||||||
["controller"] = new JsonObject { ["@id"] = "sec:controller", ["@type"] = "@id" },
|
["controller"] = new JsonObject { ["@id"] = "sec:controller", ["@type"] = "@id" },
|
||||||
["publicKeyMultibase"] = new JsonObject { ["@id"] = "sec:publicKeyMultibase", ["@type"] = "sec:multibase" },
|
["publicKeyMultibase"] = new JsonObject { ["@id"] = "sec:publicKeyMultibase", ["@type"] = "sec:multibase" },
|
||||||
["DataIntegrityProof"] = "sec:DataIntegrityProof",
|
["DataIntegrityProof"] = "sec:DataIntegrityProof",
|
||||||
["proof"] = new JsonObject { ["@id"] = "sec:proof", ["@type"] = "@id", ["@container"] = "@graph" },
|
["proof"] = new JsonObject { ["@id"] = "sec:proof", ["@type"] = "@id" },
|
||||||
["cryptosuite"] = new JsonObject { ["@id"] = "sec:cryptosuite", ["@type"] = "sec:cryptosuiteString" },
|
["cryptosuite"] = new JsonObject { ["@id"] = "sec:cryptosuite", ["@type"] = "sec:cryptosuiteString" },
|
||||||
["proofValue"] = new JsonObject { ["@id"] = "sec:proofValue", ["@type"] = "sec:multibase" },
|
["proofValue"] = new JsonObject { ["@id"] = "sec:proofValue", ["@type"] = "sec:multibase" },
|
||||||
["proofPurpose"] = new JsonObject { ["@id"] = "sec:proofPurpose", ["@type"] = "@vocab" },
|
["proofPurpose"] = new JsonObject { ["@id"] = "sec:proofPurpose", ["@type"] = "@vocab" },
|
||||||
|
|||||||
Reference in new issue
Block a user