Skip to content

Commit 4e20f77

Browse files
authored
feat: validate API requests and responses according to the OpenApi spec (#1579)
Signed-off-by: Philippe Martin <[email protected]>
1 parent f3a37a1 commit 4e20f77

File tree

5 files changed

+269
-11
lines changed

5 files changed

+269
-11
lines changed

Diff for: packages/backend/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
"dependencies": {
7373
"@huggingface/gguf": "^0.1.9",
7474
"express": "^4.19.2",
75+
"express-openapi-validator": "^5.3.1",
7576
"isomorphic-git": "^1.27.1",
7677
"mustache": "^4.2.0",
7778
"openai": "^4.56.0",
@@ -83,6 +84,7 @@
8384
},
8485
"devDependencies": {
8586
"@podman-desktop/api": "1.12.0",
87+
"@rollup/plugin-replace": "^5.0.7",
8688
"@types/express": "^4.17.21",
8789
"@types/js-yaml": "^4.0.9",
8890
"@types/mustache": "^4.2.5",

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

+13
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import {join} from 'path';
2020
import {builtinModules} from 'module';
21+
import replace from '@rollup/plugin-replace';
2122

2223
const PACKAGE_ROOT = __dirname;
2324

@@ -59,6 +60,18 @@ const config = {
5960
emptyOutDir: true,
6061
reportCompressedSize: false,
6162
},
63+
plugins: [
64+
// This is to apply the patch https://github.com/JS-DevTools/ono/pull/20
65+
// can be removed when the patch is merged
66+
replace({
67+
delimiters: ['', ''],
68+
preventAssignment: true,
69+
values: {
70+
'if (typeof module === "object" && typeof module.exports === "object") {':
71+
'if (typeof module === "object" && typeof module.exports === "object" && typeof module.exports.default === "object") {',
72+
},
73+
}),
74+
],
6275
};
6376

6477
export default config;

0 commit comments

Comments
 (0)