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