Skip to content

Commit

Permalink
feat: Improve HTTP errors
Browse files Browse the repository at this point in the history
  • Loading branch information
zAlweNy26 committed Jul 18, 2024
1 parent c61cd24 commit 9205a66
Show file tree
Hide file tree
Showing 15 changed files with 288 additions and 125 deletions.
Binary file modified bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"js-tiktoken": "^1.0.12",
"langchain": "^0.2.10",
"lodash": "^4.17.21",
"logestic": "1.2.0",
"lowdb": "^7.0.1",
"mammoth": "^1.8.0",
"nodemon": "^3.1.0",
Expand Down
91 changes: 55 additions & 36 deletions src/context.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type Elysia, t } from 'elysia'
import { Elysia, t } from 'elysia'
import { HttpError } from './errors'
import { parsedEnv } from './utils'
import { log } from './logger'

export const swaggerTags = {
status: {
Expand Down Expand Up @@ -33,22 +33,13 @@ export const swaggerTags = {
},
} as const

export class UnauthorizedError extends Error {
code = 'UNAUTHORIZED'
status = 401

constructor(message?: string) {
super(message ?? 'UNAUTHORIZED')
}
}

const jsonLiterals = t.Union([t.String(), t.Number(), t.Boolean(), t.Null()])

export function authMiddleware(app: Elysia) {
return app.onBeforeHandle(({ headers }) => {
const apiKey = headers.token, realKey = parsedEnv.apiKey
if (realKey && realKey !== apiKey)
throw new UnauthorizedError('Invalid API key')
throw HttpError.Unauthorized('Invalid API key')
})
}

Expand All @@ -59,7 +50,17 @@ export const modelInfo = t.Object({
link: t.Optional(t.String({ format: 'uri' })),
schema: t.Record(t.String(), t.Any()),
value: t.Record(t.String(), t.Any()),
}, { $id: 'modelInfo' })
}, {
$id: 'modelInfo',
examples: [{
name: 'OpenAILLM',
humanReadableName: 'OpenAI GPT',
description: 'More expensive but also more flexible model than ChatGPT',
link: 'https://platform.openai.com/docs/models/overview',
schema: {},
value: {},
}],
})

export const pluginManifest = t.Object({
name: t.String(),
Expand All @@ -70,7 +71,18 @@ export const pluginManifest = t.Object({
pluginUrl: t.Optional(t.String({ format: 'uri' })),
thumb: t.Optional(t.String({ format: 'uri' })),
tags: t.Array(t.String(), { default: ['miscellaneous', 'unknown'] }),
}, { $id: 'pluginManifest' })
}, {
$id: 'pluginManifest',
examples: [{
name: 'Core CCat',
description: 'The core Cat plugin used to define default hooks and tools. You don\'t see this plugin in the plugins folder, because it is an hidden plugin. It will be used to try out hooks and tools before they become available to other plugins. Written and delivered just for you, my furry friend.',
author_name: 'Cheshire Cat',
authorUrl: 'https://cheshirecat.ai',
pluginUrl: 'https://github.com/cheshire-cat-ai/core',
tags: ['core', 'cat', 'default', 'hidden'],
version: '0.0.1',
}],
})

export const pluginInfo = t.Object({
id: t.String(),
Expand All @@ -91,46 +103,53 @@ export const pluginInfo = t.Object({
name: t.String(),
priority: t.Number(),
})),
}, { $id: 'pluginInfo' })
}, {
$id: 'pluginInfo',
examples: [{
id: 'core_plugin',
active: true,
upgradable: false,
manifest: {},
forms: [],
tools: [],
hooks: [],
}],
})

export const pluginSettings = t.Object({
name: t.String(),
schema: t.Record(t.String(), t.Any()),
value: t.Record(t.String(), t.Any()),
}, { $id: 'pluginSettings' })
}, {
$id: 'pluginSettings',
examples: [{
name: 'Core CCat',
schema: {},
value: {},
}],
})

export function apiModels(app: Elysia) {
return app.onError(({ code, error }) => {
log.error(error)
return {
code,
message: error.message,
status: 'status' in error ? error.status : 400,
}
}).model({
error: t.Object({
code: t.String(),
message: t.String(),
status: t.Number(),
}, {
examples: [{
code: 'UNKNOWN',
message: 'The request was invalid.',
status: 400,
}],
export function apiModels() {
return new Elysia({ name: 'api-models' }).model({
generic: t.Record(t.String(), t.Any(), {
examples: [{ key: 'value' }],
$id: 'GenericObject',
}),
generic: t.Record(t.String(), t.Any(), { examples: [{ key: 'value' }] }),
json: t.Union([jsonLiterals, t.Array(jsonLiterals), t.Record(t.String(), jsonLiterals)], {
examples: [
{ key: 'value' },
['value'],
'example',
42,
],
$id: 'GenericJson',
}),
customSetting: t.Object({
name: t.String(),
value: t.Any(),
}, {
examples: [{ name: 'key', value: 'value' }],
$id: 'CustomSetting',
}),
modelInfo,
pluginManifest,
Expand Down
104 changes: 104 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { Elysia, t } from 'elysia'

export class HttpError extends Error {
public constructor(
public message: string,
public status: number,
public cause: string,
public data: any = undefined,
) {
super(message, data)
}

public static BadRequest(message: string, data?: any) {
return new HttpError('Bad Request', 400, message, data)
}

public static Unauthorized(message: string, data?: any) {
return new HttpError('Unauthorized', 401, message, data)
}

public static PaymentRequired(message: string, data?: any) {
return new HttpError('Payment Required', 402, message, data)
}

public static Forbidden(message: string, data?: any) {
return new HttpError('Forbidden', 403, message, data)
}

public static NotFound(message: string, data?: any) {
return new HttpError('Not Found', 404, message, data)
}

public static MethodNotAllowed(message: string, data?: any) {
return new HttpError('Method Not Allowed', 405, message, data)
}

public static Conflict(message: string, data?: any) {
return new HttpError('Conflict', 409, message, data)
}

public static UnsupportedMediaType(message: string, data?: any) {
return new HttpError('Unsupported Media Type', 415, message, data)
}

public static IAmATeapot(message: string, data?: any) {
return new HttpError('I Am A Teapot', 418, message, data)
}

public static TooManyRequests(message: string, data?: any) {
return new HttpError('Too Many Requests', 429, message, data)
}

public static InternalServer(message: string, data?: any) {
return new HttpError('Internal Server Error', 500, message, data)
}

public static NotImplemented(message: string, data?: any) {
return new HttpError('Not Implemented', 501, message, data)
}

public static BadGateway(message: string, data?: any) {
return new HttpError('Bad Gateway', 502, message, data)
}

public static ServiceUnavailable(message: string, data?: any) {
return new HttpError('Service Unavailable', 503, message, data)
}

public static GatewayTimeout(message: string, data?: any) {
return new HttpError('Gateway Timeout', 504, message, data)
}
}

export function httpError() {
return new Elysia({ name: 'http-error' })
.decorate('HttpError', HttpError)
.error({ HTTP_ERROR: HttpError })
.model({
error: t.Object({
code: t.String(),
status: t.Number(),
message: t.String(),
data: t.Optional(t.Any()),
}, {
examples: [{
code: 'Bad Request',
status: 400,
message: 'The request was invalid',
}],
$id: 'GenericError',
}),
})
.onError({ as: 'global' }, ({ code, error, set }) => {
if (code === 'HTTP_ERROR') {
set.status = error.status
return {
code: error.message,
status: error.status,
message: error.cause,
data: error.data,
}
}
})
}
28 changes: 27 additions & 1 deletion src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { createConsola } from 'consola'
import { Table } from 'console-table-printer'
import type { ColorName } from 'consola/utils'
import { getColor } from 'consola/utils'
import { LogLevel, parsedEnv } from './utils.ts'
import chalk from 'chalk'
import { format } from 'date-fns'
import { Logestic, type LogesticOptions } from 'logestic'
import { LogLevel, catPaths, parsedEnv } from './utils.ts'
import type { HttpError } from './errors.ts'

const logger = createConsola({
level: LogLevel.indexOf(parsedEnv.logLevel),
Expand Down Expand Up @@ -98,3 +102,25 @@ export const log = Object.freeze({
*/
debug: logger.debug,
})

export function httpLogger(options?: LogesticOptions) {
return new Logestic({
showLevel: true,
...options,
}).use(['time', 'method', 'path', 'duration']).format({
onSuccess({ time, method, path, duration }) {
const dateTime = chalk.gray(format(time, 'dd/MM/yyyy HH:mm:ss'))
const methodPath = chalk.cyan(`${method} ${decodeURIComponent(path)}`)
return `${dateTime} ${methodPath} ${Number(duration) / 1000}ms`
},
onFailure({ request, datetime, error }) {
const { method, url } = request
const err = error as HttpError
const baseUrl = url.substring(catPaths.baseUrl.length - 1)
const dateTime = chalk.gray(format(datetime, 'dd/MM/yyyy HH:mm:ss'))
const methodPath = chalk.red(`${method} ${decodeURIComponent(baseUrl)}`)
const errorCode = chalk.red(`${err.status} - ${err.message} - ${err.cause}`)
return `${dateTime} ${methodPath} ${errorCode}`
},
})
}
6 changes: 4 additions & 2 deletions src/looking_glass/cheshire-cat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ export class CheshireCat {
return llm.getModel(settings)
}
catch (error) {
log.error(`The selected LLM "${selected}" does not exist. Falling back to the default LLM.`)
log.error(error)
log.warn(`The selected LLM "${selected}" does not exist. Falling back to the default LLM.`)
return getLLM('DefaultLLM')!.getModel({})
}
}
Expand All @@ -137,7 +138,8 @@ export class CheshireCat {
return embedder.getModel(settings)
}
catch (error) {
log.error(`The selected Embedder "${selected}" does not exist. Falling back to the default Embedder.`)
log.error(error)
log.warn(`The selected Embedder "${selected}" does not exist. Falling back to the default Embedder.`)
return getEmbedder('FakeEmbedder')!.getModel({})
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/looking_glass/output-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ export class ProceduresOutputParser extends AgentActionOutputParser {
parsedOutput = await parseJson(output, agentOutputSchema)
}
catch (error) {
log.error(`Could not parse LLM output: ${output}`)
log.error(error)
log.warn(`Could not parse LLM output: ${output}`)
throw new OutputParserException(`Could not parse LLM output: ${output}`)
}

Expand Down
Loading

0 comments on commit 9205a66

Please sign in to comment.