-
-
Notifications
You must be signed in to change notification settings - Fork 229
add onError to validateRequestOptions #1101
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
ea71ea7
794aceb
a41ec31
f6fd06c
8b9dd78
4a59580
ee9ff2b
f8a4142
192ac5e
6435e7c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -61,6 +61,7 @@ export type ValidateRequestOpts = { | |
allowUnknownQueryParameters?: boolean; | ||
coerceTypes?: boolean | 'array'; | ||
removeAdditional?: boolean | 'all' | 'failing'; | ||
onError?: (err: InternalServerError, req: Request) => void; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the intend of this change to introduce onError to requests, mirroring what exists currently (but only) for responses? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes that's right. My thinking is that we currently have three platforms hitting our APIs. I believe I've captured the the real-world usage of our APIs, but there are enough edge cases that I'd believe I've missed something. I'd like to be able to introduce validation slowly, basically making sure that we are correctly extracting the openapi interface before I begin actually serving 400s using the validator. |
||
}; | ||
|
||
export type ValidateResponseOpts = { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -46,6 +46,7 @@ export class RequestValidator { | |
delete this.apiDoc.components?.examples; | ||
this.requestOpts.allowUnknownQueryParameters = | ||
options.allowUnknownQueryParameters; | ||
this.requestOpts.onError = options.onError; | ||
|
||
this.ajv = createRequestAjv( | ||
apiDoc, | ||
|
@@ -157,11 +158,19 @@ export class RequestValidator { | |
mutator.modifyRequest(req); | ||
|
||
if (!allowUnknownQueryParameters) { | ||
this.processQueryParam( | ||
req.query, | ||
schemaProperties.query, | ||
securityQueryParam, | ||
); | ||
try { | ||
this.processQueryParam( | ||
req.query, | ||
schemaProperties.query, | ||
securityQueryParam, | ||
); | ||
} catch (error) { | ||
if (this.requestOpts.onError) { | ||
this.requestOpts.onError(error, req); | ||
} else { | ||
throw error; | ||
} | ||
} | ||
} | ||
|
||
const schemaBody = <any>validator?.schemaBody; | ||
|
@@ -218,7 +227,12 @@ export class RequestValidator { | |
message: message, | ||
}); | ||
error.errors = err.errors; | ||
throw error; | ||
if (this.requestOpts.onError) { | ||
this.requestOpts.onError(error, req); | ||
next(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we need to call next() here? we should expect onError handlers to always throw, correct? or is the intent to also allow clients to eat errors? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My thinking was that we should let clients eat the error, yeah. In the case I'm implementing it for, eg, I want to basically know when If the client wants an error, they can re-throw. This is how I've been using the response validator, and as far as I can tell it is working. |
||
} else { | ||
throw error; | ||
} | ||
} | ||
}; | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
import * as path from 'path'; | ||
import { expect } from 'chai'; | ||
import * as request from 'supertest'; | ||
import { createApp } from './common/app'; | ||
import * as packageJson from '../package.json'; | ||
import { AppWithServer } from './common/app.common'; | ||
|
||
const apiSpecPath = path.join('test', 'resources', 'request.validation.yaml'); | ||
|
||
let onErrorArgs: any[] | null; | ||
|
||
async function buildApp({ | ||
allowUnknownQueryParameters, | ||
}: { | ||
allowUnknownQueryParameters: boolean; | ||
}): Promise<AppWithServer> { | ||
return await createApp( | ||
{ | ||
apiSpec: apiSpecPath, | ||
validateResponses: false, | ||
validateRequests: { | ||
allowUnknownQueryParameters, | ||
onError: function (_err, req) { | ||
onErrorArgs = [_err, req]; | ||
if (req.query['limit'] === 'bad_type_throw') { | ||
throw new Error('error in onError handler'); | ||
} | ||
}, | ||
}, | ||
}, | ||
3005, | ||
(app) => { | ||
app.get(`${app.basePath}/pets`, (req, res) => { | ||
debugger; | ||
let json = [ | ||
{ id: '1', name: 'fido' }, | ||
{ id: '2', name: 'rex' }, | ||
{ id: '3', name: 'spot' }, | ||
]; | ||
if (req.query.limit === 'not_an_integer') { | ||
json = [{ id: 'bad_limit', name: 'not an int' }]; | ||
} else if (req.query.unknown_param === '123') { | ||
json = [{ id: 'unknown_param', name: 'unknown' }]; | ||
} else if (req.query.limit === 'bad_type_throw') { | ||
json = [{ id: 'bad_limit_throw', name: 'name' }]; | ||
} | ||
res.json(json); | ||
}); | ||
app.use((err, _req, res, _next) => { | ||
res.status(err.status ?? 500).json({ | ||
message: err.message, | ||
code: err.status ?? 500, | ||
}); | ||
}); | ||
}, | ||
false, | ||
); | ||
} | ||
|
||
describe(packageJson.name, () => { | ||
let app: AppWithServer; | ||
|
||
before(async () => { | ||
// set up express app | ||
app = await buildApp({ allowUnknownQueryParameters: false }); | ||
}); | ||
|
||
afterEach(() => { | ||
onErrorArgs = null; | ||
}); | ||
|
||
after(() => { | ||
app.server.close(); | ||
}); | ||
|
||
it('custom error handler invoked if has unknown query parameter', async () => | ||
request(app) | ||
.get(`${app.basePath}/pets?limit=3&unknown_param=123`) | ||
.expect(200) | ||
.then((r: any) => { | ||
const data = [{ id: 'unknown_param', name: 'unknown' }]; | ||
expect(r.body).to.eql(data); | ||
expect(onErrorArgs?.length).to.equal(2); | ||
expect(onErrorArgs![0].message).to.equal( | ||
"Unknown query parameter 'unknown_param'", | ||
); | ||
expect(onErrorArgs![1].query).to.eql({ | ||
limit: 3, | ||
unknown_param: '123', | ||
}); | ||
})); | ||
|
||
it('custom error handler not invoked if has unknown query parameter, but is allowed', async () => { | ||
app.server.close(); | ||
app = await buildApp({ allowUnknownQueryParameters: true }); | ||
|
||
request(app) | ||
.get(`${app.basePath}/pets?limit=3&unknown_param=123`) | ||
.expect(200) | ||
.then((r: any) => { | ||
expect(r.body).is.an('array').with.length(3); | ||
expect(onErrorArgs).to.equal(null); | ||
}); | ||
}); | ||
|
||
it('custom error handler invoked if request query field has the wrong type', async () => | ||
request(app) | ||
.get(`${app.basePath}/pets?limit=not_an_integer`) | ||
.expect(200) | ||
.then((r: any) => { | ||
const data = [{ id: 'bad_limit', name: 'not an int' }]; | ||
expect(r.body).to.eql(data); | ||
expect(onErrorArgs?.length).to.equal(2); | ||
expect(onErrorArgs![0].message).to.equal( | ||
'request/query/limit must be integer', | ||
); | ||
expect(onErrorArgs![1].query).to.eql({ | ||
limit: 'not_an_integer', | ||
}); | ||
})); | ||
|
||
it('custom error handler not invoked on valid response', async () => | ||
request(app) | ||
.get(`${app.basePath}/pets?limit=3`) | ||
.expect(200) | ||
.then((r: any) => { | ||
expect(r.body).is.an('array').with.length(3); | ||
expect(onErrorArgs).to.equal(null); | ||
})); | ||
|
||
it('returns error if custom error handler throws', async () => | ||
request(app) | ||
.get(`${app.basePath}/pets?limit=bad_type_throw`) | ||
.expect(500) | ||
.then((r: any) => { | ||
expect(r.body.message).to.equal('error in onError handler'); | ||
expect(onErrorArgs!.length).to.equal(2); | ||
expect(onErrorArgs![0].message).to.equal( | ||
'request/query/limit must be integer', | ||
); | ||
expect(onErrorArgs![1].query).to.eql({ | ||
limit: 'bad_type_throw', | ||
}); | ||
})); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this change necessary? Does the list include the complete set of user specified options? If not, it will change/ break existing behavior
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought that I did need it, yes, b/c otherwise the
onError
property isn't actually passed from the constructor.Confirmed that removing it breaks the tests I added.
I thought this would be a no-op if you weren't defining this new property. I didn't break any existing tests, so I thought this was safe.
What am I missing?