Skip to content

Commit 5a3f814

Browse files
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. fix: implement custom XHRInterceptor to avoid RN private API warnings Replace dependency on React Native's internal XHRInterceptor with a custom implementation that directly patches XMLHttpRequest prototype. This fixes the deprecation warning in React Native 0.80+: "Deep imports from the 'react-native' package are deprecated" The new implementation: - Patches XMLHttpRequest.prototype.open, send, setRequestHeader - Captures response headers and body via onreadystatechange - Properly restores original methods when interception is disabled - Maintains the same callback API for compatibility with Logger.ts - Works across all React Native versions without version-specific paths
1 parent 19f7b85 commit 5a3f814

1 file changed

Lines changed: 221 additions & 26 deletions

File tree

src/XHRInterceptor.ts

Lines changed: 221 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,227 @@
1-
type XHRInterceptorModule = {
2-
isInterceptorEnabled: () => boolean;
3-
setOpenCallback: (...props: any[]) => void;
4-
setRequestHeaderCallback: (...props: any[]) => void;
5-
setSendCallback: (...props: any[]) => void;
6-
setHeaderReceivedCallback: (...props: any[]) => void;
7-
setResponseCallback: (...props: any[]) => void;
8-
enableInterception: () => void;
9-
disableInterception: () => void;
10-
};
1+
// Type declarations for globals provided by React Native runtime
2+
declare class URL {
3+
constructor(url: string, base?: string);
4+
toString(): string;
5+
}
6+
7+
declare class XMLHttpRequest {
8+
static readonly UNSENT: number;
9+
static readonly OPENED: number;
10+
static readonly HEADERS_RECEIVED: number;
11+
static readonly LOADING: number;
12+
static readonly DONE: number;
13+
14+
readonly readyState: number;
15+
readonly response: any;
16+
readonly responseText: string;
17+
responseType: string;
18+
readonly responseURL: string;
19+
readonly status: number;
20+
timeout: number;
21+
22+
onreadystatechange: ((this: XMLHttpRequest, ev: any) => any) | null;
23+
24+
open(method: string, url: string | URL, async?: boolean, username?: string | null, password?: string | null): void;
25+
send(body?: any): void;
26+
setRequestHeader(header: string, value: string): void;
27+
getResponseHeader(name: string): string | null;
28+
getAllResponseHeaders(): string;
29+
addEventListener(type: string, listener: (this: XMLHttpRequest, ev: any) => any): void;
30+
}
31+
32+
// Callback types use 'any' to match React Native's XHRInterceptor API
33+
// This allows Logger.ts to use its own types (RequestMethod, XHR, etc.)
34+
type OpenCallback = (...args: any[]) => void;
35+
type RequestHeaderCallback = (...args: any[]) => void;
36+
type SendCallback = (...args: any[]) => void;
37+
type HeaderReceivedCallback = (...args: any[]) => void;
38+
type ResponseCallback = (...args: any[]) => void;
39+
40+
// Store original XMLHttpRequest methods
41+
let originalXHROpen: typeof XMLHttpRequest.prototype.open | null = null;
42+
let originalXHRSend: typeof XMLHttpRequest.prototype.send | null = null;
43+
let originalXHRSetRequestHeader: typeof XMLHttpRequest.prototype.setRequestHeader | null =
44+
null;
45+
46+
// Callbacks
47+
let openCallback: OpenCallback = () => {};
48+
let requestHeaderCallback: RequestHeaderCallback = () => {};
49+
let sendCallback: SendCallback = () => {};
50+
let headerReceivedCallback: HeaderReceivedCallback = () => {};
51+
let responseCallback: ResponseCallback = () => {};
1152

12-
let XHRInterceptor: XHRInterceptorModule;
13-
try {
14-
// new location for React Native 0.80+
15-
const module = require('react-native/src/private/devsupport/devmenu/elementinspector/XHRInterceptor');
16-
XHRInterceptor = module.default ?? module;
17-
} catch {
18-
try {
19-
// new location for React Native 0.79+
20-
const module = require('react-native/src/private/inspector/XHRInterceptor');
21-
XHRInterceptor = module.default ?? module;
22-
} catch {
23-
try {
24-
const module = require('react-native/Libraries/Network/XHRInterceptor');
25-
XHRInterceptor = module.default ?? module;
26-
} catch {
27-
throw new Error('XHRInterceptor could not be found in either location');
53+
let isInterceptorEnabled = false;
54+
55+
function parseResponseHeaders(headersString: string): Record<string, string> {
56+
const headers: Record<string, string> = {};
57+
if (!headersString) return headers;
58+
59+
const headerLines = headersString.trim().split('\r\n');
60+
for (const line of headerLines) {
61+
const index = line.indexOf(':');
62+
if (index > 0) {
63+
const key = line.substring(0, index).trim();
64+
const value = line.substring(index + 1).trim();
65+
headers[key] = value;
2866
}
2967
}
68+
return headers;
69+
}
70+
71+
function enableInterception(): void {
72+
if (isInterceptorEnabled) return;
73+
74+
// Store original methods
75+
originalXHROpen = XMLHttpRequest.prototype.open;
76+
originalXHRSend = XMLHttpRequest.prototype.send;
77+
originalXHRSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
78+
79+
// Override open
80+
XMLHttpRequest.prototype.open = function (
81+
method: string,
82+
url: string | URL,
83+
async: boolean = true,
84+
username?: string | null,
85+
password?: string | null
86+
): void {
87+
const xhr = this as any;
88+
xhr._interception = {
89+
method,
90+
url: url.toString(),
91+
};
92+
93+
openCallback(method, url.toString(), this);
94+
95+
return originalXHROpen!.call(
96+
this,
97+
method,
98+
url,
99+
async,
100+
username ?? null,
101+
password ?? null
102+
);
103+
};
104+
105+
// Override setRequestHeader
106+
XMLHttpRequest.prototype.setRequestHeader = function (
107+
header: string,
108+
value: string
109+
): void {
110+
requestHeaderCallback(header, value, this);
111+
return originalXHRSetRequestHeader!.call(this, header, value);
112+
};
113+
114+
// Override send
115+
XMLHttpRequest.prototype.send = function (body?: any): void {
116+
const xhr = this as any;
117+
118+
const dataString = body === null || body === undefined ? '' : String(body);
119+
sendCallback(dataString, xhr);
120+
121+
// Use addEventListener which is more reliable than overriding onreadystatechange
122+
// This works even when handlers are set after send() is called
123+
this.addEventListener('readystatechange', function () {
124+
if (this.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
125+
if (!xhr._interception?.hasCalledHeaderReceived) {
126+
if (xhr._interception) {
127+
xhr._interception.hasCalledHeaderReceived = true;
128+
}
129+
const contentType = this.getResponseHeader('content-type') || '';
130+
const contentLength = this.getResponseHeader('content-length');
131+
const responseSize = contentLength ? parseInt(contentLength, 10) : 0;
132+
const responseHeaders = parseResponseHeaders(
133+
this.getAllResponseHeaders()
134+
);
135+
136+
// Set responseHeaders on xhr for compatibility with Logger.ts
137+
xhr.responseHeaders = responseHeaders;
138+
139+
headerReceivedCallback(contentType, responseSize, responseHeaders, xhr);
140+
}
141+
}
142+
143+
if (this.readyState === XMLHttpRequest.DONE) {
144+
if (!xhr._interception?.hasCalledResponse) {
145+
if (xhr._interception) {
146+
xhr._interception.hasCalledResponse = true;
147+
}
148+
let responseData = '';
149+
if (this.responseType === '' || this.responseType === 'text') {
150+
responseData = this.responseText || '';
151+
} else if (this.responseType === 'json' && this.response) {
152+
try {
153+
responseData = JSON.stringify(this.response);
154+
} catch {
155+
responseData = '[Unable to stringify response]';
156+
}
157+
} else if (this.response) {
158+
responseData = '[Non-text response]';
159+
}
160+
161+
responseCallback(
162+
this.status,
163+
this.timeout,
164+
responseData,
165+
this.responseURL,
166+
this.responseType,
167+
xhr
168+
);
169+
}
170+
}
171+
});
172+
173+
return originalXHRSend!.call(this, body);
174+
};
175+
176+
isInterceptorEnabled = true;
177+
}
178+
179+
function disableInterception(): void {
180+
if (!isInterceptorEnabled) return;
181+
182+
// Restore original methods
183+
if (originalXHROpen) {
184+
XMLHttpRequest.prototype.open = originalXHROpen;
185+
originalXHROpen = null;
186+
}
187+
if (originalXHRSend) {
188+
XMLHttpRequest.prototype.send = originalXHRSend;
189+
originalXHRSend = null;
190+
}
191+
if (originalXHRSetRequestHeader) {
192+
XMLHttpRequest.prototype.setRequestHeader = originalXHRSetRequestHeader;
193+
originalXHRSetRequestHeader = null;
194+
}
195+
196+
// Reset callbacks
197+
openCallback = () => {};
198+
requestHeaderCallback = () => {};
199+
sendCallback = () => {};
200+
headerReceivedCallback = () => {};
201+
responseCallback = () => {};
202+
203+
isInterceptorEnabled = false;
30204
}
31205

206+
const XHRInterceptor = {
207+
isInterceptorEnabled: () => isInterceptorEnabled,
208+
setOpenCallback: (callback: OpenCallback) => {
209+
openCallback = callback;
210+
},
211+
setRequestHeaderCallback: (callback: RequestHeaderCallback) => {
212+
requestHeaderCallback = callback;
213+
},
214+
setSendCallback: (callback: SendCallback) => {
215+
sendCallback = callback;
216+
},
217+
setHeaderReceivedCallback: (callback: HeaderReceivedCallback) => {
218+
headerReceivedCallback = callback;
219+
},
220+
setResponseCallback: (callback: ResponseCallback) => {
221+
responseCallback = callback;
222+
},
223+
enableInterception,
224+
disableInterception,
225+
};
226+
32227
export default XHRInterceptor;

0 commit comments

Comments
 (0)