Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,22 @@ To be released.
with the same `orderingKey` are guaranteed to be delivered in order
to each recipient server.

- Added `Federatable.setOutboxPermanentFailureHandler()` method to handle
permanent delivery failures (such as `410 Gone` or `404 Not Found`) when
sending activities to remote inboxes. This allows applications to clean
up unreachable followers and avoid future delivery attempts to permanently
failed inboxes. [[#548], [#559]]

- Added `permanentFailureStatusCodes` option to `FederationOptions` to
configure which HTTP status codes are treated as permanent delivery
failures. By default, `404` and `410` are treated as permanent failures.
[[#548], [#559]]

- Added `SendActivityError` class, a structured error that is thrown when
an activity fails to send to a remote inbox. It includes the HTTP status
code, the inbox URL, and the response body, making it easier to
programmatically handle delivery errors. [[#548], [#559]]

[#280]: https://github.com/fedify-dev/fedify/issues/280
[#366]: https://github.com/fedify-dev/fedify/issues/366
[#376]: https://github.com/fedify-dev/fedify/issues/376
Expand All @@ -144,6 +160,8 @@ To be released.
[#538]: https://github.com/fedify-dev/fedify/issues/538
[#540]: https://github.com/fedify-dev/fedify/pull/540
[#544]: https://github.com/fedify-dev/fedify/pull/544
[#548]: https://github.com/fedify-dev/fedify/issues/548
[#559]: https://github.com/fedify-dev/fedify/pull/559
[#560]: https://github.com/fedify-dev/fedify/issues/560

### @fedify/cli
Expand Down
27 changes: 27 additions & 0 deletions docs/manual/federation.md
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,33 @@ Defaults to `"rfc9421"`.
[HTTP Signatures]: https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12
[RFC 9421]: https://www.rfc-editor.org/rfc/rfc9421

### `permanentFailureStatusCodes`

*This API is available since Fedify 2.0.0.*

HTTP status codes that should be treated as permanent delivery failures.
When an inbox returns one of these codes, the delivery will not be retried
and the permanent failure handler (if registered via
`~Federatable.setOutboxPermanentFailureHandler()`) will be called.

By default, `[404, 410]`.

~~~~ typescript twoslash
// @noErrors: 2345
import { createFederation, InProcessMessageQueue } from "@fedify/fedify";

const federation = createFederation({
// Omitted for brevity; see the related section for details.
queue: new InProcessMessageQueue(),
permanentFailureStatusCodes: [404, 410, 451],
});
~~~~

See also the
[*Permanent delivery failure handling*](./send.md#permanent-delivery-failure-handling)
section in the *Sending activities* page for how to register a handler for
permanent delivery failures.

### `outboxRetryPolicy`

*This API is available since Fedify 0.12.0.*
Expand Down
83 changes: 83 additions & 0 deletions docs/manual/send.md
Original file line number Diff line number Diff line change
Expand Up @@ -833,6 +833,89 @@ const federation = createFederation({
> activity, because the delivery is retried according to the backoff schedule
> until it succeeds or reaches the maximum retry count.

### Permanent delivery failure handling

*This API is available since Fedify 2.0.0.*

When a remote inbox returns an HTTP status code that indicates a permanent
failure (such as `410 Gone` or `404 Not Found`), there is no point in retrying
the delivery. Fedify automatically skips retries for permanent failures and
allows you to handle them by registering a permanent failure handler:

~~~~ typescript twoslash
// @noErrors: 2304 2345
import { createFederation, InProcessMessageQueue } from "@fedify/fedify";

const federation = createFederation({
// Omitted for brevity; see the related section for details.
queue: new InProcessMessageQueue(),
});

federation.setOutboxPermanentFailureHandler(async (ctx, values) => {
console.warn(
`Inbox ${values.inbox.href} permanently failed ` +
`with status ${values.statusCode}`
);
// Remove followers associated with this inbox:
for (const actorId of values.actorIds) {
await removeFollower(actorId);
}
});
~~~~

The handler receives the following information:

`values.inbox`
: The inbox URL that returned the permanent failure.

`values.activity`
: The `Activity` object that failed to deliver.

`values.error`
: The `SendActivityError` that was thrown.

`values.statusCode`
: The HTTP status code returned by the inbox (e.g., `404` or `410`).

`values.actorIds`
: The actor IDs that were supposed to receive the activity at this inbox.
When `preferSharedInbox: true` is used, a single inbox URL may represent
multiple followers.

By default, the following HTTP status codes are treated as permanent failures:

- `404 Not Found` — the inbox no longer exists
- `410 Gone` — the inbox is explicitly marked as gone

You can customize which status codes are treated as permanent failures using
the `permanentFailureStatusCodes` option in `FederationOptions`:

~~~~ typescript twoslash
// @noErrors: 2345
import { createFederation, InProcessMessageQueue } from "@fedify/fedify";

const federation = createFederation({
// Omitted for brevity; see the related section for details.
queue: new InProcessMessageQueue(),
permanentFailureStatusCodes: [404, 410, 451],
});
~~~~

See also the
[`permanentFailureStatusCodes`](./federation.md#permanentfailurestatuscodes)
option in the *Federation* section for more details.

> [!NOTE]
> The permanent failure handler is called only once for each permanent
> failure, and delivery is not retried afterwards. This is different from
> `onOutboxError`, which can be called multiple times due to retries.
>
> Even if no handler is registered, permanent failures will still skip retries.

> [!TIP]
> If any errors are thrown in the permanent failure handler, they are caught,
> logged, and ignored, similar to `onOutboxError`.


HTTP Signatures
---------------
Expand Down
9 changes: 9 additions & 0 deletions packages/fedify/src/federation/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import type {
NodeInfoDispatcher,
ObjectAuthorizePredicate,
ObjectDispatcher,
OutboxPermanentFailureHandler,
SharedInboxKeyDispatcher,
WebFingerLinksDispatcher,
} from "./callback.ts";
Expand Down Expand Up @@ -106,6 +107,7 @@ export class FederationBuilderImpl<TContextData>
inboxListeners?: InboxListenerSet<TContextData>;
inboxErrorHandler?: InboxErrorHandler<TContextData>;
sharedInboxKeyDispatcher?: SharedInboxKeyDispatcher<TContextData>;
outboxPermanentFailureHandler?: OutboxPermanentFailureHandler<TContextData>;
idempotencyStrategy?:
| IdempotencyStrategy
| IdempotencyKeyCallback<TContextData>;
Expand Down Expand Up @@ -185,6 +187,7 @@ export class FederationBuilderImpl<TContextData>
f.inboxListeners = this.inboxListeners?.clone();
f.inboxErrorHandler = this.inboxErrorHandler;
f.sharedInboxKeyDispatcher = this.sharedInboxKeyDispatcher;
f.outboxPermanentFailureHandler = this.outboxPermanentFailureHandler;
f.idempotencyStrategy = this.idempotencyStrategy;
return f;
}
Expand Down Expand Up @@ -1535,6 +1538,12 @@ export class FederationBuilderImpl<TContextData>
return path;
}

setOutboxPermanentFailureHandler(
handler: OutboxPermanentFailureHandler<TContextData>,
): void {
this.outboxPermanentFailureHandler = handler;
}

/**
* Converts a name (string or symbol) to a unique string identifier.
* For symbols, generates and caches a UUID if not already present.
Expand Down
40 changes: 39 additions & 1 deletion packages/fedify/src/federation/callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Link } from "@fedify/webfinger";
import type { NodeInfo } from "../nodeinfo/types.ts";
import type { PageItems } from "./collection.ts";
import type { Context, InboxContext, RequestContext } from "./context.ts";
import type { SenderKeyPair } from "./send.ts";
import type { SendActivityError, SenderKeyPair } from "./send.ts";

/**
* A callback that dispatches a {@link NodeInfo} object.
Expand Down Expand Up @@ -232,6 +232,44 @@ export type OutboxErrorHandler = (
activity: Activity | null,
) => void | Promise<void>;

/**
* A callback that handles permanent delivery failures when sending activities
* to remote inboxes.
*
* This handler is called when an inbox returns an HTTP status code that
* indicates permanent failure (such as `410 Gone` or `404 Not Found`),
* allowing the application to clean up followers that are no longer reachable.
*
* Unlike {@link OutboxErrorHandler}, which is called for every delivery failure
* (including retries), this handler is called only once for permanent failures,
* after which delivery is not retried.
*
* If any errors are thrown in this callback, they are caught, logged,
* and ignored.
*
* @template TContextData The context data to pass to the {@link Context}.
* @param context The context.
* @param values The delivery failure information.
* @since 2.0.0
*/
export type OutboxPermanentFailureHandler<TContextData> = (
context: Context<TContextData>,
values: {
/** The inbox URL that failed. */
readonly inbox: URL;
/** The activity that failed to deliver. */
readonly activity: Activity;
/** The error that occurred. */
readonly error: SendActivityError;
/** The HTTP status code returned by the inbox. */
readonly statusCode: number;
/**
* The actor IDs that were supposed to receive the activity at this inbox.
*/
readonly actorIds: readonly URL[];
},
) => void | Promise<void>;

/**
* A callback that determines if a request is authorized or not.
*
Expand Down
32 changes: 32 additions & 0 deletions packages/fedify/src/federation/federation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import type {
ObjectAuthorizePredicate,
ObjectDispatcher,
OutboxErrorHandler,
OutboxPermanentFailureHandler,
SharedInboxKeyDispatcher,
WebFingerLinksDispatcher,
} from "./callback.ts";
Expand Down Expand Up @@ -677,6 +678,25 @@ export interface Federatable<TContextData> {
RequestContext<TContextData>,
TContextData
>;

/**
* Registers a handler for permanent delivery failures.
*
* This handler is called when an inbox returns an HTTP status code
* that indicates permanent failure (`410 Gone`, `404 Not Found`, etc.),
* allowing the application to clean up followers that are no longer
* reachable.
*
* Unlike `onOutboxError`, which is called for every delivery failure
* (including retries), this handler is called only once for permanent
* failures, after which delivery is not retried.
*
* @param handler A callback to handle permanent failures.
* @since 2.0.0
*/
setOutboxPermanentFailureHandler(
handler: OutboxPermanentFailureHandler<TContextData>,
): void;
}

/**
Expand Down Expand Up @@ -879,6 +899,18 @@ export interface FederationOptions<TContextData> {
*/
onOutboxError?: OutboxErrorHandler;

/**
* HTTP status codes that should be treated as permanent delivery failures.
* When an inbox returns one of these codes, the delivery will not be retried
* and the permanent failure handler (if registered via
* {@link Federatable.setOutboxPermanentFailureHandler}) will be called.
*
* By default, `[404, 410]`.
*
* @since 2.0.0
*/
permanentFailureStatusCodes?: readonly number[];

/**
* The time window for verifying HTTP Signatures of incoming requests. If the
* request is older or newer than this window, it is rejected. Or if it is
Expand Down
Loading
Loading