Skip to content

Commit

Permalink
change app error signature
Browse files Browse the repository at this point in the history
  • Loading branch information
topaztee committed Jul 29, 2024
1 parent 97c99af commit 30849fc
Show file tree
Hide file tree
Showing 25 changed files with 370 additions and 195 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ data
*.launch
.settings/
*.sublime-workspace
merlinn.iml

# IDE - VSCode
.vscode/*
Expand Down
1 change: 0 additions & 1 deletion services/api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ app.use("/invite", inviteRouter);
app.use("/organizations", organizationsRouter);
app.use("/index", indexRouter);
app.use("/features", featuresRouter);

app.all("*", invalidPathHandler); // Handle 404

// Global error handler
Expand Down
65 changes: 37 additions & 28 deletions services/api/src/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,41 @@ export enum ErrorCode {
NO_INDEX = 37,
}

export class AppError extends Error {
public override readonly message: string;
public readonly statusCode: number;
public readonly status: "fail" | "error";
public readonly internalCode?: ErrorCode;
constructor(
message: string,
statusCode: number,
internalCode?: ErrorCode,
stack?: string,
) {
super(message);
this.stack = stack;
this.message = message;
this.statusCode = statusCode;
this.status = `${statusCode}`.startsWith("4") ? "fail" : "error";
this.internalCode = internalCode;
Error.captureStackTrace(this, this.constructor);
}

public toJSON() {
return {
message: this.message,
status: this.status,
statusCode: this.statusCode,
internalCode: this.internalCode,
};
}
export interface ErrorPayload {
message: string;
statusCode: number;
internalCode?: ErrorCode;
stack?: string;
context?: Record<string, unknown>;
}

export const AppError = ({
message,
statusCode,
internalCode,
stack,
context,
}: ErrorPayload) => {
const status = `${statusCode}`.startsWith("4") ? "fail" : "error";

const appError = {
message,
statusCode,
status,
internalCode,
context,
stack,
toJSON() {
return {
message: this.message,
status: this.status,
statusCode: this.statusCode,
internalCode: this.internalCode,
};
},
};

Error.captureStackTrace(appError, AppError);

return appError;
};
6 changes: 5 additions & 1 deletion services/api/src/middlewares/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ export const getDBUser = catchAsync(
},
});
if (!user) {
throw new AppError("No internal user", 401, ErrorCode.NO_INTERNAL_USER);
throw AppError({
message: "No internal user",
statusCode: 401,
internalCode: ErrorCode.NO_INTERNAL_USER,
});
}
req.user = user as IUser;
next();
Expand Down
24 changes: 13 additions & 11 deletions services/api/src/middlewares/errors.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Request, Response, NextFunction } from "express";
import { AppError } from "../errors";
import { AppError, ErrorPayload } from "../errors";
import { PostHogClient } from "../telemetry/posthog";
import { uuid } from "uuidv4";

const captureErrorInTelemetry = (error: AppError, req: Request) => {
const captureErrorInTelemetry = (error: ErrorPayload, req: Request) => {
const posthog = new PostHogClient();

const distinctId = req.user?._id.toString() || uuid();
Expand All @@ -13,28 +13,29 @@ const captureErrorInTelemetry = (error: AppError, req: Request) => {
distinctId,
properties: {
message: error.message,
context: error.context,
},
});
};
const productionError = (error: AppError, req: Request, res: Response) => {
const productionError = (error: ErrorPayload, req: Request, res: Response) => {
captureErrorInTelemetry(error, req);

// Send a lean error message
res.status(error.statusCode).json({
status: error.status,
status: error.statusCode,
message: error.message,
code: error.internalCode,
});
};

// Send a detailed error message, for debugging purposes
const developmentError = (error: AppError, req: Request, res: Response) => {
const developmentError = (error: ErrorPayload, req: Request, res: Response) => {
console.error("developmentError error: ", error);

captureErrorInTelemetry(error, req);

res.status(error.statusCode).json({
status: error.status,
status: error.statusCode,
message: error.message,
code: error.internalCode,
error: error,
Expand All @@ -43,7 +44,7 @@ const developmentError = (error: AppError, req: Request, res: Response) => {
};

export const errorHandler = (
error: AppError,
error: ErrorPayload,
req: Request,
res: Response,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Expand All @@ -65,9 +66,10 @@ export const invalidPathHandler = (
_res: Response,
next: NextFunction,
) => {
const error = new AppError(
`Path ${req.originalUrl} does not exist for ${req.method} method`,
404,
);
const error = AppError({
message: `Path ${req.originalUrl} does not exist for ${req.method} method`,
statusCode: 404,
internalCode: undefined,
});
next(error);
};
8 changes: 6 additions & 2 deletions services/api/src/middlewares/slack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const getSlackUser = catchAsync(
const claimedToken = req.headers["x-slack-app-token"];
const actualToken = process.env.SLACK_APP_TOKEN as string;
if (claimedToken !== actualToken) {
throw new AppError("Token is invalid", 401);
throw AppError({ message: "Token is invalid", statusCode: 401 });
}
const email = req.headers["x-slack-email"];
const team = req.headers["x-slack-team"];
Expand All @@ -18,7 +18,11 @@ export const getSlackUser = catchAsync(
"metadata.team.id": team,
});
if (!slackIntegration) {
throw new AppError("No slack integration", 401, ErrorCode.NO_INTEGRATION);
throw AppError({
message: "No slack integration",
statusCode: 401,
internalCode: ErrorCode.NO_INTEGRATION,
});
}

const { organization } = slackIntegration;
Expand Down
16 changes: 10 additions & 6 deletions services/api/src/middlewares/webhooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,19 @@ export function getSecretFromRequest(req: Request) {
} else {
const authHeader = req.headers["authorization"] as string;
if (!authHeader) {
throw new AppError(
"Request does not contain a secret header (either custom or auth header)",
400,
);
throw AppError({
message:
"Request does not contain a secret header (either custom or auth header)",
statusCode: 400,
});
}

// Check it's a valid Bearer token
if (!authHeader.startsWith("Bearer ")) {
throw new AppError("Request does not contain a valid Bearer token", 400);
throw AppError({
message: "Request does not contain a valid Bearer token",
statusCode: 400,
});
}

const [, authHeaderSecret] = authHeader.split(" ");
Expand All @@ -41,7 +45,7 @@ export const checkWebhookSecret = catchAsync(
.populate("organization");

if (!webhook) {
throw new AppError("Secret is invalid", 400);
throw AppError({ message: "Secret is invalid", statusCode: 400 });
}
req.webhook = webhook as IWebhook;

Expand Down
61 changes: 37 additions & 24 deletions services/api/src/routers/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,17 @@ const router = express.Router();

const getCompletions = async (req: Request, res: Response) => {
if (!req.user) {
throw new AppError("No internal user", 403, ErrorCode.NO_INTERNAL_USER);
throw AppError({
message: "No internal user",
statusCode: 403,
internalCode: ErrorCode.NO_INTERNAL_USER,
});
} else if (req.user.status === "invited") {
throw new AppError(
"User hasn't accepted the invitation yet",
403,
ErrorCode.INVITATION_NOT_ACCEPTED,
);
throw AppError({
message: "User hasn't accepted the invitation yet",
statusCode: 403,
internalCode: ErrorCode.INVITATION_NOT_ACCEPTED,
});
}

if (isEnterprise()) {
Expand All @@ -39,11 +43,11 @@ const getCompletions = async (req: Request, res: Response) => {
userId: String(req.user!._id),
});
if (!queriesState.isAllowed) {
throw new AppError(
`You have exceeded your queries' quota`,
429,
ErrorCode.QUOTA_EXCEEDED,
);
throw AppError({
message: `You have exceeded your queries' quota`,
statusCode: 429,
internalCode: ErrorCode.QUOTA_EXCEEDED,
});
}

// Update quota
Expand All @@ -65,7 +69,11 @@ const getCompletions = async (req: Request, res: Response) => {
})
.populate("vendor")) as IIntegration[];
if (!integrations.length) {
throw new AppError("No integrations at all", 404, ErrorCode.NO_INTEGRATION);
throw AppError({
message: "No integrations at all",
statusCode: 404,
internalCode: ErrorCode.NO_INTEGRATION,
});
}

let output: string | null = null;
Expand All @@ -78,7 +86,7 @@ const getCompletions = async (req: Request, res: Response) => {
// const moderationResult = await validateModeration(message.content as string);

// if (!moderationResult) {
// throw new AppError(
// throw AppError({ message:
// "Text was found that violates our content policy",
// 400,
// ErrorCode.MODERATION_FAILED,
Expand Down Expand Up @@ -133,12 +141,12 @@ const getCompletions = async (req: Request, res: Response) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
console.error(error);
throw new AppError(
error.message,
500,
ErrorCode.AGENT_RUN_FAILED,
error.stack,
);
throw AppError({
message: error.message,
statusCode: 500,
internalCode: ErrorCode.AGENT_RUN_FAILED,
stack: error.stack,
});
}
} else {
try {
Expand All @@ -153,7 +161,11 @@ const getCompletions = async (req: Request, res: Response) => {
observationId = result.observationId!;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
throw new AppError(error.message, 500, ErrorCode.MODEL_RUN_FAILED);
throw AppError({
message: error.message,
statusCode: 500,
internalCode: ErrorCode.MODEL_RUN_FAILED,
});
}
}

Expand Down Expand Up @@ -211,10 +223,11 @@ router.post(
const { traceId, observationId, value, text } = req.body;
if (isLangfuseEnabled()) {
if (!traceId || !observationId || !value) {
throw new AppError(
"Bad request. Need to supply traceId, observationId and value",
400,
);
throw AppError({
message:
"Bad request. Need to supply traceId, observationId and value",
statusCode: 400,
});
}
const langfuse = new Langfuse({
secretKey: process.env.LANGFUSE_SECRET_KEY as string,
Expand Down
Loading

0 comments on commit 30849fc

Please sign in to comment.