Skip to content

Commit 84bfaf0

Browse files
committed
👌 Merge the two readReponseFunction
1 parent f803b7b commit 84bfaf0

File tree

5 files changed

+365
-93
lines changed

5 files changed

+365
-93
lines changed

‎packages/core/src/index.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,8 @@ export { CustomerDataType, CustomerContextKey, ContextManagerMethod } from './do
131131
export type { ValueHistory, ValueHistoryEntry } from './tools/valueHistory'
132132
export { createValueHistory, CLEAR_OLD_VALUES_INTERVAL } from './tools/valueHistory'
133133
export { readBytesFromStream } from './tools/readBytesFromStream'
134+
export type { ReadResponseBodyResult, ReadResponseBodyOptions, RequestContext } from './tools/readResponseBody'
135+
export { readResponseBody } from './tools/readResponseBody'
134136
export type { SessionState } from './domain/session/sessionState'
135137
export { STORAGE_POLL_DELAY } from './domain/session/sessionStore'
136138
export { SESSION_STORE_KEY } from './domain/session/storeStrategies/sessionStoreStrategy'
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import { MockResponse } from '../../test'
2+
import { readResponseBody } from './readResponseBody'
3+
4+
describe('readResponseBody', () => {
5+
describe('for XHR requests', () => {
6+
it('should read string response from XHR', (done) => {
7+
const xhr = { response: 'test response' } as XMLHttpRequest
8+
9+
readResponseBody(
10+
{ xhr },
11+
(result) => {
12+
expect(result.body).toBe('test response')
13+
expect(result.limitExceeded).toBe(false)
14+
done()
15+
},
16+
{}
17+
)
18+
})
19+
20+
it('should return non-string XHR response as-is', (done) => {
21+
const xhr = { response: { foo: 'bar' } } as XMLHttpRequest
22+
23+
readResponseBody(
24+
{ xhr },
25+
(result) => {
26+
expect(result.body).toEqual({ foo: 'bar' })
27+
expect(result.limitExceeded).toBe(false)
28+
done()
29+
},
30+
{}
31+
)
32+
})
33+
34+
it('should truncate XHR response when bytesLimit is specified', (done) => {
35+
const xhr = { response: 'Lorem ipsum dolor sit amet orci aliquam.' } as XMLHttpRequest
36+
37+
readResponseBody(
38+
{ xhr },
39+
(result) => {
40+
expect(result.body).toBe('Lorem ipsum dolor sit amet orci ...')
41+
expect(result.limitExceeded).toBe(true)
42+
done()
43+
},
44+
{ bytesLimit: 32 }
45+
)
46+
})
47+
48+
it('should not truncate XHR response when size equals limit', (done) => {
49+
const text = 'foo'
50+
const xhr = { response: text } as XMLHttpRequest
51+
52+
readResponseBody(
53+
{ xhr },
54+
(result) => {
55+
expect(result.body).toBe(text)
56+
expect(result.limitExceeded).toBe(false)
57+
done()
58+
},
59+
{ bytesLimit: text.length }
60+
)
61+
})
62+
63+
it('should return undefined body when XHR is not present', (done) => {
64+
readResponseBody(
65+
{},
66+
(result) => {
67+
expect(result.body).toBeUndefined()
68+
expect(result.limitExceeded).toBe(false)
69+
done()
70+
},
71+
{}
72+
)
73+
})
74+
})
75+
76+
describe('for Fetch requests', () => {
77+
it('should read response text from Fetch', (done) => {
78+
const response = new MockResponse({ responseText: 'fetch response' })
79+
80+
readResponseBody(
81+
{ response },
82+
(result) => {
83+
expect(result.body).toBe('fetch response')
84+
expect(result.limitExceeded).toBe(false)
85+
done()
86+
},
87+
{}
88+
)
89+
})
90+
91+
it('should return undefined when response body is null', (done) => {
92+
const response = new MockResponse({})
93+
94+
readResponseBody(
95+
{ response },
96+
(result) => {
97+
expect(result.body).toBeUndefined()
98+
expect(result.limitExceeded).toBe(false)
99+
done()
100+
},
101+
{}
102+
)
103+
})
104+
105+
it('should return undefined when response body is already used', (done) => {
106+
const response = new MockResponse({ bodyUsed: true })
107+
108+
readResponseBody(
109+
{ response },
110+
(result) => {
111+
expect(result.body).toBeUndefined()
112+
expect(result.limitExceeded).toBe(false)
113+
done()
114+
},
115+
{}
116+
)
117+
})
118+
119+
it('should return undefined when response body is disturbed', (done) => {
120+
const response = new MockResponse({ bodyDisturbed: true })
121+
122+
readResponseBody(
123+
{ response },
124+
(result) => {
125+
expect(result.body).toBeUndefined()
126+
expect(result.limitExceeded).toBe(false)
127+
done()
128+
},
129+
{}
130+
)
131+
})
132+
133+
it('should not consume the original response body', (done) => {
134+
const response = new MockResponse({ responseText: 'test' })
135+
136+
readResponseBody(
137+
{ response },
138+
() => {
139+
expect(response.bodyUsed).toBe(false)
140+
done()
141+
},
142+
{}
143+
)
144+
})
145+
146+
it('should truncate fetch response when bytesLimit is specified', (done) => {
147+
const text = 'Lorem ipsum dolor sit amet'
148+
const response = new MockResponse({ responseText: text })
149+
150+
readResponseBody(
151+
{ response },
152+
(result) => {
153+
expect(result.body).toBe('Lorem ipsu...')
154+
expect(result.limitExceeded).toBe(true)
155+
done()
156+
},
157+
{ bytesLimit: 10 }
158+
)
159+
})
160+
161+
it('should not truncate when size equals limit', (done) => {
162+
const text = 'foo'
163+
const response = new MockResponse({ responseText: text })
164+
165+
readResponseBody(
166+
{ response },
167+
(result) => {
168+
expect(result.body).toBe(text)
169+
expect(result.limitExceeded).toBe(false)
170+
done()
171+
},
172+
{ bytesLimit: text.length }
173+
)
174+
})
175+
176+
it('should handle error reading from response stream', (done) => {
177+
const response = new MockResponse({ responseTextError: new Error('locked') })
178+
179+
readResponseBody(
180+
{ response },
181+
(result) => {
182+
expect(result.body).toBeUndefined()
183+
expect(result.error).toEqual(jasmine.any(Error))
184+
expect(result.limitExceeded).toBe(false)
185+
done()
186+
},
187+
{}
188+
)
189+
})
190+
})
191+
})
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { tryToClone } from './utils/responseUtils'
2+
import { readBytesFromStream } from './readBytesFromStream'
3+
import { monitor } from './monitor'
4+
5+
export interface ReadResponseBodyResult {
6+
body?: unknown
7+
limitExceeded: boolean
8+
error?: Error
9+
}
10+
11+
export interface ReadResponseBodyOptions {
12+
bytesLimit?: number
13+
}
14+
15+
export interface RequestContext {
16+
xhr?: XMLHttpRequest
17+
response?: Response
18+
}
19+
20+
/**
21+
* Reads the response body from an XHR or Fetch request context.
22+
* For XHR requests, reads directly from xhr.response.
23+
* For Fetch requests, clones the response and reads from the stream.
24+
* Optionally truncates the response text if bytesLimit is specified.
25+
*/
26+
export function readResponseBody(
27+
request: RequestContext,
28+
callback: (result: ReadResponseBodyResult) => void,
29+
options: ReadResponseBodyOptions
30+
) {
31+
if (request.xhr) {
32+
readXhrResponseBody(request.xhr, callback, options)
33+
} else if (request.response) {
34+
readFetchResponseBody(request.response, callback, options)
35+
} else {
36+
callback({ body: undefined, limitExceeded: false })
37+
}
38+
}
39+
40+
function readXhrResponseBody(
41+
xhr: XMLHttpRequest,
42+
callback: (result: ReadResponseBodyResult) => void,
43+
options: ReadResponseBodyOptions
44+
) {
45+
if (typeof xhr.response === 'string') {
46+
const truncated = truncateText(xhr.response, options.bytesLimit)
47+
callback({
48+
body: truncated.text,
49+
limitExceeded: truncated.limitExceeded,
50+
})
51+
} else {
52+
callback({
53+
body: xhr.response,
54+
limitExceeded: false,
55+
})
56+
}
57+
}
58+
59+
function readFetchResponseBody(
60+
response: Response,
61+
callback: (result: ReadResponseBodyResult) => void,
62+
options: ReadResponseBodyOptions
63+
) {
64+
const clonedResponse = tryToClone(response)
65+
if (!clonedResponse || !clonedResponse.body) {
66+
callback({ body: undefined, limitExceeded: false })
67+
return
68+
}
69+
70+
// Legacy Edge support: use response.text() instead of reading the stream
71+
if (!window.TextDecoder) {
72+
clonedResponse.text().then(
73+
monitor((text) => {
74+
const truncated = truncateText(text, options.bytesLimit)
75+
callback({
76+
body: truncated.text,
77+
limitExceeded: truncated.limitExceeded,
78+
})
79+
}),
80+
monitor((error) => {
81+
callback({
82+
body: undefined,
83+
limitExceeded: false,
84+
error,
85+
})
86+
})
87+
)
88+
return
89+
}
90+
91+
const bytesLimit = options.bytesLimit ?? Number.POSITIVE_INFINITY
92+
readBytesFromStream(
93+
clonedResponse.body,
94+
(error, bytes, limitExceeded) => {
95+
if (error) {
96+
callback({
97+
body: undefined,
98+
limitExceeded: false,
99+
error,
100+
})
101+
return
102+
}
103+
104+
if (!bytes) {
105+
callback({ body: undefined, limitExceeded: false })
106+
return
107+
}
108+
109+
let responseText = new TextDecoder().decode(bytes)
110+
if (limitExceeded) {
111+
responseText += '...'
112+
}
113+
114+
callback({
115+
body: responseText,
116+
limitExceeded: limitExceeded || false,
117+
})
118+
},
119+
{
120+
bytesLimit,
121+
collectStreamBody: true,
122+
}
123+
)
124+
}
125+
126+
function truncateText(text: string, bytesLimit?: number): { text: string; limitExceeded: boolean } {
127+
if (bytesLimit === undefined || text.length <= bytesLimit) {
128+
return { text, limitExceeded: false }
129+
}
130+
131+
return {
132+
text: `${text.substring(0, bytesLimit)}...`,
133+
limitExceeded: true,
134+
}
135+
}

0 commit comments

Comments
 (0)