Skip to content

Commit 701d192

Browse files
committed
feat: validate API requests and responses according to the OpenApi spec
Signed-off-by: Philippe Martin <phmartin@redhat.com>
1 parent c7e74a4 commit 701d192

File tree

5 files changed

+234
-10
lines changed

5 files changed

+234
-10
lines changed

Diff for: packages/backend/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
"dependencies": {
7272
"@huggingface/gguf": "^0.1.9",
7373
"express": "^4.19.2",
74+
"express-openapi-validator": "^5.3.1",
7475
"isomorphic-git": "^1.27.1",
7576
"mustache": "^4.2.0",
7677
"openai": "^4.56.0",

Diff for: packages/backend/src/managers/apiServer.spec.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,16 @@ test('/api/version endpoint when getting package.json file fails', async () => {
114114
expect(res.body.errors[0]).toEqual('an error getting package file');
115115
});
116116

117+
test('/api/version endpoint with unexpected param', async () => {
118+
expect(server.getListener()).toBeDefined();
119+
const res = await request(server.getListener()!).get('/api/version?wrongParam').expect(400);
120+
expect(res.body.message).toEqual(`Unknown query parameter 'wrongParam'`);
121+
});
122+
117123
test('/api/wrongEndpoint', async () => {
118124
expect(server.getListener()).toBeDefined();
119-
await request(server.getListener()!).get('/api/wrongEndpoint').expect(404);
125+
const res = await request(server.getListener()!).get('/api/wrongEndpoint').expect(404);
126+
expect(res.body.message).toEqual('not found');
120127
});
121128

122129
test('/', async () => {

Diff for: packages/backend/src/managers/apiServer.ts

+26-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
***********************************************************************/
1818

1919
import type { Disposable } from '@podman-desktop/api';
20-
import type { Request, Response } from 'express';
20+
import type { NextFunction, Request, Response } from 'express';
2121
import express from 'express';
2222
import type { Server } from 'http';
2323
import path from 'node:path';
@@ -30,6 +30,8 @@ import type { components } from '../../src-generated/openapi';
3030
import type { ModelInfo } from '@shared/src/models/IModelInfo';
3131
import type { ConfigurationRegistry } from '../registries/ConfigurationRegistry';
3232
import { getFreeRandomPort } from '../utils/ports';
33+
import * as OpenApiValidator from 'express-openapi-validator';
34+
import type { HttpError, OpenApiRequest } from 'express-openapi-validator/dist/framework/types';
3335

3436
const SHOW_API_INFO_COMMAND = 'ai-lab.show-api-info';
3537
const SHOW_API_ERROR_COMMAND = 'ai-lab.show-api-error';
@@ -68,6 +70,29 @@ export class ApiServer implements Disposable {
6870
const router = express.Router();
6971
router.use(express.json());
7072

73+
// validate requests / responses based on openapi spec
74+
router.use(
75+
OpenApiValidator.middleware({
76+
apiSpec: this.getSpecFile(),
77+
validateRequests: true,
78+
validateResponses: {
79+
onError: (error, body, req) => {
80+
console.error(`Response body fails validation: `, error);
81+
console.error(`Emitted from:`, req.originalUrl);
82+
console.error(body);
83+
},
84+
},
85+
}),
86+
);
87+
88+
router.use((err: HttpError, _req: OpenApiRequest, res: Response, _next: NextFunction) => {
89+
// format errors from validator
90+
res.status(err.status || 500).json({
91+
message: err.message,
92+
errors: err.errors,
93+
});
94+
});
95+
7196
// declare routes
7297
router.get('/version', this.getVersion.bind(this));
7398
router.get('/tags', this.getModels.bind(this));

Diff for: packages/backend/vite.config.js

+2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ const config = {
3434
'/@/': join(PACKAGE_ROOT, 'src') + '/',
3535
'/@gen/': join(PACKAGE_ROOT, 'src-generated') + '/',
3636
'@shared/': join(PACKAGE_ROOT, '../shared') + '/',
37+
'@jsdevtools/ono': '@jsdevtools/ono/cjs/index.js',
38+
'ono': '@jsdevtools/ono/cjs/index.js',
3739
},
3840
mainFields: ['module', 'jsnext:main', 'jsnext'], //https://github.com/vitejs/vite/issues/16444
3941
},

0 commit comments

Comments
 (0)