Skip to content

Commit 674e3f9

Browse files
committed
fix: use addEventListener for reliable response capture
The previous implementation overrode onreadystatechange directly, which failed when handlers were set after send() was called. This fix uses addEventListener('readystatechange', ...) instead, which reliably captures responses regardless of when other handlers are attached.
1 parent 1ace166 commit 674e3f9

1 file changed

Lines changed: 39 additions & 29 deletions

File tree

src/XHRInterceptor.ts

Lines changed: 39 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ declare class XMLHttpRequest {
2828
setRequestHeader(header: string, value: string): void;
2929
getResponseHeader(name: string): string | null;
3030
getAllResponseHeaders(): string;
31+
addEventListener(type: string, listener: (this: XMLHttpRequest, ev: any) => any): void;
3132
}
3233

3334
// Callback types use 'any' to match React Native's XHRInterceptor API
@@ -85,6 +86,12 @@ function enableInterception(): void {
8586
username?: string | null,
8687
password?: string | null
8788
): void {
89+
const xhr = this as any;
90+
xhr._interception = {
91+
method,
92+
url: url.toString(),
93+
};
94+
8895
openCallback(method, url.toString(), this);
8996

9097
return originalXHROpen!.call(
@@ -113,12 +120,14 @@ function enableInterception(): void {
113120
const dataString = body === null || body === undefined ? '' : String(body);
114121
sendCallback(dataString, xhr);
115122

116-
// Listen for state changes to capture headers and response
117-
const originalOnReadyStateChange = this.onreadystatechange;
118-
this.onreadystatechange = function (event: any) {
123+
// Use addEventListener which is more reliable than overriding onreadystatechange
124+
// This works even when handlers are set after send() is called
125+
this.addEventListener('readystatechange', function () {
119126
if (this.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
120-
if (!xhr._hasCalledHeaderReceived) {
121-
xhr._hasCalledHeaderReceived = true;
127+
if (!xhr._interception?.hasCalledHeaderReceived) {
128+
if (xhr._interception) {
129+
xhr._interception.hasCalledHeaderReceived = true;
130+
}
122131
const contentType = this.getResponseHeader('content-type') || '';
123132
const contentLength = this.getResponseHeader('content-length');
124133
const responseSize = contentLength ? parseInt(contentLength, 10) : 0;
@@ -134,33 +143,34 @@ function enableInterception(): void {
134143
}
135144

136145
if (this.readyState === XMLHttpRequest.DONE) {
137-
let responseData = '';
138-
if (this.responseType === '' || this.responseType === 'text') {
139-
responseData = this.responseText || '';
140-
} else if (this.responseType === 'json' && this.response) {
141-
try {
142-
responseData = JSON.stringify(this.response);
143-
} catch {
144-
responseData = '[Unable to stringify response]';
146+
if (!xhr._interception?.hasCalledResponse) {
147+
if (xhr._interception) {
148+
xhr._interception.hasCalledResponse = true;
149+
}
150+
let responseData = '';
151+
if (this.responseType === '' || this.responseType === 'text') {
152+
responseData = this.responseText || '';
153+
} else if (this.responseType === 'json' && this.response) {
154+
try {
155+
responseData = JSON.stringify(this.response);
156+
} catch {
157+
responseData = '[Unable to stringify response]';
158+
}
159+
} else if (this.response) {
160+
responseData = '[Non-text response]';
145161
}
146-
} else if (this.response) {
147-
responseData = '[Non-text response]';
148-
}
149-
150-
responseCallback(
151-
this.status,
152-
this.timeout,
153-
responseData,
154-
this.responseURL,
155-
this.responseType,
156-
xhr
157-
);
158-
}
159162

160-
if (originalOnReadyStateChange) {
161-
originalOnReadyStateChange.call(this, event);
163+
responseCallback(
164+
this.status,
165+
this.timeout,
166+
responseData,
167+
this.responseURL,
168+
this.responseType,
169+
xhr
170+
);
171+
}
162172
}
163-
};
173+
});
164174

165175
return originalXHRSend!.call(this, body);
166176
};

0 commit comments

Comments
 (0)