Skip to content

Retire the superseded Custom Local API, and send queued bytes verbatim - #2175

Merged
karlitschek merged 2 commits into
masterfrom
debt/retire-local-api
Sep 14, 2026
Merged

Retire the superseded Custom Local API, and send queued bytes verbatim#2175
karlitschek merged 2 commits into
masterfrom
debt/retire-local-api

Conversation

@karlitschek

Copy link
Copy Markdown
Member

Item 11 of the fresh look, first two of its three parts (the browser-test layer follows as its own PR).

Retired: the superseded half of the Custom Local API

Technical-Debt.md item 2 called this "a decision rather than a task". Decided: 19 of LocalController's 31 routes had no caller anywhere in src/ (the doc said 18 — /api/v1/account/{username}/stream was listed as still used and is not) and had been documented as deprecated for a release. They are removed:

  • the eight timelines: /api/v1/stream/{home,notifications,direct,timeline,federated,liked}, /api/v1/stream/tag/{hashtag}/, /api/v1/account/{username}/stream
  • GET /local/v1/post, GET /local/v1/post/replies, POST/DELETE /api/v1/post/like
  • /api/v1/current/{info,followers,following}, PUT /api/v1/account/summary
  • /api/v1/global/actor/{info,header}, GET /local/v1/search

With them goes the whole query layer only they used: the nine StreamService::getStream*() methods and the seven StreamRequest::getTimeline*_dep() / getTimelineLiked() / getTimelineTag() queries — which were also the last timeline reads on the wide SELECT DISTINCT (see #2174) — and six constructor dependencies LocalController no longer touched (SearchService, BoostService, LikeService, MiscService, ActorService, ActivityService). LocalController goes from 1,178 to 734 lines. The twelve routes the frontend does call stay (banner uploads, global/account and global/tags searches, profile fields, avatar, POST/DELETE /api/v1/post, /api/v1/current/follow).

Release note: docs/API.md keeps the table of removed routes with the Mastodon route to use instead, under "Removed". A caller of one gets a 404.

Delivery sends the stored bytes

Technical-Debt.md item 1. ActivityService::bodyFromQueue() returned json_encode(json_decode($queue->getActivity(), true), JSON_UNESCAPED_SLASHES). That defeated ForwardService on purpose: it queues a third party's getSource() verbatim so their Linked Data signature still verifies on arrival, and re-encoding (key order, escaping) is exactly what breaks such a signature. It returns the queued string now. What this instance writes itself is encoded once, with unescaped slashes, at queue time (generateRequestQueue()), so nothing it sends changes; the digest is computed over the same bytes. ActivityServiceTest asserts the wire body byte for byte instead of "the same JSON".

Verification

  • Full unit suite green (4565 tests; 55 fewer — the tests of the removed routes and the _dep delegation rows went with them; the three knownActor() guard tests were re-pointed at the avatar route, the one public route left on it). psalm no errors, php-cs-fixer clean.
  • DocumentationTest enforces that every path in docs/API.md is a route and vice versa — it is what caught the last stray mention.
  • The forwarded-bytes change wants a real peer to confirm a forwarded signature now verifies; the unit test pins that nothing re-encodes.

Version 0.19.39. Technical-Debt.md: both items moved to "what has been done", the remaining list renumbered.

🤖 Generated with Claude Code

@karlitschek
karlitschek force-pushed the debt/retire-local-api branch 2 times, most recently from 327b95b to 567e812 Compare September 14, 2026 06:24
Frank Karlitschek and others added 2 commits September 14, 2026 08:52
Nineteen of LocalController's thirty-one routes had no caller anywhere in
src/ and had been documented as deprecated for a release: the eight
/api/v1/stream/* and /account/{username}/stream timelines, the two
/local/v1/post reads, the like pair, /current/info, /current/followers and
/following, /account/summary, /global/actor/info and /header, and
/local/v1/search. They go, and with them the whole query layer only they
used -- the nine StreamService::getStream*() methods and the seven
StreamRequest::getTimeline*_dep() / getTimelineLiked() / getTimelineTag()
queries, which were also the last timeline reads on the wide SELECT
DISTINCT -- plus six constructor dependencies LocalController no longer
touches. docs/API.md keeps the table of what replaced each route, as the
release note. The twelve routes the frontend does call stay.

Delivery also stops re-encoding what it sends. bodyFromQueue() returned
json_encode(json_decode($activity)), which defeated ForwardService on
purpose: it queues a third party's getSource() verbatim so their Linked
Data signature still verifies on arrival, and re-encoding is exactly what
breaks such a signature. What this instance writes itself is encoded once
at queue time, so nothing it sends changes; the digest covers the same
bytes. ActivityServiceTest now asserts the body byte for byte.

Signed-off-by: Frank Karlitschek <frank@nextcloud.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Frank Karlitschek <frank@nextcloud.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@karlitschek
karlitschek merged commit 3695aa1 into master Sep 14, 2026
43 checks passed
@karlitschek
karlitschek deleted the debt/retire-local-api branch September 14, 2026 06:59
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.

1 participant