diff --git a/CHANGES.md b/CHANGES.md index 78b1fe97..51e4d299 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -34,6 +34,11 @@ To be released. - `Context.sendActivity()` method now throws `TypeError` instead of silently failing when the given `Activity` object lacks the actor property. + - Added outbox error handler to `Federation`. + + - Added `onOutboxError` option to `new Federation()` constructor. + - Added `OutboxErrorHandler` type. + [Hono]: https://hono.dev/ [#25]: https://github.com/dahlia/fedify/issues/25 [#27]: https://github.com/dahlia/fedify/issues/27 diff --git a/federation/callback.ts b/federation/callback.ts index 2cd6c0bb..8b402647 100644 --- a/federation/callback.ts +++ b/federation/callback.ts @@ -85,3 +85,16 @@ export type InboxErrorHandler = ( context: RequestContext, error: Error, ) => void | Promise; + +/** + * A callback that handles errors during outbox processing. + * + * @param error The error that occurred. + * @param activity The activity that caused the error. If it is `null`, the + * error occurred during deserializing the activity. + * @since 0.6.0 + */ +export type OutboxErrorHandler = ( + error: Error, + activity: Activity | null, +) => void | Promise; diff --git a/federation/middleware.ts b/federation/middleware.ts index 2466b7de..455169d5 100644 --- a/federation/middleware.ts +++ b/federation/middleware.ts @@ -20,6 +20,7 @@ import type { InboxErrorHandler, InboxListener, NodeInfoDispatcher, + OutboxErrorHandler, } from "./callback.ts"; import type { Context, @@ -82,6 +83,17 @@ export interface FederationParameters { */ treatHttps?: boolean; + /** + * A callback that handles errors during outbox processing. Note that this + * callback can be called multiple times for the same error, because failed + * deliveries are retried. + * + * If any errors are thrown in this callback, they are ignored. + * + * @since 0.6.0 + */ + onOutboxError?: OutboxErrorHandler; + // TODO: The following option should be removed, and exponential backoff // should be used instead: backoffSchedule?: Temporal.Duration[]; @@ -129,6 +141,7 @@ export class Federation { #documentLoader: DocumentLoader; #authenticatedDocumentLoaderFactory: AuthenticatedDocumentLoaderFactory; #treatHttps: boolean; + #onOutboxError?: OutboxErrorHandler; #backoffSchedule: Temporal.Duration[]; /** @@ -143,6 +156,7 @@ export class Federation { documentLoader, authenticatedDocumentLoaderFactory, treatHttps, + onOutboxError, backoffSchedule, }: FederationParameters, ) { @@ -167,6 +181,7 @@ export class Federation { this.#authenticatedDocumentLoaderFactory = authenticatedDocumentLoaderFactory ?? getAuthenticatedDocumentLoader; + this.#onOutboxError = onOutboxError; this.#treatHttps = treatHttps ?? false; this.#backoffSchedule = backoffSchedule ?? [ 3_000, @@ -180,17 +195,24 @@ export class Federation { } async #listenQueue(message: OutboxMessage): Promise { + let activity: Activity | null = null; try { + activity = await Activity.fromJsonLd(message.activity, { + documentLoader: this.#documentLoader, + }); await sendActivity({ keyId: new URL(message.keyId), privateKey: await importJwk(message.privateKey, "private"), - activity: await Activity.fromJsonLd(message.activity, { - documentLoader: this.#documentLoader, - }), + activity, inbox: new URL(message.inbox), documentLoader: this.#documentLoader, }); - } catch (_) { + } catch (e) { + try { + this.#onOutboxError?.(e, activity); + } catch (_) { + // Ignore errors in the error handler. + } if (message.trial < this.#backoffSchedule.length) { this.#queue?.enqueue({ ...message,