1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-06 00:07:44 +01:00
unleash.unleash/src/lib/services/openapi-service.ts

104 lines
2.9 KiB
TypeScript
Raw Normal View History

import openapi, { IExpressOpenApi } from '@wesleytodd/openapi';
import { Express, RequestHandler, Response } from 'express';
import { IUnleashConfig } from '../types/option';
import {
createOpenApiSchema,
JsonSchemaProps,
removeJsonSchemaProps,
SchemaId,
} from '../openapi';
import { ApiOperation } from '../openapi/util/api-operation';
import { Logger } from '../logger';
import { validateSchema } from '../openapi/validate';
import { IFlagResolver } from '../types';
Clean up old errors (#3633) This PR attempts to improve the error handling introduced in #3607. ## About the changes ## **tl;dr:** - Make `UnleashError` constructor protected - Make all custom errors inherit from `UnleashError`. - Add tests to ensure that all special error cases include their relevant data - Remove `PasswordMismatchError` and `BadRequestError`. These don't exist. - Add a few new error types: `ContentTypeError`, `NotImplementedError`, `UnauthorizedError` - Remove the `...rest` parameter from error constructor - Add an unexported `GenericUnleashError` class - Move OpenAPI conversion function to `BadDataError` clas - Remove explicit `Error.captureStackTrace`. This is done automatically. - Extract `getPropFromString` function and add tests ### **In a more verbose fashion** The main thing is that all our internal errors now inherit from`UnleashError`. This allows us to simplify the `UnleashError` constructor and error handling in general while still giving us the extra benefits we added to that class. However, it _does_ also mean that I've had to update **all** existing error classes. The constructor for `UnleashError` is now protected and all places that called that constructor directly have been updated. Because the base error isn't available anymore, I've added three new errors to cover use cases that we didn't already have covered: `NotImplementedError`, `UnauthorizedError`, `ContentTypeError`. This is to stay consistent in how we report errors to the user. There is also an internal class, `GenericUnleashError` that inherits from the base error. This class is only used in conversions for cases where we don't know what the error is. It is not exported. In making all the errors inherit, I've also removed the `...rest` parameter from the `UnleashError` constructor. We don't need this anymore. Following on from the fixes with missing properties in #3638, I have added tests for all errors that contain extra data. Some of the error names that were originally used when creating the list don't exist in the backend. `BadRequestError` and `PasswordMismatchError` have been removed. The `BadDataError` class now contains the conversion code for OpenAPI validation errors. In doing so, I extracted and tested the `getPropFromString` function. ### Main files Due to the nature of the changes, there's a lot of files to look at. So to make it easier to know where to turn your attention: The changes in `api-error.ts` contain the main changes: protected constructor, removal of OpenAPI conversion (moved into `BadDataError`. `api-error.test.ts` contains tests to make sure that errors work as expected. Aside from `get-prop-from-string.ts` and the tests, everything else is just the required updates to go through with the changes. ## Discussion points I've gone for inheritance of the Error type over composition. This is in large part because throwing actual Error instances instead of just objects is preferable (because they collect stack traces, for instance). However, it's quite possible that we could solve the same thing in a more elegant fashion using composition. ## For later / suggestions for further improvements The `api-error` files still contain a lot of code. I think it might be beneficial to break each Error into a separate folder that includes the error, its tests, and its schema (if required). It would help decouple it a bit. We don't currently expose the schema anywhere, so it's not available in the openapi spec. We should look at exposing it too. Finally, it would be good to go through each individual error message and update each one to be as helpful as possible.
2023-05-11 11:10:57 +02:00
import { fromOpenApiValidationErrors } from '../error/bad-data-error';
export class OpenApiService {
private readonly config: IUnleashConfig;
private readonly logger: Logger;
private readonly api: IExpressOpenApi;
private flagResolver: IFlagResolver;
constructor(config: IUnleashConfig) {
this.config = config;
this.flagResolver = config.flagResolver;
this.logger = config.getLogger('openapi-service.ts');
this.api = openapi(
this.docsPath(),
createOpenApiSchema(config.server),
{
coerce: true,
extendRefs: true,
basePath: config.server.baseUriPath,
},
);
}
validPath(op: ApiOperation): RequestHandler {
return this.api.validPath(op);
}
useDocs(app: Express): void {
app.use(this.api);
app.use(this.docsPath(), this.api.swaggerui);
}
docsPath(): string {
const { baseUriPath = '' } = this.config.server ?? {};
return `${baseUriPath}/docs/openapi`;
}
registerCustomSchemas<T extends JsonSchemaProps>(
schemas: Record<string, T>,
): void {
Object.entries(schemas).forEach(([name, schema]) => {
this.api.schema(name, removeJsonSchemaProps(schema));
});
}
useErrorHandler(app: Express): void {
app.use((err, req, res, next) => {
if (err?.status && err.validationErrors) {
feat: unify error responses (#3607) This PR implements the first version of a suggested unification (and documentation) of the errors that we return from the API today. The goal is for this to be the first step towards the error type defined in this internal [linear task](https://linear.app/unleash/issue/1-629/define-the-error-type 'Define the new API error type'). ## The state of things today As things stand, we currently have no (or **very** little) documentation of the errors that are returned from the API. We mention error codes, but never what the errors may contain. Second, there is no specified format for errors, so what they return is arbitrary, and based on ... Who knows? As a result, we have multiple different errors returned by the API depending on what operation you're trying to do. What's more, with OpenAPI validation in the mix, it's absolutely possible for you to get two completely different error objects for operations to the same endpoint. Third, the errors we do return are usually pretty vague and don't really provide any real help to the user. "You don't have the right permissions". Great. Well what permissions do I need? And how would I know? "BadDataError". Sick. Why is it bad? ... You get it. ## What we want to achieve The ultimate goal is for error messages to serve both humans and machines. When the user provides bad data, we should tell them what parts of the data are bad and what they can do to fix it. When they don't have the right permissions, we should tell them what permissions they need. Additionally, it would be nice if we could provide an ID for each error instance, so that you (or an admin) can look through the logs and locate he incident. ## What's included in **this** PR? This PR does not aim to implement everything above. It's not intended to magically fix everything. Its goal is to implement the necessary **breaking** changes, so that they can be included in v5. Changing error messages is a slightly grayer area than changing APIs directly, but changing the format is definitely something I'd consider breaking. So this PR: - defines a minimal version of the error type defined in the [API error definition linear task](https://linear.app/unleash/issue/1-629/define-the-error-type). - aims to catch all errors we return today and wrap them in the error type - updates tests to match the new expectations. An important point: because we are cutting v5 very soon and because work for this wasn't started until last week, the code here isn't necessarily very polished. But it doesn't need to be. The internals can be as messy as we want, as long as the API surface is stable. That said, I'm very open to feedback about design and code completeness, etc, but this has intentionally been done quickly. Please also see my inline comments on the changes for more specific details. ### Proposed follow-ups As mentioned, this is the first step to implementing the error type. The public API error type only exposes `id`, `name`, and `message`. This is barely any more than most of the previous messages, but they are now all using the same format. Any additional properties, such as `suggestion`, `help`, `documentationLink` etc can be added as features without breaking the current format. This is an intentional limitation of this PR. Regarding additional properties: there are some error responses that must contain extra properties. Some of these are documented in the types of the new error constructor, but not all. This includes `path` and `type` properties on 401 errors, `details` on validation errors, and more. Also, because it was put together quickly, I don't yet know exactly how we (as developers) would **prefer** to use these new error messages within the code, so the internal API (the new type, name, etc), is just a suggestion. This can evolve naturally over time if (based on feedback and experience) without changing the public API. ## Returning multiple errors Most of the time when we return errors today, we only return a single error (even if many things are wrong). AJV, the OpenAPI integration we use does have a setting that allows it to return all errors in a request instead of a single one. I suggest we turn that on, but that we do it in a separate PR (because it updates a number of other snapshots). When returning errors that point to `details`, the objects in the `details` now contain a new `description` property. This "deprecates" the `message` property. Due to our general deprecation policy, this should be kept around for another full major and can be removed in v6. ```json { "name": "BadDataError", "message": "Something went wrong. Check the `details` property for more information." "details": [{ "message": "The .params property must be an object. You provided an array.", "description": "The .params property must be an object. You provided an array.", }] } ```
2023-04-25 15:40:46 +02:00
const apiError = fromOpenApiValidationErrors(
req.body,
err.validationErrors,
);
res.status(apiError.statusCode).json(apiError);
} else {
next(err);
}
});
}
respondWithValidation<T, S = SchemaId>(
status: number,
res: Response<T>,
schema: S,
data: T,
openapi: improve validation testing (#2058) ## What This PR adds an extra layer of OpenAPI validation testing to what we already have. It also fixes any issues that make the new tests fail. ## Why While the current OpenAPI validation takes care of _some_ things, there's also things it misses, as shown by #2055. By adding the OpenAPI Enforcer package, we should hopefully be able to catch more of these errors in the future. The enforcer _does_ flag the issue in #2055 as an error. ## How By adding the OpenAPI Enforcer package and making whatever changes it picks up on. By adding location headers to all our 201 endpoints. I also had to change some signatures on `create` store methods so that they actually return something (a lot of them just returned `void`). ## Discussion points ### Code changes Some of the code changes may not be necessary or we may want to change more code to align with what changes have been done. It may be worth standardizing on a pattern for `*store.create` methods, so that they always return an identifier or the stored object, for instance. ### On relative URIs The 201 location headers use relative URIs to point to the created resources. This seemed to be the easiest way to me, as we don't need to worry about figuring out what the absolute URL of the instance is (though we could probably just concat it to the request URI?). The algorithm for determining relative URIs is described in [RFC 3986 section 5](https://www.rfc-editor.org/rfc/rfc3986#section-5). There's also some places where I'm not sure we _can_ provide accurate location url. I think they're supposed to point _directly at_ whatever the resource is, but for some resources (such as api tokens), you can only get it as part of a collection. From [RFC 9110 on the location field](https://httpwg.org/specs/rfc9110.html#field.location) (emphasis mine): > the Location header field in a [201 (Created)](https://httpwg.org/specs/rfc9110.html#status.201) response is supposed to provide a URI that is **specific** to the created resource. A link to a collection is not specific. I'm not sure what best to do about this. ### Inline comments I've added a number of inline PR comments that I'd love to get some feedback on too. Have a look and let me know what you think! ### Unfinished business I've added some juicy comments to some of the files here. They contain non-blocking issues that I'm tracking (via github issues). We should resolve them in the future, but right now they can stay as they are. ## Commits * Feat: add openapi-enforcer + tests; fix _some_ issues * Test: allow non-standard string formats * validation: fix _some_ 201 created location header endpoints * #1391: fix remaining 201 location headers missing * Refactor: use the ajv options object instead of add* methods * #1391: flag validation errors and warnings as test failures * #1391: modify patch schema to specify either object or array We don't provide many patch endpoints, so we _could_ create separate patch operation objects for each one. I think that makes sense to do as part of the larger cleanup. For now, I think it's worth to simply turn it into one of these. While it's not entirely accurate, it's better than what we had before. * Refactor: make tests easier to read * #1391: use enum for valid token types This was previously only a description. This may seem like a breaking change because OpenAPI would previously accept any string. However, Joi also performs validation on this, so invalid values wouldn't work previously either. * #1391: Comment out default parameter values for now This isn't the _right_ way, but it's the pragmatic solution. It's not a big deal and this works as a stopgap solution. * #1391: add todo note for api token schema fixes * #1391: update snapshot * Revert "#1391: modify patch schema to specify either object or array" This reverts commit 0dd5d0faa1554a394178c0f632915e096204cac7. Turns out we need to allow much more than just objects and arrays. I'll leave this as is for now. * Chore(#1391): update comment explaining api token schema TODO * #1391: modify some test code and add comment * #1391: update tests and spec to allow 'any' type in schema * Chore: remove comment * #1391: add tests for context field stores * #1391: add location header for public signup links * #1391: fix query parameter description. There was indeed a bug in the package, and this has been addressed now, so we can go back to describing the params properly.
2022-09-23 15:02:09 +02:00
headers: { [header: string]: string } = {},
): void {
const errors = validateSchema<S>(schema, data);
if (errors) {
this.logger.debug(
'Invalid response:',
JSON.stringify(errors, null, 4),
);
if (this.flagResolver.isEnabled('strictSchemaValidation')) {
throw new Error(JSON.stringify(errors, null, 4));
}
}
openapi: improve validation testing (#2058) ## What This PR adds an extra layer of OpenAPI validation testing to what we already have. It also fixes any issues that make the new tests fail. ## Why While the current OpenAPI validation takes care of _some_ things, there's also things it misses, as shown by #2055. By adding the OpenAPI Enforcer package, we should hopefully be able to catch more of these errors in the future. The enforcer _does_ flag the issue in #2055 as an error. ## How By adding the OpenAPI Enforcer package and making whatever changes it picks up on. By adding location headers to all our 201 endpoints. I also had to change some signatures on `create` store methods so that they actually return something (a lot of them just returned `void`). ## Discussion points ### Code changes Some of the code changes may not be necessary or we may want to change more code to align with what changes have been done. It may be worth standardizing on a pattern for `*store.create` methods, so that they always return an identifier or the stored object, for instance. ### On relative URIs The 201 location headers use relative URIs to point to the created resources. This seemed to be the easiest way to me, as we don't need to worry about figuring out what the absolute URL of the instance is (though we could probably just concat it to the request URI?). The algorithm for determining relative URIs is described in [RFC 3986 section 5](https://www.rfc-editor.org/rfc/rfc3986#section-5). There's also some places where I'm not sure we _can_ provide accurate location url. I think they're supposed to point _directly at_ whatever the resource is, but for some resources (such as api tokens), you can only get it as part of a collection. From [RFC 9110 on the location field](https://httpwg.org/specs/rfc9110.html#field.location) (emphasis mine): > the Location header field in a [201 (Created)](https://httpwg.org/specs/rfc9110.html#status.201) response is supposed to provide a URI that is **specific** to the created resource. A link to a collection is not specific. I'm not sure what best to do about this. ### Inline comments I've added a number of inline PR comments that I'd love to get some feedback on too. Have a look and let me know what you think! ### Unfinished business I've added some juicy comments to some of the files here. They contain non-blocking issues that I'm tracking (via github issues). We should resolve them in the future, but right now they can stay as they are. ## Commits * Feat: add openapi-enforcer + tests; fix _some_ issues * Test: allow non-standard string formats * validation: fix _some_ 201 created location header endpoints * #1391: fix remaining 201 location headers missing * Refactor: use the ajv options object instead of add* methods * #1391: flag validation errors and warnings as test failures * #1391: modify patch schema to specify either object or array We don't provide many patch endpoints, so we _could_ create separate patch operation objects for each one. I think that makes sense to do as part of the larger cleanup. For now, I think it's worth to simply turn it into one of these. While it's not entirely accurate, it's better than what we had before. * Refactor: make tests easier to read * #1391: use enum for valid token types This was previously only a description. This may seem like a breaking change because OpenAPI would previously accept any string. However, Joi also performs validation on this, so invalid values wouldn't work previously either. * #1391: Comment out default parameter values for now This isn't the _right_ way, but it's the pragmatic solution. It's not a big deal and this works as a stopgap solution. * #1391: add todo note for api token schema fixes * #1391: update snapshot * Revert "#1391: modify patch schema to specify either object or array" This reverts commit 0dd5d0faa1554a394178c0f632915e096204cac7. Turns out we need to allow much more than just objects and arrays. I'll leave this as is for now. * Chore(#1391): update comment explaining api token schema TODO * #1391: modify some test code and add comment * #1391: update tests and spec to allow 'any' type in schema * Chore: remove comment * #1391: add tests for context field stores * #1391: add location header for public signup links * #1391: fix query parameter description. There was indeed a bug in the package, and this has been addressed now, so we can go back to describing the params properly.
2022-09-23 15:02:09 +02:00
Object.entries(headers).forEach(([header, value]) =>
res.header(header, value),
);
res.status(status).json(data);
}
}