From 344661f1cbf14f48a17c4aa2a07d89b6422e253b Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 6 Apr 2026 02:41:03 -0400 Subject: [PATCH 1/4] fix: ignore Undo Block when block does not exist locally When an Undo Block arrives for a block that was never received or already deleted, silently ignore it instead of raising DoesNotExist. This is a normal federation race condition. --- users/models/block.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/users/models/block.py b/users/models/block.py index b452a958..99ab3f98 100644 --- a/users/models/block.py +++ b/users/models/block.py @@ -301,13 +301,11 @@ def handle_undo_ap(cls, data): """ Handles an incoming Block Undo """ - # Resolve source and target and see if a Follow exists (it hopefully does) try: block = cls.by_ap(data["object"]) - except KeyError: - raise ValueError("No Block locally for incoming Undo", data) + except (KeyError, cls.DoesNotExist): + return # Check the block's source is the actor if data["actor"] != block.source.actor_uri: raise ValueError("Undo actor does not match its Block object", data) - # Delete the follow block.delete() From cd0aadeff3069ea84eeaaff62ff184369a8cd044 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 6 Apr 2026 02:44:26 -0400 Subject: [PATCH 2/4] fix: handle UnicodeDecodeError when parsing nodeinfo well-known response When a remote server returns a non-UTF-8 error page (e.g. 502), response.json() raises UnicodeDecodeError. Catch it alongside JSONDecodeError. --- users/models/domain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/users/models/domain.py b/users/models/domain.py index 71dbdf97..643ad086 100644 --- a/users/models/domain.py +++ b/users/models/domain.py @@ -231,7 +231,7 @@ def fetch_nodeinfo(self) -> NodeInfo | None: ): nodeinfo20_url = link.get("href", nodeinfo20_url) break - except (json.JSONDecodeError, AttributeError): + except (json.JSONDecodeError, AttributeError, UnicodeDecodeError): pass try: From 39d23b60b830a099e4ca806e5d1479d2e7d63d13 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 6 Apr 2026 02:46:34 -0400 Subject: [PATCH 3/4] fix: return 404 for non-numeric IDs in API pk lookups Add a get_object_or_404 wrapper that catches ValueError/TypeError from invalid PK types (e.g. UUIDs passed where integers are expected) and returns 404 instead of crashing with a 500 error. Update all API view imports to use the wrapper. --- api/views/__init__.py | 10 ++++++++++ api/views/accounts.py | 2 +- api/views/announcements.py | 2 +- api/views/conversations.py | 2 +- api/views/follow_requests.py | 2 +- api/views/lists.py | 2 +- api/views/media.py | 2 +- api/views/notifications.py | 2 +- api/views/polls.py | 2 +- api/views/push.py | 2 +- api/views/report.py | 2 +- api/views/statuses.py | 2 +- api/views/tags.py | 2 +- api/views/timelines.py | 2 +- 14 files changed, 23 insertions(+), 13 deletions(-) diff --git a/api/views/__init__.py b/api/views/__init__.py index e69de29b..74f6b1d2 100644 --- a/api/views/__init__.py +++ b/api/views/__init__.py @@ -0,0 +1,10 @@ +from django.http import Http404 +from django.shortcuts import get_object_or_404 as _get_object_or_404 + + +def get_object_or_404(*args, **kwargs): + """Wrapper that returns 404 for invalid PK types (e.g. non-numeric IDs).""" + try: + return _get_object_or_404(*args, **kwargs) + except (ValueError, TypeError): + raise Http404 diff --git a/api/views/accounts.py b/api/views/accounts.py index 0dffdbb5..d1816a64 100644 --- a/api/views/accounts.py +++ b/api/views/accounts.py @@ -5,7 +5,7 @@ from core.models import Config from django.core.files import File from django.http import HttpRequest -from django.shortcuts import get_object_or_404 +from api.views import get_object_or_404 from hatchway import ApiResponse, QueryOrBody, api_view from users.services import IdentityService from users.shortcuts import by_handle_or_404 diff --git a/api/views/announcements.py b/api/views/announcements.py index 27f1ab9a..d9a208a1 100644 --- a/api/views/announcements.py +++ b/api/views/announcements.py @@ -1,4 +1,4 @@ -from django.shortcuts import get_object_or_404 +from api.views import get_object_or_404 from hatchway import api_view from api import schemas diff --git a/api/views/conversations.py b/api/views/conversations.py index 2aa98fee..a31b7f91 100644 --- a/api/views/conversations.py +++ b/api/views/conversations.py @@ -1,5 +1,5 @@ from django.http import HttpRequest -from django.shortcuts import get_object_or_404 +from api.views import get_object_or_404 from activities.models.conversation import Conversation, ConversationMembership from activities.services import TimelineService diff --git a/api/views/follow_requests.py b/api/views/follow_requests.py index cf54b927..1013dcf9 100644 --- a/api/views/follow_requests.py +++ b/api/views/follow_requests.py @@ -1,5 +1,5 @@ from django.http import HttpRequest -from django.shortcuts import get_object_or_404 +from api.views import get_object_or_404 from hatchway import api_view from api import schemas diff --git a/api/views/lists.py b/api/views/lists.py index b01d5a94..8fb6bec8 100644 --- a/api/views/lists.py +++ b/api/views/lists.py @@ -1,7 +1,7 @@ from typing import Literal from django.http import HttpRequest -from django.shortcuts import get_object_or_404 +from api.views import get_object_or_404 from hatchway import Schema, api_view from api import schemas diff --git a/api/views/media.py b/api/views/media.py index 32bc6e18..44ccf429 100644 --- a/api/views/media.py +++ b/api/views/media.py @@ -1,7 +1,7 @@ import mimetypes from django.core.files import File -from django.shortcuts import get_object_or_404 +from api.views import get_object_or_404 from hatchway import ApiError, QueryOrBody, api_view from activities.models import PostAttachment, PostAttachmentStates diff --git a/api/views/notifications.py b/api/views/notifications.py index 5bdda95f..2c53ec5f 100644 --- a/api/views/notifications.py +++ b/api/views/notifications.py @@ -3,7 +3,7 @@ from activities.models import PostInteraction, TimelineEvent from activities.services import TimelineService from django.http import Http404, HttpRequest -from django.shortcuts import get_object_or_404 +from api.views import get_object_or_404 from hatchway import ApiResponse, QueryOrBody, api_view from api import schemas diff --git a/api/views/polls.py b/api/views/polls.py index 2b47f72f..04c5a319 100644 --- a/api/views/polls.py +++ b/api/views/polls.py @@ -1,5 +1,5 @@ from django.http import Http404 -from django.shortcuts import get_object_or_404 +from api.views import get_object_or_404 from hatchway import api_view, QueryOrBody from activities.models import Post, PostInteraction diff --git a/api/views/push.py b/api/views/push.py index 2fc5f7ee..52fffce2 100644 --- a/api/views/push.py +++ b/api/views/push.py @@ -1,6 +1,6 @@ from django.conf import settings from django.http import Http404 -from django.shortcuts import get_object_or_404 +from api.views import get_object_or_404 from api import schemas from api.decorators import scope_required diff --git a/api/views/report.py b/api/views/report.py index 14fe39a4..6bdc930d 100644 --- a/api/views/report.py +++ b/api/views/report.py @@ -1,4 +1,4 @@ -from django.shortcuts import get_object_or_404 +from api.views import get_object_or_404 from activities.models import Post from api import schemas diff --git a/api/views/statuses.py b/api/views/statuses.py index e1092c22..4d11157d 100644 --- a/api/views/statuses.py +++ b/api/views/statuses.py @@ -4,7 +4,7 @@ from django.db.models import Q from django.http import HttpRequest -from django.shortcuts import get_object_or_404 +from api.views import get_object_or_404 from django.utils import timezone from pydantic import ConfigDict diff --git a/api/views/tags.py b/api/views/tags.py index 2b81ba60..5b6a8bf9 100644 --- a/api/views/tags.py +++ b/api/views/tags.py @@ -1,7 +1,7 @@ import datetime from django.http import HttpRequest -from django.shortcuts import get_object_or_404 +from api.views import get_object_or_404 from django.utils import timezone from activities.models import FanOut, Hashtag, Post diff --git a/api/views/timelines.py b/api/views/timelines.py index 008857ea..4a340f1e 100644 --- a/api/views/timelines.py +++ b/api/views/timelines.py @@ -1,5 +1,5 @@ from django.http import HttpRequest -from django.shortcuts import get_object_or_404 +from api.views import get_object_or_404 from activities.models import Post, TimelineEvent from activities.services import TimelineService From 5a0adca3540f0b38bd4fce8784af93e69fca5831 Mon Sep 17 00:00:00 2001 From: Henri Dickson <90480431+alphatownsman@users.noreply.github.com> Date: Mon, 6 Apr 2026 18:12:45 -0400 Subject: [PATCH 4/4] fix: treat 410 Gone as final, no retry on POST delivery 410 Gone indicates a resource is permanently gone. Retrying delivery to such an inbox will never succeed. Treat 410 like 404 by not raising ValueError, allowing the fan_out state machine to transition to sent instead of retrying. Fixes EGGPLANT-15F Co-Authored-By: Claude Sonnet 4.6 (1M context) --- core/signatures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/signatures.py b/core/signatures.py index 2e01927a..799cb5f7 100644 --- a/core/signatures.py +++ b/core/signatures.py @@ -343,7 +343,7 @@ def signed_request( method == "post" and response.status_code >= 400 and response.status_code < 500 - and response.status_code != 404 + and response.status_code not in [404, 410] ): raise ValueError( f"POST error to {uri}: {response.status_code} {response.content!r}"