Skip to content

Commit a323814

Browse files
authored
fix: gracefully handle state handler errors (#1585)
Previously, when an error was thrown in a state handler an HTTP 500 was sent back to the core engine, making it hard to debug the issue from within test frameworks. This change wraps the state handlers with error handling, rather than propagating the error back to the pact core engine. Errors will be clearly printed to the console (and logged at ERROR level). Fixes #631 Co-authored-by: mefellows <mefellows@users.noreply.github.com>
1 parent ff914b6 commit a323814

File tree

4 files changed

+134
-46
lines changed

4 files changed

+134
-46
lines changed

package-lock.json

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

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,8 @@
116116
"lodash": "^4.17.21",
117117
"ramda": "^0.32.0",
118118
"randexp": "^0.5.3",
119-
"router": "^2.2.0"
119+
"router": "^2.2.0",
120+
"stack-utils": "^2.0.6"
120121
},
121122
"devDependencies": {
122123
"@babel/cli": "7.28.3",
@@ -134,6 +135,7 @@
134135
"@types/ramda": "0.31.1",
135136
"@types/sinon": "17.0.4",
136137
"@types/sinon-chai": "4.0.0",
138+
"@types/stack-utils": "^2.0.3",
137139
"@typescript-eslint/eslint-plugin": "5.62.0",
138140
"@typescript-eslint/parser": "5.62.0",
139141
"chai": "6.2.0",

src/dsl/verifier/proxy/stateHandler/stateHandler.spec.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import chaiAsPromised from 'chai-as-promised';
33

44
import express from 'express';
55

6+
import sinon from 'sinon';
67
import { createProxyStateHandler } from './stateHandler';
78
import { ProxyOptions, StateHandlers } from '../types';
89

@@ -54,7 +55,8 @@ describe('#createProxyStateHandler', () => {
5455
},
5556
};
5657

57-
it('returns a 500', async () => {
58+
it('returns a 200 and logs an error', async () => {
59+
const spy = sinon.spy(console, 'log');
5860
const h = createProxyStateHandler({
5961
stateHandlers: badStateHandlers,
6062
} as ProxyOptions);
@@ -65,7 +67,11 @@ describe('#createProxyStateHandler', () => {
6567
mockResponse as express.Response
6668
);
6769

68-
expect(res).to.eql(500);
70+
expect(res).to.eql(200);
71+
expect(spy.callCount).to.eql(3);
72+
expect(spy.getCall(0).args[0]).to.include(
73+
"Error executing state handler for state 'thing exists' on 'setup'."
74+
);
6975
});
7076
});
7177
});
Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,43 @@
11
import express from 'express';
22

3+
import StackUtils from 'stack-utils';
4+
import chalk from 'chalk';
35
import { ProxyOptions, ProviderState } from '../types';
46
import { setupStates } from './setupStates';
57

8+
const cleanStack = (e: Error) => {
9+
const stack = new StackUtils({
10+
cwd: process.cwd(),
11+
internals: StackUtils.nodeInternals(),
12+
});
13+
14+
if (!e.stack) return '';
15+
const cleanedStack = stack.clean(e.stack);
16+
const lines = cleanedStack.split('\n').map((line) => line.trim());
17+
18+
return lines[0];
19+
};
20+
621
export const createProxyStateHandler =
722
(config: ProxyOptions) =>
8-
(req: express.Request, res: express.Response): Promise<express.Response> => {
9-
const message: ProviderState = req.body;
23+
async (
24+
req: express.Request,
25+
res: express.Response
26+
): Promise<express.Response> => {
27+
const state: ProviderState = req.body;
28+
try {
29+
const data = await setupStates(state, config);
30+
return res.json(data);
31+
} catch (e) {
32+
const error = `\nError executing state handler for state '${state.state}' on '${state.action}'.`;
33+
const errorDetails = `↳ Error details: ${e.message}`;
34+
const errorSource = `↳ Error source: ${cleanStack(e)}\n`;
35+
/* eslint-disable no-console */
36+
console.log(chalk.red(error));
37+
console.log(chalk.red(errorDetails));
38+
console.log(chalk.red(errorSource));
39+
/* eslint-enable */
1040

11-
return Promise.resolve(setupStates(message, config))
12-
.then((data) => res.json(data))
13-
.catch((e) => res.status(500).send(e));
41+
return res.status(200).send();
42+
}
1443
};

0 commit comments

Comments
 (0)