├── .editorconfig ├── .eslintignore ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .npmrc ├── LICENSE ├── Readme.md ├── index.d.ts ├── index.js ├── lib ├── err-helpers.js ├── err-proto.js ├── err-with-cause.js ├── err.js ├── req.js └── res.js ├── package.json ├── test ├── err-with-cause.test.js ├── err.test.js ├── req.test.js ├── res.test.js └── types │ └── index.test-d.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 2 10 | trim_trailing_whitespace = true 11 | 12 | # [*.md] 13 | # trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | index.d.ts 2 | test/types/index.test-d.ts 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | open-pull-requests-limit: 10 8 | 9 | - package-ecosystem: "npm" 10 | directory: "/" 11 | schedule: 12 | interval: "weekly" 13 | open-pull-requests-limit: 10 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - 'docs/**' 7 | - '*.md' 8 | pull_request: 9 | paths-ignore: 10 | - 'docs/**' 11 | - '*.md' 12 | 13 | # This allows a subsequently queued workflow run to interrupt previous runs 14 | concurrency: 15 | group: "${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | dependency-review: 20 | name: Dependency Review 21 | if: github.event_name == 'pull_request' 22 | runs-on: ubuntu-latest 23 | permissions: 24 | contents: read 25 | steps: 26 | - name: Check out repo 27 | uses: actions/checkout@v4 28 | with: 29 | persist-credentials: false 30 | 31 | - name: Dependency review 32 | uses: actions/dependency-review-action@v4 33 | 34 | test: 35 | name: Test 36 | runs-on: ubuntu-latest 37 | permissions: 38 | contents: read 39 | strategy: 40 | fail-fast: false 41 | matrix: 42 | node-version: [18, 20] 43 | steps: 44 | - name: Check out repo 45 | uses: actions/checkout@v4 46 | with: 47 | persist-credentials: false 48 | 49 | - name: Setup Node ${{ matrix.node-version }} 50 | uses: actions/setup-node@v4 51 | with: 52 | node-version: ${{ matrix.node-version }} 53 | 54 | - name: Install dependencies 55 | run: npm install --ignore-scripts 56 | env: 57 | NODE_ENV: development 58 | 59 | - name: Lint-CI 60 | run: npm run lint-ci 61 | 62 | - name: Test-Types 63 | run: npm run test-types 64 | 65 | - name: Test-CI 66 | run: npm run test-ci 67 | 68 | automerge: 69 | name: Automerge Dependabot PRs 70 | if: > 71 | github.event_name == 'pull_request' && 72 | github.event.pull_request.user.login == 'dependabot[bot]' 73 | needs: test 74 | permissions: 75 | pull-requests: write 76 | contents: write 77 | runs-on: ubuntu-latest 78 | steps: 79 | - uses: fastify/github-action-merge-dependabot@v3 80 | with: 81 | github-token: ${{ secrets.GITHUB_TOKEN }} 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | # Vim swap files 133 | *.swp 134 | 135 | # macOS files 136 | .DS_Store 137 | 138 | # editor files 139 | .vscode 140 | .idea 141 | 142 | # lock files 143 | package-lock.json 144 | pnpm-lock.yaml 145 | yarn.lock 146 | 147 | # 0x 148 | .__browserify* 149 | profile-* 150 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | legacy-peer-deps=true 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Mateo Collina, David Mark Clements, James Sumners 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # pino-std-serializers  [![CI](https://github.com/pinojs/pino-std-serializers/workflows/CI/badge.svg)](https://github.com/pinojs/pino-std-serializers/actions?query=workflow%3ACI) 2 | 3 | This module provides a set of standard object serializers for the 4 | [Pino](https://getpino.io) logger. 5 | 6 | ## Serializers 7 | 8 | ### `exports.err(error)` 9 | Serializes an `Error` like object. Returns an object: 10 | 11 | ```js 12 | { 13 | type: 'string', // The name of the object's constructor. 14 | message: 'string', // The supplied error message. 15 | stack: 'string', // The stack when the error was generated. 16 | raw: Error // Non-enumerable, i.e. will not be in the output, original 17 | // Error object. This is available for subsequent serializers 18 | // to use. 19 | [...any additional Enumerable property the original Error had] 20 | } 21 | ``` 22 | 23 | Any other extra properties, e.g. `statusCode`, that have been attached to the 24 | object will also be present on the serialized object. 25 | 26 | If the error object has a [`cause`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause) property, the `cause`'s `message` and `stack` will be appended to the top-level `message` and `stack`. All other parameters that belong to the `error.cause` object will be omitted. 27 | 28 | Example: 29 | 30 | ```js 31 | const serializer = require('pino-std-serializers').err; 32 | 33 | const innerError = new Error("inner error"); 34 | innerError.isInner = true; 35 | const outerError = new Error("outer error", { cause: innerError }); 36 | outerError.isInner = false; 37 | 38 | const serialized = serializer(outerError); 39 | /* Result: 40 | { 41 | "type": "Error", 42 | "message": "outer error: inner error", 43 | "isInner": false, 44 | "stack": "Error: outer error 45 | at <...omitted..> 46 | caused by: Error: inner error 47 | at <...omitted..> 48 | } 49 | */ 50 | ``` 51 | 52 | ### `exports.errWithCause(error)` 53 | Serializes an `Error` like object, including any `error.cause`. Returns an object: 54 | 55 | ```js 56 | { 57 | type: 'string', // The name of the object's constructor. 58 | message: 'string', // The supplied error message. 59 | stack: 'string', // The stack when the error was generated. 60 | cause?: Error, // If the original error had an error.cause, it will be serialized here 61 | raw: Error // Non-enumerable, i.e. will not be in the output, original 62 | // Error object. This is available for subsequent serializers 63 | // to use. 64 | [...any additional Enumerable property the original Error had] 65 | } 66 | ``` 67 | 68 | Any other extra properties, e.g. `statusCode`, that have been attached to the object will also be present on the serialized object. 69 | 70 | Example: 71 | ```javascript 72 | const serializer = require('pino-std-serializers').errWithCause; 73 | 74 | const innerError = new Error("inner error"); 75 | innerError.isInner = true; 76 | const outerError = new Error("outer error", { cause: innerError }); 77 | outerError.isInner = false; 78 | 79 | const serialized = serializer(outerError); 80 | /* Result: 81 | { 82 | "type": "Error", 83 | "message": "outer error", 84 | "isInner": false, 85 | "stack": "Error: outer error 86 | at <...omitted..>", 87 | "cause": { 88 | "type": "Error", 89 | "message": "inner error", 90 | "isInner": true, 91 | "stack": "Error: inner error 92 | at <...omitted..>" 93 | }, 94 | } 95 | */ 96 | ``` 97 | 98 | ### `exports.mapHttpResponse(response)` 99 | Used internally by Pino for general response logging. Returns an object: 100 | 101 | ```js 102 | { 103 | res: {} 104 | } 105 | ``` 106 | 107 | Where `res` is the `response` as serialized by the standard response serializer. 108 | 109 | ### `exports.mapHttpRequest(request)` 110 | Used internall by Pino for general request logging. Returns an object: 111 | 112 | ```js 113 | { 114 | req: {} 115 | } 116 | ``` 117 | 118 | Where `req` is the `request` as serialized by the standard request serializer. 119 | 120 | ### `exports.req(request)` 121 | The default `request` serializer. Returns an object: 122 | 123 | ```js 124 | { 125 | id: 'string', // Defaults to `undefined`, unless there is an `id` property 126 | // already attached to the `request` object or to the `request.info` 127 | // object. Attach a synchronous function 128 | // to the `request.id` that returns an identifier to have 129 | // the value filled. 130 | method: 'string', 131 | url: 'string', // the request pathname (as per req.url in core HTTP) 132 | query: 'object', // the request query (as per req.query in express or hapi) 133 | params: 'object', // the request params (as per req.params in express or hapi) 134 | headers: Object, // a reference to the `headers` object from the request 135 | // (as per req.headers in core HTTP) 136 | remoteAddress: 'string', 137 | remotePort: Number, 138 | raw: Object // Non-enumerable, i.e. will not be in the output, original 139 | // request object. This is available for subsequent serializers 140 | // to use. In cases where the `request` input already has 141 | // a `raw` property this will replace the original `request.raw` 142 | // property 143 | } 144 | ``` 145 | 146 | ### `exports.res(response)` 147 | The default `response` serializer. Returns an object: 148 | 149 | ```js 150 | { 151 | statusCode: Number, // Response status code, will be null before headers are flushed 152 | headers: Object, // The headers to be sent in the response. 153 | raw: Object // Non-enumerable, i.e. will not be in the output, original 154 | // response object. This is available for subsequent serializers 155 | // to use. 156 | } 157 | ``` 158 | 159 | ### `exports.wrapErrorSerializer(customSerializer)` 160 | A utility method for wrapping the default error serializer. This allows 161 | custom serializers to work with the already serialized object. 162 | 163 | The `customSerializer` accepts one parameter — the newly serialized error 164 | object — and returns the new (or updated) error object. 165 | 166 | ### `exports.wrapRequestSerializer(customSerializer)` 167 | A utility method for wrapping the default request serializer. This allows 168 | custom serializers to work with the already serialized object. 169 | 170 | The `customSerializer` accepts one parameter — the newly serialized request 171 | object — and returns the new (or updated) request object. 172 | 173 | ### `exports.wrapResponseSerializer(customSerializer)` 174 | A utility method for wrapping the default response serializer. This allows 175 | custom serializers to work with the already serialized object. 176 | 177 | The `customSerializer` accepts one parameter — the newly serialized response 178 | object — and returns the new (or updated) response object. 179 | 180 | ## License 181 | 182 | MIT License 183 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for pino-std-serializers 2.4 2 | // Definitions by: Connor Fitzgerald 3 | // Igor Savin 4 | // TypeScript Version: 2.7 5 | 6 | /// 7 | import { IncomingMessage, ServerResponse } from 'http'; 8 | 9 | export interface SerializedError { 10 | /** 11 | * The name of the object's constructor. 12 | */ 13 | type: string; 14 | /** 15 | * The supplied error message. 16 | */ 17 | message: string; 18 | /** 19 | * The stack when the error was generated. 20 | */ 21 | stack: string; 22 | /** 23 | * Non-enumerable. The original Error object. This will not be included in the logged output. 24 | * This is available for subsequent serializers to use. 25 | */ 26 | raw: Error; 27 | /** 28 | * `cause` is never included in the log output, if you need the `cause`, use {@link raw.cause} 29 | */ 30 | cause?: never; 31 | /** 32 | * Any other extra properties that have been attached to the object will also be present on the serialized object. 33 | */ 34 | [key: string]: any; 35 | [key: number]: any; 36 | } 37 | 38 | /** 39 | * Serializes an Error object. Does not serialize "err.cause" fields (will append the err.cause.message to err.message 40 | * and err.cause.stack to err.stack) 41 | */ 42 | export function err(err: Error): SerializedError; 43 | 44 | /** 45 | * Serializes an Error object, including full serialization for any err.cause fields recursively. 46 | */ 47 | export function errWithCause(err: Error): SerializedError; 48 | 49 | export interface SerializedRequest { 50 | /** 51 | * Defaults to `undefined`, unless there is an `id` property already attached to the `request` object or 52 | * to the `request.info` object. Attach a synchronous function to the `request.id` that returns an 53 | * identifier to have the value filled. 54 | */ 55 | id: string | undefined; 56 | /** 57 | * HTTP method. 58 | */ 59 | method: string; 60 | /** 61 | * Request pathname (as per req.url in core HTTP). 62 | */ 63 | url: string; 64 | /** 65 | * Reference to the `headers` object from the request (as per req.headers in core HTTP). 66 | */ 67 | headers: Record; 68 | remoteAddress: string; 69 | remotePort: number; 70 | params: Record; 71 | query: Record; 72 | 73 | /** 74 | * Non-enumerable, i.e. will not be in the output, original request object. This is available for subsequent 75 | * serializers to use. In cases where the `request` input already has a `raw` property this will 76 | * replace the original `request.raw` property. 77 | */ 78 | raw: IncomingMessage; 79 | } 80 | 81 | /** 82 | * Serializes a Request object. 83 | */ 84 | export function req(req: IncomingMessage): SerializedRequest; 85 | 86 | /** 87 | * Used internally by Pino for general request logging. 88 | */ 89 | export function mapHttpRequest(req: IncomingMessage): { 90 | req: SerializedRequest 91 | }; 92 | 93 | export interface SerializedResponse { 94 | /** 95 | * HTTP status code. 96 | */ 97 | statusCode: number; 98 | /** 99 | * The headers to be sent in the response. 100 | */ 101 | headers: Record; 102 | /** 103 | * Non-enumerable, i.e. will not be in the output, original response object. This is available for subsequent serializers to use. 104 | */ 105 | raw: ServerResponse; 106 | } 107 | 108 | /** 109 | * Serializes a Response object. 110 | */ 111 | export function res(res: ServerResponse): SerializedResponse; 112 | 113 | /** 114 | * Used internally by Pino for general response logging. 115 | */ 116 | export function mapHttpResponse(res: ServerResponse): { 117 | res: SerializedResponse 118 | }; 119 | 120 | export type CustomErrorSerializer = (err: SerializedError) => Record; 121 | 122 | /** 123 | * A utility method for wrapping the default error serializer. 124 | * This allows custom serializers to work with the already serialized object. 125 | * The customSerializer accepts one parameter — the newly serialized error object — and returns the new (or updated) error object. 126 | */ 127 | export function wrapErrorSerializer(customSerializer: CustomErrorSerializer): (err: Error) => Record; 128 | 129 | export type CustomRequestSerializer = (req: SerializedRequest) => Record; 130 | 131 | /** 132 | * A utility method for wrapping the default request serializer. 133 | * This allows custom serializers to work with the already serialized object. 134 | * The customSerializer accepts one parameter — the newly serialized request object — and returns the new (or updated) request object. 135 | */ 136 | export function wrapRequestSerializer(customSerializer: CustomRequestSerializer): (req: IncomingMessage) => Record; 137 | 138 | export type CustomResponseSerializer = (res: SerializedResponse) => Record; 139 | 140 | /** 141 | * A utility method for wrapping the default response serializer. 142 | * This allows custom serializers to work with the already serialized object. 143 | * The customSerializer accepts one parameter — the newly serialized response object — and returns the new (or updated) response object. 144 | */ 145 | export function wrapResponseSerializer(customSerializer: CustomResponseSerializer): (res: ServerResponse) => Record; 146 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const errSerializer = require('./lib/err') 4 | const errWithCauseSerializer = require('./lib/err-with-cause') 5 | const reqSerializers = require('./lib/req') 6 | const resSerializers = require('./lib/res') 7 | 8 | module.exports = { 9 | err: errSerializer, 10 | errWithCause: errWithCauseSerializer, 11 | mapHttpRequest: reqSerializers.mapHttpRequest, 12 | mapHttpResponse: resSerializers.mapHttpResponse, 13 | req: reqSerializers.reqSerializer, 14 | res: resSerializers.resSerializer, 15 | 16 | wrapErrorSerializer: function wrapErrorSerializer (customSerializer) { 17 | if (customSerializer === errSerializer) return customSerializer 18 | return function wrapErrSerializer (err) { 19 | return customSerializer(errSerializer(err)) 20 | } 21 | }, 22 | 23 | wrapRequestSerializer: function wrapRequestSerializer (customSerializer) { 24 | if (customSerializer === reqSerializers.reqSerializer) return customSerializer 25 | return function wrappedReqSerializer (req) { 26 | return customSerializer(reqSerializers.reqSerializer(req)) 27 | } 28 | }, 29 | 30 | wrapResponseSerializer: function wrapResponseSerializer (customSerializer) { 31 | if (customSerializer === resSerializers.resSerializer) return customSerializer 32 | return function wrappedResSerializer (res) { 33 | return customSerializer(resSerializers.resSerializer(res)) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/err-helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // ************************************************************** 4 | // * Code initially copied/adapted from "pony-cause" npm module * 5 | // * Please upstream improvements there * 6 | // ************************************************************** 7 | 8 | const isErrorLike = (err) => { 9 | return err && typeof err.message === 'string' 10 | } 11 | 12 | /** 13 | * @param {Error|{ cause?: unknown|(()=>err)}} err 14 | * @returns {Error|Object|undefined} 15 | */ 16 | const getErrorCause = (err) => { 17 | if (!err) return 18 | 19 | /** @type {unknown} */ 20 | // @ts-ignore 21 | const cause = err.cause 22 | 23 | // VError / NError style causes 24 | if (typeof cause === 'function') { 25 | // @ts-ignore 26 | const causeResult = err.cause() 27 | 28 | return isErrorLike(causeResult) 29 | ? causeResult 30 | : undefined 31 | } else { 32 | return isErrorLike(cause) 33 | ? cause 34 | : undefined 35 | } 36 | } 37 | 38 | /** 39 | * Internal method that keeps a track of which error we have already added, to avoid circular recursion 40 | * 41 | * @private 42 | * @param {Error} err 43 | * @param {Set} seen 44 | * @returns {string} 45 | */ 46 | const _stackWithCauses = (err, seen) => { 47 | if (!isErrorLike(err)) return '' 48 | 49 | const stack = err.stack || '' 50 | 51 | // Ensure we don't go circular or crazily deep 52 | if (seen.has(err)) { 53 | return stack + '\ncauses have become circular...' 54 | } 55 | 56 | const cause = getErrorCause(err) 57 | 58 | if (cause) { 59 | seen.add(err) 60 | return (stack + '\ncaused by: ' + _stackWithCauses(cause, seen)) 61 | } else { 62 | return stack 63 | } 64 | } 65 | 66 | /** 67 | * @param {Error} err 68 | * @returns {string} 69 | */ 70 | const stackWithCauses = (err) => _stackWithCauses(err, new Set()) 71 | 72 | /** 73 | * Internal method that keeps a track of which error we have already added, to avoid circular recursion 74 | * 75 | * @private 76 | * @param {Error} err 77 | * @param {Set} seen 78 | * @param {boolean} [skip] 79 | * @returns {string} 80 | */ 81 | const _messageWithCauses = (err, seen, skip) => { 82 | if (!isErrorLike(err)) return '' 83 | 84 | const message = skip ? '' : (err.message || '') 85 | 86 | // Ensure we don't go circular or crazily deep 87 | if (seen.has(err)) { 88 | return message + ': ...' 89 | } 90 | 91 | const cause = getErrorCause(err) 92 | 93 | if (cause) { 94 | seen.add(err) 95 | 96 | // @ts-ignore 97 | const skipIfVErrorStyleCause = typeof err.cause === 'function' 98 | 99 | return (message + 100 | (skipIfVErrorStyleCause ? '' : ': ') + 101 | _messageWithCauses(cause, seen, skipIfVErrorStyleCause)) 102 | } else { 103 | return message 104 | } 105 | } 106 | 107 | /** 108 | * @param {Error} err 109 | * @returns {string} 110 | */ 111 | const messageWithCauses = (err) => _messageWithCauses(err, new Set()) 112 | 113 | module.exports = { 114 | isErrorLike, 115 | getErrorCause, 116 | stackWithCauses, 117 | messageWithCauses 118 | } 119 | -------------------------------------------------------------------------------- /lib/err-proto.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const seen = Symbol('circular-ref-tag') 4 | const rawSymbol = Symbol('pino-raw-err-ref') 5 | 6 | const pinoErrProto = Object.create({}, { 7 | type: { 8 | enumerable: true, 9 | writable: true, 10 | value: undefined 11 | }, 12 | message: { 13 | enumerable: true, 14 | writable: true, 15 | value: undefined 16 | }, 17 | stack: { 18 | enumerable: true, 19 | writable: true, 20 | value: undefined 21 | }, 22 | aggregateErrors: { 23 | enumerable: true, 24 | writable: true, 25 | value: undefined 26 | }, 27 | raw: { 28 | enumerable: false, 29 | get: function () { 30 | return this[rawSymbol] 31 | }, 32 | set: function (val) { 33 | this[rawSymbol] = val 34 | } 35 | } 36 | }) 37 | Object.defineProperty(pinoErrProto, rawSymbol, { 38 | writable: true, 39 | value: {} 40 | }) 41 | 42 | module.exports = { 43 | pinoErrProto, 44 | pinoErrorSymbols: { 45 | seen, 46 | rawSymbol 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/err-with-cause.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = errWithCauseSerializer 4 | 5 | const { isErrorLike } = require('./err-helpers') 6 | const { pinoErrProto, pinoErrorSymbols } = require('./err-proto') 7 | const { seen } = pinoErrorSymbols 8 | 9 | const { toString } = Object.prototype 10 | 11 | function errWithCauseSerializer (err) { 12 | if (!isErrorLike(err)) { 13 | return err 14 | } 15 | 16 | err[seen] = undefined // tag to prevent re-looking at this 17 | const _err = Object.create(pinoErrProto) 18 | _err.type = toString.call(err.constructor) === '[object Function]' 19 | ? err.constructor.name 20 | : err.name 21 | _err.message = err.message 22 | _err.stack = err.stack 23 | 24 | if (Array.isArray(err.errors)) { 25 | _err.aggregateErrors = err.errors.map(err => errWithCauseSerializer(err)) 26 | } 27 | 28 | if (isErrorLike(err.cause) && !Object.prototype.hasOwnProperty.call(err.cause, seen)) { 29 | _err.cause = errWithCauseSerializer(err.cause) 30 | } 31 | 32 | for (const key in err) { 33 | if (_err[key] === undefined) { 34 | const val = err[key] 35 | if (isErrorLike(val)) { 36 | if (!Object.prototype.hasOwnProperty.call(val, seen)) { 37 | _err[key] = errWithCauseSerializer(val) 38 | } 39 | } else { 40 | _err[key] = val 41 | } 42 | } 43 | } 44 | 45 | delete err[seen] // clean up tag in case err is serialized again later 46 | _err.raw = err 47 | return _err 48 | } 49 | -------------------------------------------------------------------------------- /lib/err.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = errSerializer 4 | 5 | const { messageWithCauses, stackWithCauses, isErrorLike } = require('./err-helpers') 6 | const { pinoErrProto, pinoErrorSymbols } = require('./err-proto') 7 | const { seen } = pinoErrorSymbols 8 | 9 | const { toString } = Object.prototype 10 | 11 | function errSerializer (err) { 12 | if (!isErrorLike(err)) { 13 | return err 14 | } 15 | 16 | err[seen] = undefined // tag to prevent re-looking at this 17 | const _err = Object.create(pinoErrProto) 18 | _err.type = toString.call(err.constructor) === '[object Function]' 19 | ? err.constructor.name 20 | : err.name 21 | _err.message = messageWithCauses(err) 22 | _err.stack = stackWithCauses(err) 23 | 24 | if (Array.isArray(err.errors)) { 25 | _err.aggregateErrors = err.errors.map(err => errSerializer(err)) 26 | } 27 | 28 | for (const key in err) { 29 | if (_err[key] === undefined) { 30 | const val = err[key] 31 | if (isErrorLike(val)) { 32 | // We append cause messages and stacks to _err, therefore skipping causes here 33 | if (key !== 'cause' && !Object.prototype.hasOwnProperty.call(val, seen)) { 34 | _err[key] = errSerializer(val) 35 | } 36 | } else { 37 | _err[key] = val 38 | } 39 | } 40 | } 41 | 42 | delete err[seen] // clean up tag in case err is serialized again later 43 | _err.raw = err 44 | return _err 45 | } 46 | -------------------------------------------------------------------------------- /lib/req.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | mapHttpRequest, 5 | reqSerializer 6 | } 7 | 8 | const rawSymbol = Symbol('pino-raw-req-ref') 9 | const pinoReqProto = Object.create({}, { 10 | id: { 11 | enumerable: true, 12 | writable: true, 13 | value: '' 14 | }, 15 | method: { 16 | enumerable: true, 17 | writable: true, 18 | value: '' 19 | }, 20 | url: { 21 | enumerable: true, 22 | writable: true, 23 | value: '' 24 | }, 25 | query: { 26 | enumerable: true, 27 | writable: true, 28 | value: '' 29 | }, 30 | params: { 31 | enumerable: true, 32 | writable: true, 33 | value: '' 34 | }, 35 | headers: { 36 | enumerable: true, 37 | writable: true, 38 | value: {} 39 | }, 40 | remoteAddress: { 41 | enumerable: true, 42 | writable: true, 43 | value: '' 44 | }, 45 | remotePort: { 46 | enumerable: true, 47 | writable: true, 48 | value: '' 49 | }, 50 | raw: { 51 | enumerable: false, 52 | get: function () { 53 | return this[rawSymbol] 54 | }, 55 | set: function (val) { 56 | this[rawSymbol] = val 57 | } 58 | } 59 | }) 60 | Object.defineProperty(pinoReqProto, rawSymbol, { 61 | writable: true, 62 | value: {} 63 | }) 64 | 65 | function reqSerializer (req) { 66 | // req.info is for hapi compat. 67 | const connection = req.info || req.socket 68 | const _req = Object.create(pinoReqProto) 69 | _req.id = (typeof req.id === 'function' ? req.id() : (req.id || (req.info ? req.info.id : undefined))) 70 | _req.method = req.method 71 | // req.originalUrl is for expressjs compat. 72 | if (req.originalUrl) { 73 | _req.url = req.originalUrl 74 | } else { 75 | const path = req.path 76 | // path for safe hapi compat. 77 | _req.url = typeof path === 'string' ? path : (req.url ? req.url.path || req.url : undefined) 78 | } 79 | 80 | if (req.query) { 81 | _req.query = req.query 82 | } 83 | 84 | if (req.params) { 85 | _req.params = req.params 86 | } 87 | 88 | _req.headers = req.headers 89 | _req.remoteAddress = connection && connection.remoteAddress 90 | _req.remotePort = connection && connection.remotePort 91 | // req.raw is for hapi compat/equivalence 92 | _req.raw = req.raw || req 93 | return _req 94 | } 95 | 96 | function mapHttpRequest (req) { 97 | return { 98 | req: reqSerializer(req) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /lib/res.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | mapHttpResponse, 5 | resSerializer 6 | } 7 | 8 | const rawSymbol = Symbol('pino-raw-res-ref') 9 | const pinoResProto = Object.create({}, { 10 | statusCode: { 11 | enumerable: true, 12 | writable: true, 13 | value: 0 14 | }, 15 | headers: { 16 | enumerable: true, 17 | writable: true, 18 | value: '' 19 | }, 20 | raw: { 21 | enumerable: false, 22 | get: function () { 23 | return this[rawSymbol] 24 | }, 25 | set: function (val) { 26 | this[rawSymbol] = val 27 | } 28 | } 29 | }) 30 | Object.defineProperty(pinoResProto, rawSymbol, { 31 | writable: true, 32 | value: {} 33 | }) 34 | 35 | function resSerializer (res) { 36 | const _res = Object.create(pinoResProto) 37 | _res.statusCode = res.headersSent ? res.statusCode : null 38 | _res.headers = res.getHeaders ? res.getHeaders() : res._headers 39 | _res.raw = res 40 | return _res 41 | } 42 | 43 | function mapHttpResponse (res) { 44 | return { 45 | res: resSerializer(res) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pino-std-serializers", 3 | "version": "7.0.0", 4 | "description": "A collection of standard object serializers for Pino", 5 | "main": "index.js", 6 | "type": "commonjs", 7 | "types": "index.d.ts", 8 | "scripts": { 9 | "lint": "standard | snazzy", 10 | "lint-ci": "standard", 11 | "test": "borp -p 'test/**/*.js'", 12 | "test-ci": "borp --coverage -p 'test/**/*.js'", 13 | "test-types": "tsc && tsd" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+ssh://git@github.com/pinojs/pino-std-serializers.git" 18 | }, 19 | "keywords": [ 20 | "pino", 21 | "logging" 22 | ], 23 | "author": "James Sumners ", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/pinojs/pino-std-serializers/issues" 27 | }, 28 | "homepage": "https://github.com/pinojs/pino-std-serializers#readme", 29 | "precommit": [ 30 | "lint", 31 | "test", 32 | "test-types" 33 | ], 34 | "devDependencies": { 35 | "@matteo.collina/tspl": "^0.2.0", 36 | "@types/node": "^22.0.0", 37 | "borp": "^0.20.0", 38 | "pre-commit": "^1.2.2", 39 | "snazzy": "^9.0.0", 40 | "standard": "^17.1.0", 41 | "tsd": "^0.32.0", 42 | "typescript": "~5.8.2" 43 | }, 44 | "tsd": { 45 | "directory": "test/types" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/err-with-cause.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const assert = require('node:assert') 5 | const serializer = require('../lib/err-with-cause') 6 | const { wrapErrorSerializer } = require('../') 7 | 8 | test('serializes Error objects', () => { 9 | const serialized = serializer(Error('foo')) 10 | assert.strictEqual(serialized.type, 'Error') 11 | assert.strictEqual(serialized.message, 'foo') 12 | assert.match(serialized.stack, /err-with-cause\.test\.js:/) 13 | }) 14 | 15 | test('serializes Error objects with extra properties', () => { 16 | const err = Error('foo') 17 | err.statusCode = 500 18 | const serialized = serializer(err) 19 | assert.strictEqual(serialized.type, 'Error') 20 | assert.strictEqual(serialized.message, 'foo') 21 | assert.ok(serialized.statusCode) 22 | assert.strictEqual(serialized.statusCode, 500) 23 | assert.match(serialized.stack, /err-with-cause\.test\.js:/) 24 | }) 25 | 26 | test('serializes Error objects with subclass "type"', () => { 27 | class MyError extends Error {} 28 | 29 | const err = new MyError('foo') 30 | const serialized = serializer(err) 31 | assert.strictEqual(serialized.type, 'MyError') 32 | }) 33 | 34 | test('serializes nested errors', () => { 35 | const err = Error('foo') 36 | err.inner = Error('bar') 37 | const serialized = serializer(err) 38 | assert.strictEqual(serialized.type, 'Error') 39 | assert.strictEqual(serialized.message, 'foo') 40 | assert.match(serialized.stack, /err-with-cause\.test\.js:/) 41 | assert.strictEqual(serialized.inner.type, 'Error') 42 | assert.strictEqual(serialized.inner.message, 'bar') 43 | assert.match(serialized.inner.stack, /Error: bar/) 44 | assert.match(serialized.inner.stack, /err-with-cause\.test\.js:/) 45 | }) 46 | 47 | test('serializes error causes', () => { 48 | const innerErr = Error('inner') 49 | const middleErr = Error('middle') 50 | middleErr.cause = innerErr 51 | const outerErr = Error('outer') 52 | outerErr.cause = middleErr 53 | 54 | const serialized = serializer(outerErr) 55 | 56 | assert.strictEqual(serialized.type, 'Error') 57 | assert.strictEqual(serialized.message, 'outer') 58 | assert.match(serialized.stack, /err-with-cause\.test\.js:/) 59 | 60 | assert.strictEqual(serialized.cause.type, 'Error') 61 | assert.strictEqual(serialized.cause.message, 'middle') 62 | assert.match(serialized.cause.stack, /err-with-cause\.test\.js:/) 63 | 64 | assert.strictEqual(serialized.cause.cause.type, 'Error') 65 | assert.strictEqual(serialized.cause.cause.message, 'inner') 66 | assert.match(serialized.cause.cause.stack, /err-with-cause\.test\.js:/) 67 | }) 68 | 69 | test('keeps non-error cause', () => { 70 | const err = Error('foo') 71 | err.cause = 'abc' 72 | const serialized = serializer(err) 73 | assert.strictEqual(serialized.type, 'Error') 74 | assert.strictEqual(serialized.message, 'foo') 75 | assert.strictEqual(serialized.cause, 'abc') 76 | }) 77 | 78 | test('prevents infinite recursion', () => { 79 | const err = Error('foo') 80 | err.inner = err 81 | const serialized = serializer(err) 82 | assert.strictEqual(serialized.type, 'Error') 83 | assert.strictEqual(serialized.message, 'foo') 84 | assert.match(serialized.stack, /err-with-cause\.test\.js:/) 85 | assert.ok(!serialized.inner) 86 | }) 87 | 88 | test('cleans up infinite recursion tracking', () => { 89 | const err = Error('foo') 90 | const bar = Error('bar') 91 | err.inner = bar 92 | bar.inner = err 93 | 94 | serializer(err) 95 | const serialized = serializer(err) 96 | 97 | assert.strictEqual(serialized.type, 'Error') 98 | assert.strictEqual(serialized.message, 'foo') 99 | assert.match(serialized.stack, /err-with-cause\.test\.js:/) 100 | assert.ok(serialized.inner) 101 | assert.strictEqual(serialized.inner.type, 'Error') 102 | assert.strictEqual(serialized.inner.message, 'bar') 103 | assert.match(serialized.inner.stack, /Error: bar/) 104 | assert.ok(!serialized.inner.inner) 105 | }) 106 | 107 | test('err.raw is available', () => { 108 | const err = Error('foo') 109 | const serialized = serializer(err) 110 | assert.strictEqual(serialized.raw, err) 111 | }) 112 | 113 | test('redefined err.constructor doesnt crash serializer', () => { 114 | function check (a, name) { 115 | assert.strictEqual(a.type, name) 116 | assert.strictEqual(a.message, 'foo') 117 | } 118 | 119 | const err1 = TypeError('foo') 120 | err1.constructor = '10' 121 | 122 | const err2 = TypeError('foo') 123 | err2.constructor = undefined 124 | 125 | const err3 = Error('foo') 126 | err3.constructor = null 127 | 128 | const err4 = Error('foo') 129 | err4.constructor = 10 130 | 131 | class MyError extends Error {} 132 | 133 | const err5 = new MyError('foo') 134 | err5.constructor = undefined 135 | 136 | check(serializer(err1), 'TypeError') 137 | check(serializer(err2), 'TypeError') 138 | check(serializer(err3), 'Error') 139 | check(serializer(err4), 'Error') 140 | // We do not expect 'MyError' because err5.constructor has been blown away. 141 | // `err5.name` is 'Error' from the base class prototype. 142 | check(serializer(err5), 'Error') 143 | }) 144 | 145 | test('pass through anything that does not look like an Error', () => { 146 | function check (a) { 147 | assert.strictEqual(serializer(a), a) 148 | } 149 | 150 | check('foo') 151 | check({ hello: 'world' }) 152 | check([1, 2]) 153 | }) 154 | 155 | test('can wrap err serializers', () => { 156 | const err = Error('foo') 157 | err.foo = 'foo' 158 | const serializer = wrapErrorSerializer(function (err) { 159 | delete err.foo 160 | err.bar = 'bar' 161 | return err 162 | }) 163 | const serialized = serializer(err) 164 | assert.strictEqual(serialized.type, 'Error') 165 | assert.strictEqual(serialized.message, 'foo') 166 | assert.match(serialized.stack, /err-with-cause\.test\.js:/) 167 | assert.ok(!serialized.foo) 168 | assert.strictEqual(serialized.bar, 'bar') 169 | }) 170 | 171 | test('serializes aggregate errors', { skip: !global.AggregateError }, () => { 172 | const foo = new Error('foo') 173 | const bar = new Error('bar') 174 | for (const aggregate of [ 175 | new AggregateError([foo, bar], 'aggregated message'), // eslint-disable-line no-undef 176 | { errors: [foo, bar], message: 'aggregated message', stack: 'err-with-cause.test.js:' } 177 | ]) { 178 | const serialized = serializer(aggregate) 179 | assert.strictEqual(serialized.message, 'aggregated message') 180 | assert.strictEqual(serialized.aggregateErrors.length, 2) 181 | assert.strictEqual(serialized.aggregateErrors[0].message, 'foo') 182 | assert.strictEqual(serialized.aggregateErrors[1].message, 'bar') 183 | assert.match(serialized.aggregateErrors[0].stack, /^Error: foo/) 184 | assert.match(serialized.aggregateErrors[1].stack, /^Error: bar/) 185 | assert.match(serialized.stack, /err-with-cause\.test\.js:/) 186 | } 187 | }) 188 | -------------------------------------------------------------------------------- /test/err.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const assert = require('node:assert') 4 | const { test } = require('node:test') 5 | const serializer = require('../lib/err') 6 | const { wrapErrorSerializer } = require('../') 7 | 8 | test('serializes Error objects', () => { 9 | const serialized = serializer(Error('foo')) 10 | assert.strictEqual(serialized.type, 'Error') 11 | assert.strictEqual(serialized.message, 'foo') 12 | assert.match(serialized.stack, /err\.test\.js:/) 13 | }) 14 | 15 | test('serializes Error objects with extra properties', () => { 16 | const err = Error('foo') 17 | err.statusCode = 500 18 | const serialized = serializer(err) 19 | assert.strictEqual(serialized.type, 'Error') 20 | assert.strictEqual(serialized.message, 'foo') 21 | assert.ok(serialized.statusCode) 22 | assert.strictEqual(serialized.statusCode, 500) 23 | assert.match(serialized.stack, /err\.test\.js:/) 24 | }) 25 | 26 | test('serializes Error objects with subclass "type"', () => { 27 | class MyError extends Error {} 28 | const err = new MyError('foo') 29 | const serialized = serializer(err) 30 | assert.strictEqual(serialized.type, 'MyError') 31 | }) 32 | 33 | test('serializes nested errors', () => { 34 | const err = Error('foo') 35 | err.inner = Error('bar') 36 | const serialized = serializer(err) 37 | assert.strictEqual(serialized.type, 'Error') 38 | assert.strictEqual(serialized.message, 'foo') 39 | assert.match(serialized.stack, /err\.test\.js:/) 40 | assert.strictEqual(serialized.inner.type, 'Error') 41 | assert.strictEqual(serialized.inner.message, 'bar') 42 | assert.match(serialized.inner.stack, /Error: bar/) 43 | assert.match(serialized.inner.stack, /err\.test\.js:/) 44 | }) 45 | 46 | test('serializes error causes', () => { 47 | for (const cause of [ 48 | Error('bar'), 49 | { message: 'bar', stack: 'Error: bar: err.test.js:' } 50 | ]) { 51 | const err = Error('foo') 52 | err.cause = cause 53 | err.cause.cause = Error('abc') 54 | const serialized = serializer(err) 55 | assert.strictEqual(serialized.type, 'Error') 56 | assert.strictEqual(serialized.message, 'foo: bar: abc') 57 | assert.match(serialized.stack, /err\.test\.js:/) 58 | assert.match(serialized.stack, /Error: foo/) 59 | assert.match(serialized.stack, /Error: bar/) 60 | assert.match(serialized.stack, /Error: abc/) 61 | assert.ok(!serialized.cause) 62 | } 63 | }) 64 | 65 | test('serializes error causes with VError support', function (t) { 66 | // Fake VError-style setup 67 | const err = Error('foo: bar') 68 | err.foo = 'abc' 69 | err.cause = function () { 70 | const err = Error('bar') 71 | err.cause = Error(this.foo) 72 | return err 73 | } 74 | const serialized = serializer(err) 75 | assert.strictEqual(serialized.type, 'Error') 76 | assert.strictEqual(serialized.message, 'foo: bar: abc') 77 | assert.match(serialized.stack, /err\.test\.js:/) 78 | assert.match(serialized.stack, /Error: foo/) 79 | assert.match(serialized.stack, /Error: bar/) 80 | assert.match(serialized.stack, /Error: abc/) 81 | }) 82 | 83 | test('keeps non-error cause', () => { 84 | const err = Error('foo') 85 | err.cause = 'abc' 86 | const serialized = serializer(err) 87 | assert.strictEqual(serialized.type, 'Error') 88 | assert.strictEqual(serialized.message, 'foo') 89 | assert.strictEqual(serialized.cause, 'abc') 90 | }) 91 | 92 | test('prevents infinite recursion', () => { 93 | const err = Error('foo') 94 | err.inner = err 95 | const serialized = serializer(err) 96 | assert.strictEqual(serialized.type, 'Error') 97 | assert.strictEqual(serialized.message, 'foo') 98 | assert.match(serialized.stack, /err\.test\.js:/) 99 | assert.ok(!serialized.inner) 100 | }) 101 | 102 | test('cleans up infinite recursion tracking', () => { 103 | const err = Error('foo') 104 | const bar = Error('bar') 105 | err.inner = bar 106 | bar.inner = err 107 | 108 | serializer(err) 109 | const serialized = serializer(err) 110 | 111 | assert.strictEqual(serialized.type, 'Error') 112 | assert.strictEqual(serialized.message, 'foo') 113 | assert.match(serialized.stack, /err\.test\.js:/) 114 | assert.ok(serialized.inner) 115 | assert.strictEqual(serialized.inner.type, 'Error') 116 | assert.strictEqual(serialized.inner.message, 'bar') 117 | assert.match(serialized.inner.stack, /Error: bar/) 118 | assert.ok(!serialized.inner.inner) 119 | }) 120 | 121 | test('err.raw is available', () => { 122 | const err = Error('foo') 123 | const serialized = serializer(err) 124 | assert.strictEqual(serialized.raw, err) 125 | }) 126 | 127 | test('redefined err.constructor doesnt crash serializer', () => { 128 | function check (a, name) { 129 | assert.strictEqual(a.type, name) 130 | assert.strictEqual(a.message, 'foo') 131 | } 132 | 133 | const err1 = TypeError('foo') 134 | err1.constructor = '10' 135 | 136 | const err2 = TypeError('foo') 137 | err2.constructor = undefined 138 | 139 | const err3 = Error('foo') 140 | err3.constructor = null 141 | 142 | const err4 = Error('foo') 143 | err4.constructor = 10 144 | 145 | class MyError extends Error {} 146 | const err5 = new MyError('foo') 147 | err5.constructor = undefined 148 | 149 | check(serializer(err1), 'TypeError') 150 | check(serializer(err2), 'TypeError') 151 | check(serializer(err3), 'Error') 152 | check(serializer(err4), 'Error') 153 | // We do not expect 'MyError' because err5.constructor has been blown away. 154 | // `err5.name` is 'Error' from the base class prototype. 155 | check(serializer(err5), 'Error') 156 | }) 157 | 158 | test('pass through anything that does not look like an Error', () => { 159 | function check (a) { 160 | assert.strictEqual(serializer(a), a) 161 | } 162 | 163 | check('foo') 164 | check({ hello: 'world' }) 165 | check([1, 2]) 166 | }) 167 | 168 | test('can wrap err serializers', () => { 169 | const err = Error('foo') 170 | err.foo = 'foo' 171 | const serializer = wrapErrorSerializer(function (err) { 172 | delete err.foo 173 | err.bar = 'bar' 174 | return err 175 | }) 176 | const serialized = serializer(err) 177 | assert.strictEqual(serialized.type, 'Error') 178 | assert.strictEqual(serialized.message, 'foo') 179 | assert.match(serialized.stack, /err\.test\.js:/) 180 | assert.ok(!serialized.foo) 181 | assert.strictEqual(serialized.bar, 'bar') 182 | }) 183 | 184 | test('serializes aggregate errors', { skip: !global.AggregateError }, () => { 185 | const foo = new Error('foo') 186 | const bar = new Error('bar') 187 | for (const aggregate of [ 188 | new AggregateError([foo, bar], 'aggregated message'), // eslint-disable-line no-undef 189 | { errors: [foo, bar], message: 'aggregated message', stack: 'err.test.js:' } 190 | ]) { 191 | const serialized = serializer(aggregate) 192 | assert.strictEqual(serialized.message, 'aggregated message') 193 | assert.strictEqual(serialized.aggregateErrors.length, 2) 194 | assert.strictEqual(serialized.aggregateErrors[0].message, 'foo') 195 | assert.strictEqual(serialized.aggregateErrors[1].message, 'bar') 196 | assert.match(serialized.aggregateErrors[0].stack, /^Error: foo/) 197 | assert.match(serialized.aggregateErrors[1].stack, /^Error: bar/) 198 | assert.match(serialized.stack, /err\.test\.js:/) 199 | } 200 | }) 201 | -------------------------------------------------------------------------------- /test/req.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { tspl } = require('@matteo.collina/tspl') 4 | const http = require('node:http') 5 | const { test } = require('node:test') 6 | const serializers = require('../lib/req') 7 | const { wrapRequestSerializer } = require('../') 8 | 9 | test('maps request', async (t) => { 10 | const p = tspl(t, { plan: 2 }) 11 | 12 | const server = http.createServer(handler) 13 | server.unref() 14 | server.listen(0, () => { 15 | http.get(server.address(), () => {}) 16 | }) 17 | 18 | t.after(() => server.close()) 19 | 20 | function handler (req, res) { 21 | const serialized = serializers.mapHttpRequest(req) 22 | p.ok(serialized.req) 23 | p.ok(serialized.req.method) 24 | res.end() 25 | } 26 | 27 | await p.completed 28 | }) 29 | 30 | test('does not return excessively long object', async (t) => { 31 | const p = tspl(t, { plan: 1 }) 32 | 33 | const server = http.createServer(handler) 34 | server.unref() 35 | server.listen(0, () => { 36 | http.get(server.address(), () => {}) 37 | }) 38 | 39 | t.after(() => server.close()) 40 | 41 | function handler (req, res) { 42 | const serialized = serializers.reqSerializer(req) 43 | p.strictEqual(Object.keys(serialized).length, 6) 44 | res.end() 45 | } 46 | 47 | await p.completed 48 | }) 49 | 50 | test('req.raw is available', async (t) => { 51 | const p = tspl(t, { plan: 2 }) 52 | 53 | const server = http.createServer(handler) 54 | server.unref() 55 | server.listen(0, () => { 56 | http.get(server.address(), () => {}) 57 | }) 58 | 59 | t.after(() => server.close()) 60 | 61 | function handler (req, res) { 62 | req.foo = 'foo' 63 | const serialized = serializers.reqSerializer(req) 64 | p.ok(serialized.raw) 65 | p.strictEqual(serialized.raw.foo, 'foo') 66 | res.end() 67 | } 68 | 69 | await p.completed 70 | }) 71 | 72 | test('req.raw will be obtained in from input request raw property if input request raw property is truthy', async (t) => { 73 | const p = tspl(t, { plan: 2 }) 74 | 75 | const server = http.createServer(handler) 76 | server.unref() 77 | server.listen(0, () => { 78 | http.get(server.address(), () => {}) 79 | }) 80 | 81 | t.after(() => server.close()) 82 | 83 | function handler (req, res) { 84 | req.raw = { req: { foo: 'foo' }, res: {} } 85 | const serialized = serializers.reqSerializer(req) 86 | p.ok(serialized.raw) 87 | p.strictEqual(serialized.raw.req.foo, 'foo') 88 | res.end() 89 | } 90 | 91 | await p.completed 92 | }) 93 | 94 | test('req.id defaults to undefined', async (t) => { 95 | const p = tspl(t, { plan: 1 }) 96 | 97 | const server = http.createServer(handler) 98 | server.unref() 99 | server.listen(0, () => { 100 | http.get(server.address(), () => {}) 101 | }) 102 | 103 | t.after(() => server.close()) 104 | 105 | function handler (req, res) { 106 | const serialized = serializers.reqSerializer(req) 107 | p.strictEqual(serialized.id, undefined) 108 | res.end() 109 | } 110 | 111 | await p.completed 112 | }) 113 | 114 | test('req.id has a non-function value', async (t) => { 115 | const p = tspl(t, { plan: 1 }) 116 | 117 | const server = http.createServer(handler) 118 | server.unref() 119 | server.listen(0, () => { 120 | http.get(server.address(), () => {}) 121 | }) 122 | 123 | t.after(() => server.close()) 124 | 125 | function handler (req, res) { 126 | const serialized = serializers.reqSerializer(req) 127 | p.strictEqual(typeof serialized.id === 'function', false) 128 | res.end() 129 | } 130 | 131 | await p.completed 132 | }) 133 | 134 | test('req.id will be obtained from input request info.id when input request id does not exist', async (t) => { 135 | const p = tspl(t, { plan: 1 }) 136 | 137 | const server = http.createServer(handler) 138 | server.unref() 139 | server.listen(0, () => { 140 | http.get(server.address(), () => {}) 141 | }) 142 | 143 | t.after(() => server.close()) 144 | 145 | function handler (req, res) { 146 | req.info = { id: 'test' } 147 | const serialized = serializers.reqSerializer(req) 148 | p.strictEqual(serialized.id, 'test') 149 | res.end() 150 | } 151 | 152 | await p.completed 153 | }) 154 | 155 | test('req.id has a non-function value with custom id function', async (t) => { 156 | const p = tspl(t, { plan: 2 }) 157 | 158 | const server = http.createServer(handler) 159 | server.unref() 160 | server.listen(0, () => { 161 | http.get(server.address(), () => {}) 162 | }) 163 | 164 | t.after(() => server.close()) 165 | 166 | function handler (req, res) { 167 | req.id = function () { return 42 } 168 | const serialized = serializers.reqSerializer(req) 169 | p.strictEqual(typeof serialized.id === 'function', false) 170 | p.strictEqual(serialized.id, 42) 171 | res.end() 172 | } 173 | 174 | await p.completed 175 | }) 176 | 177 | test('req.url will be obtained from input request req.path when input request url is an object', async (t) => { 178 | const p = tspl(t, { plan: 1 }) 179 | 180 | const server = http.createServer(handler) 181 | server.unref() 182 | server.listen(0, () => { 183 | http.get(server.address(), () => {}) 184 | }) 185 | 186 | t.after(() => server.close()) 187 | 188 | function handler (req, res) { 189 | req.path = '/test' 190 | const serialized = serializers.reqSerializer(req) 191 | p.strictEqual(serialized.url, '/test') 192 | res.end() 193 | } 194 | 195 | await p.completed 196 | }) 197 | 198 | test('req.url will be obtained from input request url.path when input request url is an object', async (t) => { 199 | const p = tspl(t, { plan: 1 }) 200 | 201 | const server = http.createServer(handler) 202 | server.unref() 203 | server.listen(0, () => { 204 | http.get(server.address(), () => {}) 205 | }) 206 | 207 | t.after(() => server.close()) 208 | 209 | function handler (req, res) { 210 | req.url = { path: '/test' } 211 | const serialized = serializers.reqSerializer(req) 212 | p.strictEqual(serialized.url, '/test') 213 | res.end() 214 | } 215 | 216 | await p.completed 217 | }) 218 | 219 | test('req.url will be obtained from input request url when input request url is not an object', async (t) => { 220 | const p = tspl(t, { plan: 1 }) 221 | 222 | const server = http.createServer(handler) 223 | server.unref() 224 | server.listen(0, () => { 225 | http.get(server.address(), () => {}) 226 | }) 227 | 228 | t.after(() => server.close()) 229 | 230 | function handler (req, res) { 231 | req.url = '/test' 232 | const serialized = serializers.reqSerializer(req) 233 | p.strictEqual(serialized.url, '/test') 234 | res.end() 235 | } 236 | 237 | await p.completed 238 | }) 239 | 240 | test('req.url will be empty when input request path and url are not defined', async (t) => { 241 | const p = tspl(t, { plan: 1 }) 242 | 243 | const server = http.createServer(handler) 244 | server.unref() 245 | server.listen(0, () => { 246 | http.get(server.address(), () => {}) 247 | }) 248 | 249 | t.after(() => server.close()) 250 | 251 | function handler (req, res) { 252 | const serialized = serializers.reqSerializer(req) 253 | p.strictEqual(serialized.url, '/') 254 | res.end() 255 | } 256 | 257 | await p.completed 258 | }) 259 | 260 | test('req.url will be obtained from input request originalUrl when available', async (t) => { 261 | const p = tspl(t, { plan: 1 }) 262 | 263 | const server = http.createServer(handler) 264 | server.unref() 265 | server.listen(0, () => { 266 | http.get(server.address(), () => {}) 267 | }) 268 | 269 | t.after(() => server.close()) 270 | 271 | function handler (req, res) { 272 | req.originalUrl = '/test' 273 | const serialized = serializers.reqSerializer(req) 274 | p.strictEqual(serialized.url, '/test') 275 | res.end() 276 | } 277 | 278 | await p.completed 279 | }) 280 | 281 | test('req.url will be obtained from input request url when req path is a function', async (t) => { 282 | const p = tspl(t, { plan: 1 }) 283 | 284 | const server = http.createServer(handler) 285 | server.unref() 286 | server.listen(0, () => { 287 | http.get(server.address(), () => {}) 288 | }) 289 | 290 | t.after(() => server.close()) 291 | 292 | function handler (req, res) { 293 | req.path = function () { 294 | throw new Error('unexpected invocation') 295 | } 296 | req.url = '/test' 297 | const serialized = serializers.reqSerializer(req) 298 | p.strictEqual(serialized.url, '/test') 299 | res.end() 300 | } 301 | 302 | await p.completed 303 | }) 304 | 305 | test('req.url being undefined does not throw an error', async (t) => { 306 | const p = tspl(t, { plan: 1 }) 307 | 308 | const server = http.createServer(handler) 309 | server.unref() 310 | server.listen(0, () => { 311 | http.get(server.address(), () => {}) 312 | }) 313 | 314 | t.after(() => server.close()) 315 | 316 | function handler (req, res) { 317 | req.url = undefined 318 | const serialized = serializers.reqSerializer(req) 319 | p.strictEqual(serialized.url, undefined) 320 | res.end() 321 | } 322 | 323 | await p.completed 324 | }) 325 | 326 | test('can wrap request serializers', async (t) => { 327 | const p = tspl(t, { plan: 3 }) 328 | 329 | const server = http.createServer(handler) 330 | server.unref() 331 | server.listen(0, () => { 332 | http.get(server.address(), () => {}) 333 | }) 334 | 335 | t.after(() => server.close()) 336 | 337 | const serailizer = wrapRequestSerializer(function (req) { 338 | p.ok(req.method) 339 | p.strictEqual(req.method, 'GET') 340 | delete req.method 341 | return req 342 | }) 343 | 344 | function handler (req, res) { 345 | const serialized = serailizer(req) 346 | p.ok(!serialized.method) 347 | res.end() 348 | } 349 | 350 | await p.completed 351 | }) 352 | 353 | test('req.remoteAddress will be obtained from request socket.remoteAddress as fallback', async (t) => { 354 | const p = tspl(t, { plan: 1 }) 355 | 356 | const server = http.createServer(handler) 357 | server.unref() 358 | server.listen(0, () => { 359 | http.get(server.address(), () => {}) 360 | }) 361 | 362 | t.after(() => server.close()) 363 | 364 | function handler (req, res) { 365 | req.socket = { remoteAddress: 'http://localhost' } 366 | const serialized = serializers.reqSerializer(req) 367 | p.strictEqual(serialized.remoteAddress, 'http://localhost') 368 | res.end() 369 | } 370 | 371 | await p.completed 372 | }) 373 | 374 | test('req.remoteAddress will be obtained from request info.remoteAddress if available', async (t) => { 375 | const p = tspl(t, { plan: 1 }) 376 | 377 | const server = http.createServer(handler) 378 | server.unref() 379 | server.listen(0, () => { 380 | http.get(server.address(), () => {}) 381 | }) 382 | 383 | t.after(() => server.close()) 384 | 385 | function handler (req, res) { 386 | req.info = { remoteAddress: 'http://localhost' } 387 | const serialized = serializers.reqSerializer(req) 388 | p.strictEqual(serialized.remoteAddress, 'http://localhost') 389 | res.end() 390 | } 391 | 392 | await p.completed 393 | }) 394 | 395 | test('req.remotePort will be obtained from request socket.remotePort as fallback', async (t) => { 396 | const p = tspl(t, { plan: 1 }) 397 | 398 | const server = http.createServer(handler) 399 | server.unref() 400 | server.listen(0, () => { 401 | http.get(server.address(), () => {}) 402 | }) 403 | 404 | t.after(() => server.close()) 405 | 406 | function handler (req, res) { 407 | req.socket = { remotePort: 3000 } 408 | const serialized = serializers.reqSerializer(req) 409 | p.strictEqual(serialized.remotePort, 3000) 410 | res.end() 411 | } 412 | 413 | await p.completed 414 | }) 415 | 416 | test('req.remotePort will be obtained from request info.remotePort if available', async (t) => { 417 | const p = tspl(t, { plan: 1 }) 418 | 419 | const server = http.createServer(handler) 420 | server.unref() 421 | server.listen(0, () => { 422 | http.get(server.address(), () => {}) 423 | }) 424 | 425 | t.after(() => server.close()) 426 | 427 | function handler (req, res) { 428 | req.info = { remotePort: 3000 } 429 | const serialized = serializers.reqSerializer(req) 430 | p.strictEqual(serialized.remotePort, 3000) 431 | res.end() 432 | } 433 | 434 | await p.completed 435 | }) 436 | 437 | test('req.query is available', async (t) => { 438 | const p = tspl(t, { plan: 1 }) 439 | 440 | const server = http.createServer(handler) 441 | server.unref() 442 | server.listen(0, () => { 443 | http.get(server.address(), () => {}) 444 | }) 445 | 446 | t.after(() => server.close()) 447 | 448 | function handler (req, res) { 449 | req.query = '/foo?bar=foobar&bar=foo' 450 | const serialized = serializers.reqSerializer(req) 451 | p.strictEqual(serialized.query, '/foo?bar=foobar&bar=foo') 452 | res.end() 453 | } 454 | 455 | await p.completed 456 | }) 457 | 458 | test('req.params is available', async (t) => { 459 | const p = tspl(t, { plan: 1 }) 460 | 461 | const server = http.createServer(handler) 462 | server.unref() 463 | server.listen(0, () => { 464 | http.get(server.address(), () => {}) 465 | }) 466 | 467 | t.after(() => server.close()) 468 | 469 | function handler (req, res) { 470 | req.params = '/foo/bar' 471 | const serialized = serializers.reqSerializer(req) 472 | p.strictEqual(serialized.params, '/foo/bar') 473 | res.end() 474 | } 475 | 476 | await p.completed 477 | }) 478 | -------------------------------------------------------------------------------- /test/res.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* eslint-disable no-prototype-builtins */ 4 | 5 | const { tspl } = require('@matteo.collina/tspl') 6 | const http = require('node:http') 7 | const { test } = require('node:test') 8 | const serializers = require('../lib/res') 9 | const { wrapResponseSerializer } = require('../') 10 | 11 | test('res.raw is not enumerable', async (t) => { 12 | const p = tspl(t, { plan: 1 }) 13 | 14 | const server = http.createServer(handler) 15 | server.unref() 16 | server.listen(0, () => { 17 | http.get(server.address(), () => {}) 18 | }) 19 | 20 | t.after(() => server.close()) 21 | 22 | function handler (_req, res) { 23 | const serialized = serializers.resSerializer(res) 24 | p.strictEqual(serialized.propertyIsEnumerable('raw'), false) 25 | res.end() 26 | } 27 | 28 | await p.completed 29 | }) 30 | 31 | test('res.raw is available', async (t) => { 32 | const p = tspl(t, { plan: 2 }) 33 | 34 | const server = http.createServer(handler) 35 | server.unref() 36 | server.listen(0, () => { 37 | http.get(server.address(), () => {}) 38 | }) 39 | 40 | t.after(() => server.close()) 41 | 42 | function handler (_req, res) { 43 | res.statusCode = 200 44 | const serialized = serializers.resSerializer(res) 45 | p.ok(serialized.raw) 46 | p.strictEqual(serialized.raw.statusCode, 200) 47 | res.end() 48 | } 49 | 50 | await p.completed 51 | }) 52 | 53 | test('can wrap response serializers', async (t) => { 54 | const p = tspl(t, { plan: 3 }) 55 | 56 | const server = http.createServer(handler) 57 | server.unref() 58 | server.listen(0, () => { 59 | http.get(server.address(), () => {}) 60 | }) 61 | 62 | t.after(() => server.close()) 63 | 64 | const serializer = wrapResponseSerializer(function (res) { 65 | p.ok(res.statusCode) 66 | p.strictEqual(res.statusCode, 200) 67 | delete res.statusCode 68 | return res 69 | }) 70 | 71 | function handler (_req, res) { 72 | res.end() 73 | res.statusCode = 200 74 | const serialized = serializer(res) 75 | p.ok(!serialized.statusCode) 76 | } 77 | 78 | await p.completed 79 | }) 80 | 81 | test('res.headers is serialized', async (t) => { 82 | const p = tspl(t, { plan: 1 }) 83 | 84 | const server = http.createServer(handler) 85 | server.unref() 86 | server.listen(0, () => { 87 | http.get(server.address(), () => {}) 88 | }) 89 | 90 | t.after(() => server.close()) 91 | 92 | function handler (_req, res) { 93 | res.setHeader('x-custom', 'y') 94 | const serialized = serializers.resSerializer(res) 95 | p.strictEqual(serialized.headers['x-custom'], 'y') 96 | res.end() 97 | } 98 | 99 | await p.completed 100 | }) 101 | 102 | test('req.url will be obtained from input request url when input request url is not an object', async (t) => { 103 | const p = tspl(t, { plan: 1 }) 104 | 105 | const server = http.createServer(handler) 106 | server.unref() 107 | server.listen(0, () => { 108 | http.get(server.address(), () => {}) 109 | }) 110 | 111 | t.after(() => server.close()) 112 | 113 | function handler (_req, res) { 114 | const serialized = serializers.resSerializer(res) 115 | p.strictEqual(serialized.statusCode, null) 116 | res.end() 117 | } 118 | 119 | await p.completed 120 | }) 121 | -------------------------------------------------------------------------------- /test/types/index.test-d.ts: -------------------------------------------------------------------------------- 1 | import {IncomingMessage, ServerResponse} from "http"; 2 | import { 3 | err, 4 | errWithCause, 5 | req, 6 | res, 7 | SerializedError, 8 | SerializedRequest, 9 | wrapErrorSerializer, 10 | wrapRequestSerializer, 11 | wrapResponseSerializer, 12 | SerializedResponse 13 | } from '../../'; 14 | 15 | const customErrorSerializer = (error: SerializedError) => { 16 | return { 17 | myOwnError: { 18 | data: `${error.type}-${error.message}\n\n${error.stack}`, 19 | } 20 | }; 21 | }; 22 | 23 | const customRequestSerializer = (req: SerializedRequest) => { 24 | const { 25 | headers, 26 | id, 27 | method, 28 | raw, 29 | remoteAddress, 30 | remotePort, 31 | url, 32 | query, 33 | params, 34 | } = req; 35 | return { 36 | myOwnRequest: { 37 | data: `${method}-${id}-${remoteAddress}-${remotePort}-${url}`, 38 | headers, 39 | raw, 40 | } 41 | }; 42 | }; 43 | 44 | const customResponseSerializer = (res: SerializedResponse) => { 45 | const {headers, raw, statusCode} = res; 46 | return { 47 | myOwnResponse: { 48 | data: statusCode, 49 | headers, 50 | raw, 51 | } 52 | }; 53 | }; 54 | 55 | const fakeError = new Error('A fake error for testing'); 56 | const serializedError: SerializedError = err(fakeError); 57 | const mySerializer = wrapErrorSerializer(customErrorSerializer); 58 | 59 | const fakeErrorWithCause = new Error('A fake error for testing with cause', { cause: new Error('An inner fake error') }); 60 | const serializedErrorWithCause: SerializedError = errWithCause(fakeError); 61 | 62 | const request: IncomingMessage = {} as IncomingMessage 63 | const serializedRequest: SerializedRequest = req(request); 64 | const myReqSerializer = wrapRequestSerializer(customRequestSerializer); 65 | 66 | const response: ServerResponse = {} as ServerResponse 67 | const myResSerializer = wrapResponseSerializer(customResponseSerializer); 68 | const serializedResponse = res(response); 69 | 70 | myResSerializer(response) 71 | 72 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": [ "es2022" ], 5 | "module": "commonjs", 6 | "noEmit": true, 7 | "strict": true 8 | }, 9 | "include": [ 10 | "./test/types/*.test-d.ts", 11 | "./index.d.ts" 12 | ] 13 | } 14 | --------------------------------------------------------------------------------