-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Throw all expected errors up to the route catch handler
- Loading branch information
1 parent
e001c7d
commit 90a4964
Showing
9 changed files
with
280 additions
and
178 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
import { AilaAuthenticationError, AilaThreatDetectionError } from "@oakai/aila"; | ||
import * as moderationErrorHandling from "@oakai/aila/src/utils/moderation/moderationErrorHandling"; | ||
import { UserBannedError } from "@oakai/core/src/models/safetyViolations"; | ||
import { TracingSpan } from "@oakai/core/src/tracing/serverTracing"; | ||
import { RateLimitExceededError } from "@oakai/core/src/utils/rateLimiting/userBasedRateLimiter"; | ||
import { PrismaClientWithAccelerate } from "@oakai/db"; | ||
|
||
import { consumeStream } from "@/utils/testHelpers/consumeStream"; | ||
|
||
import { handleChatException } from "./errorHandling"; | ||
|
||
describe("handleChatException", () => { | ||
describe("AilaThreatDetectionError", () => { | ||
it("should forward the message from handleHeliconeError", async () => { | ||
jest | ||
.spyOn(moderationErrorHandling, "handleHeliconeError") | ||
.mockResolvedValue({ | ||
type: "error", | ||
value: "Threat detected", | ||
message: "Threat was detected", | ||
}); | ||
|
||
const span = { setTag: jest.fn() } as unknown as TracingSpan; | ||
const error = new AilaThreatDetectionError("test error"); | ||
const prisma = {} as unknown as PrismaClientWithAccelerate; | ||
|
||
const response = await handleChatException( | ||
span, | ||
error, | ||
"test-user-id", | ||
"test-chat-id", | ||
prisma, | ||
); | ||
|
||
expect(response.status).toBe(200); | ||
|
||
const message = JSON.parse( | ||
await consumeStream(response.body as ReadableStream), | ||
); | ||
expect(message).toEqual({ | ||
type: "error", | ||
value: "Threat detected", | ||
message: "Threat was detected", | ||
}); | ||
}); | ||
}); | ||
|
||
describe("AilaAuthenticationError", () => { | ||
it("should return an error chat message", async () => { | ||
const span = { setTag: jest.fn() } as unknown as TracingSpan; | ||
const error = new AilaAuthenticationError("test error"); | ||
const prisma = {} as unknown as PrismaClientWithAccelerate; | ||
|
||
const response = await handleChatException( | ||
span, | ||
error, | ||
"test-user-id", | ||
"test-chat-id", | ||
prisma, | ||
); | ||
|
||
expect(response.status).toBe(401); | ||
|
||
const message = await consumeStream(response.body as ReadableStream); | ||
expect(message).toEqual("Unauthorized"); | ||
}); | ||
}); | ||
|
||
describe("RateLimitExceededError", () => { | ||
it("should return an error chat message", async () => { | ||
const span = { setTag: jest.fn() } as unknown as TracingSpan; | ||
const error = new RateLimitExceededError(100, Date.now() + 3600 * 1000); | ||
const prisma = {} as unknown as PrismaClientWithAccelerate; | ||
|
||
const response = await handleChatException( | ||
span, | ||
error, | ||
"test-user-id", | ||
"test-chat-id", | ||
prisma, | ||
); | ||
|
||
expect(response.status).toBe(200); | ||
|
||
const message = JSON.parse( | ||
await consumeStream(response.body as ReadableStream), | ||
); | ||
expect(message).toEqual({ | ||
type: "error", | ||
value: "Rate limit exceeded", | ||
message: | ||
"**Unfortunately you’ve exceeded your fair usage limit for today.** Please come back in 1 hour. If you require a higher limit, please [make a request](https://forms.gle/tHsYMZJR367zydsG8).", | ||
}); | ||
}); | ||
}); | ||
|
||
describe("UserBannedError", () => { | ||
it("should return an error chat message", async () => { | ||
const span = { setTag: jest.fn() } as unknown as TracingSpan; | ||
const error = new UserBannedError("test error"); | ||
const prisma = {} as unknown as PrismaClientWithAccelerate; | ||
|
||
const response = await handleChatException( | ||
span, | ||
error, | ||
"test-user-id", | ||
"test-chat-id", | ||
prisma, | ||
); | ||
|
||
expect(response.status).toBe(200); | ||
|
||
const message = JSON.parse( | ||
await consumeStream(response.body as ReadableStream), | ||
); | ||
expect(message).toEqual({ | ||
type: "action", | ||
action: "SHOW_ACCOUNT_LOCKED", | ||
}); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
import { AilaAuthenticationError, AilaThreatDetectionError } from "@oakai/aila"; | ||
import { | ||
ActionDocument, | ||
ErrorDocument, | ||
} from "@oakai/aila/src/protocol/jsonPatchProtocol"; | ||
import { handleHeliconeError } from "@oakai/aila/src/utils/moderation/moderationErrorHandling"; | ||
import { UserBannedError } from "@oakai/core/src/models/safetyViolations"; | ||
import { TracingSpan } from "@oakai/core/src/tracing/serverTracing"; | ||
import { RateLimitExceededError } from "@oakai/core/src/utils/rateLimiting/userBasedRateLimiter"; | ||
import { PrismaClientWithAccelerate } from "@oakai/db"; | ||
|
||
import { streamingJSON } from "./protocol"; | ||
|
||
function reportErrorTelemetry( | ||
span: TracingSpan, | ||
error: Error, | ||
errorType: string, | ||
statusMessage: string, | ||
additionalAttributes: Record< | ||
string, | ||
string | number | boolean | undefined | ||
> = {}, | ||
) { | ||
span.setTag("error", true); | ||
span.setTag("error.type", errorType); | ||
span.setTag("error.message", statusMessage); | ||
span.setTag("error.stack", error.stack); | ||
Object.entries(additionalAttributes).forEach(([key, value]) => { | ||
span.setTag(key, value); | ||
}); | ||
} | ||
|
||
async function handleThreatDetectionError( | ||
span: TracingSpan, | ||
e: AilaThreatDetectionError, | ||
userId: string, | ||
id: string, | ||
prisma: PrismaClientWithAccelerate, | ||
) { | ||
const heliconeErrorMessage = await handleHeliconeError(userId, id, e, prisma); | ||
reportErrorTelemetry(span, e, "AilaThreatDetectionError", "Threat detected"); | ||
return streamingJSON(heliconeErrorMessage); | ||
} | ||
|
||
async function handleAilaAuthenticationError( | ||
span: TracingSpan, | ||
e: AilaAuthenticationError, | ||
) { | ||
reportErrorTelemetry(span, e, "AilaAuthenticationError", "Unauthorized"); | ||
return new Response("Unauthorized", { status: 401 }); | ||
} | ||
|
||
export async function handleRateLimitError( | ||
span: TracingSpan, | ||
error: RateLimitExceededError, | ||
) { | ||
reportErrorTelemetry(span, error, "RateLimitExceededError", "Rate limited"); | ||
|
||
const timeRemainingHours = Math.ceil( | ||
(error.reset - Date.now()) / 1000 / 60 / 60, | ||
); | ||
const hours = timeRemainingHours === 1 ? "hour" : "hours"; | ||
|
||
return streamingJSON({ | ||
type: "error", | ||
value: error.message, | ||
message: `**Unfortunately you’ve exceeded your fair usage limit for today.** Please come back in ${timeRemainingHours} ${hours}. If you require a higher limit, please [make a request](${process.env.RATELIMIT_FORM_URL}).`, | ||
} as ErrorDocument); | ||
} | ||
|
||
async function handleUserBannedError() { | ||
return streamingJSON({ | ||
type: "action", | ||
action: "SHOW_ACCOUNT_LOCKED", | ||
} as ActionDocument); | ||
} | ||
|
||
async function handleGenericError(span: TracingSpan, e: Error) { | ||
reportErrorTelemetry(span, e, e.name, e.message); | ||
return streamingJSON({ | ||
type: "error", | ||
message: e.message, | ||
value: `Sorry, an error occurred: ${e.message}`, | ||
} as ErrorDocument); | ||
} | ||
|
||
export async function handleChatException( | ||
span: TracingSpan, | ||
e: unknown, | ||
userId: string | undefined, | ||
chatId: string, | ||
prisma: PrismaClientWithAccelerate, | ||
): Promise<Response> { | ||
if (e instanceof AilaAuthenticationError) { | ||
return handleAilaAuthenticationError(span, e); | ||
} | ||
|
||
if (e instanceof AilaThreatDetectionError && userId) { | ||
return handleThreatDetectionError(span, e, userId, chatId, prisma); | ||
} | ||
|
||
if (e instanceof RateLimitExceededError && userId) { | ||
return handleRateLimitError(span, e); | ||
} | ||
|
||
if (e instanceof UserBannedError) { | ||
return handleUserBannedError(); | ||
} | ||
|
||
if (e instanceof Error) { | ||
return handleGenericError(span, e); | ||
} | ||
|
||
throw e; | ||
} |
Oops, something went wrong.