fix(federation): friend-add returns graceful 503 instead of 500 on peer lookup failure (BUG-3)

lookupRemoteUser now maps peer HTTP failures (403/5xx, malformed body) to a
structured {ok:false,reason:'unreachable'} instead of throwing, and the
federated friend-add wraps the call in try/catch as defense-in-depth. A
desynced/unreachable peer no longer surfaces as a raw 500 on a user action.
README.md left unstaged.
This commit is contained in:
Jannis Braun
2026-07-02 11:23:08 +02:00
parent 43d1dad1d7
commit 6de14b281b
4 changed files with 68 additions and 9 deletions
@@ -274,6 +274,19 @@ describe('POST /api/social/requests — federated branch (lookup failures)', ()
expect(JSON.parse(res.body).error).toBe('peer_unreachable');
});
it('returns 503 (not 500) when lookup throws unexpectedly — defense-in-depth (BUG-3)', async () => {
lookupRemoteUserMock.mockRejectedValue(new Error('boom: peer returned HTTP 403'));
const app = await buildApp();
const res = await app.inject({
method: 'POST',
url: '/api/social/requests',
payload: { username: 'alice@orbit.test' },
});
expect(res.statusCode).toBe(503);
expect(JSON.parse(res.body).error).toBe('peer_unreachable');
expect(testDb.select().from(schema.friendRequests).all()).toHaveLength(0);
});
it('returns 429 lookup_rate_limited with Retry-After header when lookup returns rate_limited', async () => {
lookupRemoteUserMock.mockResolvedValue({ ok: false, reason: 'rate_limited', retryAfter: 30 });
const app = await buildApp();
+13 -2
View File
@@ -241,8 +241,19 @@ async function handleFederatedFriendRequest(
}
// peering.status === 'active' — continue
// 3. Lookup
const lookup = await lookupRemoteUser(peerOrigin, baseName);
// 3. Lookup — a peer's HTTP/transport failure must never surface as a raw 500
// on a user action. lookupRemoteUser already maps peer HTTP failures (403/5xx,
// malformed body) to a structured `unreachable`; this try/catch is
// defense-in-depth so that any *unexpected* throw (e.g. a missing peer row) is
// still returned to the user as a graceful 503 rather than an Internal Server
// Error. (BUG-3, 2026-07-02: a desynced peer returned 403 → unhandled throw → 500.)
let lookup: Awaited<ReturnType<typeof lookupRemoteUser>>;
try {
lookup = await lookupRemoteUser(peerOrigin, baseName);
} catch (err) {
console.error(`[social] federated friend-add lookup failed for ${peerOrigin}:`, err);
return reply.code(503).send({ error: 'peer_unreachable', statusCode: 503, domain: targetDomain });
}
if (!lookup.ok) {
if (lookup.reason === 'not_found') {
return reply.code(404).send({ error: 'user_not_found', statusCode: 404, domain: targetDomain, handle: baseName });