Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/modern-hairs-think.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@smartthings/core-sdk": patch
---

make trace-level logging safe even if responses have circular references
29 changes: 23 additions & 6 deletions src/endpoint-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,31 @@ export interface EndpointClientRequestOptions <T> {
dryRunReturnValue?: T
}

/**
* Create a function that can be passed to `JSON.stringify` making it work safely on objects with
* circular references.
*/
const createCircularReplacerFn = (): (key: string, value: unknown) => unknown => {
const seen = new WeakSet()
return (_key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return '[Circular]'
}
seen.add(value)
}
return value
}
}

/**
* Convert to string and scrub sensitive values like auth tokens
* Meant to be used before logging the request
*/
function scrubConfig(config: AxiosRequestConfig): string {
const scrubConfig = (config: AxiosRequestConfig): string => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { paramsSerializer: _unused, ...cleanerConfig } = config
const message = JSON.stringify(cleanerConfig)
const message = JSON.stringify(cleanerConfig, createCircularReplacerFn())
const bearerRegex = /"(Bearer [0-9a-f]{8})[0-9a-f-]{28}"/i

if (bearerRegex.test(message)) {
Expand Down Expand Up @@ -228,7 +245,7 @@ export class EndpointClient {
try {
const response = await axios.request(axiosConfig)
if (this.logger.isTraceEnabled()) {
this.logger.trace(`axios response ${response.status}: data=${JSON.stringify(response.data)}`)
this.logger.trace(`axios response ${response.status}: data=${JSON.stringify(response.data, createCircularReplacerFn())}`)
}
if (response.headers?.warning && this.config.warningLogger) {
// warningLogger allows for return of a promise or just void for flexibility
Expand All @@ -242,10 +259,10 @@ export class EndpointClient {
// https://www.npmjs.com/package/axios#handling-errors
if (error.response) {
// server responded with non-200 response code
this.logger.trace(`axios response ${error.response.status}: data=${JSON.stringify(error.response.data)}`)
this.logger.trace(`axios response ${error.response.status}: data=${JSON.stringify(error.response.data, createCircularReplacerFn())}`)
} else if (error.request) {
// server never responded
this.logger.trace(`no response from server for request ${JSON.stringify(error.request)}`)
this.logger.trace(`no response from server for request ${JSON.stringify(error.request, createCircularReplacerFn())}`)
} else {
this.logger.trace(`error making request: ${error.message}`)
}
Expand All @@ -270,7 +287,7 @@ export class EndpointClient {
}
// Annotate message with SmartThings API error data
if (error.response && error.response.data) {
error.message = error.message + ': ' + JSON.stringify(error.response.data)
error.message = error.message + ': ' + JSON.stringify(error.response.data, createCircularReplacerFn())
}
throw error
}
Expand Down
19 changes: 19 additions & 0 deletions test/unit/endpoint-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,25 @@ describe('EndpointClient', () => {
expect(traceMock).toHaveBeenCalledWith('error making request: things done broke!')
})

it('gracefully handles circular references in axios response', async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const data: any = {
status: 'ok',
objectField: {
key: 'value',
},
}
data.objectField.theFieldItself = data.objectField
mockRequest.mockResolvedValue({ status: 200, data } as AxiosResponse)
await client.request('GET', 'my/path')

expect(isTraceEnabledMock).toHaveBeenCalledTimes(1)
expect(traceMock).toHaveBeenCalledTimes(1)
expect(traceMock).toHaveBeenCalledWith(
'axios response 200: data={"status":"ok","objectField":{"key":"value","theFieldItself":"[Circular]"}}',
)
})

it('logs failed axios response with response info', async () => {
const error = Error('error message') as AxiosError
error.response = {
Expand Down