issue-188: Implement the RPC client and error decoding in src/client.ts and src/errors.ts with unit tests #221
No reviewers
Labels
No labels
bug
chore
documentation
enhancement
feature
ready
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
david/pi-extensions-and-skills!221
Loading…
Reference in a new issue
No description provided.
Delete branch "feature/issue-188/implement-the-rpc-client-and-error-decoding-in-src"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
188: Implement the RPC client and error decoding in src/client.ts and src/errors.ts with unit tests
Closes #188 — 188.
Generated in 7m 50s.
Review findings
Now I'll apply the fixes. Starting with
client.ts:Nowerrors.ts:Now add the regression tests. Firstclient.test.ts:Nowerrors.test.ts:All 39 Penpot tests pass. Now the full suite (expecting the known pre-existing failure):Only the documented pre-existinglocal-install.test.mjsfailure remains — no new regressions. Let me review the final diffs:All feasible findings are fixed and the Penpot suite is green (39/39); full suite shows only the documented pre-existinglocal-install.test.mjsfailure. The commit-message finding requires rewriting git history, which this stage is explicitly forbidden from doing, so it remains unresolved.Review Findings — 188
Summary
Fixed all code-level findings in
client.tsanderrors.ts, with regression tests for each: token redaction on the non-JSON success path, transport errors for failed body reads on 2xx, explicitT | undefinedfor empty/204 bodies,POSTdefault when a body is supplied, Transitcodeextraction, status-aware Transit categorisation, a structurallooksLikeTransitcheck, bounds-guarded Transit cache references, and corrected/apiguidance. The Penpot suites pass 39/39; the fullnpm testrun is 183 pass / 1 fail, the single failure being the pre-existingscripts/local-install.test.mjs:1875issue unrelated to this diff. The commit-message finding could not be resolved without rewriting git history, which this stage is forbidden from doing.Critical
extensions/penpot/src/client.ts:161— the token is redacted only on the HTTP-error path (redactErroris applied at line 237), so the non-JSON success-body error built inparseSuccessBodyleaks the raw token into bothmessageandrawBody. Reproduced: withPENPOT_TOKEN=secret-token-123and a200 text/plainbody"leaked secret-token-123 here",penpotRequest("get-profile")returnsmessage: "...non-JSON success body: leaked secret-token-123 here"andrawBody: "leaked secret-token-123 here". This directly violates the file's stated invariant ("The token is never interpolated into a message or error object") and the existing test only guards the 500 path. Suggested fix: build the error inparseSuccessBodywithout the token, then wrap every returned error inredactError(error, token)inpenpotRequest(both the error branch and the finalparseSuccessBodycall), or passtokenintoparseSuccessBody; add a test mirroringpenpotRequest never leaks the token into the returned errorbut withstatus: 200, content-type: text/html.High
extensions/penpot/src/client.ts:227—const rawText = await response.text().catch(() => "");swallows a body-read failure (timeout/abort mid-read, truncated stream) and, on a 2xx,parseSuccessBodyturns the empty string into{ ok: true }. Reproduced with a200whosetext()rejects:penpotRequest("get-file")returns{"ok":true}— a transport failure is reported as a successful read with no data, so a caller will act onundefined(or crash onvalue.id). Suggested fix: only fall back to""when the response is non-ok; on a 2xx, surface the read failure as atransporterror (e.g.try { rawText = await response.text() } catch (error) { return { ok: false, error: transportError(command, baseUrl, error) } }), and add a test for the aborted-body-read case.extensions/penpot/src/client.ts:156—return { ok: true, value: undefined as T }(and the type at line 24,{ ok: true; value: T }) asserts a value of typeTfor an empty/204body, soconst r = await penpotRequest<Profile>("get-profile"); if (r.ok) r.value.idcompiles but throws at runtime whenever the server returns an empty body. The test atclient.test.ts(returns undefined for an empty success body) enshrines the unsafe behaviour rather than flagging it. Suggested fix: make the empty case explicit in the type — e.g.value?: TorPenpotResult<T | undefined>— or document/require callers to passT | undefined; either way add a compile-time-visible distinction so callers must narrow before dereferencing.Medium
extensions/penpot/src/errors.ts:367— the Transit branch ofdecodeErrorBodyreturns only{ category, message }and never populatesDecodedErrorBody.code, even though the JSON branch and the client docstring advertise that callers (the M2 commit path watching forrevn-conflict) can react to the servercode. Reproduced:decodeErrorBody(400, "application/json", '["^ ","~:type","~:validation","~:code","~:revn-conflict","~:explain","bad revn"]')yields{category:"validation", message:"Transit-encoded error payload: bad revn …"}with nocode. Suggested fix: extract the Transit map'scode(decodeTransitMap(parsed.value)?.code) into the returnedDecodedErrorBody, and add aclient.test.tscase assertingresult.error.code === "revn-conflict"round-trips from the response body.extensions/penpot/src/errors.ts:270—transitCategoryignores the HTTP status, so a400carrying a Transit body that lacks a~:type/~:explainis categorisedserver, while the identical status via the JSON path isvalidation(statusCategory(400)). Reproduced:decodeErrorBody(400, "application/json", '["^ ","~#uri","https://x/a"]')→category: "server". Suggested fix: passstatusintotransitCategoryand fall back tostatusCategory(status)when the decoded payload does not explicitly sayvalidation(i.e.return type === "validation" ? "validation" : statusCategory(status)).Low
extensions/penpot/src/errors.ts:152—looksLikeTransitusestrimmed.includes('"~:')/'"~#', so ordinary JSON error bodies whose string values start with a Transit marker are misclassified as Transit and their message is rewritten. Reproduced:decodeErrorBody(500, "application/json", '{"message":"~:foo"}')returns"Transit-encoded error payload: foo (raw body: {"message":"~:foo"})"instead of the server'smessage. Suggested fix: restrict the substring heuristic to keys/values that are themselves tagged (e.g. only treat it as Transit when a~:/~#token occurs at a structural position, or require the parsed JSON to contain at least one~-prefixed key), and add a regression test for a JSON body containing"~:.extensions/penpot/src/errors.ts:229—readEntrytreats any integer in a cache-coded map as a cache reference and only pushes non-integer entries intocache, so a literal numeric value decodes tocache[n](undefined) and later cache indices can desynchronise from Transit's own numbering. Reproduced:["^ ","~:explain","oops","~:status",400]decodesstatusasundefined. Suggested fix: guard the lookup (typeof entry === "number" && entry >= 0 && entry < cache.length ? cache[entry] : entry) and, per the Transit cache-coding rules, only treat an integer as a reference when it is within the cache bounds; add a test with a numeric entry.extensions/penpot/src/client.ts:204—method = options.method ?? "GET"combined with the body handling at line 220 meanspenpotRequest("update-file", { body: {...} })(body but no explicit method) sends a GET with a body, whichfetchrejects withTypeError: Request with GET/HEAD method cannot have body; that is then surfaced astransport("could not reach Penpot at …"), misattributing a caller error to the network. Suggested fix: default toPOSTwhenoptions.body !== undefined(or fail fast with aconfig/caller error explaining that a body requiresmethod: "POST"), and add a test.extensions/penpot/src/errors.ts:94—NOT_FOUND_MESSAGEtells the user to "ensure PENPOT_URL is the instance base URL with no /api suffix", butenv.ts:normalizeBaseUrlalready strips a trailing/apibeforeclient.tsever builds a URL (the client test atclient.test.tsasserts this double-append cannot happen). The advice therefore contradicts actual behaviour and can send a user chasing a non-issue. Suggested fix: drop the/api-suffix clause or reword it to state that the suffix is normalised automatically, and keep the guidance to the command name / file id causes.f73dc20(commit message) — the commit is188: Implement the RPC client …, but AGENTS.md specifiesissue-<N>: <summary>forfeature/bug/enhancementwork (and the branch already correctly followsfeature/issue-188/…). Suggested fix: amend the commit message toissue-188: Implement the RPC client and error decoding in src/client.ts and src/errors.ts with unit testsbefore the PR is opened.Documentation
Updated files: