Skip to content

Validate API responses at the boundary instead of trusting the generated types #1050

Description

@EhabY

Problem

CoderApi extends the SDK's Api, which casts response bodies to the generated TypeScript types with no runtime check. Any endpoint that answers 2xx with a body that isn't the expected shape flows into the extension as if it were valid, and we crash later at the first unguarded property access — far from the cause, with a message that says nothing about which URL or endpoint was wrong.

Concrete case: if /api/v2/users/me returns 200 with something that isn't a User (a non-Coder service on the configured URL, an HTML error page from a proxy, a partial body), login succeeds and we fail here instead:

// src/deployment/deploymentManager.ts
const isOwner = user?.roles.some((r) => r.name === "owner") ?? false;
// TypeError: Cannot read properties of undefined (reading 'some')

The user sees Cannot read properties of undefined (reading 'some'). Via a deep link it surfaces as a Failed to handle URI modal with that same text. Note the ?. guards user but not roles, so the error is doubly misleading — it can only happen when user is defined.

The same exposure exists at every other getAuthenticatedUser() call site (loginCoordinator ×4, oauth/authorizer, DeploymentManager.#verifyCredentials) and for every other response we read into without validating.

Proposal

1. Validate in one place. zod is already in use (DeploymentSchema, src/api/api-helper.ts). Add a UserSchema and override the method on CoderApi, which covers all six call sites without touching any of them:

override async getAuthenticatedUser(): Promise<User> {
  return parseApiResponse(
    UserSchema,
    await super.getAuthenticatedUser(),
    "/api/v2/users/me",
  );
}

Keep schemas permissive — only the fields we actually read, unknown fields allowed — so a newer deployment adding fields never breaks login. The point is to catch "this isn't a user object", not to mirror the API.

2. Throw a typed error with an actionable message, not a raw ZodError:

export class InvalidApiResponseError extends Error {
  constructor(endpoint: string, url: string, options?: { cause?: unknown }) {
    super(
      `${url} did not return a valid Coder API response for ${endpoint}. ` +
        `Check that the URL points to a Coder deployment.`,
      options,
    );
  }
}

Login and URI-handling paths already funnel errors through errToStr, so this reaches the modal as-is. src/command/diagnosticFlow.ts already does the equivalent for CLI output (… did not match the expected format) — worth matching that wording and its "check Output > Coder" hint. Log the ZodError details rather than showing them.

3. Guard the immediate crash: user?.roles?.some(...). One character, but it only relocates the failure — validation is the real fix, so this shouldn't land alone.

Scope

Start with the endpoints on the login and connect paths (users/me, workspace, agents, deployment SSH config). No need to schema-ify the whole SDK surface; the value is at the boundary where a URL might not be a Coder deployment at all.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions