react-native-network-logger
Version:
An HTTP traffic monitor for React Native with zero dependencies
265 lines (233 loc) • 7.92 kB
text/typescript
// Type declarations for globals provided by React Native runtime
declare class URL {
constructor(url: string, base?: string);
toString(): string;
}
declare class XMLHttpRequest {
static readonly UNSENT: number;
static readonly OPENED: number;
static readonly HEADERS_RECEIVED: number;
static readonly LOADING: number;
static readonly DONE: number;
readonly readyState: number;
readonly response: any;
readonly responseText: string;
responseType: string;
readonly responseURL: string;
readonly status: number;
timeout: number;
onreadystatechange: ((this: XMLHttpRequest, ev: any) => any) | null;
open(
method: string,
url: string | URL,
async?: boolean,
username?: string | null,
password?: string | null
): void;
send(body?: any): void;
setRequestHeader(header: string, value: string): void;
getResponseHeader(name: string): string | null;
getAllResponseHeaders(): string;
addEventListener(
type: string,
listener: (this: XMLHttpRequest, ev: any) => any
): void;
}
// Callback types use 'any' to match React Native's XHRInterceptor API
// This allows Logger.ts to use its own types (RequestMethod, XHR, etc.)
type OpenCallback = (...args: any[]) => void;
type RequestHeaderCallback = (...args: any[]) => void;
type SendCallback = (...args: any[]) => void;
type HeaderReceivedCallback = (...args: any[]) => void;
type ResponseCallback = (...args: any[]) => void;
// Store original XMLHttpRequest methods
let originalXHROpen: typeof XMLHttpRequest.prototype.open | null = null;
let originalXHRSend: typeof XMLHttpRequest.prototype.send | null = null;
let originalXHRSetRequestHeader:
| typeof XMLHttpRequest.prototype.setRequestHeader
| null = null;
// Callbacks
let openCallback: OpenCallback = () => {};
let requestHeaderCallback: RequestHeaderCallback = () => {};
let sendCallback: SendCallback = () => {};
let headerReceivedCallback: HeaderReceivedCallback = () => {};
let responseCallback: ResponseCallback = () => {};
let isInterceptorEnabled = false;
function parseResponseHeaders(headersString: string): Record<string, string> {
const headers: Record<string, string> = {};
if (!headersString) return headers;
const headerLines = headersString.trim().split('\r\n');
for (const line of headerLines) {
const index = line.indexOf(':');
if (index > 0) {
const key = line.substring(0, index).trim();
const value = line.substring(index + 1).trim();
headers[key] = value;
}
}
return headers;
}
function enableInterception(): void {
if (isInterceptorEnabled) return;
// Store original methods
originalXHROpen = XMLHttpRequest.prototype.open;
originalXHRSend = XMLHttpRequest.prototype.send;
originalXHRSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
// Override open
XMLHttpRequest.prototype.open = function (
method: string,
url: string | URL,
async: boolean = true,
username?: string | null,
password?: string | null
): void {
const xhr = this as any;
xhr._interception = {
method,
url: url.toString(),
};
try {
openCallback(method, url.toString(), this);
} catch {
// Interceptor callbacks must not break network requests.
}
return originalXHROpen!.call(
this,
method,
url,
async,
username ?? null,
password ?? null
);
};
// Override setRequestHeader
XMLHttpRequest.prototype.setRequestHeader = function (
header: string,
value: string
): void {
try {
requestHeaderCallback(header, value, this);
} catch {
// Interceptor callbacks must not break network requests.
}
return originalXHRSetRequestHeader!.call(this, header, value);
};
// Override send
XMLHttpRequest.prototype.send = function (body?: any): void {
const xhr = this as any;
const dataString = body === null || body === undefined ? '' : String(body);
try {
sendCallback(dataString, xhr);
} catch {
// Interceptor callbacks must not break network requests.
}
// Use addEventListener which is more reliable than overriding onreadystatechange
// This works even when handlers are set after send() is called
if (typeof this.addEventListener === 'function') {
this.addEventListener('readystatechange', function () {
if (this.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
if (!xhr._interception?.hasCalledHeaderReceived) {
if (xhr._interception) {
xhr._interception.hasCalledHeaderReceived = true;
}
const contentType = this.getResponseHeader('content-type') || '';
const contentLength = this.getResponseHeader('content-length');
const responseSize = contentLength
? parseInt(contentLength, 10)
: 0;
const responseHeaders = parseResponseHeaders(
this.getAllResponseHeaders()
);
// Set responseHeaders on xhr for compatibility with Logger.ts
xhr.responseHeaders = responseHeaders;
try {
headerReceivedCallback(
contentType,
responseSize,
responseHeaders,
xhr
);
} catch {
// Interceptor callbacks must not break network requests.
}
}
}
if (this.readyState === XMLHttpRequest.DONE) {
if (!xhr._interception?.hasCalledResponse) {
if (xhr._interception) {
xhr._interception.hasCalledResponse = true;
}
let responseData: any = this.response;
if (this.responseType === '' || this.responseType === 'text') {
responseData = this.responseText || '';
} else if (this.responseType === 'json' && this.response) {
try {
responseData = JSON.stringify(this.response);
} catch {
responseData = '[Unable to stringify response]';
}
}
try {
responseCallback(
this.status,
this.timeout,
responseData,
this.responseURL,
this.responseType,
xhr
);
} catch {
// Interceptor callbacks must not break network requests.
}
}
}
});
}
return originalXHRSend!.call(this, body);
};
isInterceptorEnabled = true;
}
function disableInterception(): void {
if (!isInterceptorEnabled) return;
// Restore original methods
if (originalXHROpen) {
XMLHttpRequest.prototype.open = originalXHROpen;
originalXHROpen = null;
}
if (originalXHRSend) {
XMLHttpRequest.prototype.send = originalXHRSend;
originalXHRSend = null;
}
if (originalXHRSetRequestHeader) {
XMLHttpRequest.prototype.setRequestHeader = originalXHRSetRequestHeader;
originalXHRSetRequestHeader = null;
}
// Reset callbacks
openCallback = () => {};
requestHeaderCallback = () => {};
sendCallback = () => {};
headerReceivedCallback = () => {};
responseCallback = () => {};
isInterceptorEnabled = false;
}
const XHRInterceptor = {
isInterceptorEnabled: () => isInterceptorEnabled,
setOpenCallback: (callback: OpenCallback) => {
openCallback = callback;
},
setRequestHeaderCallback: (callback: RequestHeaderCallback) => {
requestHeaderCallback = callback;
},
setSendCallback: (callback: SendCallback) => {
sendCallback = callback;
},
setHeaderReceivedCallback: (callback: HeaderReceivedCallback) => {
headerReceivedCallback = callback;
},
setResponseCallback: (callback: ResponseCallback) => {
responseCallback = callback;
},
enableInterception,
disableInterception,
};
export default XHRInterceptor;