Skip to content

Commit

Permalink
fix(docs, fastify-example): add example for an endpoint with multi qu…
Browse files Browse the repository at this point in the history
…ery param (#133)

add documentation about how to troubleshoot a request validation issue.
  • Loading branch information
Xavier-Redondo committed Feb 19, 2024
1 parent 2a4f575 commit 5b04c50
Show file tree
Hide file tree
Showing 8 changed files with 241 additions and 7 deletions.
106 changes: 106 additions & 0 deletions docs/guides/troubleshooting-schema-validation-issues.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Troubleshooting schema validation issues

Note: If you just want an overall vision of how the schema validation works in Bauta.js is best to start in the [validation](../validation.md) section.

This section covers a very specific topic, but that may become complex and tricky to troubleshoot: What happens if you are having an unexpected error during the schema validation phase.

## Conditions

This issue may happen after the following conditions meet:

- You have a valid schema swagger with the endpoint you are having issues with correctly defined.
- Bauta.js starts as expected and publishes accordingly the endpoint you are having issues with.
- For any reason the schema does not conform to your expectations: This may mean that you are getting an schema error validation when you would expect the request to be valid or the opposite.
- While this may happen for request or responses, we assume for the sake of the explanation that it happens for a request.

## Context for troubleshooting possible issues

You have to understand how the different components that allow for the schema validator work.

1. Bauta.js does not validate anything directly.
2. Instead, it delegates the validation to [the AJV library](https://ajv.js.org/guide/getting-started.html).
3. The validation is done in the request lifecycle that is a responsability of the server implementaiton used.
4. The AJV validator is created by default with an specific set of values that may not feed your needs, especially if you are having unexpected issues.

The default values for creating the AJV validator are the following:

```js
{
logger: false,
strict: false,
coerceTypes: false,
useDefaults: true,
removeAdditional: true,
allErrors: true
}
```

## Troubleshooting issues

If you are having unexpected validation issues the first culprit could be because one of the previous default options are not good for your use case. In that case you should check the information for those flags and make sure you overwrite them in the `validatorOptions` field exposed by Bauta.js.

However, these fields belong to the AJV schema validator initialization and Bauta.js passes them to it. The information for these fields is in the [relevant documentation of AJV](https://ajv.js.org/options.html#options-to-modify-validated-data).


## GET endpoint with an array query param

Let's assume that you want to implement an enpoint which is a GET that uses a query string param that may have N values that are mapped to an array.

This example is implemented for Fastify in the example project `bautajs-fastify-example`, using the operationId `multiplePathSpecific`.

With the default instance of AJV validator in bauta.js you will be able to call this endpoint with url's like this:

`http://localhost:3000/api/array-query-param?chickenIds=jack&chickenIds=peter`

But if you do a request with a single parameter:

`http://localhost:3000/api/array-query-param?chickenIds=jack`

You will get an unexpected result, because instead of a valid response you get an error:

```js
{
"error": {
"code": "BadArgument",
"message": "The request was not valid",
"details": [
{
"target": "querystring.properties.subscriptionIds.type",
"message": "must be array",
"code": "type"
}
]
}
}
```

### What is happening and how to solve it?

1. Following the instructions of this chapter, we decide to check the options of AJV constructor.

2. After an analysis of those options and its related documentation, and we discover that the option `coerceTypes`, set to **false** by default in Bauta.js *does not support arrays*.

3. We overwrite the value of this field passing to the Bauta.js constructor the following extra options:

```js
validatorOptions: {
coerceTypes: 'array'
}
```

4. Now, this solves the issue, so now, if you make the request

`http://localhost:3000/api/array-query-param?chickenIds=jack`

you will get the expected result instead of an unexpected schema validation error.

5. But with this option, AJV does not validate the inputs as string anymore, but coerces them into an array. This is relevant because now, this request (assuming that the field `chickenIds` is mandatory):

`http://localhost:3000/api/array-query-param?chickenIds=`

will not fail with an expected error but instead will continue with an array containing `["undefined"]`.


This is a simple use case to show you the issues that you can have the moment that your requirements do not meet the default options used by Bauta.js and AJV.


18 changes: 18 additions & 0 deletions docs/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,24 @@ Additionally, it allow to specify againts what HTTPS response status code you wa
});
```

### Request validation flow

Bauta.js delegates into the AJV validator the validation of the schemas. This may be done directly or through the server application. Thus, the behaviour of the request schema validation depends on the AJV options that are exposed to Bauta.js constructor through the `validatorOptions` field.

You may check details about this options [in the AJV documentation page](https://ajv.js.org/options.html#options-to-modify-validated-data).

If you have reached this page because you are having troubling during the schema validation phase, it may be interesting checking this [troubleshooting guide during schema validation](./guides/troubleshooting-schema-validation-issues.md)

#### Side note about request parsing

While this section involves request validation, it is important to note that before validation, the request has to be parsed. This applies to entire bodies in a PUT/POST request or the url's parameters in any request.

Bauta.js does not parse anything because this is delegated to the server instance used by Bauta.js (currently Express or Fastify). This means that to adapt the request parsing to your needs you will have to check the Server documentation to check how you can do it so.

An archetypical example would be query strings as array using comma separated values. This is not supported out of the box by the query parser used by node and thus, you would need to make modifications to the server instance used by Bauta.js before initializing the Bauta.js instance.

The conclusion of this section is that if you have issues when parsing the request, you will have to check the documentation of the Server instance to see how you can add a custom query parser that meets your needs.

### Response validation flow

#### HTTP status code 2xx
Expand Down
16 changes: 10 additions & 6 deletions packages/bautajs-core/src/open-api/ajv-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ import {
} from './validator-utils';
import { AJVOperationValidators } from './operation-validators';

const VALIDATOR_DEFAULT_OPTIONS: Options = {
logger: false,
strict: false,
coerceTypes: false,
useDefaults: true,
removeAdditional: true,
allErrors: true
} as const;

export class AjvValidator implements Validator<ValidateFunction> {
private ajv: Ajv;

Expand All @@ -29,12 +38,7 @@ export class AjvValidator implements Validator<ValidateFunction> {

private static buildAjv(validatorOptions: Options = {}) {
return new Ajv({
logger: false,
strict: false,
coerceTypes: false,
useDefaults: true,
removeAdditional: true,
allErrors: true,
...VALIDATOR_DEFAULT_OPTIONS,
...validatorOptions
});
}
Expand Down
38 changes: 38 additions & 0 deletions packages/bautajs-fastify-example/api-definition.json
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,44 @@
}
}
}
},
"/array-query-param" :{
"get": {
"operationId": "queryParamAsArray",
"summary": "Generates an array with all the input query params",
"parameters": [
{
"name": "chickenIds",
"in": "query",
"required": true,
"schema": {
"type": "array",
"items": {
"type": "string"
},
"minItems": 1,
"maxItems": 1000
},
"style": "form",
"explode": true
}
],
"responses": {
"200": {
"description": "Something!"
},
"default": {
"description": "unexpected error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
}
},
"components": {
Expand Down
5 changes: 4 additions & 1 deletion packages/bautajs-fastify-example/registrator.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ async function registerFastifyServer(fastify) {
staticConfig: {
someVar: 2
},
strictResponseSerialization: false
strictResponseSerialization: false,
validatorOptions: {
coerceTypes: 'array'
}
})
.after(err => {
if (err) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const { pipe, resolver, step } = require('@axa/bautajs-core');
const { getRequest } = require('@axa/bautajs-fastify');

const transformResponse = step(response => {
return {
message: response
};
});

function getQueryParamStep(_prev, ctx) {
const req = getRequest(ctx);

const { chickenIds } = req.query;

return `This is the general text for requests and now we are receiving: ${JSON.stringify(
chickenIds
)}`;
}

module.exports = resolver(operations => {
operations.queryParamAsArray
.validateRequest(true)
.validateResponse(false)
.setup(pipe(getQueryParamStep, transformResponse));
});
28 changes: 28 additions & 0 deletions packages/bautajs-fastify-example/test/fastify-example.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,4 +190,32 @@ describe('bautajs-fastify-example regressions tests', () => {
expect(res.statusCode).toBe(200);
expect(nock.isDone()).toBe(true);
});

test('GET api/array-query-param should work even with a single element', async () => {
const chickenId = 'elliot';

const res = await fastify.inject({
method: 'GET',
url: `/api/array-query-param?chickenIds=${chickenId}`
});
expect(res.statusCode).toBe(200);
expect(JSON.parse(res.body)).toEqual({
message: `This is the general text for requests and now we are receiving: ["${chickenId}"]`
});
});

test('GET api/array-query-param-csv is not capable of parsing comma separated values with default node query string parser', async () => {
const chickenId = 'elliot';
const chickenId2 = 'jeanne';

const res = await fastify.inject({
method: 'GET',
url: `/api/array-query-param?chickenIds=${chickenId},${chickenId2}`
});
expect(res.statusCode).toBe(200);
// Note here that we are not parsing both ids as a TWO elements but we are parsing everything as a single element
expect(JSON.parse(res.body)).toEqual({
message: `This is the general text for requests and now we are receiving: ["${chickenId},${chickenId2}"]`
});
});
});
12 changes: 12 additions & 0 deletions packages/bautajs-fastify/src/expose-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@ function createHandler(operation: Operation) {
let response;
// Convert the fastify validation error to the bautajs validation error format
if (request.validationError) {
// This error is intentionally logged as trace because for most of the errors is an expected error
request.log.trace(
{
error: {
name: request.validationError.name,
message: request.validationError.message,
stack: request.validationError.stack
}
},
`Fastify schema validation error found on the request`
);

reply.status(400);
throw new ValidationError(
'The request was not valid',
Expand Down

0 comments on commit 5b04c50

Please sign in to comment.