Skip to content

Commit 1ace166

Browse files
committed
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 1ace166

1 file changed

Lines changed: 213 additions & 26 deletions

File tree

src/XHRInterceptor.ts

Lines changed: 213 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,219 @@
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+
}
32+
33+
// Callback types use 'any' to match React Native's XHRInterceptor API
34+
// This allows Logger.ts to use its own types (RequestMethod, XHR, etc.)
35+
type OpenCallback = (...args: any[]) => void;
36+
type RequestHeaderCallback = (...args: any[]) => void;
37+
type SendCallback = (...args: any[]) => void;
38+
type HeaderReceivedCallback = (...args: any[]) => void;
39+
type ResponseCallback = (...args: any[]) => void;
40+
41+
// Store original XMLHttpRequest methods
42+
let originalXHROpen: typeof XMLHttpRequest.prototype.open | null = null;
43+
let originalXHRSend: typeof XMLHttpRequest.prototype.send | null = null;
44+
let originalXHRSetRequestHeader: typeof XMLHttpRequest.prototype.setRequestHeader | null =
45+
null;
1146

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

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

0 commit comments

Comments
 (0)