Skip to content

fix(api): apiError discards the status and the conflict payload the API deliberately returns #82

Description

@xe-nvdk

Found while wiring #35's save flow.

The problem

src/lib/cloudApi.ts:

async function apiError(res: Response, fallback: string) {
  ...
  return new Error(body?.error ?? fallback);
}

It returns a bare Error carrying only a message. Two things are thrown away:

  1. res.status — so no caller can distinguish a 409 from a 500, or a 403 from a 404, without string-matching the message.
  2. Everything else in the body. The dashboards API deliberately puts structured data there. src/routes/api/v1/orgs/[org_id]/dashboards/_shared.ts returns currentVersion on a version conflict, and its comment says why: "so a save-conflict dialog can offer more than 'someone changed this, reload'". warnings is discarded the same way — and feat(dashboards): gate auto-refresh on owner/admin rather than any writer #60 requires a clamp to be "a visible warning, not a silent change".

So the one place that produces a rich error and the one place that consumes it are connected by a function that flattens it.

Why it matters now

src/routes/(app)/d/[uid]/+page.svelte had to bypass cloudApi entirely and hand-roll its fetch to get at status, currentVersion and warnings. That is a second HTTP path to the same endpoint, which is how the two diverge later.

Fix

Give apiError a typed error that keeps what the response said:

export class ApiError extends Error {
  constructor(
    readonly status: number,
    message: string,
    readonly body: Record<string, unknown> = {},
  ) {
    super(message);
    this.name = 'ApiError';
  }
}

Then add updateDashboardRequest(orgId, uid, model, expectedVersion) alongside the existing createDashboardRequest, returning { dashboard, warnings }, and migrate the view page onto it so there is one path again.

Good first issue notes

Self-contained: one file plus one call site. Read createDashboardRequest and deleteDashboardRequest in the same file for the shape to follow, and _shared.ts's toErrorResponse for what the server actually sends.

Two things to be careful about:

  • Every existing throw await apiError(...) call site must keep working. ApiError extends Error, so err instanceof Error and err.message stay true — check the catch blocks that do err instanceof Error ? err.message : ... before assuming.
  • Do not log or render body wholesale. Some endpoints put upstream detail in error bodies; the view page shows message and currentVersion and nothing else. The same rule is documented on QueryError.detail in src/lib/dashboard/queryRunner.ts.

Activity

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 workinggood first issueGood for newcomers

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions