Skip to content

Commit 5ed0cad

Browse files
fix(api): treat every provider-outage code as unavailable on create and update
1 parent b5f0d01 commit 5ed0cad

4 files changed

Lines changed: 57 additions & 8 deletions

File tree

apps/sim/app/api/v2/credentials/[id]/route.test.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,14 @@ vi.mock('@/lib/credentials/access', () => ({
3636
getCredentialActorContext: mockGetCredentialActorContext,
3737
}))
3838

39-
vi.mock('@/lib/credentials/orchestration', () => ({
40-
performUpdateCredential: mockPerformUpdateCredential,
41-
performDeleteCredential: mockPerformDeleteCredential,
42-
}))
39+
vi.mock('@/lib/credentials/orchestration', async () => {
40+
const actual = await import('@/lib/credentials/orchestration/credential-create')
41+
return {
42+
isProviderOutageCode: actual.isProviderOutageCode,
43+
performUpdateCredential: mockPerformUpdateCredential,
44+
performDeleteCredential: mockPerformDeleteCredential,
45+
}
46+
})
4347

4448
vi.mock('@/app/api/v2/lib/gate', () => ({
4549
v2ApiGateError: vi.fn().mockResolvedValue(null),
@@ -295,6 +299,29 @@ describe('PATCH /api/v2/credentials/[id]', () => {
295299
expect(mockPerformUpdateCredential).toHaveBeenCalled()
296300
})
297301

302+
it('503s on an Atlassian outage too, not just a token-provider one', async () => {
303+
mockPerformUpdateCredential.mockResolvedValue({
304+
success: false,
305+
error: 'atlassian_unavailable',
306+
errorCode: 'validation',
307+
providerErrorCode: 'atlassian_unavailable',
308+
})
309+
const res = await callPatch({ workspaceId: WORKSPACE_ID, apiToken: 'tok' })
310+
expect(res.status).toBe(503)
311+
expect((await res.json()).error.code).toBe('SERVICE_UNAVAILABLE')
312+
})
313+
314+
it('keeps a rejected secret a 400, not a 503', async () => {
315+
mockPerformUpdateCredential.mockResolvedValue({
316+
success: false,
317+
error: 'invalid_credentials',
318+
errorCode: 'validation',
319+
providerErrorCode: 'invalid_credentials',
320+
})
321+
const res = await callPatch({ workspaceId: WORKSPACE_ID, apiToken: 'tok' })
322+
expect(res.status).toBe(400)
323+
})
324+
298325
it('rotates a secret without echoing it back', async () => {
299326
const res = await callPatch({ workspaceId: WORKSPACE_ID, apiToken: 'brand-new-token' })
300327
const body = await res.json()

apps/sim/app/api/v2/credentials/[id]/route.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ import { parseRequest } from '@/lib/api/server'
1010
import { generateRequestId } from '@/lib/core/utils/request'
1111
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1212
import { getCredentialActorContext } from '@/lib/credentials/access'
13-
import { performDeleteCredential, performUpdateCredential } from '@/lib/credentials/orchestration'
13+
import {
14+
isProviderOutageCode,
15+
performDeleteCredential,
16+
performUpdateCredential,
17+
} from '@/lib/credentials/orchestration'
1418
import { getWorkspaceCredential } from '@/lib/credentials/queries'
1519
import { checkRateLimit, resolveWorkspaceAccess } from '@/app/api/v1/middleware'
1620
import { toV2CredentialRow, v2CredentialOrchestrationError } from '@/app/api/v2/credentials/utils'
@@ -140,7 +144,7 @@ export const PATCH = withRouteHandler(async (request: NextRequest, context: Rout
140144
return v2CredentialOrchestrationError(
141145
result.errorCode,
142146
result.error ?? 'Failed to update credential',
143-
{ providerUnavailable: result.providerErrorCode === 'provider_unavailable' }
147+
{ providerUnavailable: isProviderOutageCode(result.providerErrorCode) }
144148
)
145149
}
146150

apps/sim/lib/credentials/orchestration/credential-create.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -512,7 +512,10 @@ export async function performCreateCredential(
512512
upstreamStatus: error.status,
513513
...error.logDetail,
514514
})
515-
return failure(error.code, 'validation', { providerErrorCode: error.code })
515+
return failure(error.code, 'validation', {
516+
providerErrorCode: error.code,
517+
providerUnavailable: isProviderOutageCode(error.code),
518+
})
516519
}
517520
if (error instanceof TokenServiceAccountValidationError) {
518521
logger.warn(`Token service-account credential rejected: ${error.code}`, {
@@ -523,7 +526,7 @@ export async function performCreateCredential(
523526
// A provider outage is an infra failure, not a bad request.
524527
return failure(error.code, 'validation', {
525528
providerErrorCode: error.code,
526-
providerUnavailable: error.code === 'provider_unavailable',
529+
providerUnavailable: isProviderOutageCode(error.code),
527530
})
528531
}
529532
if (error instanceof DuplicateCredentialError) {
@@ -557,6 +560,20 @@ export async function performCreateCredential(
557560
}
558561
}
559562

563+
/**
564+
* Provider error codes that mean the upstream service could not be reached,
565+
* rather than that the caller's secret was rejected. Each provider family names
566+
* its own — Atlassian raises `atlassian_unavailable`, the token service accounts
567+
* raise `provider_unavailable` — and both must map to 503, not 400. Kept as one
568+
* set so a new provider family is added in a single place instead of being
569+
* missed on whichever call path nobody re-checked.
570+
*/
571+
const PROVIDER_OUTAGE_CODES = new Set(['provider_unavailable', 'atlassian_unavailable'])
572+
573+
export function isProviderOutageCode(code: string | undefined): boolean {
574+
return code !== undefined && PROVIDER_OUTAGE_CODES.has(code)
575+
}
576+
560577
/** HTTP status for a credential orchestration failure, shared by every route surface. */
561578
export function statusForCredentialOrchestrationError(
562579
code: CredentialOrchestrationErrorCode | undefined,

apps/sim/lib/credentials/orchestration/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { captureServerEvent } from '@/lib/posthog/server'
2323
const logger = createLogger('CredentialOrchestration')
2424

2525
export {
26+
isProviderOutageCode,
2627
type PerformCreateCredentialParams,
2728
type PerformCreateCredentialResult,
2829
performCreateCredential,

0 commit comments

Comments
 (0)