diff --git a/docs/site/Error-handling.md b/docs/site/Error-handling.md index 030952bc592f..182c9503979a 100644 --- a/docs/site/Error-handling.md +++ b/docs/site/Error-handling.md @@ -11,9 +11,12 @@ permalink: /doc/en/lb4/Error-handling.html In order to allow clients to reliably detect individual error causes, LoopBack sets the error `code` property to a machine-readable string. -| Error code | Description | -| :--------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| ENTITY_NOT_FOUND | The entity (model) was not found. This error is returned for example by [`EntityCrudRepository.prototype.findById`](http://apidocs.loopback.io/@loopback%2fdocs/repository.html#EntityCrudRepository.prototype.findById) | +| Error code | Description | +| :------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| ENTITY_NOT_FOUND | The entity (model) was not found. This error is returned for example by [`EntityCrudRepository.prototype.findById`](http://apidocs.loopback.io/@loopback%2fdocs/repository.html#EntityCrudRepository.prototype.findById) | +| VALIDATION_FAILED | The data provided by the client is not a valid entity. | +| INVALID_PARAMETER_VALUE | The value provided for a parameter of a REST endpoint is not valid. For example, a string value was provided for a numeric parameter. | +| MISSING_REQUIRED_PARAMETER | No value was provided for a required parameter. | Besides LoopBack-specific error codes, your application can encounter low-level error codes from Node.js and the underlying operating system. For example, when diff --git a/packages/rest/src/rest-http-error.ts b/packages/rest/src/rest-http-error.ts index 76776067220d..8dd9537c4868 100644 --- a/packages/rest/src/rest-http-error.ts +++ b/packages/rest/src/rest-http-error.ts @@ -7,12 +7,22 @@ export namespace RestHttpErrors { extraProperties?: Props, ): HttpErrors.HttpError & Props { const msg = `Invalid data ${JSON.stringify(data)} for parameter ${name}!`; - return Object.assign(new HttpErrors.BadRequest(msg), extraProperties); + return Object.assign( + new HttpErrors.BadRequest(msg), + { + code: 'INVALID_PARAMETER_VALUE', + parameterName: name, + }, + extraProperties, + ); } export function missingRequired(name: string): HttpErrors.HttpError { const msg = `Required parameter ${name} is missing!`; - return new HttpErrors.BadRequest(msg); + return Object.assign(new HttpErrors.BadRequest(msg), { + code: 'MISSING_REQUIRED_PARAMETER', + parameterName: name, + }); } export function invalidParamLocation(location: string): HttpErrors.HttpError { @@ -23,7 +33,12 @@ export namespace RestHttpErrors { export const INVALID_REQUEST_BODY_MESSAGE = 'The request body is invalid. See error object `details` property for more info.'; export function invalidRequestBody(): HttpErrors.HttpError { - return new HttpErrors.UnprocessableEntity(INVALID_REQUEST_BODY_MESSAGE); + return Object.assign( + new HttpErrors.UnprocessableEntity(INVALID_REQUEST_BODY_MESSAGE), + { + code: 'VALIDATION_FAILED', + }, + ); } /** diff --git a/packages/rest/src/validation/request-body.validator.ts b/packages/rest/src/validation/request-body.validator.ts index 4f86d5841561..bdbccb5fb5b4 100644 --- a/packages/rest/src/validation/request-body.validator.ts +++ b/packages/rest/src/validation/request-body.validator.ts @@ -33,8 +33,18 @@ export function validateRequestBody( requestBodySpec: RequestBodyObject | undefined, globalSchemas?: SchemasObject, ) { - if (requestBodySpec && requestBodySpec.required && body == undefined) - throw new HttpErrors.BadRequest('Request body is required'); + if (!requestBodySpec) return; + + if (requestBodySpec.required && body == undefined) { + const err = Object.assign( + new HttpErrors.BadRequest('Request body is required'), + { + code: 'MISSING_REQUIRED_PARAMETER', + parameterName: 'request body', + }, + ); + throw err; + } const schema = getRequestBodySchema(requestBodySpec); debug('Request body schema: %j', util.inspect(schema, {depth: null})); @@ -49,10 +59,8 @@ export function validateRequestBody( * @param requestBodySpec The requestBody specification defined in `@requestBody()`. */ function getRequestBodySchema( - requestBodySpec: RequestBodyObject | undefined, + requestBodySpec: RequestBodyObject, ): SchemaObject | undefined { - if (!requestBodySpec) return; - const content = requestBodySpec.content; // FIXME(bajtos) we need to find the entry matching the content-type // header from the incoming request (e.g. "application/json"). diff --git a/packages/rest/test/unit/request-body.validator.test.ts b/packages/rest/test/unit/request-body.validator.test.ts index 9fdd269856c2..4f32d3a02ee4 100644 --- a/packages/rest/test/unit/request-body.validator.test.ts +++ b/packages/rest/test/unit/request-body.validator.test.ts @@ -67,6 +67,7 @@ describe('validateRequestBody', () => { ]; verifyValidationRejectsInputWithError( INVALID_MSG, + 'VALIDATION_FAILED', details, { description: 'missing required "title"', @@ -86,6 +87,7 @@ describe('validateRequestBody', () => { ]; verifyValidationRejectsInputWithError( INVALID_MSG, + 'VALIDATION_FAILED', details, { title: 'todo with a string value of "isComplete"', @@ -112,6 +114,7 @@ describe('validateRequestBody', () => { ]; verifyValidationRejectsInputWithError( INVALID_MSG, + 'VALIDATION_FAILED', details, { description: 'missing title and a string value of "isComplete"', @@ -140,6 +143,7 @@ describe('validateRequestBody', () => { ]; verifyValidationRejectsInputWithError( INVALID_MSG, + 'VALIDATION_FAILED', details, {description: 'missing title'}, aBodySpec({$ref: '#/components/schemas/Todo'}), @@ -150,6 +154,7 @@ describe('validateRequestBody', () => { it('rejects empty values when body is required', () => { verifyValidationRejectsInputWithError( 'Request body is required', + 'MISSING_REQUIRED_PARAMETER', undefined, null, aBodySpec(TODO_SCHEMA, {required: true}), @@ -176,6 +181,7 @@ describe('validateRequestBody', () => { }; verifyValidationRejectsInputWithError( INVALID_MSG, + 'VALIDATION_FAILED', details, {count: 'string value'}, aBodySpec(schema), @@ -205,6 +211,7 @@ describe('validateRequestBody', () => { }; verifyValidationRejectsInputWithError( INVALID_MSG, + 'VALIDATION_FAILED', details, {orders: ['order1', 1]}, aBodySpec(schema), @@ -228,6 +235,7 @@ describe('validateRequestBody', () => { }; verifyValidationRejectsInputWithError( INVALID_MSG, + 'VALIDATION_FAILED', details, [{title: 'a good todo'}, {description: 'a todo item missing title'}], aBodySpec(schema), @@ -263,6 +271,7 @@ describe('validateRequestBody', () => { }; verifyValidationRejectsInputWithError( INVALID_MSG, + 'VALIDATION_FAILED', details, { todos: [ @@ -298,6 +307,7 @@ describe('validateRequestBody', () => { }; verifyValidationRejectsInputWithError( INVALID_MSG, + 'VALIDATION_FAILED', details, { accounts: [ @@ -314,8 +324,9 @@ describe('validateRequestBody', () => { // ----- HELPERS ----- / function verifyValidationRejectsInputWithError( - errorMatcher: Error | RegExp | string, - details: RestHttpErrors.ValidationErrorDetails[] | undefined, + expectedMessage: string, + expectedCode: string, + expectedDetails: RestHttpErrors.ValidationErrorDetails[] | undefined, body: object | null, spec: RequestBodyObject | undefined, schemas?: SchemasObject, @@ -326,7 +337,8 @@ function verifyValidationRejectsInputWithError( "expected Function { name: 'validateRequestBody' } to throw exception", ); } catch (err) { - expect(err.message).to.equal(errorMatcher); - expect(err.details).to.deepEqual(details); + expect(err.message).to.equal(expectedMessage); + expect(err.code).to.equal(expectedCode); + expect(err.details).to.deepEqual(expectedDetails); } }