Skip to content

Commit

Permalink
Merge pull request #1046 from parasgera/master
Browse files Browse the repository at this point in the history
JSON schema validation for http request body
  • Loading branch information
dherault committed Jul 18, 2020
2 parents e752997 + 4f5513d commit 81c3851
Show file tree
Hide file tree
Showing 7 changed files with 127 additions and 7 deletions.
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Expand Up @@ -163,6 +163,7 @@
"java-invoke-local": "0.0.6",
"js-string-escape": "^1.0.1",
"jsonpath-plus": "^3.0.0",
"jsonschema": "^1.2.6",
"jsonwebtoken": "^8.5.1",
"jszip": "^3.2.2",
"luxon": "^1.22.0",
Expand Down
40 changes: 33 additions & 7 deletions src/events/http/HttpServer.js
Expand Up @@ -15,6 +15,7 @@ import {
VelocityContext,
} from './lambda-events/index.js'
import parseResources from './parseResources.js'
import payloadSchemaValidator from './payloadSchemaValidator.js'
import debugLog from '../../debugLog.js'
import serverlessLog, { logRoutes } from '../../serverlessLog.js'
import {
Expand Down Expand Up @@ -466,6 +467,12 @@ export default class HttpServer {
? requestTemplates[contentType]
: ''

const schema =
typeof endpoint?.request?.schema !== 'undefined' &&
integration === 'AWS'
? endpoint.request.schema[contentType]
: ''

// https://hapijs.com/api#route-configuration doesn't seem to support selectively parsing
// so we have to do it ourselves
const contentTypesThatRequirePayloadParsing = [
Expand Down Expand Up @@ -493,6 +500,16 @@ export default class HttpServer {
debugLog('requestTemplate:', requestTemplate)
debugLog('payload:', request.payload)

/* REQUEST PAYLOAD SCHEMA VALIDATION */
if (schema) {
debugLog('schema:', request.schema)
try {
payloadSchemaValidator.validate(schema, request.payload)
} catch (err) {
return this._reply400(response, err.message, err)
}
}

/* REQUEST TEMPLATE PROCESSING (event population) */

let event = {}
Expand All @@ -509,7 +526,7 @@ export default class HttpServer {
requestPath,
).create()
} catch (err) {
return this._reply500(
return this._reply502(
response,
`Error while parsing template "${contentType}" for ${functionKey}`,
err,
Expand Down Expand Up @@ -566,7 +583,7 @@ export default class HttpServer {
// it here and reply in the same way that we would have above when
// we lazy-load the non-IPC handler function.
if (this.#options.useChildProcesses && err.ipcException) {
return this._reply500(
return this._reply502(
response,
`Error while loading ${functionKey}`,
err,
Expand Down Expand Up @@ -763,7 +780,7 @@ export default class HttpServer {
} else if (typeof result === 'string') {
response.source = JSON.stringify(result)
} else if (result && result.body && typeof result.body !== 'string') {
return this._reply500(
return this._reply502(
response,
'According to the API Gateway specs, the body content must be stringified. Check your Lambda response and make sure you are invoking JSON.stringify(YOUR_CONTENT) on your body object',
{},
Expand Down Expand Up @@ -835,7 +852,7 @@ export default class HttpServer {
response.variety = 'buffer'
} else {
if (result && result.body && typeof result.body !== 'string') {
return this._reply500(
return this._reply502(
response,
'According to the API Gateway specs, the body content must be stringified. Check your Lambda response and make sure you are invoking JSON.stringify(YOUR_CONTENT) on your body object',
{},
Expand Down Expand Up @@ -872,15 +889,14 @@ export default class HttpServer {
})
}

// Bad news
_reply500(response, message, error) {
_replyError(statusCode, response, message, error) {
serverlessLog(message)

console.error(error)

response.header('Content-Type', 'application/json')

response.statusCode = 502 // APIG replies 502 by default on failures;
response.statusCode = statusCode
response.source = {
errorMessage: message,
errorType: error.constructor.name,
Expand All @@ -892,6 +908,16 @@ export default class HttpServer {
return response
}

// Bad news
_reply502(response, message, error) {
// APIG replies 502 by default on failures;
return this._replyError(502, response, message, error)
}

_reply400(response, message, error) {
return this._replyError(400, response, message, error)
}

createResourceRoutes() {
const resourceRoutesOptions = this.#options.resourceRoutes

Expand Down
15 changes: 15 additions & 0 deletions src/events/http/payloadSchemaValidator.js
@@ -0,0 +1,15 @@
'use strict'

const { validate: validateJsonSchema } = require('jsonschema')

exports.validate = function validate(model, body) {
const result = validateJsonSchema(body, model)

if (result.errors.length > 0) {
throw new Error(
`Request body validation failed: ${result.errors
.map((e) => e.message)
.join(', ')}`,
)
}
}
7 changes: 7 additions & 0 deletions tests/integration/handler/handler.js
Expand Up @@ -209,3 +209,10 @@ exports.TestResourceVariable = (event, context, callback) => {
body: stringify(event.resource),
})
}

exports.TestPayloadSchemaValidation = (event, context, callback) => {
callback(null, {
statusCode: 200,
body: stringify(event.body),
})
}
48 changes: 48 additions & 0 deletions tests/integration/handler/handlerPayload.test.js
Expand Up @@ -322,3 +322,51 @@ describe('handler payload tests with prepend off', () => {
})
})
})

describe('handler payload scehma validation tests', () => {
// init
beforeAll(() =>
setup({
servicePath: resolve(__dirname),
args: ['--noPrependStageInUrl'],
}),
)

// cleanup
afterAll(() => teardown())
;[
{
description: 'test with valid payload',
expectedBody: `{"foo":"bar"}`,
path: '/test-payload-schema-validator',
body: {
foo: 'bar',
},
status: 200,
},

{
description: 'test with invalid payload',
path: '/test-payload-schema-validator',
body: {},
status: 400,
},
].forEach(({ description, expectedBody, path, body, status }) => {
test(description, async () => {
const url = joinUrl(TEST_BASE_URL, path)

const response = await fetch(url, {
method: 'post',
body: JSON.stringify(body),
headers: { 'Content-Type': 'application/json' },
})

expect(response.status).toEqual(status)

if (expectedBody) {
const json = await response.json()
expect(json.body).toEqual(expectedBody)
}
})
})
})
18 changes: 18 additions & 0 deletions tests/integration/handler/serverless.yml
Expand Up @@ -162,3 +162,21 @@ functions:
method: get
path: /{id}/test-resource-variable-handler
handler: handler.TestResourceVariable

TestPayloadSchemaValidation:
events:
- http:
method: post
path: /test-payload-schema-validator
integration: lambda
request:
schema:
application/json:
$schema: http://json-schema.org/draft-07/schema
type: object
required:
- foo
properties:
foo:
type: string
handler: handler.TestPayloadSchemaValidation

0 comments on commit 81c3851

Please sign in to comment.