Interceptors for ‘uni.request’: Modifying Request Options or Handling Responses Globally.

Interceptors for ‘uni.request’: Modifying Request Options or Handling Responses Globally – A Hilariously Insightful Lecture

Alright class, settle down! Settle down! I see some of you are still recovering from last night’s all-nighter trying to wrestle with asynchronous JavaScript promises. Fear not, today we’re diving into something that will drastically improve your uni.request workflow: Interceptors! 🦸‍♀️

Think of interceptors as your code’s personal bodyguards for network requests. They stand between you and the wild, untamed internet, allowing you to tweak the request before it’s sent out into the digital abyss, and to sanitize the response after it crawls back, potentially wounded and covered in JSON errors.

(Dramatic music swell)

This isn’t just about making your code cleaner; it’s about writing robust, maintainable, and dare I say, enjoyable code. Because let’s be honest, who enjoys debugging network requests at 3 AM? 😴

What We’ll Cover Today:

  1. Why Interceptors? The Problem They Solve (and the problems they prevent!)
  2. Anatomy of an Interceptor: Breaking it Down
  3. Request Interceptors: Your Pre-Flight Checklist for Data Glory
  4. Response Interceptors: Turning JSON Chaos into Sweet, Sweet Data
  5. Implementation: Rolling Your Own Interceptor System (It’s easier than you think!)
  6. Advanced Techniques: Refresh Tokens, Error Handling, and More!
  7. Best Practices: Avoiding Common Pitfalls and Becoming an Interceptor Master

1. Why Interceptors? The Problem They Solve (and the problems they prevent!)

Imagine this: You’re building a fantastic UniApp application. You need to make API calls to your backend for everything – fetching user data, posting updates, ordering virtual pizza 🍕 (a crucial feature, obviously).

Without interceptors, you might find yourself repeating the same code over and over again:

  • Adding authentication headers to every request.
  • Logging every request and response for debugging.
  • Handling common error codes (e.g., 401 Unauthorized, 500 Internal Server Error).
  • Transforming data before displaying it to the user.

This leads to:

  • Code Duplication: Copy-pasting the same code across multiple components. This is a recipe for disaster! 💥 When you need to change something, you have to hunt down every instance.
  • Maintenance Nightmare: Debugging becomes a Herculean task as you sift through redundant code.
  • Inconsistency: What happens when you forget to add an auth header to one request? Chaos! 🔥

Interceptors solve these problems by providing a central location to:

  • Modify Request Options: Add headers, transform data, set timeouts, etc. before the request is sent.
  • Handle Responses Globally: Intercept responses, check for errors, transform data, and display notifications before the component receives the data.

Think of it like this:

Scenario Without Interceptors With Interceptors
Authentication Manually adding headers to each uni.request call. Interceptor automatically adds the auth header to all requests.
Error Handling Repeating the same error handling logic in every component. Interceptor handles common errors centrally and consistently.
Data Transformation Transforming the response data in each component. Interceptor transforms the data before it reaches the component.
Logging Manually logging each request and response. Interceptor automatically logs all requests and responses.

The Result? Cleaner, more maintainable, and more robust code. You’ll thank yourself later. Trust me. 🙏

2. Anatomy of an Interceptor: Breaking it Down

An interceptor is essentially a function that is executed before or after a network request. It has the power to:

  • Request Interceptors:
    • Access and modify the request configuration (URL, headers, data, etc.).
    • Cancel the request entirely (e.g., if the user is not authenticated).
    • Return a new request configuration.
  • Response Interceptors:
    • Access and modify the response data.
    • Handle errors (e.g., display a user-friendly message).
    • Return a new response.
    • Throw an error to propagate it to the component.

The basic structure generally involves two functions:

  • request(config): This function is called before the request is sent. It takes the request configuration object as an argument.
  • response(response): This function is called after the request is received. It takes the response object as an argument. It also often includes an error handling function, responseError(error).
// A simplified example (we'll build a real one later)

const interceptors = {
  request: (config) => {
    // Modify the request config
    config.header['Authorization'] = 'Bearer your_token';
    console.log('Request Intercepted:', config);
    return config; // Important: Return the modified config!
  },
  response: (response) => {
    // Modify the response data
    console.log('Response Intercepted:', response);
    if (response.statusCode >= 400) {
      // Handle errors
      console.error('API Error:', response);
      uni.showToast({
        title: 'Something went wrong!',
        icon: 'none'
      });
      return Promise.reject(response); // Reject the promise to propagate the error
    }
    return response; // Important: Return the (potentially modified) response!
  },
  responseError: (error) => {
    // Handle response errors like network issues
    console.error('Response Error Intercepted:', error);
    uni.showToast({
        title: 'Network Error!',
        icon: 'none'
    });
    return Promise.reject(error);
  }
};

Important Note: You must return the modified config object from the request interceptor and the modified response object from the response interceptor. If you don’t, your request or response will be effectively nullified, leading to very confusing bugs! 🐛

3. Request Interceptors: Your Pre-Flight Checklist for Data Glory

Request interceptors are your first line of defense. They allow you to modify the request before it’s sent to the server. This is incredibly useful for tasks like:

  • Adding Authentication Headers: Automatically adding the Authorization header with a JWT token.
  • Transforming Request Data: Converting data to a specific format required by the API.
  • Adding Common Query Parameters: Adding API keys or version numbers to the URL.
  • Logging Requests: Logging the request URL, method, and data for debugging.
  • Cancelling Requests: Preventing requests from being sent based on certain conditions (e.g., user is not logged in, network connection is unavailable).

Example: Adding Authentication Headers

Let’s say you store your JWT token in uni.getStorageSync('token'). Here’s how you can use a request interceptor to add it to every request:

const requestInterceptor = (config) => {
  const token = uni.getStorageSync('token');
  if (token) {
    config.header['Authorization'] = `Bearer ${token}`; // Or whatever your API expects
  }
  console.log('Request Intercepted:', config);
  return config;
};

Example: Logging Requests

Keep track of your requests for debugging purposes.

const loggingInterceptor = (config) => {
  console.log(`[${new Date().toISOString()}] Request: ${config.url}`, config);
  return config;
};

Example: Transforming Request Data

Suppose your API requires data in a specific format (e.g., all keys in snake_case instead of camelCase).

const toSnakeCase = (obj) => {
  const newObj = {};
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      const snakeCaseKey = key.replace(/([A-Z])/g, '_$1').toLowerCase();
      newObj[snakeCaseKey] = obj[key];
    }
  }
  return newObj;
};

const transformDataInterceptor = (config) => {
  if (config.data) {
    config.data = toSnakeCase(config.data);
  }
  console.log('Request Intercepted (Data Transformed):', config);
  return config;
};

4. Response Interceptors: Turning JSON Chaos into Sweet, Sweet Data

Response interceptors are your last line of defense. They allow you to process the response after it’s received from the server, but before your component gets its hands on it. This is invaluable for:

  • Handling Errors Globally: Catching common error codes (401, 500, etc.) and displaying user-friendly messages.
  • Refreshing Tokens: Automatically refreshing expired JWT tokens.
  • Transforming Response Data: Converting data to a format that’s easier to work with in your component.
  • Logging Responses: Logging the response status code, data, and headers for debugging.

Example: Handling Authentication Errors (401 Unauthorized)

Let’s say you want to automatically redirect the user to the login page if they receive a 401 Unauthorized error.

const authErrorInterceptor = (response) => {
  if (response.statusCode === 401) {
    uni.showToast({
      title: 'Your session has expired. Please log in again.',
      icon: 'none'
    });
    // Redirect to login page (replace with your actual login page path)
    uni.redirectTo({
      url: '/pages/login/login'
    });
    return Promise.reject(response); //Reject the promise so the component knows an error occurred.
  }
  return response;
};

Example: Transforming Response Data

Imagine your API returns dates as timestamps, but you want to display them in a human-readable format.

const formatDate = (timestamp) => {
  const date = new Date(timestamp);
  return date.toLocaleDateString();
};

const transformDateInterceptor = (response) => {
  if (response.data && response.data.items) {
    response.data.items = response.data.items.map(item => {
      if (item.date) {
        item.formattedDate = formatDate(item.date);
      }
      return item;
    });
  }
  console.log('Response Intercepted (Dates Formatted):', response);
  return response;
};

Example: Generic Error Handling

Catch general errors and display a user-friendly message.

const responseErrorInterceptor = (error) => {
  console.error('API Error:', error);
  uni.showToast({
    title: 'Oops! Something went wrong. Please try again later.',
    icon: 'none'
  });
  return Promise.reject(error); // Re-throw the error to propagate it.
};

5. Implementation: Rolling Your Own Interceptor System (It’s easier than you think!)

Okay, enough theory! Let’s build a simple interceptor system for uni.request. This isn’t as scary as it sounds, I promise. 🤞

// api.js (or whatever you name your API module)

const requestInterceptors = [];
const responseInterceptors = [];
const responseErrorInterceptors = []; //New!

export const addRequestInterceptor = (interceptor) => {
  requestInterceptors.push(interceptor);
};

export const addResponseInterceptor = (interceptor) => {
  responseInterceptors.push(interceptor);
};

export const addResponseErrorInterceptor = (interceptor) => {
    responseErrorInterceptors.push(interceptor);
}

export const request = (options) => {
  let config = { ...options }; // Create a copy of the options

  // Apply request interceptors
  for (const interceptor of requestInterceptors) {
    config = interceptor(config) || config; // Handle cases where interceptor might return undefined
  }

  return new Promise((resolve, reject) => {
    uni.request({
      ...config,
      success: (res) => {
        let response = { ...res }; // Copy the response

        // Apply response interceptors
        for (const interceptor of responseInterceptors) {
          response = interceptor(response) || response; // Handle undefined returns
        }

        resolve(response);
      },
      fail: (err) => {
          //Apply response error interceptors
          let errorResponse = {...err};
          for (const interceptor of responseErrorInterceptors) {
            errorResponse = interceptor(errorResponse) || errorResponse;
          }
          reject(errorResponse);
      }
    });
  });
};

// Export the `request` function to be used in your components
export default {
  request,
  addRequestInterceptor,
  addResponseInterceptor,
  addResponseErrorInterceptor
};

How to Use It:

  1. Import the api.js module into your components.
  2. Register your interceptors using api.addRequestInterceptor() and api.addResponseInterceptor() in your App.vue or a similar initialization file.
  3. Use the api.request() function instead of uni.request() in your components.
// In App.vue or a similar initialization file

import api from './api.js';

// Register interceptors
api.addRequestInterceptor((config) => {
  config.header['X-Custom-Header'] = 'Hello from interceptor!';
  return config;
});

api.addResponseInterceptor((response) => {
  console.log('Global Response Interceptor:', response);
  return response;
});

api.addResponseErrorInterceptor((error) => {
    console.log("Global Error Interceptor", error);
    return error;
});
// In your component

import api from './api.js';

export default {
  methods: {
    async fetchData() {
      try {
        const response = await api.request({
          url: 'https://your-api.com/data',
          method: 'GET'
        });
        console.log('Data received:', response.data);
        this.data = response.data;
      } catch (error) {
        console.error('Error fetching data:', error); // Error should be handled by the error interceptor
      }
    }
  }
};

6. Advanced Techniques: Refresh Tokens, Error Handling, and More!

Now that you have a basic interceptor system, let’s explore some advanced techniques:

  • Refresh Tokens: When your JWT token expires, you need to refresh it without interrupting the user experience. Here’s a simplified example:
// Assuming you have a refresh token endpoint
const refreshToken = async () => {
  try {
    const refreshToken = uni.getStorageSync('refreshToken');
    const response = await api.request({
      url: '/api/refresh-token',
      method: 'POST',
      data: { refreshToken }
    });
    const newToken = response.data.token;
    uni.setStorageSync('token', newToken);
    return newToken;
  } catch (error) {
    console.error('Failed to refresh token:', error);
    uni.redirectTo({ url: '/pages/login/login' }); // Redirect to login on refresh failure
    return null;
  }
};

let isRefreshing = false; // Prevent multiple refresh token requests

api.addRequestInterceptor(async (config) => {
  const token = uni.getStorageSync('token');
  if (token) {
    config.header['Authorization'] = `Bearer ${token}`;
  }
  return config;
});

api.addResponseInterceptor(async (response) => {
  if (response.statusCode === 401) {
    if (!isRefreshing) {
      isRefreshing = true;
      const newToken = await refreshToken();
      isRefreshing = false;

      if (newToken) {
        // Retry the original request with the new token
        config.header['Authorization'] = `Bearer ${newToken}`;
        return api.request(config); // Recursively call api.request with the original config
      } else {
        return Promise.reject(response); // Refresh failed, propagate error
      }
    } else {
      //If already refreshing, wait and retry.  This prevents infinite loops.
      return new Promise((resolve) => {
          setTimeout(() => {
              const token = uni.getStorageSync('token');
              config.header['Authorization'] = `Bearer ${token}`;
              resolve(api.request(config));
          }, 500)
      });
    }
  }
  return response;
});
  • Conditional Interceptors: Sometimes you only want to apply an interceptor to specific requests. You can add a check within the interceptor function:
api.addRequestInterceptor((config) => {
  if (config.url.includes('/api/v2')) { // Only apply to API version 2
    config.header['X-API-Version'] = '2';
  }
  return config;
});
  • Prioritizing Interceptors: The order in which you register interceptors matters. The first interceptor you register will be the first one executed. Consider the order carefully!

7. Best Practices: Avoiding Common Pitfalls and Becoming an Interceptor Master

  • Keep Interceptors Focused: Each interceptor should have a specific responsibility (e.g., authentication, logging, data transformation).
  • Handle Errors Gracefully: Don’t just swallow errors. Log them, display user-friendly messages, and potentially retry the request.
  • Avoid Infinite Loops: Be careful when retrying requests (especially with refresh tokens). Make sure you have a mechanism to prevent infinite loops. Use a flag like isRefreshing in the example above.
  • Test Your Interceptors Thoroughly: Write unit tests to ensure your interceptors are working as expected.
  • Document Your Interceptors: Explain what each interceptor does and why it’s necessary.

Conclusion

Interceptors are a powerful tool for writing cleaner, more maintainable, and more robust UniApp applications. By using interceptors, you can centralize common tasks like authentication, error handling, and data transformation, making your code easier to understand and debug.

Now go forth and intercept! May your requests be successful, your responses be clean, and your code be bug-free. 🚀 And remember, with great power comes great responsibility… to write awesome interceptors! 😉 Now get out there and make some magic! Class dismissed! 👨‍🏫

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *