frontend
Fetch Request and Response Interceptor in JavaScript
January 24, 2026
Fetch Request and Response Interceptor in JavaScript
Overview
Fetch interceptors allow you to intercept and modify HTTP requests and responses globally. This is useful for adding authentication tokens, logging, error handling, request/response transformation, and implementing cross-cutting concerns like retry logic or caching.
Basic Implementation
/**
* Create custom fetch request and response interceptor
*/
const originalFetch = window.fetch;
/** Global var for interceptor requests */
window.requestInterceptor = (args) => {
console.log('Request intercepted:', args);
return args;
};
/** Global var for interceptor responses */
window.responseInterceptor = (response) => {
console.log('Response intercepted:', response);
return response;
};
window.fetch = async (...args) => {
args = requestInterceptor(args);
let response = await originalFetch(...args);
response = responseInterceptor(response);
return response;
};
// Usage
fetch("https://jsonplaceholder.typicode.com/todos/")
.then((res) => res.json())
.then((json) => console.log(json));
Advanced Implementation
class FetchInterceptor {
constructor() {
this.originalFetch = window.fetch;
this.requestInterceptors = [];
this.responseInterceptors = [];
this.errorInterceptors = [];
this.setup();
}
setup() {
const self = this;
window.fetch = async function(...args) {
try {
// Apply request interceptors
let modifiedArgs = args;
for (const interceptor of self.requestInterceptors) {
modifiedArgs = await interceptor(modifiedArgs);
}
// Make the actual request
let response = await self.originalFetch(...modifiedArgs);
// Apply response interceptors
for (const interceptor of self.responseInterceptors) {
response = await interceptor(response);
}
return response;
} catch (error) {
// Apply error interceptors
for (const interceptor of self.errorInterceptors) {
error = await interceptor(error);
}
throw error;
}
};
}
addRequestInterceptor(interceptor) {
this.requestInterceptors.push(interceptor);
return () => {
this.requestInterceptors = this.requestInterceptors.filter(i => i !== interceptor);
};
}
addResponseInterceptor(interceptor) {
this.responseInterceptors.push(interceptor);
return () => {
this.responseInterceptors = this.responseInterceptors.filter(i => i !== interceptor);
};
}
addErrorInterceptor(interceptor) {
this.errorInterceptors.push(interceptor);
return () => {
this.errorInterceptors = this.errorInterceptors.filter(i => i !== interceptor);
};
}
removeAllInterceptors() {
this.requestInterceptors = [];
this.responseInterceptors = [];
this.errorInterceptors = [];
}
}
Common Use Cases
1. Adding Authentication Headers
const interceptor = new FetchInterceptor();
interceptor.addRequestInterceptor(async (args) => {
const [url, options = {}] = args;
const token = localStorage.getItem('authToken');
const headers = new Headers(options.headers);
if (token) {
headers.set('Authorization', `Bearer ${token}`);
}
return [url, { ...options, headers }];
});
// All fetch calls now include auth token
fetch('/api/users');
2. Request Logging
interceptor.addRequestInterceptor(async (args) => {
const [url, options] = args;
console.log(`[Request] ${options?.method || 'GET'} ${url}`, {
headers: options?.headers,
body: options?.body
});
return args;
});
interceptor.addResponseInterceptor(async (response) => {
console.log(`[Response] ${response.status} ${response.url}`);
return response;
});
3. Error Handling
interceptor.addErrorInterceptor(async (error) => {
console.error('Fetch error:', error);
// Show user-friendly error message
if (error.message.includes('Failed to fetch')) {
showNotification('Network error. Please check your connection.');
}
return error;
});
interceptor.addResponseInterceptor(async (response) => {
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `HTTP ${response.status}`);
}
return response;
});
4. Request Retry Logic
interceptor.addResponseInterceptor(async (response) => {
if (response.status === 429 || response.status >= 500) {
// Retry logic
const maxRetries = 3;
for (let i = 0; i < maxRetries; i++) {
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
const retryResponse = await fetch(response.url, {
method: response.url.includes('?') ? 'GET' : 'POST'
});
if (retryResponse.ok) {
return retryResponse;
}
}
}
return response;
});
5. Response Transformation
interceptor.addResponseInterceptor(async (response) => {
const clonedResponse = response.clone();
const data = await clonedResponse.json();
// Transform response data
if (Array.isArray(data)) {
return new Response(JSON.stringify(data.map(transformItem)), {
status: response.status,
statusText: response.statusText,
headers: response.headers
});
}
return response;
});
6. Request/Response Caching
const cache = new Map();
interceptor.addRequestInterceptor(async (args) => {
const [url, options] = args;
// Only cache GET requests
if (options?.method === 'GET' || !options?.method) {
const cacheKey = `${url}-${JSON.stringify(options)}`;
if (cache.has(cacheKey)) {
const cached = cache.get(cacheKey);
if (Date.now() - cached.timestamp < 60000) { // 1 minute cache
return new Promise(resolve => {
resolve(new Response(JSON.stringify(cached.data), {
status: 200,
headers: { 'Content-Type': 'application/json' }
}));
});
}
}
}
return args;
});
interceptor.addResponseInterceptor(async (response) => {
const url = response.url;
const clonedResponse = response.clone();
const data = await clonedResponse.json();
// Cache the response
if (response.ok) {
const cacheKey = url;
cache.set(cacheKey, {
data,
timestamp: Date.now()
});
}
return response;
});
7. Adding Request ID
interceptor.addRequestInterceptor(async (args) => {
const [url, options = {}] = args;
const requestId = `req-${Date.now()}-${Math.random()}`;
const headers = new Headers(options.headers);
headers.set('X-Request-ID', requestId);
return [url, { ...options, headers }];
});
8. Request Timing
const requestTimings = new Map();
interceptor.addRequestInterceptor(async (args) => {
const [url] = args;
requestTimings.set(url, Date.now());
return args;
});
interceptor.addResponseInterceptor(async (response) => {
const startTime = requestTimings.get(response.url);
if (startTime) {
const duration = Date.now() - startTime;
console.log(`Request to ${response.url} took ${duration}ms`);
requestTimings.delete(response.url);
}
return response;
});
Complete Example: API Client with Interceptors
class APIClient {
constructor() {
this.interceptor = new FetchInterceptor();
this.setupInterceptors();
}
setupInterceptors() {
// Auth interceptor
this.interceptor.addRequestInterceptor(async (args) => {
const [url, options = {}] = args;
const token = this.getAuthToken();
if (token) {
const headers = new Headers(options.headers);
headers.set('Authorization', `Bearer ${token}`);
return [url, { ...options, headers }];
}
return args;
});
// Error handling
this.interceptor.addResponseInterceptor(async (response) => {
if (response.status === 401) {
// Handle unauthorized
this.handleUnauthorized();
throw new Error('Unauthorized');
}
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.message || `HTTP ${response.status}`);
}
return response;
});
// Logging
this.interceptor.addRequestInterceptor(async (args) => {
console.log('[API Request]', args[0]);
return args;
});
}
getAuthToken() {
return localStorage.getItem('authToken');
}
handleUnauthorized() {
// Redirect to login
window.location.href = '/login';
}
async get(url) {
const response = await fetch(url);
return response.json();
}
async post(url, data) {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
return response.json();
}
}
// Usage
const api = new APIClient();
api.get('/api/users');
Best Practices
- Preserve Original Fetch: Always store the original fetch function
- Handle Errors: Implement proper error handling in interceptors
- Clone Responses: Clone responses before reading to avoid consuming the stream
- Async Support: Make interceptors async to support async operations
- Remove Interceptors: Provide a way to remove interceptors when not needed
- Order Matters: Apply interceptors in the correct order
- Test Thoroughly: Test interceptors with various scenarios
Limitations
- Global Modification: Modifies global fetch - affects all fetch calls
- Order Dependency: Interceptor order can affect behavior
- Error Propagation: Errors in interceptors can break the chain
- Performance: Multiple interceptors add overhead