Decrypting Responses Using Axios Interceptors
This article explains how to implement automated client-side response decryption using Axios interceptors. By intercepting incoming HTTP responses before they are passed to application-level handlers, you can transparently decrypt encrypted payloads, parse the underlying data, and maintain a clean separation between network security concerns and application logic.
1. Understanding Axios Response Interceptors
Axios interceptors act as middleware for HTTP requests and responses.
A response interceptor receives the raw HTTP response from the server
before your then() or catch() blocks process
it. This makes it the ideal location to inspect, transform, or decrypt
payload data globally.
2. Creating the Decryption Utility
Before modifying the Axios pipeline, define your decryption logic. In
this example, standard AES decryption using the popular
crypto-js library is used:
import CryptoJS from 'crypto-js';
const SECRET_KEY = 'your-secure-shared-key';
export function decryptPayload(ciphertext) {
try {
const bytes = CryptoJS.AES.decrypt(ciphertext, SECRET_KEY);
const decryptedString = bytes.toString(CryptoJS.enc.Utf8);
if (!decryptedString) {
throw new Error('Malformed payload or invalid key');
}
return JSON.parse(decryptedString);
} catch (error) {
throw new Error(`Decryption failed: ${error.message}`);
}
}3. Integrating Decryption into the Axios Instance
Create a custom Axios instance and register a response interceptor
with axiosInstance.interceptors.response.use().
import axios from 'axios';
import { decryptPayload } from './cryptoUtils';
// 1. Create a dedicated Axios instance
const apiClient = axios.create({
baseURL: 'https://api.example.com',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
// 2. Attach the response interceptor
apiClient.interceptors.response.use(
(response) => {
// Check if the response contains an encrypted payload
if (response.data && response.data.encryptedData) {
try {
// Overwrite response.data with the decrypted content
response.data = decryptPayload(response.data.encryptedData);
} catch (error) {
return Promise.reject(new Error(`Response decryption error: ${error.message}`));
}
}
return response;
},
(error) => {
// Handle HTTP errors or errors forwarded from failed decryption
if (error.response && error.response.data && error.response.data.encryptedData) {
try {
error.response.data = decryptPayload(error.response.data.encryptedData);
} catch {
// Fall back to original error if error payload fails to decrypt
}
}
return Promise.reject(error);
}
);
export default apiClient;4. Consuming Decrypted Data in the Application
Because the transformation happens entirely within the interceptor, downstream code consumes data in its standard decrypted format without additional steps:
import apiClient from './apiClient';
async function fetchUserProfile(userId) {
try {
const response = await apiClient.get(`/users/${userId}`);
// response.data contains the plain, parsed JSON object
console.log('User Profile:', response.data);
} catch (error) {
console.error('Request failed:', error.message);
}
}Key Considerations
- Selective Decryption: Check response headers (such
as
x-encrypted: true) or specific response payload structures before running decryption to avoid processing unencrypted endpoints. - Key Security: Avoid hardcoding static symmetric keys in client-side bundles. Use asymmetric encryption (e.g., Web Crypto API with public/private key pairs) or derive session keys via a secure key-exchange handshake (such as Diffie-Hellman) during user authentication.
- Error Propagation: Always reject the promise when decryption fails to prevent invalid or corrupted data from reaching the UI layer.