Fix: resolve Bluesky @mention handles to DIDs (mentions caused "Invalid post data")#93
Open
Schrall wants to merge 2 commits into
Open
Fix: resolve Bluesky @mention handles to DIDs (mentions caused "Invalid post data")#93Schrall wants to merge 2 commits into
Schrall wants to merge 2 commits into
Conversation
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>
There was a problem hiding this comment.
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Any scheduled Bluesky post that contains an
@mentionfails to publish withInvalid post data(AT ProtocolInvalidRequest). Posts without a mention publish fine.Root cause
In
BlueskyPublisher::parseFacets(), the mention facet is built with the raw handle in thedidfield:That comment is incorrect. An
app.bsky.richtext.facet#mentionrequires the target's DID (e.g.did:plc:…) — Bluesky does not resolve a handle placed indid. Sending a handle there makes the wholeapp.bsky.feed.postrecord invalid, socom.atproto.repo.createRecordrejects it withInvalidRequest, 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.resolveHandlebefore building the facet:public.api.bsky.app) andbsky.social.@handleas 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
socialHttp()client.#mentionfacet with the resolveddid:plc:….🤖 Generated with Claude Code