Skip to content

Commit 60bf5b1

Browse files
authored
Merge pull request #2660 from bugsnag/hotfix/xhr-handle-reponse
[HOTFIX] Ensure XMLHttpRequest response types are handled gracefully
2 parents 9b687b1 + 05a9bf7 commit 60bf5b1

12 files changed

Lines changed: 234 additions & 45 deletions

File tree

.buildkite/basic/expo-pipeline.yml

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,3 @@ steps:
2525
BUGSNAG_JS_COMMIT: "${BUILDKITE_COMMIT}"
2626
# a branch name that's safe to use as a docker cache identifier
2727
BUGSNAG_JS_CACHE_SAFE_BRANCH_NAME: "${BRANCH_NAME}"
28-
29-
- label: "@bugsnag/expo v52/next"
30-
depends_on: "publish-js"
31-
trigger: "bugsnag-expo"
32-
build:
33-
branch: "v52/next"
34-
env:
35-
BUGSNAG_JS_BRANCH: "${BUILDKITE_BRANCH}"
36-
BUGSNAG_JS_COMMIT: "${BUILDKITE_COMMIT}"
37-
# a branch name that's safe to use as a docker cache identifier
38-
BUGSNAG_JS_CACHE_SAFE_BRANCH_NAME: "${BRANCH_NAME}"

dockerfiles/Dockerfile.browser

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ RUN find . -name package.json -type f -mindepth 2 -maxdepth 3 ! -path "./node_mo
5555
RUN rm -fr **/*/node_modules/
5656

5757
# The maze-runner browser tests (W3C protocol)
58-
FROM 855461928731.dkr.ecr.us-west-1.amazonaws.com/maze-runner-releases:latest-v10-cli AS browser-maze-runner
58+
FROM 855461928731.dkr.ecr.us-west-1.amazonaws.com/maze-runner-releases:v10.10.1-cli AS browser-maze-runner
5959

6060
COPY --from=browser-feature-builder /app/test/browser /app/test/browser/
6161
WORKDIR /app/test/browser

dockerfiles/Dockerfile.node

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# CI test image for unit/lint/type tests
2-
FROM node:18-alpine@sha256:974afb6cbc0314dc6502b14243b8a39fbb2d04d975e9059dd066be3e274fbb25 as node-feature-builder
2+
FROM node:18-alpine@sha256:974afb6cbc0314dc6502b14243b8a39fbb2d04d975e9059dd066be3e274fbb25 AS node-feature-builder
33

44
RUN apk add --update bash python3 make gcc g++ musl-dev xvfb-run curl
55

@@ -22,7 +22,7 @@ RUN npm pack --verbose packages/plugin-restify/
2222
RUN npm pack --verbose packages/plugin-hono/
2323

2424
# The maze-runner node tests
25-
FROM 855461928731.dkr.ecr.us-west-1.amazonaws.com/maze-runner-releases:latest-v10-cli as node-maze-runner
25+
FROM 855461928731.dkr.ecr.us-west-1.amazonaws.com/maze-runner-releases:v10.10.1-cli AS node-maze-runner
2626
WORKDIR /app/
2727
COPY packages/node/ .
2828
COPY test/node/features test/node/features

packages/plugin-network-instrumentation/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ Bugsnag.start({
2020
apiKey: 'YOUR_API_KEY_HERE',
2121
plugins: [BugsnagPluginNetworkInstrumentation({
2222
httpErrorCodes = [400, 401, { min: 450: max 499 }], // Status codes to report as errors
23-
maxRequestSize = 20_000, // Truncate the request and response body over this size (in kb)
23+
maxRequestSize = 20_000, // Truncate the request body over this size (in bytes) defaults to 0 (nothing is captured)
24+
maxResponseSize = 20_000, // Truncate the response body over this size (in bytes) defaults to 0 (nothing is captured)
2425
onHttpError: ({ request, response }) => {
2526
request.headers['x-custom-header'] = 'value' // Modify any request values before sending
2627
response.body = 'custom body' // Modify any response values before sending

packages/plugin-network-instrumentation/network-instrumentation.js

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ const shouldCaptureStatusCode = require('./lib/should-capture-status-code')
1010
const truncate = require('./lib/truncate')
1111

1212
const DEFAULT_HTTP_ERROR_CODES = [{ min: 400, max: 599 }]
13-
const DEFAULT_MAX_REQUEST_SIZE = 5000
13+
const DEFAULT_MAX_RESPONSE_SIZE = 0
14+
const DEFAULT_MAX_REQUEST_SIZE = 0
1415

1516
/**
1617
* Creates the HTTP errors plugin with configuration
@@ -24,6 +25,7 @@ const DEFAULT_MAX_REQUEST_SIZE = 5000
2425
module.exports = (config = {}, global = window) => {
2526
const {
2627
httpErrorCodes = DEFAULT_HTTP_ERROR_CODES,
28+
maxResponseSize = DEFAULT_MAX_RESPONSE_SIZE,
2729
maxRequestSize = DEFAULT_MAX_REQUEST_SIZE,
2830
onHttpError
2931
} = config
@@ -92,9 +94,7 @@ module.exports = (config = {}, global = window) => {
9294
url: startContext.url,
9395
httpMethod: startContext.method,
9496
headers: startContext.headers,
95-
params: requestParams,
96-
body: startContext.body,
97-
bodyLength: startContext.body ? startContext.body.length : undefined
97+
params: requestParams
9898
}
9999
const responseObj = {
100100
statusCode: endContext.status,
@@ -114,13 +114,15 @@ module.exports = (config = {}, global = window) => {
114114
}
115115

116116
// Truncate request body
117-
if (requestObj.body) {
118-
requestObj.body = truncate(requestObj.body, maxRequestSize)
117+
if (maxRequestSize > 0 && startContext.body) {
118+
requestObj.body = truncate(startContext.body, maxRequestSize)
119+
requestObj.bodyLength = startContext.body.length
119120
}
120121

121122
// Truncate response body - XHR only
122-
if (responseObj.body) {
123-
responseObj.body = truncate(responseObj.body, maxRequestSize)
123+
if (maxResponseSize > 0 && endContext.body) {
124+
responseObj.body = truncate(endContext.body, maxResponseSize)
125+
responseObj.bodyLength = endContext.body.length
124126
}
125127

126128
// Strip query parameters from URL

packages/plugin-network-instrumentation/test/network-instrumentation-xhr.test.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,8 @@ describe('plugin-network-instrumentation', () => {
2020
status: number | null
2121
statusText: string
2222
responseURL: string
23-
response: string
24-
responseText: string
25-
responseType: string
23+
response: typeof XMLHttpRequest.prototype.response
24+
responseType: typeof XMLHttpRequest.prototype.responseType
2625
_method: string
2726
_url: string
2827
_requestHeaders: Headers
@@ -34,7 +33,6 @@ describe('plugin-network-instrumentation', () => {
3433
this.statusText = ''
3534
this.responseURL = ''
3635
this.response = ''
37-
this.responseText = ''
3836
this.responseType = ''
3937
this._method = 'GET'
4038
this._url = ''
@@ -102,7 +100,9 @@ describe('plugin-network-instrumentation', () => {
102100
const notifyCallbacks: Event[] = []
103101

104102
plugin = createPlugin({
105-
httpErrorCodes: { min: 400, max: 499 }
103+
httpErrorCodes: { min: 400, max: 499 },
104+
maxRequestSize: 1000,
105+
maxResponseSize: 1000
106106
})
107107

108108
const client = new Client({ apiKey: 'api_key', plugins: [plugin] })
@@ -114,7 +114,7 @@ describe('plugin-network-instrumentation', () => {
114114
xhr.statusText = 'Not Found'
115115
xhr.responseURL = 'https://api.example.com/users/123'
116116
xhr.response = '{"error": "User not found", "code": "USER_NOT_FOUND"}'
117-
xhr.responseText = '{"error": "User not found", "code": "USER_NOT_FOUND"}'
117+
xhr.responseType = 'json'
118118

119119
// Simulate an XHR request
120120
xhr.open('POST', 'https://api.example.com/users/123')
@@ -143,22 +143,22 @@ describe('plugin-network-instrumentation', () => {
143143
expect(event.request.httpMethod).toBe('POST')
144144
expect(event.request.body).toBe(requestBody)
145145
expect(event.request.bodyLength).toBe(requestBody.length)
146-
// expect(event.request.headers?.['content-type']).toBe('application/json')
146+
expect(event.request.headers).toStrictEqual({ 'Content-Type': 'application/json' })
147147

148148
// Verify response metadata including body
149149
expect(event.response.statusCode).toBe(404)
150150
expect(event.response.headers['content-type']).toBe('application/json')
151151
expect(event.response.headers['content-length']).toBe('45')
152-
expect(event.response.body).toBe('{"error": "User not found", "code": "USER_NOT_FOUND"}')
153-
expect(event.response.bodyLength).toBe(xhr.responseText.length)
152+
expect(event.response.body).toBe(JSON.stringify(xhr.response))
153+
expect(event.response.bodyLength).toBe(JSON.stringify(xhr.response).length)
154154
})
155155

156-
it('should truncate XHR response body when it exceeds maxRequestSize', async () => {
156+
it('should truncate XHR response body when it exceeds maxResponseSize', async () => {
157157
const notifyCallbacks: Event[] = []
158158

159159
plugin = createPlugin({
160160
httpErrorCodes: { min: 400, max: 499 },
161-
maxRequestSize: 20
161+
maxResponseSize: 20
162162
})
163163

164164
const client = new Client({ apiKey: 'api_key', plugins: [plugin] })
@@ -171,7 +171,6 @@ describe('plugin-network-instrumentation', () => {
171171
xhr.statusText = 'Internal Server Error'
172172
xhr.responseURL = 'https://api.example.com/error'
173173
xhr.response = largeResponseBody
174-
xhr.responseText = largeResponseBody
175174

176175
xhr.open('GET', 'https://api.example.com/error')
177176
xhr.send()
@@ -225,7 +224,6 @@ describe('plugin-network-instrumentation', () => {
225224
xhr.status = 403
226225
xhr.statusText = 'Forbidden'
227226
xhr.response = 'Forbidden'
228-
xhr.responseText = 'Forbidden'
229227

230228
xhr.open('GET', 'https://api.example.com/data?userId=42')
231229
xhr.send()

packages/plugin-network-instrumentation/test/network-instrumentation.test.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ describe('plugin-network-instrumentation', () => {
282282
expect(requestMetadata.bodyLength).toBe(100)
283283
})
284284

285-
it('should use default maxRequestSize of 5000 when not specified', async () => {
285+
it('should use default maxRequestSize of 0 when not specified', async () => {
286286
const notifyCallbacks: Event[] = []
287287

288288
plugin = createPlugin({
@@ -311,8 +311,8 @@ describe('plugin-network-instrumentation', () => {
311311
const event = notifyCallbacks[0]
312312
const requestMetadata = event.request
313313

314-
expect(requestMetadata.body.length).toBeLessThanOrEqual(5000)
315-
expect(requestMetadata.bodyLength).toBe(10000)
314+
expect(requestMetadata.body).toBeUndefined()
315+
expect(requestMetadata.bodyLength).toBeUndefined()
316316
})
317317
})
318318

@@ -528,7 +528,9 @@ describe('plugin-network-instrumentation', () => {
528528
const notifyCallbacks: Event[] = []
529529

530530
plugin = createPlugin({
531-
httpErrorCodes: { min: 400, max: 499 }
531+
httpErrorCodes: { min: 400, max: 499 },
532+
maxRequestSize: 1000,
533+
maxResponseSize: 1000
532534
})
533535

534536
const client = new Client({ apiKey: 'api_key', plugins: [plugin] })

packages/plugin-network-instrumentation/types/bugsnag-plugin-network-instrumentation.d.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,21 @@ export interface BugsnagPluginHttpErrorsConfiguration {
2525
httpErrorCodes?: number | HttpErrorRange | Array<number | HttpErrorRange>
2626

2727
/**
28-
* Maximum size of the request body to capture (in characters)
29-
* @default 5000
28+
* Maximum size in bytes of the request body to capture
29+
* Disabled as default
30+
* @default 0
3031
*/
3132
maxRequestSize?: number
3233

34+
/**
35+
* Maximum size in bytes of the response body to capture
36+
* Does not capture streaming responses, such as a
37+
* fetch request with a ReadableStream body
38+
* Disabled as default
39+
* @default 0
40+
*/
41+
maxResponseSize?: number
42+
3343
/**
3444
* Callback function to intercept HTTP errors before they are reported.
3545
* Return false to prevent the error from being reported.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* Receives the XHR response and parses it based on the responseType
3+
* @param {XMLHttpRequest} xhr The XHR instance
4+
* @returns {string | undefined} The parsed response
5+
*/
6+
module.exports = function xhrResponseParser ({ response, responseType }) {
7+
if (response === null || response === undefined) {
8+
return undefined
9+
}
10+
11+
switch (responseType) {
12+
case 'arraybuffer':
13+
case 'blob':
14+
return '[Binary Data]'
15+
case 'document':
16+
return '[Document]'
17+
case 'json':
18+
try {
19+
return JSON.stringify(response)
20+
} catch (e) {
21+
return '[Unserializable JSON]'
22+
}
23+
case 'text':
24+
case '':
25+
default:
26+
return String(response)
27+
}
28+
}

packages/request-tracker/lib/xhr-tracker.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const RequestTracker = require('./request-tracker')
22
const xhrHeaderStringToObject = require('./xhr-header-string-to-object')
3+
const xhrResponseParser = require('./xhr-response-parser')
34

45
/**
56
* Create XHR request tracker with singleton pattern
@@ -72,7 +73,7 @@ function createXhrTracker (global, options = {}) {
7273
status: this.status,
7374
state: 'success',
7475
headers: getResponseHeaders(),
75-
body: this.responseText
76+
body: xhrResponseParser(this)
7677
})
7778
}
7879

@@ -81,7 +82,7 @@ function createXhrTracker (global, options = {}) {
8182
endTime: Date.now(),
8283
state: 'error',
8384
headers: getResponseHeaders(),
84-
body: this.responseText
85+
body: xhrResponseParser(this)
8586
})
8687
}
8788

0 commit comments

Comments
 (0)