{"record":{"id":"a8080b48b8862de6","repo":"different-ai/openwork","slug":"failed-to-revoke-access-response-status-a8080b","errorCode":null,"errorMessage":"Failed to revoke access (${response.status}).","messagePattern":"Failed to revoke access \\((.+?)\\)\\.","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"ee/apps/den-web/app/(den)/dashboard/_components/team-access-data.tsx","lineNumber":146,"sourceCode":"        .filter((item): item is TeamPluginAccessItem => item !== null);\n    },\n  });\n}\n\nexport function useRevokeTeamPluginAccess() {\n  const queryClient = useQueryClient();\n  const { runReauthableAction } = useOrgDashboard();\n\n  return useMutation({\n    mutationFn: async (input: { teamId: string; pluginId: string; grantId: string }) => {\n      await runReauthableAction(\"revoke-team-plugin-access\", async () => {\n        const { response, payload } = await requestJson(\n          `/v1/plugins/${encodeURIComponent(input.pluginId)}/access/${encodeURIComponent(input.grantId)}`,\n          { method: \"DELETE\" },\n          15000,\n        );\n        if (response.status !== 204 && !response.ok) {\n          throw getRequestError(payload, response, `Failed to revoke access (${response.status}).`);\n        }\n      });\n      return input.teamId;\n    },\n    onSuccess: (teamId) => {\n      queryClient.invalidateQueries({ queryKey: teamAccessQueryKeys.detail(teamId) });\n    },\n  });\n}\n","sourceCodeStart":128,"sourceCodeEnd":156,"githubUrl":"https://github.com/different-ai/openwork/blob/2b7df46e8ae1517d64c896c7793d2d52ec845669/ee/apps/den-web/app/(den)/dashboard/_components/team-access-data.tsx#L128-L156","documentation":"Thrown by useRevokeTeamPluginAccess when DELETE /v1/plugins/{pluginId}/access/{grantId} returns a status other than 204 or generic non-ok. This removes a team's granted access to a plugin; on success the team-access detail query is invalidated. Like the other den-web mutations, getRequestError appends the server message and converts 403 error:'reauth' into ReauthRequiredError.","triggerScenarios":"DELETE /v1/plugins/{pluginId}/access/{grantId} returns 401 (expired session), 403 (no admin rights over the plugin/org, or reauth challenge), 404 (pluginId or grantId no longer exists — grant already revoked elsewhere), 409 (grant is locked, e.g. required by an assignment policy), 429, or 5xx. 15s timeout.","commonSituations":"Two admins revoke the same grant concurrently (second gets 404); the plugin was deleted leaving stale grant ids in the UI; role downgraded mid-session; long-idle tab with expired token.","solutions":["Read the appended server message to distinguish 403 (permissions) vs 404 (already revoked).","On 404, treat the grant as already revoked: invalidate teamAccessQueryKeys.detail(teamId) and refresh the UI.","Handle ReauthRequiredError (sign-in) and retry the revoke.","Verify the caller still has plugin/org admin rights for 403.","Retry on 429/5xx with backoff; check Den server health for persistent failures."],"exampleFix":"// before\nrevokeAccess.mutate(input, { onError: (e) => toast(e.message) });\n// after: tolerate already-revoked\nrevokeAccess.mutate(input, {\n  onError: (e) => {\n    if (/\\b404\\b/.test(e.message)) {\n      queryClient.invalidateQueries({ queryKey: teamAccessQueryKeys.detail(input.teamId) });\n      return;\n    }\n    toast(e.message);\n  },\n});","handlingStrategy":"try-catch","validationCode":"if (!input.pluginId || !input.grantId) throw new Error(\"pluginId and grantId are required to revoke access.\");","typeGuard":"function isReauthRequiredError(e: unknown): e is ReauthRequiredError {\n  return e instanceof ReauthRequiredError;\n}","tryCatchPattern":"try {\n  await revokeAccess.mutateAsync(input);\n} catch (err) {\n  if (isReauthRequiredError(err)) { promptSignIn(); return; }\n  if (/\\b404\\b/.test(err.message)) { // grant already revoked elsewhere\n    queryClient.invalidateQueries({ queryKey: teamAccessQueryKeys.detail(input.teamId) });\n    return;\n  }\n  showError(err.message);\n}","preventionTips":["Invalidate teamAccessQueryKeys.detail(teamId) on success (hook already does) so stale grant ids leave the UI.","Treat 404 as idempotent success for revoke.","Gate revoke buttons on plugin/org admin permission.","Refresh the grant list if the plugin detail query indicates the plugin changed.","Use backoff for 429/5xx instead of immediate user retries."],"tags":["http","api","plugins","access-control","den-web"],"backgroundTag":"http-request-failed","analyzedSha":"2b7df46e8ae1517d64c896c7793d2d52ec845669","analyzedAt":"2026-09-01T07:59:23.713Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}