Issues calling an API endpoint from React custom component?

Hi,

I’ve used one of the many example on this site to run a React custom component which returns values back to the Streamlit app. This works fine if the values are generated in the React app itself. When I tried to retrieve the values from a remote API, I’m unable to resolve the value from the fetch call. I can see the value being returned by the API in the network inspector, but it’s not being bound to the variable I’m passing back to Streamlit. If I run the React app outside the Streamlit component host, it works fine. There’s probably a sandbox issue here, but I don’t see any meaningful error messages, or can I find guidance on how to achieve my goal. (I know I can make the API call that React is making directly from Streamlit, but that’d be working around the issue for my case).

Below is the React effect in my code (and there are more details in this post). The /api/ping end point is running on a different port and I use the React package.json proxy flag to forward the request:

useEffect(() => {
  // https://javascript.info/async
  fetch('/api/ping', {
    credentials: 'same-origin',
    mode: 'no-cors',
    headers: {
      'Accept':'text/plain',
      'Content-Type': 'text/plain',
      'Access-Control-Allow-Origin': '*'
    }
  })
  .then(resp => resp.text())
  .then(data => setPing(data))

  message = ping + '(' + numClicks +')'

}, [clicks, ping]);

Any guidance would be much appreciated. Thanks.

Hey @asehmi, it sounds like your endpoint’s CORS response probably doesn’t handle the “null” origin. Currently, the Streamlit component iframe sandbox omits the “allow-same-origin” flag for security reasons, which means that the component is mounted in its iframe with a “null” origin. That is: your component’s origin is the string literal “null”. Some (many!) endpoints don’t handle this.

To fix it, you need to set an Access-Control-Allow-Origin: null header on your endpoint’s response. (Note that Access-Control-Allow-Origin: * will not work here - the “*” wildcard doesn’t handle the null origin.)

Here’s an example Flask app that demonstrates it:

import flask

app = flask.Flask(__name__)


@app.route("/api/ping")
def ping():
    return "pong!"


@app.after_request
def after_request(response):
    # If the request comes from a sandboxed iframe, the origin will be
    # the string "null", which is not covered by the "*" wildcard.
    # To handle this, we set "Access-Control-Allow-Origin: null".
    response.headers.add(
        "Access-Control-Allow-Origin",
        "null" if flask.request.origin == "null" else "*")
    response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization")
    response.headers.add("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE,OPTIONS")
    return response

(Oh, worth noting: we have some upcoming plans that should mitigate some of this pain. Because it’s a huge pain!)

2 Likes

Hello @tim,

Many thanks for your suggestions which unfortunately didn’t work for me. I have also tried serving the React component (hosted in Streamlit) and API requests (made by the React app) via a proxy. Technically Streamlit should only be aware of the proxy domain, on port 9009: (i) when it loads the React component, which is actually running on port 3001, and (ii) when the React component makes API calls, to a Flask server actually running on port 8888. The proxy has forwarding rules to serve both (i) and (ii) as if they were running on port 9009.

The proxy injects the following headers in all responses from API requests.

“access-control-allow-headers”: “authorization, content-type”,
“access-control-allow-origin”: “null”,
“access-control-allow-methods”: “GET, PUT, POST, DELETE, HEAD”