Skip to content

Commit e198a31

Browse files
authored
Merge pull request #42 from tinystacks/error-handling
Better Error Handling
2 parents c4ed3f5 + ff007eb commit e198a31

File tree

7 files changed

+135
-39
lines changed

7 files changed

+135
-39
lines changed

package-lock.json

Lines changed: 49 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"@types/cors": "^2.8.13",
3838
"@types/express": "^4.17.16",
3939
"@types/http-errors": "^2.0.1",
40+
"@types/http-status-codes": "^1.2.0",
4041
"@types/jest": "^29.4.0",
4142
"@types/js-yaml": "^4.0.5",
4243
"@types/lodash.camelcase": "^4.3.7",
@@ -69,8 +70,8 @@
6970
"@aws-sdk/client-api-gateway": "^3.267.0",
7071
"@aws-sdk/client-s3": "^3.288.0",
7172
"@aws-sdk/credential-providers": "^3.282.0",
72-
"@tinystacks/ops-core": "^0.3.2",
73-
"@tinystacks/ops-model": "^0.4.0",
73+
"@tinystacks/ops-core": "^0.4.0",
74+
"@tinystacks/ops-model": "^0.5.0",
7475
"@types/react": "^18.0.28",
7576
"body-parser": "^1.20.1",
7677
"cached": "^6.1.0",
@@ -80,6 +81,7 @@
8081
"express": "^4.18.2",
8182
"express-openapi": "^12.1.0",
8283
"http-errors": "^2.0.0",
84+
"http-status-codes": "^2.2.0",
8385
"js-yaml": "^4.1.0",
8486
"json-refs": "^3.0.15",
8587
"lodash.camelcase": "^4.3.0",

src/clients/console-client/local.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import yaml from 'js-yaml';
21
import isNil from 'lodash.isnil';
32
import { ConsoleParser } from '@tinystacks/ops-core';
4-
import { Console as ConsoleType, YamlConsole } from '@tinystacks/ops-model';
3+
import { Config, Console as ConsoleType, YamlConsole } from '@tinystacks/ops-model';
54
import HttpError from 'http-errors';
65
import {
76
writeFileSync
@@ -11,6 +10,7 @@ import {
1110
} from 'path';
1211
import FsUtils from '../../utils/fs-utils.js';
1312
import IConsoleClient from './i-console-client.js';
13+
import Yaml from '../../utils/yaml.js';
1414

1515
class LocalConsoleClient implements IConsoleClient {
1616
async getConsole (_consoleName?: string): Promise<ConsoleParser> {
@@ -20,10 +20,10 @@ class LocalConsoleClient implements IConsoleClient {
2020
// console.debug('configFilePath: ', configFilePath);
2121
const configFile = FsUtils.tryToReadFile(configFilePath);
2222
if (!configFile) throw HttpError.NotFound(`Cannot fetch console! Config file ${configPath} not found!`);
23-
const configJson = (yaml.load(configFile.toString()) as any)?.Console as YamlConsole;
23+
const configJson = Yaml.parseAs<Config>(configFile.toString());
2424
// console.debug('configJson: ', JSON.stringify(configJson));
25-
if (!isNil(configJson)) {
26-
const consoleType: ConsoleType = ConsoleParser.parse(configJson);
25+
if (!isNil(configJson?.Console)) {
26+
const consoleType: ConsoleType = ConsoleParser.parse(configJson?.Console as YamlConsole);
2727
return ConsoleParser.fromJson(consoleType);
2828
}
2929
throw HttpError.InternalServerError('Cannot fetch console! The contents of the config file was empty or invalid!');
@@ -41,7 +41,7 @@ class LocalConsoleClient implements IConsoleClient {
4141
const previousConsole = await this.getConsole(consoleName);
4242
console.providers = previousConsole.providers;
4343
const yamlConsole = await console.toYaml();
44-
const consoleYml = yaml.dump({ Console: yamlConsole });
44+
const consoleYml = Yaml.stringify({ Console: yamlConsole });
4545
const configPath = process.env.CONFIG_PATH;
4646
if (isNil(configPath)) throw HttpError.InternalServerError(`Cannot save console ${console.name}! No value was found for CONFIG_PATH!`);
4747
try {

src/clients/console-client/s3.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import yaml from 'js-yaml';
21
import isNil from 'lodash.isnil';
32
import { ConsoleParser } from '@tinystacks/ops-core';
4-
import { Console as ConsoleType, YamlConsole } from '@tinystacks/ops-model';
3+
import { Config, Console as ConsoleType, YamlConsole } from '@tinystacks/ops-model';
54
import HttpError from 'http-errors';
65
import {
76
mkdirSync,
@@ -12,6 +11,7 @@ import { S3 } from '@aws-sdk/client-s3';
1211
import FsUtils from '../../utils/fs-utils.js';
1312
import IConsoleClient from './i-console-client.js';
1413
import { TMP_DIR } from '../../constants.js';
14+
import Yaml from '../../utils/yaml.js';
1515

1616
type S3Info = {
1717
bucketName: string;
@@ -177,10 +177,10 @@ class S3ConsoleClient implements IConsoleClient {
177177
*/
178178
const configFile = await this.getConfig();
179179
if (!configFile) throw HttpError.NotFound(`Cannot fetch console! Config file ${configPath} not found!`);
180-
const configJson = (yaml.load(configFile.toString()) as any)?.Console as YamlConsole;
180+
const configJson = Yaml.parseAs<Config>(configFile.toString());
181181
// console.debug('configJson: ', JSON.stringify(configJson));
182-
if (!isNil(configJson)) {
183-
const consoleType: ConsoleType = ConsoleParser.parse(configJson);
182+
if (!isNil(configJson?.Console)) {
183+
const consoleType: ConsoleType = ConsoleParser.parse(configJson?.Console as YamlConsole);
184184
return ConsoleParser.fromJson(consoleType);
185185
}
186186
throw HttpError.InternalServerError('Cannot fetch console! The contents of the config file was empty or invalid!');
@@ -200,7 +200,7 @@ class S3ConsoleClient implements IConsoleClient {
200200
const previousConsole = await this.getConsole(consoleName);
201201
console.providers = previousConsole.providers;
202202
const yamlConsole = await console.toYaml();
203-
const consoleYml = yaml.dump({ Console: yamlConsole });
203+
const consoleYml = Yaml.stringify({ Console: yamlConsole });
204204
const configPath = process.env.CONFIG_PATH;
205205
if (isNil(configPath)) throw HttpError.InternalServerError(`Cannot save console ${console.name}! No value was found for CONFIG_PATH!`);
206206
try {

src/middleware/error.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import HttpError from 'http-errors';
22
import { Request, Response, NextFunction } from 'express';
33
import { TinyStacksError } from '@tinystacks/ops-core';
44

5-
export default async function errorMiddleware (error: unknown, request: Request, response: Response, next: NextFunction) {
6-
console.error(error);
7-
if (TinyStacksError.isTinyStacksError(error) || HttpError.isHttpError(error)) {
8-
const { status, message } = error as TinyStacksError | HttpError.HttpError;
9-
response.status(status).json({ status, message });
5+
export default async function errorMiddleware (e: unknown, _request: Request, response: Response, next: NextFunction) {
6+
console.error(e);
7+
if (TinyStacksError.isTinyStacksError(e) || HttpError.isHttpError(e)) {
8+
const error = TinyStacksError.fromJson(e as any);
9+
response.status(error.status).json(error);
1010
} else {
1111
const ise = HttpError.InternalServerError('An unexpected error occured! See the API logs for more details.');
1212
response.status(ise.status).json(ise);

src/utils/yaml.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import jsYaml, { YAMLException } from 'js-yaml';
2+
import { TinyStacksError } from '@tinystacks/ops-core';
3+
import { StatusCodes } from 'http-status-codes';
4+
5+
class Yaml {
6+
static handleError (e: any, message: string): never {
7+
const tsError = TinyStacksError.fromJson({ message, status: StatusCodes.UNPROCESSABLE_ENTITY });
8+
if (e.name === 'YAMLException') {
9+
const error = e as YAMLException;
10+
tsError.cause = error?.reason?.trim();
11+
tsError.context = error?.mark?.snippet?.split('\n')?.map(s => s.trim())?.join('\n');
12+
}
13+
throw tsError;
14+
}
15+
16+
static parseAs<T> (yaml: string): T {
17+
try {
18+
return jsYaml.load(yaml) as T;
19+
} catch (e) {
20+
this.handleError(e, 'Failed to parse yaml!');
21+
}
22+
}
23+
24+
static parse (yaml: string): any {
25+
try {
26+
return jsYaml.load(yaml) as any;
27+
} catch (e) {
28+
this.handleError(e, 'Failed to parse yaml!');
29+
}
30+
}
31+
32+
static stringify (json: any): string {
33+
try {
34+
return jsYaml.dump(json);
35+
} catch (e) {
36+
this.handleError(e, 'Failed to stringify object to yaml!');
37+
}
38+
}
39+
}
40+
41+
export {
42+
Yaml
43+
};
44+
45+
export default Yaml;

test/middleware/error.test.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,13 @@ describe('error middleware tests', () => {
4040
expect(mockStatus).toBeCalled();
4141
expect(mockStatus).toBeCalledWith(418);
4242
expect(mockJson).toBeCalled();
43-
expect(mockJson).toBeCalledWith({ status: 418, message: 'mock-error' });
43+
expect(mockJson).toBeCalledWith({
44+
status: 418,
45+
message: 'mock-error',
46+
name: 'TinyStacksError',
47+
type: 'I\'m a teapot',
48+
stack: expect.any(String)
49+
});
4450
expect(mockNext).toBeCalled();
4551
});
4652

@@ -49,7 +55,7 @@ describe('error middleware tests', () => {
4955
name: 'TinyStacksError',
5056
status: 418,
5157
message: 'mock-error',
52-
type: TinyStacksErrorType.type.VALIDATION
58+
type: 'Validation'
5359
});
5460

5561
await errorMiddleware(mockError, mockRequest, mockResponse, mockNext);
@@ -59,7 +65,12 @@ describe('error middleware tests', () => {
5965
expect(mockStatus).toBeCalled();
6066
expect(mockStatus).toBeCalledWith(418);
6167
expect(mockJson).toBeCalled();
62-
expect(mockJson).toBeCalledWith({ status: 418, message: 'mock-error' });
68+
expect(mockJson).toBeCalledWith({
69+
status: 418,
70+
message: 'mock-error',
71+
name: 'TinyStacksError',
72+
type: 'Validation'
73+
});
6374
expect(mockNext).toBeCalled();
6475
});
6576

@@ -78,7 +89,12 @@ describe('error middleware tests', () => {
7889
expect(mockStatus).toBeCalled();
7990
expect(mockStatus).toBeCalledWith(418);
8091
expect(mockJson).toBeCalled();
81-
expect(mockJson).toBeCalledWith({ status: 418, message: 'mock-error' });
92+
expect(mockJson).toBeCalledWith({
93+
status: 418,
94+
message: 'mock-error',
95+
name: 'TinyStacksError',
96+
type: 'Validation'
97+
});
8298
expect(mockNext).toBeCalled();
8399
});
84100

0 commit comments

Comments
 (0)