Skip to content

Commit 4d6d005

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 4d6d005

1 file changed

Lines changed: 223 additions & 26 deletions

File tree

src/XHRInterceptor.ts

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

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');
48+
// Callbacks
49+
let openCallback: OpenCallback = () => {};
50+
let requestHeaderCallback: RequestHeaderCallback = () => {};
51+
let sendCallback: SendCallback = () => {};
52+
let headerReceivedCallback: HeaderReceivedCallback = () => {};
53+
let responseCallback: ResponseCallback = () => {};
54+
55+
let isInterceptorEnabled = false;
56+
57+
function parseResponseHeaders(headersString: string): Record<string, string> {
58+
const headers: Record<string, string> = {};
59+
if (!headersString) return headers;
60+
61+
const headerLines = headersString.trim().split('\r\n');
62+
for (const line of headerLines) {
63+
const index = line.indexOf(':');
64+
if (index > 0) {
65+
const key = line.substring(0, index).trim();
66+
const value = line.substring(index + 1).trim();
67+
headers[key] = value;
2868
}
2969
}
70+
return headers;
3071
}
3172

73+
function enableInterception(): void {
74+
if (isInterceptorEnabled) return;
75+
76+
// Store original methods
77+
originalXHROpen = XMLHttpRequest.prototype.open;
78+
originalXHRSend = XMLHttpRequest.prototype.send;
79+
originalXHRSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
80+
81+
// Override open
82+
XMLHttpRequest.prototype.open = function (
83+
method: string,
84+
url: string | URL,
85+
async: boolean = true,
86+
username?: string | null,
87+
password?: string | null
88+
): void {
89+
const xhr = this as any;
90+
xhr._interception = {
91+
method,
92+
url: url.toString(),
93+
};
94+
95+
openCallback(method, url.toString(), this);
96+
97+
return originalXHROpen!.call(
98+
this,
99+
method,
100+
url,
101+
async,
102+
username ?? null,
103+
password ?? null
104+
);
105+
};
106+
107+
// Override setRequestHeader
108+
XMLHttpRequest.prototype.setRequestHeader = function (
109+
header: string,
110+
value: string
111+
): void {
112+
requestHeaderCallback(header, value, this);
113+
return originalXHRSetRequestHeader!.call(this, header, value);
114+
};
115+
116+
// Override send
117+
XMLHttpRequest.prototype.send = function (body?: any): void {
118+
const xhr = this as any;
119+
120+
const dataString = body === null || body === undefined ? '' : String(body);
121+
sendCallback(dataString, xhr);
122+
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 () {
126+
if (this.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
127+
if (!xhr._interception?.hasCalledHeaderReceived) {
128+
if (xhr._interception) {
129+
xhr._interception.hasCalledHeaderReceived = true;
130+
}
131+
const contentType = this.getResponseHeader('content-type') || '';
132+
const contentLength = this.getResponseHeader('content-length');
133+
const responseSize = contentLength ? parseInt(contentLength, 10) : 0;
134+
const responseHeaders = parseResponseHeaders(
135+
this.getAllResponseHeaders()
136+
);
137+
138+
// Set responseHeaders on xhr for compatibility with Logger.ts
139+
xhr.responseHeaders = responseHeaders;
140+
141+
headerReceivedCallback(contentType, responseSize, responseHeaders, xhr);
142+
}
143+
}
144+
145+
if (this.readyState === XMLHttpRequest.DONE) {
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]';
161+
}
162+
163+
responseCallback(
164+
this.status,
165+
this.timeout,
166+
responseData,
167+
this.responseURL,
168+
this.responseType,
169+
xhr
170+
);
171+
}
172+
}
173+
});
174+
175+
return originalXHRSend!.call(this, body);
176+
};
177+
178+
isInterceptorEnabled = true;
179+
}
180+
181+
function disableInterception(): void {
182+
if (!isInterceptorEnabled) return;
183+
184+
// Restore original methods
185+
if (originalXHROpen) {
186+
XMLHttpRequest.prototype.open = originalXHROpen;
187+
originalXHROpen = null;
188+
}
189+
if (originalXHRSend) {
190+
XMLHttpRequest.prototype.send = originalXHRSend;
191+
originalXHRSend = null;
192+
}
193+
if (originalXHRSetRequestHeader) {
194+
XMLHttpRequest.prototype.setRequestHeader = originalXHRSetRequestHeader;
195+
originalXHRSetRequestHeader = null;
196+
}
197+
198+
// Reset callbacks
199+
openCallback = () => {};
200+
requestHeaderCallback = () => {};
201+
sendCallback = () => {};
202+
headerReceivedCallback = () => {};
203+
responseCallback = () => {};
204+
205+
isInterceptorEnabled = false;
206+
}
207+
208+
const XHRInterceptor = {
209+
isInterceptorEnabled: () => isInterceptorEnabled,
210+
setOpenCallback: (callback: OpenCallback) => {
211+
openCallback = callback;
212+
},
213+
setRequestHeaderCallback: (callback: RequestHeaderCallback) => {
214+
requestHeaderCallback = callback;
215+
},
216+
setSendCallback: (callback: SendCallback) => {
217+
sendCallback = callback;
218+
},
219+
setHeaderReceivedCallback: (callback: HeaderReceivedCallback) => {
220+
headerReceivedCallback = callback;
221+
},
222+
setResponseCallback: (callback: ResponseCallback) => {
223+
responseCallback = callback;
224+
},
225+
enableInterception,
226+
disableInterception,
227+
};
228+
32229
export default XHRInterceptor;

0 commit comments

Comments
 (0)