diff --git a/lib/chain-error.ts b/lib/chain-error.ts new file mode 100644 index 000000000..219b7eb01 --- /dev/null +++ b/lib/chain-error.ts @@ -0,0 +1,33 @@ +/** + * (C) Copyright IBM Corp. 2023. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Given two Error instances 'error' and 'causedBy', this function will + * update 'error' by chaining 'causedBy' to it. + * Specifically, 'causedBy''s message and stack will be appended + * to 'error''s message and stack, respectively, to simulate chained Errors. + * + * @param error the Error object to be updated + * @param causedBy an Error object that represents the cause of 'error' + * @returns 'error' after updating its message and stack fields + */ +export function chainError(error: Error, causedBy: Error): Error { + error.message += ` ${causedBy.toString()}`; + if (causedBy.stack) { + error.stack += `\nCaused by: ${causedBy.stack}`; + } + return error; +} diff --git a/lib/request-wrapper.ts b/lib/request-wrapper.ts index 6712cc59c..eedec22ae 100644 --- a/lib/request-wrapper.ts +++ b/lib/request-wrapper.ts @@ -36,6 +36,7 @@ import { import logger from './logger'; import { streamToPromise } from './stream-to-promise'; import { CookieInterceptor } from './cookie-support'; +import { chainError } from './chain-error'; /** * Retry configuration options. @@ -577,9 +578,9 @@ function ensureJSONResponseBodyIsObject(response: any): any | string { try { dataAsObject = JSON.parse(response.data); } catch (e) { - logger.error('Response body was supposed to have JSON content but JSON parsing failed.'); - logger.error(`Malformed JSON string: ${response.data}`); - throw e; + logger.verbose('Response body was supposed to have JSON content but JSON parsing failed.'); + logger.verbose(`Malformed JSON string: ${response.data}`); + throw chainError(new Error('Error processing HTTP response:'), e); } return dataAsObject; diff --git a/test/unit/request-wrapper.test.js b/test/unit/request-wrapper.test.js index c41637c43..e074786f3 100644 --- a/test/unit/request-wrapper.test.js +++ b/test/unit/request-wrapper.test.js @@ -59,6 +59,7 @@ const requestWrapperInstance = new RequestWrapper(); const warnLogSpy = jest.spyOn(logger, 'warn').mockImplementation(() => {}); const errorLogSpy = jest.spyOn(logger, 'error').mockImplementation(() => {}); const debugLogSpy = jest.spyOn(logger, 'debug').mockImplementation(() => {}); +const verboseLogSpy = jest.spyOn(logger, 'verbose').mockImplementation(() => {}); describe('axios', () => { let env; @@ -706,14 +707,14 @@ describe('sendRequest', () => { mockAxiosInstance.mockResolvedValue(axiosResolveValue); await expect(requestWrapperInstance.sendRequest(parameters)).rejects.toThrow( - 'Unexpected end of JSON input' + 'Error processing HTTP response: SyntaxError: Unexpected end of JSON input' ); - expect(errorLogSpy).toHaveBeenCalledTimes(2); - expect(errorLogSpy.mock.calls[0][0]).toBe( + expect(verboseLogSpy).toHaveBeenCalledTimes(2); + expect(verboseLogSpy.mock.calls[0][0]).toBe( 'Response body was supposed to have JSON content but JSON parsing failed.' ); - expect(errorLogSpy.mock.calls[1][0]).toBe('Malformed JSON string: {"key": "value"'); - errorLogSpy.mockClear(); + expect(verboseLogSpy.mock.calls[1][0]).toBe('Malformed JSON string: {"key": "value"'); + verboseLogSpy.mockClear(); }); // Need to rewrite this to test instantiation with userOptions @@ -754,6 +755,7 @@ describe('formatError', () => { warnLogSpy.mockClear(); errorLogSpy.mockClear(); debugLogSpy.mockClear(); + verboseLogSpy.mockClear(); }); const basicAxiosError = { @@ -967,12 +969,12 @@ describe('formatError', () => { expect(() => { requestWrapperInstance.formatError(newAxiosError); - }).toThrow('Unexpected end of JSON input'); - expect(errorLogSpy).toHaveBeenCalledTimes(2); - expect(errorLogSpy.mock.calls[0][0]).toBe( + }).toThrow('Error processing HTTP response: SyntaxError: Unexpected end of JSON input'); + expect(verboseLogSpy).toHaveBeenCalledTimes(2); + expect(verboseLogSpy.mock.calls[0][0]).toBe( 'Response body was supposed to have JSON content but JSON parsing failed.' ); - expect(errorLogSpy.mock.calls[1][0]).toBe( + expect(verboseLogSpy.mock.calls[1][0]).toBe( 'Malformed JSON string: { "errorMessage": "some error"' ); }); @@ -992,6 +994,7 @@ describe('gzipRequestBody', () => { gzipSpy.mockClear(); errorLogSpy.mockClear(); debugLogSpy.mockClear(); + verboseLogSpy.mockClear(); }); it('should return unaltered data if encoding header is already set to gzip', async () => {