From f11455f54ef7ec074332a82c2bf672d11cae4433 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Fri, 28 Aug 2026 04:57:25 -0700 Subject: [PATCH 1/2] Add tests for disconnect resolving a concurrent 404 Covers a second/concurrent disconnect where the catalog-provider or provider DELETE 404s because a prior call already removed the row: disconnectConnector should resolve instead of throwing. --- packages/connections/src/routes.test.ts | 66 +++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/packages/connections/src/routes.test.ts b/packages/connections/src/routes.test.ts index c58d6669e..e70679bd4 100644 --- a/packages/connections/src/routes.test.ts +++ b/packages/connections/src/routes.test.ts @@ -972,6 +972,72 @@ describe("disconnectConnector", () => { expect(result.disconnected).toBe(false); expect(calls.some((call) => call.method === "DELETE")).toBe(false); }); + + test("a concurrent disconnect (catalog provider DELETE 404s because a first call already removed it) resolves instead of throwing", async () => { + const { api } = fakeDisconnectAPI(({ method, path }) => { + if ( + method === "GET" && + path === `/api/tenants/${TENANT_ID}/catalog/providers` + ) { + return page([catalogProviderRow("mprv_1", "anthropic")]); + } + if ( + method === "DELETE" && + path === `/api/tenants/${TENANT_ID}/catalog/providers/mprv_1` + ) { + return { status: 404, data: { error: { code: "not_found" } } }; + } + if ( + method === "GET" && + path === `/api/tenants/${TENANT_ID}/providers?inherited=false` + ) { + return page([]); + } + return undefined; + }); + + const result = await disconnectConnector( + api, + [], + { tenantId: TENANT_ID, connectorId: "anthropic" }, + () => {}, + ); + + expect(result.disconnected).toBe(false); + }); + + test("a concurrent disconnect (provider DELETE 404s because a first call already removed it) resolves instead of throwing", async () => { + const { api } = fakeDisconnectAPI(({ method, path }) => { + if ( + method === "GET" && + path === `/api/tenants/${TENANT_ID}/catalog/providers` + ) { + return page([]); + } + if ( + method === "GET" && + path === `/api/tenants/${TENANT_ID}/providers?inherited=false` + ) { + return page([providerRow("prv_1", "anthropic")]); + } + if ( + method === "DELETE" && + path === `/api/tenants/${TENANT_ID}/providers/prv_1` + ) { + return { status: 404, data: { error: { code: "not_found" } } }; + } + return undefined; + }); + + const result = await disconnectConnector( + api, + [], + { tenantId: TENANT_ID, connectorId: "anthropic" }, + () => {}, + ); + + expect(result.disconnected).toBe(false); + }); }); describe("DELETE /:connectorId/disconnect", () => { From a671051d8a9adc22243c3b4078c4f73cef17756f Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Fri, 28 Aug 2026 04:58:00 -0700 Subject: [PATCH 2/2] Disconnect: treat a 404 on the provider deletes as already removed disconnectConnector threw on any non-204 from DELETE /catalog/providers/:id or DELETE /providers/:id, so a second or concurrent disconnect (the hub 404s once a prior call already removed the row) collapsed into a 500 "Couldn't disconnect - try again" even though the connector was disconnected. A 404 is now treated as already removed; only a genuinely unexpected status still fails. The route's catch block also reports the failure through @corbits/error-sink so a real 500 carries a refId and the upstream cause. Fixes CL-7127. --- bun.lock | 1 + packages/connections/package.json | 1 + packages/connections/src/routes.ts | 28 +++++++++++++++++++++++----- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/bun.lock b/bun.lock index 61acacdc1..9800ea5f2 100644 --- a/bun.lock +++ b/bun.lock @@ -568,6 +568,7 @@ "version": "0.0.1", "dependencies": { "@corbits/credential-providers": "workspace:*", + "@corbits/error-sink": "workspace:*", "@corbits/inference-settings": "workspace:*", "@corbits/mcp-tools": "workspace:*", "@intx/crypto": "0.3.0", diff --git a/packages/connections/package.json b/packages/connections/package.json index 897ed8882..188faf99b 100644 --- a/packages/connections/package.json +++ b/packages/connections/package.json @@ -19,6 +19,7 @@ }, "dependencies": { "@corbits/credential-providers": "workspace:*", + "@corbits/error-sink": "workspace:*", "@corbits/inference-settings": "workspace:*", "@corbits/mcp-tools": "workspace:*", "@intx/crypto": "0.3.0", diff --git a/packages/connections/src/routes.ts b/packages/connections/src/routes.ts index c2b713c75..a181e73a3 100644 --- a/packages/connections/src/routes.ts +++ b/packages/connections/src/routes.ts @@ -22,6 +22,7 @@ import { } from "@intx/types"; import type { RequireGrant, TenantEnv } from "@intx/hub-api"; import { hasUsableModel } from "@corbits/inference-settings"; +import { reportError } from "@corbits/error-sink"; import { cookiesFromHeader, createHubAPI, @@ -114,11 +115,17 @@ export async function disconnectConnector( cookies, ); if (deletedCatalogProvider.status !== 204) { - throw new Error( - `couldn't remove the catalog provider for ${args.connectorId} (status ${String(deletedCatalogProvider.status)})`, + if (deletedCatalogProvider.status !== 404) { + throw new Error( + `couldn't remove the catalog provider for ${args.connectorId} (status ${String(deletedCatalogProvider.status)})`, + ); + } + log( + `catalog provider ${args.connectorId} was already removed (concurrent disconnect)`, ); + } else { + log(`removed catalog provider ${args.connectorId} and its offerings`); } - log(`removed catalog provider ${args.connectorId} and its offerings`); } const providers = await api( @@ -144,9 +151,15 @@ export async function disconnectConnector( cookies, ); if (deletedProvider.status !== 204) { - throw new Error( - `couldn't remove the provider row for ${args.connectorId} (status ${String(deletedProvider.status)})`, + if (deletedProvider.status !== 404) { + throw new Error( + `couldn't remove the provider row for ${args.connectorId} (status ${String(deletedProvider.status)})`, + ); + } + log( + `provider ${args.connectorId} was already removed (concurrent disconnect)`, ); + return { disconnected: false }; } log(`removed provider ${args.connectorId} and its credentials`); return { disconnected: true }; @@ -581,6 +594,11 @@ export function createConnectionRoutes( deps.log( `disconnect failed for connector ${connectorId} on tenant ${tenant.id}: ${message}`, ); + reportError(cause, { + operation: "disconnect_connector", + tenantId: tenant.id, + extra: { connectorId }, + }); return c.json( ErrorEnvelope( "disconnect_failed",