diff --git a/lib/logger/__snapshots__/err-serializer.spec.ts.snap b/lib/logger/__snapshots__/err-serializer.spec.ts.snap index fc37540fc113757fefb262a50a11b1c5a18c4141..57913961ec95722454056cd1766e3eeab18af0fe 100644 --- a/lib/logger/__snapshots__/err-serializer.spec.ts.snap +++ b/lib/logger/__snapshots__/err-serializer.spec.ts.snap @@ -4,16 +4,63 @@ exports[`logger/err-serializer expands errors 1`] = ` Object { "a": 1, "b": 2, - "body": "some response body", - "gotOptions": Object { - "auth": "test:***********", + "message": "some message", + "options": Object { "headers": Object { - "authorization": "** redacted **", + "authorization": "Bearer abc", }, }, - "message": "some message", "response": Object { "body": "some response body", + "url": "some/path", + }, +} +`; + +exports[`logger/err-serializer got handles http error 1`] = ` +Array [ + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate", + "authorization": "Basic OnRva2Vu", + "host": "github.com", + "user-agent": "https://github.com/renovatebot/renovate", + }, + "method": "POST", + "url": "https://:token@github.com/api", + }, +] +`; + +exports[`logger/err-serializer got handles http error 2`] = ` +Object { + "code": undefined, + "message": "Response code 412 (Precondition Failed)", + "name": "HTTPError", + "options": Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate", + "user-agent": "https://github.com/renovatebot/renovate", + }, + "http2": false, + "method": "POST", + "password": "***********", + "url": "https://:**redacted**@github.com/api", + "username": "", + }, + "response": Object { + "body": Object { + "err": Object { + "message": "failed", + }, + }, + "headers": Object { + "content-type": "application/json", + }, + "statusCode": 412, + "statusMessage": "Precondition Failed", }, } `; diff --git a/lib/logger/err-serializer.spec.ts b/lib/logger/err-serializer.spec.ts index c44eb76a400c6c14f89247bbd665b4e2d0713f4b..2aa9d3ff76a9b2c7cdfd59eafd53372baf0208b2 100644 --- a/lib/logger/err-serializer.spec.ts +++ b/lib/logger/err-serializer.spec.ts @@ -1,25 +1,78 @@ +import * as httpMock from '../../test/httpMock'; +import { partial } from '../../test/util'; +import * as hostRules from '../util/host-rules'; +import { Http } from '../util/http'; import configSerializer from './err-serializer'; +import { sanitizeValue } from './utils'; describe('logger/err-serializer', () => { it('expands errors', () => { - const err = { + const err = partial<Error & Record<string, unknown>>({ a: 1, b: 2, message: 'some message', response: { body: 'some response body', + url: 'some/path', }, - gotOptions: { + options: { headers: { authorization: 'Bearer abc', }, - auth: 'test:token', }, - }; + }); expect(configSerializer(err)).toMatchSnapshot(); }); it('handles missing fields', () => { - const err = { a: 1, stack: 'foo', body: 'some body' }; + const err = partial<Error & Record<string, unknown>>({ + a: 1, + stack: 'foo', + body: 'some body', + }); expect(configSerializer(err)).toMatchSnapshot(); }); + + describe('got', () => { + const baseUrl = 'https://github.com'; + + beforeEach(() => { + // reset module + jest.resetAllMocks(); + httpMock.setup(); + // clean up hostRules + hostRules.clear(); + hostRules.add({ + hostType: 'any', + baseUrl, + token: 'token', + }); + }); + afterEach(() => httpMock.reset()); + + it('handles http error', async () => { + httpMock + .scope(baseUrl) + .post('/api') + .reply(412, { err: { message: 'failed' } }); + let err: any; + try { + await new Http('any').postJson('https://:token@github.com/api'); + } catch (error) { + err = configSerializer(error); + } + + expect(httpMock.getTrace()).toMatchSnapshot(); + expect(err).toBeDefined(); + + expect(err.response.body).toBeDefined(); + expect(err.options).toBeDefined(); + + // remove platform related props + delete err.timings; + delete err.stack; + + // sanitize like Bunyan + expect(sanitizeValue(err)).toMatchSnapshot(); + }); + }); }); diff --git a/lib/logger/err-serializer.ts b/lib/logger/err-serializer.ts index b62225f9b71c5a1d07a3158633b9531199a3bb44..74056f72aeb570915ec95bbf210542fa0f914ded 100644 --- a/lib/logger/err-serializer.ts +++ b/lib/logger/err-serializer.ts @@ -1,60 +1,53 @@ import is from '@sindresorhus/is'; +import { RequestError } from 'got'; +import { clone } from '../util/clone'; Error.stackTraceLimit = 20; -interface Err { - body?: unknown; - response?: { - body?: unknown; - }; - message?: unknown; - stack?: unknown; - gotOptions?: { - auth?: unknown; - headers?: unknown; - }; -} - -export default function errSerializer(err: Err): any { - const response = { +export default function errSerializer(err: Error): any { + const response: Record<string, unknown> = { ...err, }; - if (err.body) { - response.body = err.body; - } else if (err.response?.body) { - response.body = err.response.body; - } - if (err.message) { + + // Can maybe removed? + if (!response.message && err.message) { response.message = err.message; } - if (err.stack) { + + // Can maybe removed? + if (!response.stack && err.stack) { response.stack = err.stack; } - if (response.gotOptions) { - if (is.string(response.gotOptions.auth)) { - response.gotOptions.auth = response.gotOptions.auth.replace( - /:.*/, - ':***********' - ); + + // handle got error + if (err instanceof RequestError) { + const options: Record<string, unknown> = { + headers: clone(err.options.headers), + url: err.options.url?.toString(), + }; + response.options = options; + + for (const k of ['username', 'password', 'method', 'http2']) { + options[k] = err.options[k]; } - if (err.gotOptions.headers) { - const redactedHeaders = [ - 'authorization', - 'private-header', - 'Private-header', - ]; - redactedHeaders.forEach((header) => { - if (response.gotOptions.headers[header]) { - response.gotOptions.headers[header] = '** redacted **'; - } - }); + + if (err.response) { + response.response = { + statusCode: err.response?.statusCode, + statusMessage: err.response?.statusMessage, + body: clone(err.response.body), + headers: clone(err.response.headers), + }; } } + + // already done by `sanitizeValue` ? const redactedFields = ['message', 'stack', 'stdout', 'stderr']; for (const field of redactedFields) { - if (is.string(response[field])) { - response[field] = response[field].replace( - /https:\/\/[^@]*@/g, + const val = response[field]; + if (is.string(val)) { + response[field] = val.replace( + /https:\/\/[^@]*?@/g, 'https://**redacted**@' ); } diff --git a/lib/logger/utils.ts b/lib/logger/utils.ts index af1d97cd1664226c8efd33189502b3ca6c19c1dc..50a2cdd118f8a4f0ef9c283df0f595606b802768 100644 --- a/lib/logger/utils.ts +++ b/lib/logger/utils.ts @@ -45,7 +45,7 @@ const contentFields = [ 'yarnLockParsed', ]; -function sanitizeValue(value: any, seen = new WeakMap()): any { +export function sanitizeValue(value: unknown, seen = new WeakMap()): any { if (Array.isArray(value)) { const length = value.length; const arrayResult = Array(length); @@ -71,7 +71,7 @@ function sanitizeValue(value: any, seen = new WeakMap()): any { } const objectResult: Record<string, any> = {}; - seen.set(value, objectResult); + seen.set(value as any, objectResult); for (const [key, val] of Object.entries<any>(value)) { let curValue: any; if (redactedFields.includes(key)) { @@ -89,7 +89,7 @@ function sanitizeValue(value: any, seen = new WeakMap()): any { return objectResult; } - return valueType === 'string' ? sanitize(value) : value; + return valueType === 'string' ? sanitize(value as string) : value; } type BunyanStream = (NodeJS.WritableStream | Stream) & {