You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Bug: refreshMessageMetadata decodes a response shape the route never sends — always returns [] #104
Top level links, no metadata wrapper. serialize() normalizes values; it never restructures.
So response.metadata is always nil and the function always returns [] against the live API.
Each entry is also a full LinkMetadataItem — nested metadata object, thumbnail rather than image, plus platform / fetchStatus / fetchedAt. MessageLinkPreview's flat {url,title,description,image} shape matches nothing the backend sends; the closest existing type is CanonicalLinkPreview, which uses imageUrl.
The test is the worrying part
test_refreshMessageMetadata_* passes today. It passes because its fixture was hand-written to the assumed shape rather than a captured response. So the suite actively asserts the wrong contract.
This is the response-direction twin of the defaultVisibility bug (#46): there, a request key the
route ignored; here, a response shape the route never produces. Both are green-and-wrong.
Build this
Decode what the route actually returns: top-level links, entries shaped as LinkMetadataItem.
Prefer reusing LinkMetadataItem over keeping MessageLinkPreview alive — check whether MessageLinkPreview has any other consumer first; if not, delete it with its tests.
Rewrite the test fixture from the route source (or a captured live response), not from the
Swift type. Add a comment in the test naming the route file it was derived from.
Confirm the same mistake is not present in the sibling GET on that route (:47-48 returns
the same { links } shape).
Acceptance criteria
A fixture matching the real route shape decodes to a non-empty array.
#103 / #96 are inert until this lands. Does not affect #94, which ignores the response — the POST
still causes the server to fetch and persist metadata.
Implemented in #106 (stack: #106 → #103 → #94). Closing here — further work tracks in the PR.
What the route actually sends, traced to the producer
Read from app/api/messages/[id]/metadata/route.ts, lib/messages/metadata-fetcher.ts, lib/http/serialize.ts and lib/types/index.ts: { "links": [...] } at the top level from both
exits, entries produced by fetchMultipleLinkMetadata — i.e. exactly LinkMetadataItem
(url, platform, optional nested metadata{thumbnail,title,description,text,type,ogType}, fetchStatus, fetchedAt). Same producer as GET /api/link-metadata, which iOS already decoded
correctly — so the right shape was in the codebase all along, one type away.
links is now decoded as required, so a future shape change throws instead of silently
yielding [] again. The publish path's try? still swallows it, so behaviour there is unchanged.
The old fixture is retained as a test asserting it now throws — the wrong contract is pinned as
wrong rather than deleted.
Note for whoever merges
#106 undoes part of #103, so the two are worth squashing together at merge rather than landing a
conversion and then removing it in the next commit.
Still blocked for the user-visible outcome
#105 — FeedView renders from a private @State mirror keyed on .count, so the in-place linkMetadata update still isn't drawn until a full reload. Being fixed separately.
Activity
Adron commentedon Sep 16, 2026
Implemented in #106 (stack: #106 → #103 → #94). Closing here — further work tracks in the PR.
What the route actually sends, traced to the producer
Read from
app/api/messages/[id]/metadata/route.ts,lib/messages/metadata-fetcher.ts,lib/http/serialize.tsandlib/types/index.ts:{ "links": [...] }at the top level from bothexits, entries produced by
fetchMultipleLinkMetadata— i.e. exactlyLinkMetadataItem(
url,platform, optional nestedmetadata{thumbnail,title,description,text,type,ogType},fetchStatus,fetchedAt). Same producer asGET /api/link-metadata, which iOS already decodedcorrectly — so the right shape was in the codebase all along, one type away.
Consequences
row's shape, so
LinkMetadataItem.init(preview:)had nothing to convert. The rest of feat(messages): apply refreshed link metadata to posted row #103 —AppDataStore.applyLinkMetadata,Message.linkMetadatabecomingvar, theComposeViewwiring —survives and now receives real entries.
MessageLinkPreviewis deleted. Its only references were the broken decode and feat(messages): apply refreshed link metadata to posted row #103'sconversion.
linksis now decoded as required, so a future shape change throws instead of silentlyyielding
[]again. The publish path'stry?still swallows it, so behaviour there is unchanged.wrong rather than deleted.
Note for whoever merges
#106 undoes part of #103, so the two are worth squashing together at merge rather than landing a
conversion and then removing it in the next commit.
Still blocked for the user-visible outcome
#105 —
FeedViewrenders from a private@Statemirror keyed on.count, so the in-placelinkMetadataupdate still isn't drawn until a full reload. Being fixed separately.