Skip to content

Replace Swagger UI with a custom API explorer - #1637

Open
dawsontoth wants to merge 1 commit into
stagefrom
claude/custom-api-explorer-1e00fd
Open

Replace Swagger UI with a custom API explorer#1637
dawsontoth wants to merge 1 commit into
stagefrom
claude/custom-api-explorer-1e00fd

Conversation

@dawsontoth

@dawsontoth dawsontoth commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Replaces the swagger-ui-react embed on the instance/cluster APIs tab with a custom, in-house
API explorer built entirely from the Studio design system. It reads the same runtime spec
(GET /api/openapi/rest) and preserves the CORS warning + one-click Enable CORS flow verbatim.

What's new

  • Hierarchical sidebar — resource → path → method, collapsible and filterable. Harper's specs are
    untagged, so grouping is by path/resource rather than the useless single "default" tag.
  • Per-operation docs — parameters, request/response schemas via a recursive model renderer.
  • Try it out — a credentialed request runner with a JSON body editor (Monaco), path/query/header
    inputs, a live request preview, a real response view, and a copy-able fetch snippet.
  • Authorize — a sidebar item that takes over the detail pane with Server + Authorization
    settings. Auth offers Cookie (session, default), Basic, and Bearer.
  • Server selector — Studio's computed REST URL plus the spec's declared servers, so try-it-out
    works even when REST isn't on the port Studio guesses.
  • Persistence — server + auth selections are saved to localStorage per entity, and cleared
    on sign-out.

Pure spec-parsing and request-building logic is extracted into unit-tested modules, with a mounted
component test for the explorer. Removes swagger-ui-react + @types/swagger-ui-react and their
~1,290-line lockfile subtree.

Closes #1530 — the RUM TypeError came from inside the vendored swagger-ui-react Authorize/persist
bundle; deleting the dependency removes it. Our Authorize never writes document.cookie or reads
.schema off a security scheme.

Closes #1580 — Swagger auto-populated a huge query string for GET /Table/ on relationship-heavy
schemas (→ 431). Our explorer starts query params empty and omits blanks, so no query string is
sent unless the user types one.

For the human reviewer

Start in spec.ts and
request.ts — the pure, unit-tested core; the
components render them. APIDocs.tsx keeps the CORS flow.

The judgment calls, most-consequential first:

  • Credentials in localStorage. Per request, the server + Basic password / Bearer token persist
    per entity, cleared on sign-out (authStore.signOutLocally; full sign-out already clears all of
    localStorage), and written via a fresh read-merge-write so a concurrent tab's sign-out isn't
    clobbered. Tradeoff: creds at rest between sessions (XSS/shared-machine exposure) vs. convenience
    and parity with the old Swagger persistAuthorization. Alternative: persist only server + auth
    type, keep secrets in memory. Chose persist-and-clear-on-signout. (The default Cookie mode
    stores no secret at all — it just uses the session cookie.)
  • Default server is Studio's computed URL, not the spec's. Cloud-safe (preserves prior behavior).
    On a split-port local instance the computed URL guesses port 80 and isn't reachable, so the
    reachable spec-declared server is one click away in the dropdown. The maintainer confirmed the
    cloud path runs the same code (only response JSON differs); happy to flip the default to the spec
    server if preferred.
  • Response cap = 512 KB, applied while streaming. readCappedBody reads response.body only up
    to the cap and cancels the reader, so a multi-MB collection is never fully buffered or rendered.
  • Resource grouping is by first path segment, and $ref resolution doesn't JSON-Pointer-unescape.
    Correct for Harper's generated specs (single-segment resources, simple schema names); noted as
    limitations for arbitrary OpenAPI producers.

Comments were audited twice against Harper's zero-new-comments default; the remainder are deliberate
contracts/rationale (the credentialed-fetch model, the ancestor-scoped cycle detection, the
per-entity persistence invariant). Flag any specific one you'd still cut.

Verification

  • Live smoke (dev:local against a local Harper instance): GET /leaderboard/200 OK;
    POST /game/{id} path-param substitution + Send gating; server selector switching to the reachable
    :9926; Authorize Cookie/Basic/Bearer; selections persisting across reload; sign-out clearing the
    stored settings. The maintainer confirmed the cloud/cluster path runs the same code — only the
    spec/response JSON differs — so the local smoke is representative.
  • Full gate on the pinned Node 24.19.0 — vitest run (311 files, 2541 passed), tsc -b,
    oxlint ., dprint check: all green. New tests cover spec parsing (incl. the example node budget),
    request building, streaming response cap, per-entity persistence (incl. the cross-tab stale-writer
    sequence), status colors, and a mounted-component test.

Review coverage

Four cross-model pre-push rounds via the standard CLI (--author claude), each independent:

  • Round 1 (7f12099): codex (graded) + gemini. Fixed: Unicode Basic-auth btoa crash,
    cross-instance try-it-out state leak (per-entity remount), generateExample sibling-$ref
    truncation, response render cap, path-template param derivation, filter allocation.
  • Round 2 (7209c74, --full): gemini. Added sign-out credential clearing; lowered the
    response cap.
  • Round 3 (c22edd8): gemini + cursor-grok. Fixed: authStore in-on-non-object
    crash-safety guard, mergeParameters $ref-param collision guard, memoized example generation.
  • Round 4 (1ff48aac, convergence): gemini — no new actionable findings; only repeats of
    decided items, so iteration stopped here.

Not run: Codex was unavailable from round 2 on (a ChatGPT-workspace spend cap, not a code
issue); the domain adjudicator failed locally every round (a known zero-byte-log issue on this
machine), so outside findings were hand-triaged rather than auto-adjudicated; Cursor was
available only in round 3. Dismissed with rationale, not silence: a claimed buildFetchSnippet
template-literal injection (traced — every ${ is escaped, so no interpolation survives a paste), an
"undefined body" crash (generateExample has no undefined return path; body is always a string),
and a "cookies sent to external servers" leak (browsers scope cookies to the destination origin).
Deferred as not-producible-by-Harper-specs: multi-segment resource grouping, JSON-Pointer $ref
unescaping, $ref parameter resolution, and the snippet's .json() on 204 responses.

Post-open, the gemini-code-assist PR bot raised three prototype-safety points; applied in
eb7ab94d (Object.hasOwn guards in forgetApiExplorerSettings, resolveRef, and
fillPathTemplate) and the threads resolved.

@kriszyp (Codex-assisted) then reviewed and caught five real issues, all fixed in c74932cc:
(1) the response cap only bounded rendering — now readCappedBody streams response.body up to the
cap and cancels the reader, so a multi-MB collection is never fully buffered; (2) a cross-tab
stale-writer could resurrect a signed-out entity's credentials — persistence now does a fresh
read-merge-write per entity (settings.ts) plus a storage-event refresh; (3) generateExample
was bounded against cycles but not acyclic 2^N fan-out — added a depth + node budget; (4) Send now
gates on required query/header/body inputs, not just path params; (5) the sidebar used max-height
so its list never scrolled — now a definite height. His cookie question is answered on the thread:
the default Cookie mode already authenticates via the session cookie and stores no secret in
localStorage.

Human-Review-Need: 4 @ c74932c

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request replaces the third-party swagger-ui-react dependency with a custom, in-house API explorer built using the native design system. The changes include the new explorer components, unit tests, and integration with the authentication store to clear persisted credentials on logout. The review feedback focuses on enhancing robustness against prototype pollution and unexpected keys by replacing direct property access and the in operator with safer checks (such as Object.prototype.hasOwnProperty.call or explicit type checks) when handling settings, schemas, and path parameters.

Comment thread src/features/auth/store/authStore.ts Outdated
Comment thread src/features/instance/apis/explorer/spec.ts
Comment thread src/features/instance/apis/explorer/spec.ts
@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 58.4% 7558 / 12941
🔵 Statements 58.92% 8122 / 13784
🔵 Functions 51.06% 1912 / 3744
🔵 Branches 52.6% 5461 / 10382
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/features/auth/store/authStore.ts 67.74% 56.09% 80.95% 67.44% 31-32, 103-109, 125-134, 144, 165, 184-195, 202-211, 266-269, 413-424, 468-474, 508, 512-519, 531, 544-572
src/features/instance/apis/explorer/ApiExplorer.tsx 84.21% 66.66% 72.22% 92.3% 41, 46-47, 51, 52
src/features/instance/apis/explorer/CodeBlock.tsx 66.66% 66.66% 50% 66.66% 27
src/features/instance/apis/explorer/EndpointList.tsx 63.63% 66.66% 69.23% 60% 50-57, 86, 108
src/features/instance/apis/explorer/MethodBadge.tsx 100% 100% 100% 100%
src/features/instance/apis/explorer/OperationDetail.tsx 100% 71.73% 100% 100%
src/features/instance/apis/explorer/SchemaView.tsx 24.44% 15.06% 20% 25.58% 23, 48-55, 58, 71-197
src/features/instance/apis/explorer/SettingsPanel.tsx 66.66% 70.58% 36.36% 66.66% 28, 84-144, 166, 185-196
src/features/instance/apis/explorer/StatusBadge.tsx 100% 100% 100% 100%
src/features/instance/apis/explorer/TryItOut.tsx 67.5% 59.32% 56.25% 68.57% 26, 30, 77-78, 84-88, 127, 216-243
src/features/instance/apis/explorer/request.ts 97.61% 87.75% 92.3% 97.61% 199-200
src/features/instance/apis/explorer/settings.ts 100% 88.88% 100% 100%
src/features/instance/apis/explorer/spec.ts 91.83% 80.79% 93.1% 92.08% 28, 234-242, 245, 259-261, 275, 290-292
src/features/instance/apis/explorer/types.ts 100% 100% 100% 100%
src/lib/storage/localStorageKeys.ts 100% 100% 100% 100%
Generated in workflow #1769 for commit c74932c by the Vitest Coverage Report Action

@dawsontoth
dawsontoth force-pushed the claude/custom-api-explorer-1e00fd branch from 1ff48aa to eb7ab94 Compare August 19, 2026 17:09
@dawsontoth
dawsontoth marked this pull request as ready for review August 19, 2026 17:16
@dawsontoth
dawsontoth requested a review from a team as a code owner August 19, 2026 17:16

@kriszyp kriszyp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sounds great. But can users use cookies to authenticate, without have to hand over credentials that get stored in localStorage?

🤖 Reviewed with Codex

Comment thread src/features/instance/apis/explorer/request.ts Outdated
Comment thread src/features/instance/apis/explorer/ApiExplorer.tsx Outdated
Comment thread src/features/instance/apis/explorer/spec.ts
Comment thread src/features/instance/apis/explorer/TryItOut.tsx Outdated
Comment thread src/features/instance/apis/explorer/ApiExplorer.tsx
Swap the swagger-ui-react embed on the instance/cluster "APIs" tab for a
custom explorer built from the in-house design system (Radix + Tailwind
tokens, Monaco, useCopyToClipboard). It reads the same runtime spec
(GET /api/openapi/rest) and preserves the CORS warning + one-click enable
flow verbatim.

- Hierarchical sidebar: resource -> path -> method, collapsible, filterable,
  with an "Authorize" item that takes over the detail pane with Server +
  Authorization settings.
- Per-operation docs (parameters, request/response schemas via a recursive
  model renderer) and an interactive "Try it out" runner with a JSON body
  editor, path/query/header inputs, live request preview, and a fetch code
  sample.
- Auth offers Cookie (session, default), Basic, and Bearer; Basic/Bearer add
  an Authorization header. Server is a selector (Studio-computed URL plus the
  spec's declared servers) so try-it-out works even when REST isn't on the
  guessed port.
- Server + auth selections persist to localStorage scoped per entity, and are
  cleared on sign-out (authStore.signOutLocally) so credentials can't outlive
  a session.

Closes the swagger-ui-react cookie-apiKey persist TypeError (#1530) by
removing the dependency, and the long auto-generated query strings that
caused 431s on relationship-heavy schemas (#1580) by never auto-populating
query params.

Pure spec-parsing and request-building logic is extracted and unit-tested,
plus a mounted-component test for the explorer. Removes swagger-ui-react/@types
and its lockfile subtree.

Closes #1530
Closes #1580

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dawsontoth
dawsontoth force-pushed the claude/custom-api-explorer-1e00fd branch from eb7ab94 to c74932c Compare August 20, 2026 04:34
@dawsontoth

Copy link
Copy Markdown
Contributor Author

Great question — yes. The default Cookie auth mode uses the browser's instance session cookie (credentials: "include") and stores no secret in localStorage; only {type:"cookie"} is persisted. Basic/Bearer are opt-in for testing as a specific identity, and they are the only modes that persist a secret (now cleared on sign-out). So a user who just wants cookie auth never hands over stored credentials.

🤖 Addressed by Claude Code

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.

Long queries from Swagger UI [RUM] New handled TypeError on API Docs — swagger-ui-react cookie apiKey persist (reading 'schema')

2 participants