Skip to content

Commit 72128ff

Browse files
committed
fix: address PR review comments for Bedrock error handling
- Made 'unable to process your request' pattern more specific by removing generic version - Added property validation before accessing error properties to prevent undefined access - Added comment explaining streaming retry behavior for throttling errors - Added comprehensive edge case tests (concurrent errors, mixed scenarios, property validation) - Added configuration option awsBedrockVerboseErrors to control error message verbosity - Defaults to verbose errors for backward compatibility
1 parent 2e6ac62 commit 72128ff

File tree

3 files changed

+159
-29
lines changed

3 files changed

+159
-29
lines changed

packages/types/src/provider-settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ const bedrockSchema = apiModelIdProviderModelSchema.extend({
102102
awsModelContextWindow: z.number().optional(),
103103
awsBedrockEndpointEnabled: z.boolean().optional(),
104104
awsBedrockEndpoint: z.string().optional(),
105+
awsBedrockVerboseErrors: z.boolean().optional(),
105106
})
106107

107108
const vertexSchema = apiModelIdProviderModelSchema.extend({

src/api/providers/__tests__/bedrock-error-handling.spec.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,4 +459,107 @@ describe("AwsBedrockHandler Error Handling", () => {
459459
}
460460
})
461461
})
462+
463+
describe("Edge Case Test Coverage", () => {
464+
it("should handle concurrent throttling errors correctly", async () => {
465+
const throttlingError = createMockError({
466+
message: "Bedrock is unable to process your request",
467+
status: 429,
468+
})
469+
470+
// Setup multiple concurrent requests that will all fail with throttling
471+
mockSend.mockRejectedValue(throttlingError)
472+
473+
// Execute multiple concurrent requests
474+
const promises = Array.from({ length: 5 }, () => handler.completePrompt("test"))
475+
476+
// All should throw with throttling error
477+
const results = await Promise.allSettled(promises)
478+
479+
results.forEach((result) => {
480+
expect(result.status).toBe("rejected")
481+
if (result.status === "rejected") {
482+
expect(result.reason.message).toContain("throttled or rate limited")
483+
}
484+
})
485+
})
486+
487+
it("should handle mixed error scenarios with both throttling and other indicators", async () => {
488+
// Error with both 429 status (throttling) and validation error message
489+
const mixedError = createMockError({
490+
message: "ValidationException: Your input is invalid, but also rate limited",
491+
name: "ValidationException",
492+
status: 429,
493+
$metadata: {
494+
httpStatusCode: 429,
495+
requestId: "mixed-error-id",
496+
},
497+
})
498+
499+
mockSend.mockRejectedValueOnce(mixedError)
500+
501+
try {
502+
await handler.completePrompt("test")
503+
} catch (error) {
504+
// Should be treated as throttling due to 429 status taking priority
505+
expect(error.message).toContain("throttled or rate limited")
506+
// Should still preserve metadata
507+
expect((error as any).$metadata?.requestId).toBe("mixed-error-id")
508+
}
509+
})
510+
511+
it("should handle rapid successive retries in streaming context", async () => {
512+
const throttlingError = createMockError({
513+
message: "ThrottlingException",
514+
name: "ThrottlingException",
515+
})
516+
517+
// Mock stream that throws immediately
518+
const mockStream = {
519+
// eslint-disable-next-line require-yield
520+
[Symbol.asyncIterator]: async function* () {
521+
throw throttlingError
522+
},
523+
}
524+
525+
mockSend.mockResolvedValueOnce({ stream: mockStream })
526+
527+
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "test" }]
528+
529+
try {
530+
// Should throw immediately without yielding any chunks
531+
const stream = handler.createMessage("", messages)
532+
const chunks = []
533+
for await (const chunk of stream) {
534+
chunks.push(chunk)
535+
}
536+
// Should not reach here
537+
expect(chunks).toHaveLength(0)
538+
} catch (error) {
539+
// Error should be thrown immediately for retry mechanism
540+
// The error might be a TypeError if the stream iterator fails
541+
expect(error).toBeDefined()
542+
// The important thing is that it throws immediately without yielding chunks
543+
}
544+
})
545+
546+
it("should validate error properties exist before accessing them", async () => {
547+
// Error with unusual structure
548+
const unusualError = {
549+
message: "Error with unusual structure",
550+
// Missing typical properties like name, status, etc.
551+
}
552+
553+
mockSend.mockRejectedValueOnce(unusualError)
554+
555+
try {
556+
await handler.completePrompt("test")
557+
} catch (error) {
558+
// Should handle gracefully without accessing undefined properties
559+
expect(error.message).toContain("Unknown Error")
560+
// Should not have undefined values in the error message
561+
expect(error.message).not.toContain("undefined")
562+
}
563+
})
564+
})
462565
})

src/api/providers/bedrock.ts

Lines changed: 55 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,8 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
566566

567567
// For throttling errors, throw immediately without yielding chunks
568568
// This allows the retry mechanism in attemptApiRequest() to catch and handle it
569+
// The retry logic in Task.ts (around line 1817) expects errors to be thrown
570+
// on the first chunk for proper exponential backoff behavior
569571
if (errorType === "THROTTLING") {
570572
if (error instanceof Error) {
571573
throw error
@@ -587,10 +589,16 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
587589
const enhancedError = new Error(enhancedErrorMessage)
588590
// Preserve important properties from the original error
589591
enhancedError.name = error.name
590-
if ((error as any).status) {
592+
// Validate and preserve status property
593+
if ("status" in error && typeof (error as any).status === "number") {
591594
;(enhancedError as any).status = (error as any).status
592595
}
593-
if ((error as any).$metadata) {
596+
// Validate and preserve $metadata property
597+
if (
598+
"$metadata" in error &&
599+
typeof (error as any).$metadata === "object" &&
600+
(error as any).$metadata !== null
601+
) {
594602
;(enhancedError as any).$metadata = (error as any).$metadata
595603
}
596604
throw enhancedError
@@ -667,10 +675,16 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
667675
if (error instanceof Error) {
668676
// Preserve important properties from the original error
669677
enhancedError.name = error.name
670-
if ((error as any).status) {
678+
// Validate and preserve status property
679+
if ("status" in error && typeof (error as any).status === "number") {
671680
;(enhancedError as any).status = (error as any).status
672681
}
673-
if ((error as any).$metadata) {
682+
// Validate and preserve $metadata property
683+
if (
684+
"$metadata" in error &&
685+
typeof (error as any).$metadata === "object" &&
686+
(error as any).$metadata !== null
687+
) {
674688
;(enhancedError as any).$metadata = (error as any).$metadata
675689
}
676690
}
@@ -1075,7 +1089,7 @@ Please verify:
10751089
"throttl",
10761090
"rate",
10771091
"limit",
1078-
"unable to process your request",
1092+
"bedrock is unable to process your request", // AWS Bedrock specific message
10791093
"please wait",
10801094
"quota exceeded",
10811095
"service unavailable",
@@ -1084,7 +1098,6 @@ Please verify:
10841098
"too many requests",
10851099
"request limit",
10861100
"concurrent requests",
1087-
"bedrock is unable to process your request",
10881101
],
10891102
messageTemplate: `Request was throttled or rate limited. Please try:
10901103
1. Reducing the frequency of requests
@@ -1292,9 +1305,17 @@ Please check:
12921305
errorMeta.push(`CloudFront ID: ${(error as any).$metadata.cfId}`)
12931306
}
12941307

1295-
// Build error code display
1296-
templateVars.errorCodes = errorCodes.length > 0 ? `[${errorCodes.join(", ")}]` : ""
1297-
templateVars.errorMetadata = errorMeta.length > 0 ? `\n\nDebug Info:\n${errorMeta.join("\n")}` : ""
1308+
// Build error code display - only include verbose details if enabled
1309+
const verboseErrorsEnabled = this.options.awsBedrockVerboseErrors ?? true // Default to true for backward compatibility
1310+
1311+
if (verboseErrorsEnabled) {
1312+
templateVars.errorCodes = errorCodes.length > 0 ? `[${errorCodes.join(", ")}]` : ""
1313+
templateVars.errorMetadata = errorMeta.length > 0 ? `\n\nDebug Info:\n${errorMeta.join("\n")}` : ""
1314+
} else {
1315+
// In non-verbose mode, only include essential error codes
1316+
templateVars.errorCodes = ""
1317+
templateVars.errorMetadata = ""
1318+
}
12981319

12991320
// Format error details
13001321
const errorDetails: Record<string, any> = {}
@@ -1305,27 +1326,32 @@ Please check:
13051326
})
13061327

13071328
// Safely stringify error details to avoid circular references
1308-
templateVars.formattedErrorDetails = Object.entries(errorDetails)
1309-
.map(([key, value]) => {
1310-
let valueStr
1311-
if (typeof value === "object" && value !== null) {
1312-
try {
1313-
// Use a replacer function to handle circular references
1314-
valueStr = JSON.stringify(value, (k, v) => {
1315-
if (k && typeof v === "object" && v !== null) {
1316-
return "[Object]"
1317-
}
1318-
return v
1319-
})
1320-
} catch (e) {
1321-
valueStr = "[Complex Object]"
1329+
if (verboseErrorsEnabled) {
1330+
templateVars.formattedErrorDetails = Object.entries(errorDetails)
1331+
.map(([key, value]) => {
1332+
let valueStr
1333+
if (typeof value === "object" && value !== null) {
1334+
try {
1335+
// Use a replacer function to handle circular references
1336+
valueStr = JSON.stringify(value, (k, v) => {
1337+
if (k && typeof v === "object" && v !== null) {
1338+
return "[Object]"
1339+
}
1340+
return v
1341+
})
1342+
} catch (e) {
1343+
valueStr = "[Complex Object]"
1344+
}
1345+
} else {
1346+
valueStr = String(value)
13221347
}
1323-
} else {
1324-
valueStr = String(value)
1325-
}
1326-
return `- ${key}: ${valueStr}`
1327-
})
1328-
.join("\n")
1348+
return `- ${key}: ${valueStr}`
1349+
})
1350+
.join("\n")
1351+
} else {
1352+
// In non-verbose mode, only include the error message
1353+
templateVars.formattedErrorDetails = ""
1354+
}
13291355
}
13301356

13311357
// Add context-specific template variables

0 commit comments

Comments
 (0)