Skip to content

Fix: resolve Bluesky @mention handles to DIDs (mentions caused "Invalid post data")#93

Open
Schrall wants to merge 2 commits into
trypostit:mainfrom
Schrall:fix/bluesky-mention-facet-resolve-did
Open

Fix: resolve Bluesky @mention handles to DIDs (mentions caused "Invalid post data")#93
Schrall wants to merge 2 commits into
trypostit:mainfrom
Schrall:fix/bluesky-mention-facet-resolve-did

Conversation

@Schrall

@Schrall Schrall commented Jun 11, 2026

Copy link
Copy Markdown

Problem

Any scheduled Bluesky post that contains an @mention fails to publish with Invalid post data (AT Protocol InvalidRequest). Posts without a mention publish fine.

Root cause

In BlueskyPublisher::parseFacets(), the mention facet is built with the raw handle in the did field:

'$type' => 'app.bsky.richtext.facet#mention',
'did' => $handle, // Will be resolved by Bluesky

That comment is incorrect. An app.bsky.richtext.facet#mention requires the target's DID (e.g. did:plc:…) — Bluesky does not resolve a handle placed in did. Sending a handle there makes the whole app.bsky.feed.post record invalid, so com.atproto.repo.createRecord rejects it with InvalidRequest, surfaced to the user as "Invalid post data". The result is that every post containing a mention silently fails.

Fix

Resolve each handle to a DID via com.atproto.identity.resolveHandle before building the facet:

  • Tries the account's own PDS first (authenticated), then falls back to the public AppView (public.api.bsky.app) and bsky.social.
  • Resolutions are cached per post (a handle mentioned twice is resolved once).
  • If a handle cannot be resolved, the mention facet is skipped so the post still publishes with the @handle as plain text, rather than failing the entire post.

This keeps mentions working as real, clickable, notifying mentions and removes the failure mode entirely.

Tests

Adds two feature tests to tests/Feature/Services/Social/BlueskyPublisherTest.php:

  • bluesky publisher resolves mentions to DIDs as facets — asserts the mention facet carries the resolved DID, not the handle.
  • bluesky publisher skips mention facet when handle cannot be resolved — asserts graceful degradation (post still publishes, handle stays as plain text, no mention facet).

Notes

  • No new dependencies; uses the existing socialHttp() client.
  • Verified end-to-end against a live self-hosted instance: before the fix every mention post returned "Invalid post data"; after it, the published record contains a proper #mention facet with the resolved did:plc:….

🤖 Generated with Claude Code

A mention facet (app.bsky.richtext.facet#mention) requires the target's
DID in its `did` field. The publisher put the raw handle there with the
comment "Will be resolved by Bluesky", but Bluesky does not resolve
handles at that point — it rejects the whole record with InvalidRequest,
surfaced to users as "Invalid post data". As a result, every scheduled
post that contained an @mention failed to publish, while posts without a
mention published fine.

Resolve each handle to a DID via com.atproto.identity.resolveHandle
(account PDS first, then the public AppView / bsky.social as fallback),
cache resolutions per post, and use the resolved DID in the facet. If a
handle cannot be resolved, the facet is skipped so the post still
publishes with the @handle as plain text instead of failing entirely.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 11, 2026 13:28

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds robust Bluesky mention handling by resolving @handles to DIDs before publishing, preventing invalid post records when mentions are present.

Changes:

  • Update facet parsing to resolve mention handles to DIDs (with caching + skip-on-failure behavior).
  • Add handle→DID resolution helper with multi-endpoint fallback.
  • Add feature tests covering resolved-mention facets and unresolved-handle behavior.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
tests/Feature/Services/Social/BlueskyPublisherTest.php Adds tests validating DID mention facets and behavior when handle resolution fails.
app/Services/Social/BlueskyPublisher.php Resolves mention handles to DIDs during facet generation and adds resolution helper with fallback endpoints.

Comment on lines +281 to +297
$endpoints = array_values(array_unique(array_filter([
$service,
'https://public.api.bsky.app',
'https://bsky.social',
])));

foreach ($endpoints as $endpoint) {
try {
$request = $this->socialHttp();
if ($account && $endpoint === $service) {
$request = $request->withToken($account->access_token);
}

$response = $request->get(
"{$endpoint}/xrpc/com.atproto.identity.resolveHandle",
['handle' => $handle],
);
Comment on lines +303 to +310
} catch (\Throwable $e) {
Log::warning('Bluesky handle resolution failed', [
'handle' => $handle,
'endpoint' => $endpoint,
'error' => $e->getMessage(),
]);
}
}
Comment on lines +107 to +115
Http::fake([
'https://bsky.social/xrpc/com.atproto.identity.resolveHandle*' => Http::response([
'did' => 'did:plc:friend456',
], 200),
'https://bsky.social/xrpc/com.atproto.repo.createRecord' => Http::response([
'uri' => 'at://did:plc:testuser123/app.bsky.feed.post/3abc123xyz',
'cid' => 'bafyreiabc123',
], 200),
]);
Comment on lines +119 to +124
Http::assertSent(function ($request) {
$record = $request['record'] ?? null;

if (! $record || ! isset($record['facets'])) {
return false;
}
- resolveHandleToDid: rtrim trailing slash on the service URL before building
  the request URL and before the `=== $service` auth check (avoids double-slash
  URLs and mis-detected auth).
- Log per-endpoint resolution attempts at debug; emit a single warning only
  after all endpoints are exhausted (less noise during transient outages).
- Tests: fake resolveHandle with a wildcard so the case is isolated from the
  configured service URL; read request payload via $request->data().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants