Skip to content

Bug: refreshMessageMetadata decodes a response shape the route never sends — always returns [] #104

Closed

Description

@Adron

Found while implementing #96 (see PR #103), and verified independently against the backend source.

The mismatch

APIClient.refreshMessageMetadata (Services/APIClient.swift:382-391) decodes:

struct Response: Decodable {
    struct Meta: Decodable { let links: [MessageLinkPreview]? }
    let metadata: Meta?          // <- expects a nested "metadata" object
}
return response.metadata?.links ?? []

app/api/messages/[id]/metadata/route.ts returns, at both exit points (:86, :105):

return NextResponse.json(serialize({ links: linkMetadataItems }), …)

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

Blocks

#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.

Files: Services/APIClient.swift:382, Models/Message.swift,
InterlinedListTests/APIClientTests/*.

Activity

  1. added
    bugSomething isn't working
    ws:papercutsW8 — small parity papercuts
    on Sep 16, 2026
  2. Adron commented on Sep 16, 2026

    @Adron
    MemberAuthor

    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.

    Consequences

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingws:papercutsW8 — small parity papercuts

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions