hcengineering/platform · error

Workspace mismatch

Error message

Workspace mismatch

What it means

withOwnerAsync rejects with 401 'Workspace mismatch' when the route defines req.params.workspace and the token's workspace claim (req.token.workspace) does not equal it. The token is valid but was issued for a different workspace than the one the request targets.

Source

Thrown at services/payment/pod-payment/src/middleware.ts:80

    res.status(403).json({ message: 'Missing auth info' }).end()
    return
  }

  req.loginInfo = loginInfo
  next()
}

export const withOwner = (req: RequestWithAuth, res: Response, next: NextFunction): void => {
  void withOwnerAsync(req, res, next)
}

const withOwnerAsync = async (req: RequestWithAuth, res: Response, next: NextFunction): Promise<void> => {
  if (req.token === undefined || req.token == null) {
    res.status(401).json({ message: 'Token error' }).end()
    return
  }
  if (req.params.workspace != null && req.token.workspace !== req.params.workspace) {
    res.status(401).json({ message: 'Workspace mismatch' }).end()
    return
  }
  if (req.token.account !== systemAccountUuid && req.token.extra?.admin !== 'true') {
    const accountClient = getAccountClient(req.headers.authorization?.split(' ')[1])
    const loginInfo = req.loginInfo ?? (await accountClient.getLoginInfoByToken())
    if (loginInfo == null) {
      res.status(403).json({ message: 'Missing auth info' }).end()
      return
    }
    if (!('role' in loginInfo)) {
      res.status(401).json({ message: 'Missing workspace role' }).end()
      return
    }
    if (loginInfo.role !== AccountRole.Owner) {
      res.status(401).json({ message: 'Workspace owners only' }).end()
      return
    }
  }

View on GitHub (pinned to 63e28dc964)

Solutions

  1. Use the workspace id that matches the token's workspace claim, or re-authenticate to get a token for the target workspace
  2. In the client, refresh the token whenever the active workspace changes
  3. Check for stale/hardcoded workspace ids in API calls and fix the URL construction
  4. Verify token issuance includes the correct workspace claim for the session

Example fix

// before
await api.get(`/ws/${oldWorkspaceId}/invoices`, { headers: { Authorization: `Bearer ${oldWsToken}` } })
// after
await api.get(`/ws/${currentWorkspaceId}/invoices`, { headers: { Authorization: `Bearer ${currentWorkspaceToken}` } })
Defensive patterns

Strategy: validation

Validate before calling

// decode token workspace claim client-side and compare to target workspace
const claims = JSON.parse(atob(token.split('.')[1]))
if (claims.workspace !== targetWorkspaceId) {
  await switchWorkspaceToken(targetWorkspaceId) // refresh token scoped to target workspace
}

Type guard

function tokenMatchesWorkspace(token: { workspace?: string } | null, workspaceId: string): token is { workspace: string } {
  return token != null && typeof token.workspace === 'string' && token.workspace === workspaceId
}

Try / catch

try {
  const res = await callWorkspaceApi(workspaceId)
} catch (err) {
  if (err.response?.status === 401 && err.response.data?.message === 'Workspace mismatch') {
    await refreshTokenForWorkspace(workspaceId)
    // retry once; otherwise reset client to the token's workspace
  }
}

Prevention

When it happens

Trigger: Calling /ws/:workspace/... with a :workspace path param that differs from the workspace encoded in the bearer token — e.g. client app switched workspaces but kept the old token, or a hardcoded workspace id in the URL.

Common situations: Frontend switching active workspace without refreshing the token; copying an API URL from another workspace/user; cached tokens stored per-user rather than per-workspace; workspace renamed/re-created so old token claims no longer match.

Related errors


AI-assisted analysis of hcengineering/platform@63e28dc964 (2026-08-29). Data as JSON: /api/errors/19e55db6b1e17373. Report an issue: GitHub.