diff --git a/packages/backend/src/server/api/openapi/errors.ts b/packages/backend/src/server/api/openapi/errors.ts deleted file mode 100644 index 3f733b4ea..000000000 --- a/packages/backend/src/server/api/openapi/errors.ts +++ /dev/null @@ -1,69 +0,0 @@ - -export const errors = { - '400': { - 'INVALID_PARAM': { - value: { - error: { - message: 'Invalid param.', - code: 'INVALID_PARAM', - id: '3d81ceae-475f-4600-b2a8-2bc116157532', - }, - }, - }, - }, - '401': { - 'CREDENTIAL_REQUIRED': { - value: { - error: { - message: 'Credential required.', - code: 'CREDENTIAL_REQUIRED', - id: '1384574d-a912-4b81-8601-c7b1c4085df1', - }, - }, - }, - }, - '403': { - 'AUTHENTICATION_FAILED': { - value: { - error: { - message: 'Authentication failed. Please ensure your token is correct.', - code: 'AUTHENTICATION_FAILED', - id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14', - }, - }, - }, - }, - '418': { - 'I_AM_A_TEAPOT': { - value: { - error: { - message: 'I am a teapot.', - code: 'I_AM_A_TEAPOT', - id: '60c46cd1-f23a-46b1-bebe-5d2b73951a84', - }, - }, - }, - }, - '429': { - 'RATE_LIMIT_EXCEEDED': { - value: { - error: { - message: 'Rate limit exceeded. Please try again later.', - code: 'RATE_LIMIT_EXCEEDED', - id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef', - }, - }, - }, - }, - '500': { - 'INTERNAL_ERROR': { - value: { - error: { - message: 'Internal error occurred. Please contact us if the error persists.', - code: 'INTERNAL_ERROR', - id: '5d37dbcb-891e-41ca-a3d6-e690c97775ac', - }, - }, - }, - }, -}; diff --git a/packages/backend/src/server/api/openapi/gen-spec.ts b/packages/backend/src/server/api/openapi/gen-spec.ts index 48b57a03c..497230596 100644 --- a/packages/backend/src/server/api/openapi/gen-spec.ts +++ b/packages/backend/src/server/api/openapi/gen-spec.ts @@ -1,7 +1,8 @@ import config from '@/config/index.js'; +import { errors as errorDefinitions } from '../error.js'; import endpoints from '../endpoints.js'; -import { errors as basicErrors } from './errors.js'; import { schemas, convertSchemaToOpenApiSchema } from './schemas.js'; +import { httpCodes } from './http-codes.js'; export function genOpenapiSpec() { const spec = { @@ -43,19 +44,75 @@ export function genOpenapiSpec() { }; for (const endpoint of endpoints.filter(ep => !ep.meta.secure)) { - const errors = {} as any; + // generate possible responses, first starting with errors + const responses = [ + // general error codes that can always happen + 'INVALID_PARAM', + 'INTERNAL_ERROR', + // error codes that happen only if authentication is required + ...(!endpoint.meta.requireCredential ? [] : [ + 'ACCESS_DENIED', + 'AUTHENTICATION_REQUIRED', + 'AUTHENTICATION_FAILED', + 'SUSPENDED', + ]), + // error codes that happen only if a rate limit is defined + ...(!endpoint.meta.limit ? [] : [ + 'RATE_LIMIT_EXCEEDED', + ]), + // error codes that happen only if a file is required + ...(!endpoint.meta.requireFile ? [] : [ + 'FILE_REQUIRED', + ]), + // endpoint specific error codes + ...(endpoint.meta.errors ?? []), + ] + .reduce((acc, code) => { + const { message, httpStatusCode } = errorDefinitions[code]; + const httpCode = httpStatusCode.toString(); - if (endpoint.meta.errors) { - for (const e of Object.values(endpoint.meta.errors)) { - errors[e.code] = { - value: { - error: e, + if(!(httpCode in acc)) { + acc[httpCode] = { + description: httpCodes[httpCode], + content: { + 'application/json': { + schema: { + '$ref': '#/components/schemas/Error', + }, + examples: {}, + }, }, }; } - } - const resSchema = endpoint.meta.res ? convertSchemaToOpenApiSchema(endpoint.meta.res) : {}; + acc[httpCode].content['application/json'].examples[code] = { + value: { + error: { + code, + message, + endpoint: endpoint.name, + }, + }, + }; + + return acc; + }, {}); + + // add successful response + if (endpoint.meta.res) { + responses['200'] = { + description: 'OK', + content: { + 'application/json': { + schema: convertSchemaToOpenApiSchema(endpoint.meta.res), + }, + }, + }; + } else { + responses['204'] = { + description: 'No Content', + }; + } let desc = (endpoint.meta.description ? endpoint.meta.description : 'No description provided.') + '\n\n'; desc += `**Credential required**: *${endpoint.meta.requireCredential ? 'Yes' : 'No'}*`; @@ -107,90 +164,7 @@ export function genOpenapiSpec() { }, }, }, - responses: { - ...(endpoint.meta.res ? { - '200': { - description: 'OK (with results)', - content: { - 'application/json': { - schema: resSchema, - }, - }, - }, - } : { - '204': { - description: 'OK (without any results)', - }, - }), - '400': { - description: 'Client error', - content: { - 'application/json': { - schema: { - $ref: '#/components/schemas/Error', - }, - examples: { ...errors, ...basicErrors['400'] }, - }, - }, - }, - '401': { - description: 'Authentication error', - content: { - 'application/json': { - schema: { - $ref: '#/components/schemas/Error', - }, - examples: basicErrors['401'], - }, - }, - }, - '403': { - description: 'Forbidden error', - content: { - 'application/json': { - schema: { - $ref: '#/components/schemas/Error', - }, - examples: basicErrors['403'], - }, - }, - }, - '418': { - description: 'I\'m Ai', - content: { - 'application/json': { - schema: { - $ref: '#/components/schemas/Error', - }, - examples: basicErrors['418'], - }, - }, - }, - ...(endpoint.meta.limit ? { - '429': { - description: 'Too many requests', - content: { - 'application/json': { - schema: { - $ref: '#/components/schemas/Error', - }, - examples: basicErrors['429'], - }, - }, - }, - } : {}), - '500': { - description: 'Internal server error', - content: { - 'application/json': { - schema: { - $ref: '#/components/schemas/Error', - }, - examples: basicErrors['500'], - }, - }, - }, - }, + responses, }; const path = { @@ -200,6 +174,7 @@ export function genOpenapiSpec() { path.get = { ...info }; // API Key authentication is not permitted for GET requests path.get.security = path.get.security.filter(elem => !Object.prototype.hasOwnProperty.call(elem, 'ApiKeyAuth')); + // fix the way parameters are passed delete path.get.requestBody; path.get.parameters = []; diff --git a/packages/backend/src/server/api/openapi/http-codes.ts b/packages/backend/src/server/api/openapi/http-codes.ts new file mode 100644 index 000000000..4727ef485 --- /dev/null +++ b/packages/backend/src/server/api/openapi/http-codes.ts @@ -0,0 +1,67 @@ +export const httpCodes: Record = { + "100": "Continue", + "101": "Switching Protocols", + "102": "Processing", + "103": "Early Hints", + "200": "OK", + "201": "Created", + "202": "Accepted", + "203": "Non-Authoritative Information", + "204": "No Content", + "205": "Reset Content", + "206": "Partial Content", + "207": "Multi-Status", + "208": "Already Reported", + "226": "IM Used", + "300": "Multiple Choices", + "301": "Moved Permanently", + "302": "Found", + "303": "See Other", + "304": "Not Modified", + "305": "Use Proxy", + "307": "Temporary Redirect", + "308": "Permanent Redirect", + "400": "Bad Request", + "401": "Unauthorized", + "402": "Payment Required", + "403": "Forbidden", + "404": "Not Found", + "405": "Method Not Allowed", + "406": "Not Acceptable", + "407": "Proxy Authentication Required", + "408": "Request Timeout", + "409": "Conflict", + "410": "Gone", + "411": "Length Required", + "412": "Precondition Failed", + "413": "Content Too Large", + "414": "URI Too Long", + "415": "Unsupported Media Type", + "416": "Range Not Satisfiable", + "417": "Expectation Failed", + "418": "I'm a Teapot", + "421": "Misdirected Request", + "422": "Unprocessable Content", + "423": "Locked", + "424": "Failed Dependency", + "425": "Too Early", + "426": "Upgrade Required", + "427": "Unassigned", + "428": "Precondition Required", + "429": "Too Many Requests", + "430": "Unassigned", + "431": "Request Header Fields Too Large", + "451": "Unavailable For Legal Reasons", + "500": "Internal Server Error", + "501": "Not Implemented", + "502": "Bad Gateway", + "503": "Service Unavailable", + "504": "Gateway Timeout", + "505": "HTTP Version Not Supported", + "506": "Variant Also Negotiates", + "507": "Insufficient Storage", + "508": "Loop Detected", + "509": "Unassigned", + "510": "Not Extended", + "511": "Network Authentication Required" +} diff --git a/packages/backend/src/server/api/openapi/schemas.ts b/packages/backend/src/server/api/openapi/schemas.ts index 4a0844b42..4bb72cd8a 100644 --- a/packages/backend/src/server/api/openapi/schemas.ts +++ b/packages/backend/src/server/api/openapi/schemas.ts @@ -36,19 +36,21 @@ export const schemas = { properties: { code: { type: 'string', - description: 'An error code. Unique within the endpoint.', + description: 'A machine and human readable error code.', + }, + endpoint: { + type: 'string', + description: 'Name of the API endpoint the error happened in.', }, message: { type: 'string', - description: 'An error message.', - }, - id: { - type: 'string', - format: 'uuid', - description: 'An error ID. This ID is static.', + description: 'A human readable error description in English.', }, + info: { + description: 'Potentially more information, primarily intended for developers.', + } }, - required: ['code', 'id', 'message'], + required: ['code', 'endpoint', 'message'], }, }, required: ['error'],