Skip to content

Commit a0f8d7d

Browse files
mariayordschwma
andauthored
fix: server crash by SyntaxError from express JSON parser (#214)
Co-authored-by: Marcel Schwarz <[email protected]>
1 parent 52d6797 commit a0f8d7d

File tree

6 files changed

+54
-10
lines changed

6 files changed

+54
-10
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717

1818
- Sanitization of logged queries, when running in production, if literal values contain parentheses
1919
- Read GraphiQL HTML file only once on startup, and only if GraphiQL is enabled, to avoid unnecessary file system access
20+
- Server crash by `SyntaxError` from express JSON parser
2021

2122
### Removed
2223

lib/logger.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,17 @@ const LOG = cds.log('graphql')
88
const { parse, visit, Kind, print } = require('graphql')
99

1010
const _isEmptyObject = o => Object.keys(o).length === 0
11+
class InvalidJSON extends Error {}
12+
InvalidJSON.prototype.name = 'Invalid JSON body'
13+
InvalidJSON.prototype.status = 400
1114

12-
router.use(express.json({ ...cds.env.server.body_parser })) //> required by logger below
15+
router.use(function jsonBodyParser(req, res, next) {
16+
express.json({ ...cds.env.server.body_parser })(req, res, function http_body_parser_next(err) {
17+
// Need to wrap, as CAP server deliberately crashes on SyntaxErrors
18+
if (err) return next(new InvalidJSON(err.message))
19+
next()
20+
})
21+
})
1322

1423
router.use(function queryLogger(req, _, next) {
1524
let query = req.body?.query || (req.query.query && decodeURIComponent(req.query.query))

test/tests/concurrency.test.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ describe('graphql - resolver concurrency', () => {
88
axios.defaults.validateStatus = false
99

1010
describe('execution order of query and mutation resolvers', () => {
11-
1211
let _log = []
1312

1413
beforeEach(() => {

test/tests/error-handling-prod.test.js

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ describe('graphql - error handling in production', () => {
2121
_error = []
2222
})
2323

24-
2524
describe('Errors thrown by CDS', () => {
2625
test('Single @mandatory validation error', async () => {
2726
const query = gql`
@@ -39,7 +38,7 @@ describe('graphql - error handling in production', () => {
3938
{
4039
message: expect.any(String),
4140
extensions: {
42-
code: expect.stringMatching(/ASSERT_MANDATORY|ASSERT_NOT_NULL/) ,
41+
code: expect.stringMatching(/ASSERT_MANDATORY|ASSERT_NOT_NULL/),
4342
target: 'notEmptyI'
4443
}
4544
}
@@ -48,7 +47,10 @@ describe('graphql - error handling in production', () => {
4847
expect(response.data).toMatchObject({ errors })
4948
expect(response.data.errors[0].extensions).not.toHaveProperty('stacktrace') // No stacktrace in production
5049
const log = _warn[0][1] || JSON.parse(_warn[0][0])
51-
expect(log).toMatchObject({ code: expect.stringMatching(/ASSERT_MANDATORY|ASSERT_NOT_NULL/) , target: 'notEmptyI' })
50+
expect(log).toMatchObject({
51+
code: expect.stringMatching(/ASSERT_MANDATORY|ASSERT_NOT_NULL/),
52+
target: 'notEmptyI'
53+
})
5254
})
5355

5456
test('Multiple @mandatory validation errors', async () => {
@@ -70,12 +72,12 @@ describe('graphql - error handling in production', () => {
7072
code: 'MULTIPLE_ERRORS',
7173
details: [
7274
{
73-
code: expect.stringMatching(/ASSERT_MANDATORY|ASSERT_NOT_NULL/) ,
75+
code: expect.stringMatching(/ASSERT_MANDATORY|ASSERT_NOT_NULL/),
7476
message: expect.any(String),
7577
target: 'notEmptyI'
7678
},
7779
{
78-
code: expect.stringMatching(/ASSERT_MANDATORY|ASSERT_NOT_NULL/) ,
80+
code: expect.stringMatching(/ASSERT_MANDATORY|ASSERT_NOT_NULL/),
7981
message: expect.any(String),
8082
target: 'notEmptyS'
8183
}
@@ -135,7 +137,7 @@ describe('graphql - error handling in production', () => {
135137
code: 'MULTIPLE_ERRORS',
136138
details: [
137139
{
138-
code: expect.stringMatching(/ASSERT_MANDATORY|ASSERT_NOT_NULL/) ,
140+
code: expect.stringMatching(/ASSERT_MANDATORY|ASSERT_NOT_NULL/),
139141
message: 'Wert ist erforderlich',
140142
target: 'inRange'
141143
},
@@ -158,7 +160,7 @@ describe('graphql - error handling in production', () => {
158160
expect(log).toMatchObject({
159161
code: 'MULTIPLE_ERRORS',
160162
details: [
161-
{ code: expect.stringMatching(/ASSERT_MANDATORY|ASSERT_NOT_NULL/) , target: 'inRange' },
163+
{ code: expect.stringMatching(/ASSERT_MANDATORY|ASSERT_NOT_NULL/), target: 'inRange' },
162164
{
163165
code: 'ASSERT_ENUM',
164166
target: 'oneOfEnumValues'

test/tests/http.test.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
const cds = require('@sap/cds')
2+
const path = require('path')
3+
const util = require('util')
4+
const { axios } = cds
5+
.test('serve', 'srv/empty-service.cds')
6+
.in(path.join(__dirname, '../resources/empty-csn-definitions'))
7+
const _format = e => util.formatWithOptions({ colors: false, depth: null }, ...(Array.isArray(e) ? e : [e]))
8+
9+
let _error = []
10+
11+
describe('GraphQL express json parser error scenario', () => {
12+
beforeEach(() => {
13+
console.warn = (...s) => _error.push(s) // eslint-disable-line no-console
14+
})
15+
16+
afterEach(() => {
17+
_error = []
18+
})
19+
test('should trigger InvalidJSON for malformed JSON', async () => {
20+
expect.hasAssertions()
21+
try {
22+
response = await axios.request({
23+
method: 'POST',
24+
url: `/graphql`,
25+
data: '{ some_value'
26+
})
27+
} catch (err) {
28+
expect(err.status).toBe(400)
29+
expect(err.response.data.error.message).toMatch(/not valid JSON/)
30+
expect(_format(_error[0])).toContain('InvalidJSON')
31+
}
32+
})
33+
})

test/tests/schema.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ const path = require('path')
33
// Load @cap-js/graphql plugin to ensure .to.gql and .to.graphql compile targets are registered
44
require('../../cds-plugin')
55

6-
const { models } = require('../resources')
6+
const { models } = require('../resources')
77
const { SCHEMAS_DIR } = require('../util')
88
const { printSchema, validateSchema } = require('graphql')
99

0 commit comments

Comments
 (0)