@camcima/nestjs-rfc9457 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -77,6 +77,7 @@ Extension members (arbitrary key-value pairs) are allowed for problem-type-speci
|
|
|
77
77
|
- Four `instance` strategies: `'request-uri'`, `'uuid'`, `'none'`, or a custom callback
|
|
78
78
|
- Optional catch-all mode for non-`HttpException` throwables (produces 500 Problem Details)
|
|
79
79
|
- Custom `exceptionMapper` callback for full control over any exception
|
|
80
|
+
- Default `error`-level logging of unhandled exceptions when `catchAllExceptions: true` (override via `onUnhandled` callback)
|
|
80
81
|
- `ProblemDetailsFactory` is injectable — use it directly in GraphQL, microservices, or custom filters
|
|
81
82
|
- Optional `@nestjs/swagger` integration: `ProblemDetailDto` and `ValidationProblemDetailDto` for OpenAPI documentation, plus a `applyProblemDetailResponses()` helper that auto-applies `@ApiResponse` decorators to all controllers under `application/problem+json`
|
|
82
83
|
- Works with both Express and Fastify adapters
|
|
@@ -260,6 +261,8 @@ When `false` (default), exceptions that are not `HttpException` instances are pa
|
|
|
260
261
|
Rfc9457Module.forRoot({ catchAllExceptions: true });
|
|
261
262
|
```
|
|
262
263
|
|
|
264
|
+
**Observability:** when this branch fires (a non-`HttpException` reaches the filter and no `exceptionMapper` claims it), the library logs the exception at `error` level via NestJS's built-in `Logger` (context `Rfc9457ExceptionFilter`) before sending the generic 500. This keeps unexpected throwables visible in server logs even though the response body is intentionally bland. To redirect or replace this logging, use the [`onUnhandled`](#onunhandled) callback described below.
|
|
265
|
+
|
|
263
266
|
### `exceptionMapper`
|
|
264
267
|
|
|
265
268
|
**Type**: `(exception: unknown, request: Rfc9457Request) => ProblemDetail | null`
|
|
@@ -284,6 +287,28 @@ Rfc9457Module.forRoot({
|
|
|
284
287
|
|
|
285
288
|
If the returned `ProblemDetail` omits `status`, the factory falls back to `exception.getStatus()` (if it is an `HttpException`) or `500`.
|
|
286
289
|
|
|
290
|
+
### `onUnhandled`
|
|
291
|
+
|
|
292
|
+
**Type**: `(exception: unknown, request: Rfc9457Request) => void` | **Default**: built-in `Logger.error(...)` (context `Rfc9457ExceptionFilter`)
|
|
293
|
+
|
|
294
|
+
Called when a non-`HttpException` reaches the catch-all branch (i.e. `catchAllExceptions: true` AND the `exceptionMapper` returned `null`). Use this to send unhandled exceptions to a structured sink (Sentry, Datadog, a custom pino child logger) or to suppress the default log entirely.
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
Rfc9457Module.forRoot({
|
|
298
|
+
catchAllExceptions: true,
|
|
299
|
+
onUnhandled: (exception, request) => {
|
|
300
|
+
// Route to Sentry, Datadog, etc.
|
|
301
|
+
sentry.captureException(exception, {
|
|
302
|
+
tags: { method: request.method, url: request.url },
|
|
303
|
+
});
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
**The filter still sends the generic 500 Problem Details response after invoking `onUnhandled`.** This callback exists purely for observability — it never changes the HTTP response.
|
|
309
|
+
|
|
310
|
+
When `onUnhandled` is **not** provided, the library calls `Logger.error(...)` with either the exception's `stack` string or a `{ exception }` structured context (for non-`Error` values). The log context is `Rfc9457ExceptionFilter` so it can be filtered or silenced via NestJS's logger configuration.
|
|
311
|
+
|
|
287
312
|
### `validationExceptionMapper`
|
|
288
313
|
|
|
289
314
|
**Type**: `(messages: string[], request: Rfc9457Request) => ProblemDetail`
|
|
@@ -930,13 +955,13 @@ Gitleaks must be [installed locally](https://github.com/gitleaks/gitleaks#instal
|
|
|
930
955
|
|
|
931
956
|
```bash
|
|
932
957
|
# Dependency audit (production only)
|
|
933
|
-
|
|
958
|
+
pnpm run audit:deps
|
|
934
959
|
|
|
935
960
|
# Secret scanning
|
|
936
|
-
|
|
961
|
+
pnpm run audit:secrets
|
|
937
962
|
|
|
938
|
-
# Full
|
|
939
|
-
|
|
963
|
+
# Full pnpm audit (all dependencies)
|
|
964
|
+
pnpm audit
|
|
940
965
|
```
|
|
941
966
|
|
|
942
967
|
---
|
|
@@ -951,19 +976,19 @@ git clone https://github.com/camcima/nestjs-rfc9457.git
|
|
|
951
976
|
cd nestjs-rfc9457
|
|
952
977
|
|
|
953
978
|
# Install dependencies
|
|
954
|
-
|
|
979
|
+
pnpm install
|
|
955
980
|
|
|
956
981
|
# Run unit tests
|
|
957
|
-
|
|
982
|
+
pnpm run test:unit
|
|
958
983
|
|
|
959
984
|
# Run e2e tests
|
|
960
|
-
|
|
985
|
+
pnpm run test:e2e
|
|
961
986
|
|
|
962
987
|
# Run all tests with coverage
|
|
963
|
-
|
|
988
|
+
pnpm run test:cov
|
|
964
989
|
|
|
965
990
|
# Build
|
|
966
|
-
|
|
991
|
+
pnpm run build
|
|
967
992
|
```
|
|
968
993
|
|
|
969
994
|
This project uses [Conventional Commits](https://www.conventionalcommits.org/) enforced by commitlint, and [Lefthook](https://github.com/evilmartians/lefthook) for pre-commit hooks (lint + format on staged files).
|
|
@@ -6,6 +6,7 @@ export declare class Rfc9457ExceptionFilter extends BaseExceptionFilter {
|
|
|
6
6
|
private readonly factory;
|
|
7
7
|
private readonly options;
|
|
8
8
|
private readonly adapterHost;
|
|
9
|
+
private readonly logger;
|
|
9
10
|
constructor(factory: ProblemDetailsFactory, options: Rfc9457ModuleOptions, adapterHost: HttpAdapterHost);
|
|
10
11
|
catch(exception: unknown, host: ArgumentsHost): void;
|
|
11
12
|
}
|
|
@@ -11,18 +11,20 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
11
11
|
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
12
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
13
|
};
|
|
14
|
+
var Rfc9457ExceptionFilter_1;
|
|
14
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
16
|
exports.Rfc9457ExceptionFilter = void 0;
|
|
16
17
|
const common_1 = require("@nestjs/common");
|
|
17
18
|
const core_1 = require("@nestjs/core");
|
|
18
19
|
const problem_details_factory_1 = require("./problem-details.factory");
|
|
19
20
|
const rfc9457_constants_1 = require("./rfc9457.constants");
|
|
20
|
-
let Rfc9457ExceptionFilter = class Rfc9457ExceptionFilter extends core_1.BaseExceptionFilter {
|
|
21
|
+
let Rfc9457ExceptionFilter = Rfc9457ExceptionFilter_1 = class Rfc9457ExceptionFilter extends core_1.BaseExceptionFilter {
|
|
21
22
|
constructor(factory, options, adapterHost) {
|
|
22
23
|
super(adapterHost.httpAdapter);
|
|
23
24
|
this.factory = factory;
|
|
24
25
|
this.options = options;
|
|
25
26
|
this.adapterHost = adapterHost;
|
|
27
|
+
this.logger = new common_1.Logger(Rfc9457ExceptionFilter_1.name);
|
|
26
28
|
}
|
|
27
29
|
catch(exception, host) {
|
|
28
30
|
if (host.getType() !== 'http') {
|
|
@@ -47,6 +49,18 @@ let Rfc9457ExceptionFilter = class Rfc9457ExceptionFilter extends core_1.BaseExc
|
|
|
47
49
|
super.catch(exception, host);
|
|
48
50
|
return;
|
|
49
51
|
}
|
|
52
|
+
if (!isHttpException) {
|
|
53
|
+
if (this.options.onUnhandled) {
|
|
54
|
+
const ctx = host.switchToHttp();
|
|
55
|
+
this.options.onUnhandled(exception, ctx.getRequest());
|
|
56
|
+
}
|
|
57
|
+
else if (exception instanceof Error) {
|
|
58
|
+
this.logger.error(`Unhandled non-HTTP exception: ${exception.message}`, exception.stack);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
this.logger.error('Unhandled non-HTTP exception (non-Error value thrown)', exception);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
50
64
|
const ctx = host.switchToHttp();
|
|
51
65
|
const request = ctx.getRequest();
|
|
52
66
|
const response = ctx.getResponse();
|
|
@@ -57,7 +71,7 @@ let Rfc9457ExceptionFilter = class Rfc9457ExceptionFilter extends core_1.BaseExc
|
|
|
57
71
|
}
|
|
58
72
|
};
|
|
59
73
|
exports.Rfc9457ExceptionFilter = Rfc9457ExceptionFilter;
|
|
60
|
-
exports.Rfc9457ExceptionFilter = Rfc9457ExceptionFilter = __decorate([
|
|
74
|
+
exports.Rfc9457ExceptionFilter = Rfc9457ExceptionFilter = Rfc9457ExceptionFilter_1 = __decorate([
|
|
61
75
|
(0, common_1.Catch)(),
|
|
62
76
|
__param(1, (0, common_1.Inject)(rfc9457_constants_1.RFC9457_MODULE_OPTIONS)),
|
|
63
77
|
__metadata("design:paramtypes", [problem_details_factory_1.ProblemDetailsFactory, Object, core_1.HttpAdapterHost])
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rfc9457.exception-filter.js","sourceRoot":"","sources":["../src/rfc9457.exception-filter.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"rfc9457.exception-filter.js","sourceRoot":"","sources":["../src/rfc9457.exception-filter.ts"],"names":[],"mappings":"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"}
|
|
@@ -24,6 +24,7 @@ export interface Rfc9457ModuleOptions {
|
|
|
24
24
|
catchAllExceptions?: boolean;
|
|
25
25
|
exceptionMapper?: (exception: unknown, request: Rfc9457Request) => ProblemDetail | null;
|
|
26
26
|
validationExceptionMapper?: (messages: string[], request: Rfc9457Request) => ProblemDetail;
|
|
27
|
+
onUnhandled?: (exception: unknown, request: Rfc9457Request) => void;
|
|
27
28
|
}
|
|
28
29
|
export interface Rfc9457OptionsFactory {
|
|
29
30
|
createRfc9457Options(): Promise<Rfc9457ModuleOptions> | Rfc9457ModuleOptions;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camcima/nestjs-rfc9457",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "NestJS library for RFC 9457 Problem Details responses",
|
|
5
5
|
"author": "Carlos Cima",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"engines": {
|
|
23
23
|
"node": ">=18"
|
|
24
24
|
},
|
|
25
|
+
"packageManager": "pnpm@9.15.0",
|
|
25
26
|
"scripts": {
|
|
26
27
|
"build": "tsc -p tsconfig.build.json",
|
|
27
28
|
"lint": "eslint 'src/**/*.ts' 'test/**/*.ts'",
|
|
@@ -31,17 +32,17 @@
|
|
|
31
32
|
"test:unit": "jest --testPathPattern=test/unit",
|
|
32
33
|
"test:e2e": "jest --testPathPattern=test/e2e",
|
|
33
34
|
"test:cov": "jest --coverage",
|
|
34
|
-
"prepublishOnly": "
|
|
35
|
+
"prepublishOnly": "pnpm run build",
|
|
35
36
|
"release": "release-it",
|
|
36
|
-
"audit:deps": "
|
|
37
|
+
"audit:deps": "pnpm audit --prod",
|
|
37
38
|
"audit:secrets": "gitleaks git --no-banner --redact -v"
|
|
38
39
|
},
|
|
39
40
|
"peerDependencies": {
|
|
40
41
|
"@nestjs/common": "^10.0.0 || ^11.0.0",
|
|
41
42
|
"@nestjs/core": "^10.0.0 || ^11.0.0",
|
|
43
|
+
"@nestjs/swagger": "^7.0.0 || ^8.0.0 || ^11.0.0",
|
|
42
44
|
"class-validator": "^0.14.0",
|
|
43
|
-
"reflect-metadata": "^0.1.13 || ^0.2.0"
|
|
44
|
-
"@nestjs/swagger": "^7.0.0 || ^8.0.0 || ^11.0.0"
|
|
45
|
+
"reflect-metadata": "^0.1.13 || ^0.2.0"
|
|
45
46
|
},
|
|
46
47
|
"peerDependenciesMeta": {
|
|
47
48
|
"class-validator": {
|
|
@@ -54,14 +55,14 @@
|
|
|
54
55
|
"devDependencies": {
|
|
55
56
|
"@commitlint/cli": "^20.5.0",
|
|
56
57
|
"@commitlint/config-conventional": "^20.5.0",
|
|
57
|
-
"@nestjs/common": "^11.1.
|
|
58
|
-
"@nestjs/core": "^11.1.
|
|
59
|
-
"@nestjs/
|
|
60
|
-
"@nestjs/platform-
|
|
61
|
-
"@nestjs/
|
|
62
|
-
"@nestjs/testing": "^11.1.
|
|
58
|
+
"@nestjs/common": "^11.1.19",
|
|
59
|
+
"@nestjs/core": "^11.1.19",
|
|
60
|
+
"@nestjs/platform-express": "^11.1.19",
|
|
61
|
+
"@nestjs/platform-fastify": "^11.1.19",
|
|
62
|
+
"@nestjs/swagger": "^11.2.7",
|
|
63
|
+
"@nestjs/testing": "^11.1.19",
|
|
63
64
|
"@types/jest": "^30.0.0",
|
|
64
|
-
"@types/node": "^
|
|
65
|
+
"@types/node": "^25.6.0",
|
|
65
66
|
"@types/supertest": "^7.2.0",
|
|
66
67
|
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
|
67
68
|
"@typescript-eslint/parser": "^7.0.0",
|
|
@@ -70,10 +71,9 @@
|
|
|
70
71
|
"eslint": "^8.0.0",
|
|
71
72
|
"eslint-config-prettier": "^9.0.0",
|
|
72
73
|
"eslint-plugin-prettier": "^5.0.0",
|
|
73
|
-
"fastify": "^4.0.0",
|
|
74
74
|
"jest": "^30.3.0",
|
|
75
75
|
"lefthook": "^1.0.0",
|
|
76
|
-
"prettier": "^3.
|
|
76
|
+
"prettier": "^3.8.3",
|
|
77
77
|
"reflect-metadata": "^0.2.0",
|
|
78
78
|
"release-it": "^18.0.0",
|
|
79
79
|
"rxjs": "^7.0.0",
|
|
@@ -82,6 +82,15 @@
|
|
|
82
82
|
"ts-node": "^10.9.2",
|
|
83
83
|
"typescript": "^6.0.2"
|
|
84
84
|
},
|
|
85
|
+
"pnpm": {
|
|
86
|
+
"overrides": {
|
|
87
|
+
"basic-ftp@<5.3.0": ">=5.3.0",
|
|
88
|
+
"fastify@<5.8.5": ">=5.8.5",
|
|
89
|
+
"lodash@<4.18.0": ">=4.18.0",
|
|
90
|
+
"path-to-regexp@<8.4.0": ">=8.4.0",
|
|
91
|
+
"undici@<6.24.0": ">=6.24.0"
|
|
92
|
+
}
|
|
93
|
+
},
|
|
85
94
|
"repository": {
|
|
86
95
|
"type": "git",
|
|
87
96
|
"url": "git+https://github.com/camcima/nestjs-rfc9457.git"
|