api: five reads that were declared as writes are now GET
The permission model being built reads the HTTP method to decide whether a
non-owner may make a call: safe methods are reads, everything else is a write.
That only works if the method tells the truth. These five read something and
returned it while announcing themselves as writes, so a member would have been
denied a read they are entitled to because of a habit in how the route was
declared.
/api/file-browser/video-info POST {url} -> GET ?url=
/api/file-browser/video-playlist POST {url} -> GET ?url=
/api/server-settings/ocr/models POST {url} -> GET ?url=
/api/transmission/_officer/port-test POST -> GET
/api/jellyfin/_config/:id/test POST|GET -> GET only
The last one already answered to both, which is worse than either: a method that
means nothing cannot be the thing authorisation reads.
Deliberately stops at five. A sweep of all 100 mutating routes found many more
reads wearing POST, and they are staying, for two reasons that are not going
away: some need a request body GET cannot carry (/stt takes multipart audio;
/tts, /ocr, /transcribe take payloads), and some carry a credential, where a
query string is the wrong place — access logs, shell history and Referer headers
all capture those, request bodies do not (/tts/voices takes an apiKey, the four
/test endpoints take connection secrets, /local-providers/probe takes auth).
So the method alone can never carry the permission model, and the registry will
need an explicit per-route classification regardless. Converting these five is
worth it because it is free; converting the rest would be a breaking change
across 117 mobile call sites that buys nothing.
Web callers updated in the same commit; the sidecar contract comments now match.
Mobile has exactly one caller to change — transmissionPortTest in
packages/core/src/services/transmission.ts — and no shim was added, because an
endpoint answering to both methods is the problem this commit exists to fix.
docs/api-method-changes-2026-08-06.md is the handoff for the mobile team: what
changed, the one line to edit, what deliberately did NOT change and why, and how
to verify.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
# API method changes — 2026-08-06
|
||||
|
||||
**For the mobile team.** Five endpoints changed from `POST` to `GET`. **One of them affects the mobile
|
||||
app**; the other four have no mobile caller. This is a breaking change with no compatibility shim — see
|
||||
*What breaks and when* below.
|
||||
|
||||
---
|
||||
|
||||
## Why
|
||||
|
||||
Officer is growing a permission model. Members will get access to specific apps, and within an app the
|
||||
default is **read everything, write only your own data**. The rule that separates those two is the HTTP
|
||||
method: safe methods (`GET`, `HEAD`) are reads, everything else is a write.
|
||||
|
||||
That only works if the method tells the truth. These five endpoints read something and returned it,
|
||||
while announcing themselves as writes — so under the new model a member would be denied a read they are
|
||||
entitled to, for no reason other than a habit in how the route was declared.
|
||||
|
||||
Nothing else about them changed: same path, same response shape, same auth.
|
||||
|
||||
---
|
||||
|
||||
## The changes
|
||||
|
||||
| # | Endpoint | Was | Now | Mobile affected |
|
||||
|---|----------|-----|-----|-----------------|
|
||||
| 1 | `/api/file-browser/video-info` | `POST` body `{url}` | `GET ?url=` | no |
|
||||
| 2 | `/api/file-browser/video-playlist` | `POST` body `{url}` | `GET ?url=` | no |
|
||||
| 3 | `/api/server-settings/ocr/models` | `POST` body `{url}` | `GET ?url=` | no |
|
||||
| 4 | `/api/transmission/_officer/port-test` | `POST` no body | `GET` | **YES** |
|
||||
| 5 | `/api/jellyfin/_config/:id/test` | `POST` body `{}` | `GET` | no |
|
||||
|
||||
Note on #5: it already accepted `GET` as well as `POST`. It is now `GET` only, so that the method is a
|
||||
reliable signal rather than "whichever the caller felt like".
|
||||
|
||||
---
|
||||
|
||||
## What the mobile app has to change
|
||||
|
||||
**One line.** `packages/core/src/services/transmission.ts:126`
|
||||
|
||||
```ts
|
||||
// before
|
||||
export const transmissionPortTest = () =>
|
||||
request<{ open: boolean }>(`${T}/port-test`, { method: 'POST' });
|
||||
|
||||
// after
|
||||
export const transmissionPortTest = () =>
|
||||
request<{ open: boolean }>(`${T}/port-test`);
|
||||
```
|
||||
|
||||
The response is unchanged: `{ open: boolean }`.
|
||||
|
||||
I searched `monorepo-mobile/packages` and `monorepo-mobile/apps` for callers of the other four and
|
||||
found none. If you know of one outside those trees, it needs the same treatment — path and response are
|
||||
identical, only the method and the location of `url` move.
|
||||
|
||||
For the three that take a `url`, it moves from the JSON body to a query parameter and **must be
|
||||
percent-encoded**:
|
||||
|
||||
```ts
|
||||
`${base}/file-browser/video-info?url=${encodeURIComponent(url)}`
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What breaks and when
|
||||
|
||||
`port-test` returns **405** to a `POST` from the moment the platform is deployed. There is deliberately
|
||||
no transitional shim accepting both — the whole point of the change is that the method means something,
|
||||
and an endpoint answering to both methods means nothing.
|
||||
|
||||
The blast radius is one button in the Transmission screen ("test peer port"). It does not affect
|
||||
torrents, downloads, or anything else in that app. If that is still unacceptable timing, a shim is a
|
||||
two-line change on the platform side — ask and it can go in, with a date for removal.
|
||||
|
||||
---
|
||||
|
||||
## What did NOT change, and will not
|
||||
|
||||
Several endpoints look like the ones above but are staying `POST` **on purpose**. If you are tempted to
|
||||
"fix" them for consistency, please don't — both reasons below are deliberate.
|
||||
|
||||
**They need a request body that `GET` cannot carry:**
|
||||
|
||||
- `POST /api/chat/stt` — multipart audio upload
|
||||
- `POST /api/file-browser/tts`, `/tts-text`, `/ocr`, `/transcribe` — payloads to transform
|
||||
|
||||
**They carry a credential, and a query string is the wrong place for one.** Query strings are written to
|
||||
access logs, shell history, proxy logs and `Referer` headers; request bodies are not:
|
||||
|
||||
- `POST /api/server-settings/tts/voices` — takes an `apiKey`
|
||||
- `POST /api/server-settings/{smtp,tts,stt,ocr}/test` — take connection secrets
|
||||
- `POST /api/server-settings/local-providers/probe` — takes `{url, auth}`
|
||||
|
||||
These are reads that must remain `POST`. The permission model handles them with an explicit annotation
|
||||
rather than by inferring from the method, which is why the method change stops at five endpoints instead
|
||||
of sweeping the whole API.
|
||||
|
||||
Everything else that is `POST`/`PUT`/`PATCH`/`DELETE` genuinely mutates something and is unaffected.
|
||||
|
||||
---
|
||||
|
||||
## Verifying
|
||||
|
||||
Against a running platform, with a valid token:
|
||||
|
||||
```bash
|
||||
# should be 200
|
||||
curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $TOKEN" \
|
||||
"$BASE/api/transmission/_officer/port-test"
|
||||
|
||||
# should be 405
|
||||
curl -s -o /dev/null -w '%{http_code}\n' -X POST -H "Authorization: Bearer $TOKEN" \
|
||||
"$BASE/api/transmission/_officer/port-test"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Platform-side changes, for reference
|
||||
|
||||
Backend:
|
||||
- `src/servers/api/file-browser/router.ts` — `/video-info`, `/video-playlist`
|
||||
- `src/servers/api/server-settings/ocr.ts` — `/models`
|
||||
- `src/servers/sidecar/transmission/routes.ts` — `handlePortTest`
|
||||
- `src/servers/sidecar/jellyfin/config.ts` — the `test` action
|
||||
|
||||
Web callers, already updated:
|
||||
- `src/workspaces/officerdev/src/hooks/useFilesAPI.ts`
|
||||
- `src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/OCRSection.tsx`
|
||||
- `src/workspaces/officerdev/src/apps/Transmission/useTransmissionData.ts`
|
||||
- `src/workspaces/officerdev/src/apps/Jellyfin/useJellyfinData.ts`
|
||||
|
||||
The sidecar contract comments at the top of `transmission/index.ts` and `jellyfin/index.ts` were updated
|
||||
to match.
|
||||
Reference in New Issue
Block a user