Advice on re authenticate flow

Hello!
Using nodejs here..

Backend is not using server connect, not made on Wappler..

On login, cookie is set
On remember me, another cookie (refresh token) is set

Is there a way to agroup all API's events so the frontend can re authenticate the user using another api call?

For the moment I'm changing one by one, but I have a lot of them..

Ok i just used chat gpt :slight_smile:

Hey! Got it — you're using Node.js backend, not Wappler Server Connect, and you want a global way to catch all API responses to auto-reauthenticate when a token (cookie) expires, instead of manually modifying every API call.
Yes, you can definitely centralize it!

:white_check_mark: Here’s the concept:

You need to create a global interceptor or a wrapper for your frontend API calls (Fetch, Axios, etc.), and automatically:

  • Detect when a request fails because of "Unauthorized" (example 401 or 403).
  • Then call your refresh-token API automatically.
  • Retry the original API call with the new token.

If you're using fetch:

You can wrap fetch() into a reusable function.

// apiClient.js
async function apiFetch(url, options = {}) {
    let response = await fetch(url, options);

    // If unauthorized, try to refresh token
    if (response.status === 401) {
        console.log('Token expired, trying refresh...');
        
        const refreshResponse = await fetch('/api/refresh-token', { method: 'POST', credentials: 'include' });

        if (refreshResponse.ok) {
            // Token refreshed, retry original request
            response = await fetch(url, options);
        } else {
            // Refresh failed, maybe redirect to login
            window.location.href = '/login';
            throw new Error('Session expired, please log in again.');
        }
    }

    return response;
}

export default apiFetch;

Then replace all your fetches:

Instead of:

fetch('/api/some-data')

you use:

import apiFetch from './apiClient';

apiFetch('/api/some-data')
  .then(response => response.json())
  .then(data => console.log(data));

this was the idea.. dont know if it will do the trick for you

1 Like

Thanks @Mozzi

Yes but this is something that got me thinking..

What I need is a global thing that can catch any 401, I don't want to manually change hundred of them if there's something better I can do..
I'm thinking maybe a listener on main.js, maybe window.fetch(myrequest) like:
https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch#examples

Thanks again btw

1 Like

lol... you using up my "credits" :wink: :wink:

Perfect — I understand exactly what you want:
You don't want to touch all your existing fetch() calls.
You want a true global hook for fetch, in one place, probably in main.js, without refactoring hundreds of lines.

:white_check_mark: Yes, it's possible.

You can "monkey patch" the window.fetch globally — basically, overwrite window.fetch once, and every fetch in your app will pass through your custom logic automatically!

so for a bonus
:exclamation: If 5 or 10 API calls happen while the token is expired, we don't want 5 refresh calls — only one refresh, and then retry all the failed API calls after refresh.

:white_check_mark: We'll queue pending requests while the refresh is happening.

// main.js or bootstrap file

// Save the original fetch
const originalFetch = window.fetch;

// Global lock and queue
let isRefreshing = false;
let refreshPromise = null;
let requestQueue = [];

// Function to refresh token
async function refreshToken() {
    if (!refreshPromise) {
        refreshPromise = originalFetch('/api/refresh-token', { method: 'POST', credentials: 'include' })
            .then(res => {
                if (!res.ok) {
                    throw new Error('Refresh token failed');
                }
                return res;
            })
            .finally(() => {
                isRefreshing = false;
                refreshPromise = null;
            });
    }
    return refreshPromise;
}

// Override window.fetch globally
window.fetch = async (...args) => {
    try {
        let response = await originalFetch(...args);

        if (response.status !== 401) {
            return response; // Normal response
        }

        console.log('Caught 401 globally');

        if (!isRefreshing) {
            console.log('No refresh ongoing, refreshing now...');
            isRefreshing = true;
            try {
                await refreshToken();
                console.log('Refresh successful.');
                
                // Retry all queued requests
                requestQueue.forEach(cb => cb());
                requestQueue = [];
            } catch (err) {
                console.log('Refresh failed. Redirecting to login.');
                requestQueue = [];
                window.location.href = '/login';
                throw err;
            }
        }

        // Wait for the refresh to complete, then retry
        return new Promise((resolve, reject) => {
            requestQueue.push(async () => {
                try {
                    const retryResponse = await originalFetch(...args);
                    resolve(retryResponse);
                } catch (retryError) {
                    reject(retryError);
                }
            });
        });

    } catch (error) {
        console.error('Global fetch error:', error);
        throw error;
    }
};

What this does:

Feature How it works
Catches all fetch calls globally Overwrites window.fetch
Detects 401 Unauthorized Globally
If no refresh in progress Starts refresh
If refresh already running Waits for it to complete
Queues the retry After refresh, retries all failed requests
If refresh fails Redirects to login page

Visual Flow:

[ User clicks ] 
    -> [ fetch /api/data ] 
        -> [ 401 Unauthorized ] 
            -> [ Start refresh-token call ]
            -> [ While refreshing, queue other requests ]
            -> [ If refresh successful, retry all ]
            -> [ If refresh failed, redirect login ]

Benefits:

  • Only 1 refresh request ever, no matter how many failed API calls.
  • Retry automatically after refresh.
  • Minimal user disruption — seamless.
  • Single place maintenance — no code scattered everywhere.

Haha sorry
I will try that and tell you how it goes

Thanks again :slight_smile:

1 Like

PS! Just on a side note.. i am in the "paid" version of chatgpt.. and i cant tell you .. its worth every penny.... i used stakoverflow and github before.. all cool .. but this saves me hours.. and hours or research and finding solitons and even come up with better ones, provide alternatives.. i was skeptical in the beginning ... but worth it... 'penny wise, pound foolish'.... comes to mind... as i would search the net and spend hours to find a solution.. just one solution via chatgpt saved me time = money... ...

1 Like

Yes, already have it too, but honestly, not using it for general code, using it for some js thing or maybe some custom query..
Neither using the Wappler AI stuff..
I asked expecting some Wappler way to handle this, but seems there's no such thing..

1 Like

After some battle we got a deal with chatgpt..
Seems the code you share is not working because App Connect uses XMLHttpRequest instead of fetch

This is what I got working:

(function() {
  const originalOpen = XMLHttpRequest.prototype.open;
  const originalSend = XMLHttpRequest.prototype.send;

  let isRefreshing = false;
  let refreshQueue = [];

  function retryQueue() {
    refreshQueue.forEach(cb => cb());
    refreshQueue = [];
  }

  XMLHttpRequest.prototype.open = function(method, url) {
    this._url = url;
    return originalOpen.apply(this, arguments);
  };

  XMLHttpRequest.prototype.send = function() {
    const xhr = this;

    const originalOnReadyStateChange = xhr.onreadystatechange;

    xhr.onreadystatechange = function() {
      if (xhr.readyState === 4 && xhr.status === 401) {
        console.log('Interceptado 401 en XHR desde:', xhr._url);

        if (!isRefreshing) {
          isRefreshing = true;

          fetch('/api/refresh-token', {
            method: 'POST',
            credentials: 'include'
          }).then(res => {
            if (res.ok) {
              console.log('Refresh OK. Reintentando peticiones fallidas...');
              retryQueue();
            } else {
              console.warn('Refresh FAIL. Redirigiendo al login...');
              window.location.href = '/login';
            }
          }).catch(err => {
            console.error('Error durante refresh:', err);
            window.location.href = '/login';
          }).finally(() => {
            isRefreshing = false;
          });
        }

        // Guardamos la función para que se reintente luego
        refreshQueue.push(() => {
          const retryXhr = new XMLHttpRequest();
          retryXhr.open(xhr._method, xhr._url, true);
          retryXhr.withCredentials = true;
          retryXhr.setRequestHeader('Content-Type', 'application/json');
          retryXhr.onload = xhr.onload;
          retryXhr.send(xhr._body);
        });
      }

      if (originalOnReadyStateChange) {
        return originalOnReadyStateChange.apply(this, arguments);
      }
    };

    // Guardamos los datos para reintentar después
    this._method = this._method || this.method || 'GET';
    this._body = arguments[0];

    return originalSend.apply(this, arguments);
  };
})();

API's are called again, but values are not printed on DOM..

Trying to avoid window.location.reload(); but I think I got no other solution..


EDIT: Asked: Can you reload all dmx-serverconnect/dmx-api-action after re-authenticate?

(function() {
  const originalOpen = XMLHttpRequest.prototype.open;
  const originalSend = XMLHttpRequest.prototype.send;

  let isRefreshing = false;
  let refreshQueue = [];

  function retryQueue() {
    refreshQueue.forEach(cb => cb());
    refreshQueue = [];
  }

  XMLHttpRequest.prototype.open = function(method, url) {
    this._url = url;
    return originalOpen.apply(this, arguments);
  };

  XMLHttpRequest.prototype.send = function() {
    const xhr = this;

    const originalOnReadyStateChange = xhr.onreadystatechange;

    xhr.onreadystatechange = function() {
      if (xhr.readyState === 4 && xhr.status === 401) {
        console.warn('Interceptado 401 desde:', xhr._url);

        const wapplerComponent = findWapplerComponent(xhr._url);

        if (!isRefreshing) {
          isRefreshing = true;

          fetch('/api/refresh-token', {
            method: 'POST',
            credentials: 'include'
          }).then(res => {
            if (res.ok) {
              console.log('Refresh OK. Reintentando cargas...');
              retryQueue();

              // Fallback: si en 1.5s no hay cambio visual, recargamos la página
              setTimeout(() => {
                console.warn('No se actualizó el DOM. Recargando...');
                window.location.reload();
              }, 1500);

            } else {
              console.warn('Refresh FAIL. Redirigiendo al login...');
              window.location.href = '/login';
            }
          }).catch(err => {
            console.error('Error durante refresh:', err);
            window.location.href = '/login';
          }).finally(() => {
            isRefreshing = false;
          });
        }

        refreshQueue.push(() => {
          if (wapplerComponent) {
            console.log('Reintentando:', wapplerComponent.id);
            wapplerComponent.load();
          } else {
            console.warn('No se encontró componente Wappler para:', xhr._url);
          }
        });
      }

      if (originalOnReadyStateChange) {
        return originalOnReadyStateChange.apply(this, arguments);
      }
    };

    return originalSend.apply(this, arguments);
  };

  // Detecta tanto dmx-api-action como dmx-serverconnect
  function findWapplerComponent(url) {
    const components = document.querySelectorAll('dmx-api-action, dmx-serverconnect');
    for (const comp of components) {
      const actualUrl = comp.dmxConnect?.settings?.url;
      if (actualUrl && url.startsWith(actualUrl)) {
        return comp;
      }
    }
    return null;
  }
})();

And all is working now :slight_smile:

Amazing...
I really keep some distance with AI, but I have to say it would have taken me a long time without that..

Thanks @Mozzi again :slight_smile:

1 Like

im glad you found a solution that is working for you.. no thanks to me.. all chatgpt... glad you have a working solution at the end of the day..