React-embed not working - CORS error

  1. My goal: embed retool app on a page in my react site

  2. Issue: following the guide at Embed web apps | Retool Docs and calling /api/embedUrl produces CORS error.

  3. Steps I've taken to troubleshoot:

I've made sure domains match and I'm using HTTPS. retool app URL is retool.bbb.com and the parent app url is ccc-staging.bbb.com (url info replaced with b and c).

I am completely sure the retool.bbb.com custom domain works and DNS has been set up correctly. both URLS are at bbb.com, so there should be any CORS errors.

  1. Additional info: Cloud-Hosted, exact error is:

Access to fetch at 'https://retool.bbb.com/api/embedUrl' from origin 'https://ccc-staging.bbb.com' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

I saw in a post that you guys recently had to roll back a breaking change that caused CORS errors. wonder if this is related.....

*shouldn't be any CORS errors... typo and can't edit original post

bump. anyone? This is a large issue that's blocking a release and needs to be addressed

Welcome to the community, @John_Hinkel! Thanks for reaching out.

Maybe I'm misunderstanding, but it sounds like you're trying to hit the https://retool.example.com/api/embed-url/external-user endpoint via a XHR request from your browser. If that's the case, the error you're seeing is the expected and correct behavior, as doing so would expose your API key to the client.

Depending on your specific use case, it might be sufficient to generate a static pre-authenticated URL that you can embed. Do you need to uniquely identify users in your Retool app?

There can be a lot of nuance in cases like this and, given the fact that you're facing an imminent launch, it might make sense for you to join our community Office Hours if you're able! I'd be happy to help you work through this. :+1:

Have you had a chance to revisit this, @John_Hinkel?

@Darren I was able to get the embed working! It turned out I had to use https://retool.bxlck.com/api/embed-url/external-user as the URL to post to...

I can see the retool app when I navigate to the page in my site now. However, over time, calls to that endpoint start returning errors (alternating between ETIMEDOUT errors that list the ip addresses of the retool servers and 401 errors that just deny me the abiity to get the embed URL). And then over time, it works again if I let it sit for a while. Your docs don't mention any sort of rate limit, and I'm not automating that call in such a way that would come up against a rate limit.

Please advise! Thanks in advance for any help you could give me.

Glad to hear you got it working! I didn't actually catch that you weren't using the full path in your original post, so it's a good thing I wrote out the whole thing. :+1:

That particular endpoint historically hasn't been rate-limited and that is still the case, as far as I know, so I'm not sure why you'd start running into issues after repeated use. How are you currently making that request? Is it happening once on page load or in a useEffect hook, for example? It might be helpful to take a look at the relevant request in the Network tab of your browser's dev tools.

The request is currently happening on page load, and it happens once! No useEffect or anything like that .... Our client calls our server, and then our server makes the embed request to retool (using the axios library). Looked at our server logs and the timeout errors I see in the network tab on the client are the response that retool sends back to our server. Timeout only happens on subsequent loads (i.e. if user refreshes their page). Server is on AWS if that makes a difference! Hope that helps!

that ETIMEDOUT error can be caused from a few things, one of the most common is the node.js default timeout to resolve a domain IP.... it apparently can also be caused by IPs that resolve to both IPv4 and IPv6. Normally the solution would be export NODE_OPTIONS="--network-family-autoselection-attempt-timeout=500", maybe give that a try on the server? sometimes I've seen this solved by disabling IPv6 outright, or forcing IPv4 priority with node --dns-result-order=ipv4first or

import dnsPromises from 'node:dns/promises';
dnsPromises.setDefaultResultOrder('ipv4first'); 

I'm a bit out of my depth when it comes to network stuff, but I found

this rather interesting. there's this SO page where the person had to add host: url to the request options:

const options = 
{
   host: 'retool.bxlck.com',
   headers: { 'x-ms-client-id': client_id }
}

//url to get lat/long
let url = 'https://retool.bxlck.com/api/embed-url/external-user'

//make req
let res = await axios.get(url, options)

since it seems to reset itself if you wait, you could try implementing axios-retry:

import axios from 'axios';
import axiosRetry from 'axios-retry';

const axiosInstance = axios.create({
  baseURL: 'https://your-api-url.com',
  timeout: 10000,
});

// setup axios-retry
axiosRetry(axiosInstance, { 
    retries: 3,
    retryDelay: axiosRetry.exponentialDelay,
    retryCondition: (error) => {
        // Don't retry on 401s. Let the interceptor handle it.
        return error.response.status !== 401 && axiosRetry.isNetworkOrIdempotentRequestError(error);
    },
    onRetry: (retryCount, error, requestConfig) => {
        console.log(`Retrying request (attempt ${retryCount}): ${requestConfig.url}`);
    }
});

// it looks like you already implemented logging, but just incase
axios.interceptors.response.use(
  (response) => {
    console.log('Request successful:', response);
    return response;
  },
  (error) => {
    console.error('Request failed:', error);
    const originalRequest = error.config;
    const status = error.response ? error.response.status : null
    if(status === 401 && !originalRequest._retry){
        originalRequest._retry = true;

        try{
            const newToken = await refreshToken();
            originalRequest.headers['Authorization'] = `Bearer ${newToken}`;
            return axiosInstance(originalRequest);
        } catch (refreshError) {
            // Handle token refresh failure (e.g., redirect to login)
            return Promise.reject(error); // Or handle the error as needed
        }
    }
    return Promise.reject(error);
  }
);

async function refreshToken() {
  // Implement your token refresh logic here. This could involve
  // making another API call to get a new access token using a refresh token.
  // For example:
  // const refreshResponse = await axios.post('/api/refresh-token', { refreshToken });
  // return refreshResponse.data.accessToken;
  // Or throw an error if refresh token is also invalid
  return 'new-access-token'; // Replace with the actual token
}

or u could use axios-auth-refresh to handle token refresh instead of making axios-retry handle it

import axios from 'axios';
import axiosRetry from 'axios-retry';
import createAuthRefreshInterceptor from 'axios-auth-refresh';

// Function that will be called to refresh authorization
const refreshAuthLogic = (failedRequest) =>
    axios.post('https://www.example.com/auth/token/refresh', { 
        skipAuthRefresh: true 
    }).then((tokenRefreshResponse) => {
        localStorage.setItem('token', tokenRefreshResponse.data.token);
        failedRequest.response.config.headers['Authorization'] = 'Bearer ' + tokenRefreshResponse.data.token;
        return Promise.resolve();
    });


const axiosInstance = axios.create({
  baseURL: 'https://your-api-url.com',
  timeout: 10000,
});

// setup axios auth refresh interceptor
createAuthRefreshInterceptor(axiosInstance , refreshAuthLogic, {
    statusCodes: [401, 403]
});

// setup axios-retry
axiosRetry(axiosInstance, { 
    retries: 3,
    retryDelay: axiosRetry.exponentialDelay,
    retryCondition: (error) => {
        // Don't retry on 401s. Let the interceptor handle it.
        return error.response.status !== 401 && axiosRetry.isNetworkOrIdempotentRequestError(error);
    },
    onRetry: (retryCount, error, requestConfig) => {
        console.log(`Retrying request (attempt ${retryCount}): ${requestConfig.url}`);
    }
});
1 Like

Hey @John_Hinkel - it's been a while, but I'm wondering if you've continued to have issues with this.