fix(federation): replace DNS hairpin self-POST with direct function call (FED-005)

Extract processRelayEvents() from the relay HTTP handler and call it
directly in runInitialSyncForNewPeers(), eliminating the HTTP round-trip
through public DNS that failed on networks without hairpin NAT.
This commit is contained in:
Jannis Braun
2026-03-31 05:20:16 +02:00
parent 37fc52d85b
commit 2cac39a460
4 changed files with 88 additions and 99 deletions
+75 -58
View File
@@ -419,64 +419,7 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
}
// 3. Process each event
const accepted: string[] = [];
const rejected: Array<{ messageId: string; reason: string }> = [];
for (const event of body.events) {
try {
switch (event.eventType) {
case 'create':
processCreateEvent(event, sourceInstance, peer.origin, db, accepted, rejected);
break;
case 'update':
processUpdateEvent(event, sourceInstance, db, accepted, rejected);
break;
case 'delete':
processDeleteEvent(event, sourceInstance, db, accepted, rejected);
break;
case 'reaction_add':
processReactionAddEvent(event, sourceInstance, db, accepted, rejected);
break;
case 'reaction_remove':
processReactionRemoveEvent(event, sourceInstance, db, accepted, rejected);
break;
case 'member_add':
processMemberAddEvent(event, sourceInstance, db, accepted, rejected);
break;
case 'member_remove':
processMemberRemoveEvent(event, sourceInstance, db, accepted, rejected);
break;
case 'ownership_transfer':
processOwnershipTransferEvent(event, sourceInstance, db, accepted, rejected);
break;
case 'friend_request_create':
processFriendRequestCreateEvent(event, sourceInstance, db, accepted, rejected);
break;
case 'friend_request_update':
processFriendRequestUpdateEvent(event, sourceInstance, db, accepted, rejected);
break;
case 'friend_request_cancel':
processFriendRequestCancelEvent(event, sourceInstance, db, accepted, rejected);
break;
case 'friend_add':
processFriendAddEvent(event, sourceInstance, db, accepted, rejected);
break;
case 'friend_remove':
processFriendRemoveEvent(event, sourceInstance, db, accepted, rejected);
break;
case 'file_rejected':
processFileRejectedEvent(event, sourceInstance, db, accepted, rejected);
break;
default:
rejected.push({ messageId: event.messageId, reason: 'unknown_event_type' });
break;
}
} catch (err) {
const errMsg = err instanceof Error ? err.message : 'unknown_error';
console.error(`[federation-relay] Error processing event ${event.messageId}:`, errMsg);
rejected.push({ messageId: event.messageId, reason: 'processing_error' });
}
}
const { accepted, rejected } = processRelayEvents(body.events, sourceInstance, peer.origin, db);
// 4. Update peer status
db.update(schema.federationPeers)
@@ -865,6 +808,80 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
);
}
// ─── Relay Event Processing (shared by HTTP handler and initial sync) ────────
/**
* Process an array of federation relay events. Used by the HTTP relay endpoint
* and directly by the initial-sync worker (which skips the HTTP round-trip).
*/
export function processRelayEvents(
events: FederationRelayEvent[],
sourceInstance: string,
peerOrigin: string,
db: ReturnType<typeof getDb>,
): { accepted: string[]; rejected: Array<{ messageId: string; reason: string }> } {
const accepted: string[] = [];
const rejected: Array<{ messageId: string; reason: string }> = [];
for (const event of events) {
try {
switch (event.eventType) {
case 'create':
processCreateEvent(event, sourceInstance, peerOrigin, db, accepted, rejected);
break;
case 'update':
processUpdateEvent(event, sourceInstance, db, accepted, rejected);
break;
case 'delete':
processDeleteEvent(event, sourceInstance, db, accepted, rejected);
break;
case 'reaction_add':
processReactionAddEvent(event, sourceInstance, db, accepted, rejected);
break;
case 'reaction_remove':
processReactionRemoveEvent(event, sourceInstance, db, accepted, rejected);
break;
case 'member_add':
processMemberAddEvent(event, sourceInstance, db, accepted, rejected);
break;
case 'member_remove':
processMemberRemoveEvent(event, sourceInstance, db, accepted, rejected);
break;
case 'ownership_transfer':
processOwnershipTransferEvent(event, sourceInstance, db, accepted, rejected);
break;
case 'friend_request_create':
processFriendRequestCreateEvent(event, sourceInstance, db, accepted, rejected);
break;
case 'friend_request_update':
processFriendRequestUpdateEvent(event, sourceInstance, db, accepted, rejected);
break;
case 'friend_request_cancel':
processFriendRequestCancelEvent(event, sourceInstance, db, accepted, rejected);
break;
case 'friend_add':
processFriendAddEvent(event, sourceInstance, db, accepted, rejected);
break;
case 'friend_remove':
processFriendRemoveEvent(event, sourceInstance, db, accepted, rejected);
break;
case 'file_rejected':
processFileRejectedEvent(event, sourceInstance, db, accepted, rejected);
break;
default:
rejected.push({ messageId: event.messageId, reason: 'unknown_event_type' });
break;
}
} catch (err) {
const errMsg = err instanceof Error ? err.message : 'unknown_error';
console.error(`[federation-relay] Error processing event ${event.messageId}:`, errMsg);
rejected.push({ messageId: event.messageId, reason: 'processing_error' });
}
}
return { accepted, rejected };
}
// ─── Relay Event Processors ──────────────────────────────────────────────────
/**
+5 -28
View File
@@ -9,6 +9,7 @@ import { generateSnowflake } from './snowflake.js';
import { getDmMessageWithUser } from '../routes/dm.js';
import { connectionManager } from '../ws/handler.js';
import { generateThumbnail } from './thumbnail.js';
import { processRelayEvents } from '../routes/federation.js';
import type { FederationRelayRequest, FederationRelayResponse, FederationRelayEvent } from '@backspace/shared';
import fs from 'node:fs';
import path from 'node:path';
@@ -781,21 +782,8 @@ async function runInitialSyncForNewPeers(): Promise<void> {
if (data.events.length === 0) break;
// Relay the events through our own relay endpoint logic
// For simplicity, POST them to ourselves
const relayBody = JSON.stringify({
version: 1,
sourceInstance: peer.origin,
events: data.events,
});
const relayHeaders = buildFederationHeaders(relayBody, peer.hmacSecret, peer.origin);
await fetch(`${ourOrigin}/api/federation/relay`, {
method: 'POST',
headers: relayHeaders,
body: relayBody,
signal: AbortSignal.timeout(30_000),
});
// Process events directly — no HTTP round-trip (FED-005)
processRelayEvents(data.events, peer.origin, peer.origin, db);
totalEvents += data.events.length;
sinceTimestamp = data.checkpoint;
@@ -825,19 +813,8 @@ async function runInitialSyncForNewPeers(): Promise<void> {
if (friendData.events.length === 0) break;
const friendRelayBody = JSON.stringify({
version: 1,
sourceInstance: peer.origin,
events: friendData.events,
});
const friendRelayHeaders = buildFederationHeaders(friendRelayBody, peer.hmacSecret, peer.origin);
await fetch(`${ourOrigin}/api/federation/relay`, {
method: 'POST',
headers: friendRelayHeaders,
body: friendRelayBody,
signal: AbortSignal.timeout(30_000),
});
// Process events directly — no HTTP round-trip (FED-005)
processRelayEvents(friendData.events, peer.origin, peer.origin, db);
totalEvents += friendData.events.length;
friendSinceTimestamp = friendData.checkpoint;