Skip to content
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

feat(openapi): throw OpenAPIValidatorMiddlewareError #446

Merged
merged 10 commits into from
Mar 26, 2024
8 changes: 8 additions & 0 deletions .changeset/tasty-spoons-pay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@interledger/openapi': major
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be major since it changes how createValidatorMiddleware is used.
But don't mind using minor since its just Rafiki using it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Major sounds right, if I'm understanding correctly. This will break consumers where NODE_ENV is production and a response will fail validation.

---

Changes to the `createValidatorMiddleware` behaviour:

- The middleware now throws a `OpenAPIValidatorMiddlewareError` instead of directly using Koa's `ctx.throw` function. This error class has `message` and `status` properties that describe the exact validation error as well as the status code that should be thrown.
- The middleware now takes in an optional `validationOptions` argument that determines whether to validate just the request, the response or both. By default, both are validated, and the middleware no longer uses `process.env.NODE_ENV` to determine whether to validate the response or not.
2 changes: 1 addition & 1 deletion packages/open-payments/src/client/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ const handleError = (
errorStatus = error.response?.status
} else if (isValidationError(error)) {
errorDescription = 'Could not validate OpenAPI response'
validationErrors = error.errors
validationErrors = error.errors.map((e) => e.message)
errorStatus = error.status
} else if (error instanceof Error) {
errorDescription = error.message
Expand Down
11 changes: 6 additions & 5 deletions packages/open-payments/src/test/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
DIDDocument
} from '../types'
import { v4 as uuid } from 'uuid'
import { ResponseValidator } from '@interledger/openapi'
import { ResponseValidator, ValidationError } from '@interledger/openapi'
import base64url from 'base64url'
import { BaseDeps } from '../client'

Expand All @@ -41,10 +41,11 @@ export const mockOpenApiResponseValidators = () => ({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
true) as ResponseValidator<any>,
failedValidator: ((data: unknown): data is unknown => {
throw {
errors: ['Failed to validate response'],
message: 'Failed to validate response'
} // to mock validationError
const err: ValidationError = {
errors: [{ message: 'Failed to validate response' }]
}

throw err
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}) as ResponseValidator<any>
})
Expand Down
31 changes: 26 additions & 5 deletions packages/openapi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ npm install @interledger/openapi

## Usage

### Validators

First, instantiate an `OpenAPI` validator object with a reference to your OpenAPI spec:

```ts
Expand Down Expand Up @@ -40,7 +42,7 @@ validateResponse({ body: response.body, status }) // throws or returns true
>
> The underlying response & request validator [packages](https://github.com/kogosoftwarellc/open-api/tree/master/packages) use the [Ajv schema validator](https://ajv.js.org) library. Each time validators are created via `createRequestValidator` and `createResponseValidator`, a `new Ajv()` instance is also [created](https://github.com/kogosoftwarellc/open-api/blob/master/packages/openapi-response-validator/index.ts). Since Ajv [recommends](https://ajv.js.org/guide/managing-schemas.html#compiling-during-initialization) instantiating once at initialization, these validators should also be instantiated just once during the lifecycle of the application to avoid any issues.

<br>
### Middleware

Likewise, you can validate both requests and responses inside a [Koa](https://github.com/koajs/koa) middleware method, using `createValidatorMiddleware`:

Expand All @@ -49,9 +51,28 @@ const openApi = await createOpenAPI(OPEN_API_URL)
const router = new SomeRouter()
router.get(
'/resource/{id}',
createValidatorMiddleware(openApi, {
path: '/resource/{id}',
method: HttpMethod.GET
})
createValidatorMiddleware(
openApi,
{
path: '/resource/{id}',
method: HttpMethod.GET
},
{ validateRequest: true, validateResponse: false } // optional arguments to determine what you want the middleware to validate. Both properties are true by default. Setting both variables to false results in a noop middleware.
)
)
```

If a validation error occurs, the middleware will throw an `OpenAPIValidatorMiddlewareError`:

```ts
app.use(async (ctx, next) => {
try {
await next()
} catch (err) {
if (err instanceof OpenAPIValidatorMiddlewareError) {
console.log(err.message) // e.g. Received error validating OpenAPI response: response.receivedAmount.value must match format "uint64"
console.log(err.status) // e.g. 400
}
}
})
```
10 changes: 7 additions & 3 deletions packages/openapi/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,10 +155,14 @@ class OpenAPIImpl implements OpenAPI {
}
}

interface TransformedError {
message: string
}

const errorTransformer = (
_openapiError: OpenAPIResponseValidatorError,
ajvError: ErrorObject
) => {
): TransformedError => {
// Remove preceding 'data/'
// Delineate subfields with '.'
const message = ajv.errorsText([ajvError]).slice(5).replace(/\//g, '.')
Expand All @@ -182,9 +186,9 @@ const customFormats = {
}
}

interface ValidationError {
export interface ValidationError {
status?: number
errors: string[]
errors: TransformedError[]
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
Loading
Loading