Skip to content

Instantly share code, notes, and snippets.

@petercossey
Created March 4, 2025 19:32
Show Gist options
  • Save petercossey/4519ebce4265a18d3224a6df318d76a1 to your computer and use it in GitHub Desktop.
Save petercossey/4519ebce4265a18d3224a6df318d76a1 to your computer and use it in GitHub Desktop.
XHR interceptor to add your own business logic at key points in the request lifecycle
<script>
(function() {
// Store the original XMLHttpRequest
const OriginalXHR = window.XMLHttpRequest;
// Store the original fetch (we'll use this for our side requests)
const originalFetch = window.fetch;
// Create a unique ID for this interceptor
const interceptorId = 'xhr-' + Math.random().toString(36).substr(2, 9);
// Flag to prevent recursive interception
const pendingSideRequests = new Set();
// Track request count for unique IDs
let requestCounter = 0;
// Storage for active requests
const activeRequests = {};
// ====== SIDE REQUEST FUNCTIONS ======
// Make a non-blocking side request (won't delay the original request)
const makeAsyncSideRequest = function(url, options = {}) {
// Add a marker to identify this as a side request
const requestId = `side-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`;
pendingSideRequests.add(requestId);
console.log(`%cMaking async side request: ${options.method || 'GET'} ${url}`,
'color: #2196F3; font-weight: bold;');
// Use the original fetch to avoid recursion
return originalFetch(url, options)
.then(response => response.json())
.then(data => {
console.log(`%cAsync side request completed: ${url}`,
'color: #4CAF50; font-weight: bold;', data);
pendingSideRequests.delete(requestId);
return data;
})
.catch(error => {
console.error(`%cAsync side request failed: ${url}`,
'color: #F44336; font-weight: bold;', error);
pendingSideRequests.delete(requestId);
throw error;
});
};
// Make a blocking side request (will delay the original request until completed)
const makeSyncSideRequest = async function(url, options = {}) {
// Add a marker to identify this as a side request
const requestId = `side-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`;
pendingSideRequests.add(requestId);
console.log(`%cMaking blocking side request: ${options.method || 'GET'} ${url}`,
'color: #FF9800; font-weight: bold;');
try {
// Use the original fetch to avoid recursion
const response = await originalFetch(url, options);
const data = await response.json();
console.log(`%cBlocking side request completed: ${url}`,
'color: #4CAF50; font-weight: bold;', data);
pendingSideRequests.delete(requestId);
return data;
} catch (error) {
console.error(`%cBlocking side request failed: ${url}`,
'color: #F44336; font-weight: bold;', error);
pendingSideRequests.delete(requestId);
throw error;
}
};
// Alternative: Make a side request using a new XMLHttpRequest
const makeXHRSideRequest = function(url, options = {}) {
return new Promise((resolve, reject) => {
// Create a genuine XHR request (not intercepted)
const sideXhr = new OriginalXHR();
console.log(`%cMaking XHR side request: ${options.method || 'GET'} ${url}`,
'color: #9C27B0; font-weight: bold;');
sideXhr.open(options.method || 'GET', url);
// Add headers
if (options.headers) {
Object.entries(options.headers).forEach(([key, value]) => {
sideXhr.setRequestHeader(key, value);
});
}
sideXhr.onload = function() {
if (this.status >= 200 && this.status < 300) {
let result;
try {
result = JSON.parse(this.responseText);
} catch (e) {
result = this.responseText;
}
console.log(`%cXHR side request completed: ${url}`,
'color: #4CAF50; font-weight: bold;', result);
resolve(result);
} else {
console.error(`%cXHR side request failed: ${url}`,
'color: #F44336; font-weight: bold;', this.status, this.statusText);
reject(new Error(`${this.status} ${this.statusText}`));
}
};
sideXhr.onerror = function() {
console.error(`%cXHR side request error: ${url}`,
'color: #F44336; font-weight: bold;');
reject(new Error('Network error'));
};
sideXhr.send(options.body);
});
};
// ====== BUSINESS LOGIC HOOKS ======
// Hook: Before request is opened
// Return modified arguments to change method or URL
const beforeOpen = function(method, url, async, user, password) {
// You can make a side request here if you need
// to determine something before the request is opened
if (url.includes('/api/product/')) {
// Example: Non-blocking side request to get product metadata
// This won't delay the original request
makeAsyncSideRequest('/api/product-metadata', {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
}).then(metadata => {
console.log('Product metadata received:', metadata);
// You could store this for later use
window._productMetadata = metadata;
});
}
return [method, url, async, user, password];
};
// Hook: Before request headers are set
const beforeSetRequestHeader = function(header, value) {
// Example: Add a custom header based on a side request
// This is just an example of what you could do,
// but this specific implementation isn't practical
// since this would be called for every header
return [header, value];
};
// Hook: Before request is sent (with async support)
// Important: This will be awaited if it returns a Promise
const beforeSend = async function(body, requestData) {
// Example: Conditionally make a blocking side request
// This WILL delay the original request until completed
if (requestData.url.includes('/api/checkout') &&
typeof body === 'string' &&
body.includes('"productId"')) {
try {
// Parse the request body to get the product ID
const data = JSON.parse(body);
const productId = data.productId;
// Make a blocking request to validate product availability
const availabilityData = await makeSyncSideRequest(`/api/inventory/${productId}`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
});
// Modify the request body based on availability
if (availabilityData && availabilityData.inStock) {
data.availabilityChecked = true;
data.availabilityTimestamp = new Date().toISOString();
return JSON.stringify(data);
} else {
// You could even cancel or modify the request based on results
console.warn('Product not available, modifying checkout request');
data.availabilityError = true;
return JSON.stringify(data);
}
} catch (error) {
console.error('Error checking product availability:', error);
// Continue with original request if the side request fails
}
}
return body;
};
// Hook: After response is received
const afterResponse = function(xhr, requestData) {
// Example: Send telemetry data about responses
if (xhr.status >= 400) {
// Send error telemetry (non-blocking)
makeAsyncSideRequest('/api/telemetry/error', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: requestData.url,
method: requestData.method,
status: xhr.status,
timestamp: new Date().toISOString(),
requestId: requestData.id
})
}).catch(err => {
// Silently handle telemetry failures
console.error('Failed to send error telemetry:', err);
});
}
return xhr;
};
// ====== IMPLEMENTATION (MINIMAL CHANGES BELOW THIS POINT) ======
// Helper to create log groups
const createLogGroup = (method, url, requestId) => {
const styles = 'color: #fff; padding: 2px 5px; border-radius: 3px;';
console.group(
`%c${method}%c ${url} %c#${requestId}`,
`${styles} background: #9C27B0;`,
'color: inherit;',
'color: #6c757d; font-size: 90%;'
);
};
// Format response body
const formatResponseBody = (xhr, requestId) => {
const contentType = xhr.getResponseHeader('content-type');
if (!contentType || xhr.responseText === '') {
console.log('Response Body: [Empty]');
return;
}
if (contentType.includes('application/json')) {
try {
const json = JSON.parse(xhr.responseText);
console.log('Response Body (JSON):', json);
} catch (e) {
console.log('Response Body (Text - Invalid JSON):', xhr.responseText);
}
} else if (contentType.includes('text/')) {
const text = xhr.responseText;
console.log('Response Body (Text):', text.substring(0, 1000) + (text.length > 1000 ? '...' : ''));
} else if (contentType.includes('xml')) {
console.log('Response Body (XML):', xhr.responseXML || xhr.responseText);
} else {
console.log('Response Body: [Binary data not displayed]');
}
};
// Create custom XMLHttpRequest implementation
window.XMLHttpRequest = function() {
const xhr = new OriginalXHR();
const requestId = ++requestCounter;
const requestData = {
id: requestId,
method: null,
url: null,
startTime: null,
headers: {},
body: null
};
activeRequests[requestId] = requestData;
// Override open method
const originalOpen = xhr.open;
xhr.open = function(method, url, async = true, user, password) {
const modifiedArgs = beforeOpen(method, url, async, user, password) || arguments;
requestData.method = modifiedArgs[0] || method;
requestData.url = modifiedArgs[1] || url;
createLogGroup(requestData.method, requestData.url, requestId);
console.log('Request Time:', new Date().toISOString());
console.log('URL:', requestData.url);
console.log('Method:', requestData.method);
return originalOpen.apply(xhr, modifiedArgs);
};
// Override setRequestHeader method
const originalSetRequestHeader = xhr.setRequestHeader;
xhr.setRequestHeader = function(header, value) {
const modifiedArgs = beforeSetRequestHeader(header, value);
if (modifiedArgs === false) {
console.log(`Header blocked: ${header}`);
return;
}
const headerToUse = modifiedArgs?.[0] || header;
const valueToUse = modifiedArgs?.[1] || value;
requestData.headers[headerToUse] = valueToUse;
return originalSetRequestHeader.call(xhr, headerToUse, valueToUse);
};
// Override send method
const originalSend = xhr.send;
xhr.send = function(body) {
requestData.startTime = performance.now();
requestData.body = body;
// Log request headers
console.log('Request Headers:', requestData.headers);
// Log request body
if (body) {
if (body instanceof FormData) {
console.log('Request Body: [FormData]');
} else if (body instanceof Blob) {
console.log('Request Body: [Blob]');
} else if (body instanceof ArrayBuffer) {
console.log('Request Body: [ArrayBuffer]');
} else if (typeof body === 'string') {
try {
const json = JSON.parse(body);
console.log('Request Body (JSON):', json);
} catch (e) {
console.log('Request Body (Text):', body);
}
} else if (body) {
console.log('Request Body:', body);
}
}
// Helper function to actually send the request
const sendRequest = async (bodyToSend) => {
// Add handlers for response
xhr.addEventListener('readystatechange', function() {
if (xhr.readyState === 4) {
const endTime = performance.now();
const duration = endTime - requestData.startTime;
console.log('Status:', xhr.status, xhr.statusText);
console.log('Response Time:', duration.toFixed(2) + 'ms');
// Get response headers
const responseHeaders = {};
const headerString = xhr.getAllResponseHeaders();
if (headerString) {
const headerLines = headerString.split('\r\n');
for (let i = 0; i < headerLines.length; i++) {
const line = headerLines[i];
if (line) {
const parts = line.split(': ');
const header = parts.shift();
const value = parts.join(': ');
responseHeaders[header] = value;
}
}
}
console.log('Response Headers:', responseHeaders);
// Format and log response body
formatResponseBody(xhr, requestId);
// Apply business logic to response
afterResponse(xhr, requestData);
console.groupEnd();
// Clean up
delete activeRequests[requestId];
}
});
// Handle errors
xhr.addEventListener('error', function() {
console.error('Network Error');
console.groupEnd();
delete activeRequests[requestId];
});
xhr.addEventListener('abort', function() {
console.warn('Request Aborted');
console.groupEnd();
delete activeRequests[requestId];
});
// Finally, send the request
return originalSend.call(xhr, bodyToSend);
};
// Check if beforeSend returns a Promise
const beforeSendResult = beforeSend(body, requestData);
if (beforeSendResult instanceof Promise) {
// Handle async beforeSend (will wait for the Promise to resolve)
beforeSendResult
.then(modifiedBody => {
requestData.body = modifiedBody || body;
return sendRequest(requestData.body);
})
.catch(error => {
console.error('Error in beforeSend hook:', error);
// Send the original body if there's an error
return sendRequest(body);
});
} else {
// Handle synchronous beforeSend
requestData.body = beforeSendResult || body;
return sendRequest(requestData.body);
}
};
return xhr;
};
// Log that the interceptor is installed
console.log(
'%cXHR Interceptor with Side Requests Installed!%c You can now make additional network requests during interception.',
'background: #9C27B0; color: white; padding: 2px 5px; border-radius: 3px;',
'color: inherit;'
);
// Function to restore original XHR
window.restoreOriginalXHR = function() {
window.XMLHttpRequest = OriginalXHR;
console.log(
'%cXHR Interceptor Removed!%c Original XMLHttpRequest restored.',
'background: #dc3545; color: white; padding: 2px 5px; border-radius: 3px;',
'color: inherit;'
);
delete window.restoreOriginalXHR;
};
// Expose side request methods globally (for testing)
window.xhrInterceptor = {
makeAsyncSideRequest,
makeSyncSideRequest,
makeXHRSideRequest,
id: interceptorId
};
return `XHR interceptor with side request capability installed (ID: ${interceptorId}).
- Use window.xhrInterceptor.makeAsyncSideRequest() for non-blocking requests
- Use window.xhrInterceptor.makeSyncSideRequest() for blocking requests
- Use window.xhrInterceptor.makeXHRSideRequest() for XHR-based requests
Run window.restoreOriginalXHR() to disable.`;
})();
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment