WebGL CORS error from Google Drive images

SOLUTION IN THREAD

Hi! I am building a virtual art museum that’s dependent on Unity pulling images from a Google Sheet.

The museum is comprised of two parts: The website portion, created in NodeJS and hosted on Heroku, and a Unity portion, containing the 3D art museum experience and to be connected to the website through WebGL.

However, while the Unity project works fine in the editor, it gets a CORS error once launched through WebGL.

I was able to get through the CORS error surrounding accessing the Google Sheet by automatically copying its contents over to a secret page on the website. However, I am now left with CORS errors from attempting to access the images of the artworks, which are posted on Google Drive.

I have tried several methods (Google Drive API, Firebase, and Base64) to attempt to get through this issue, but none have worked out. My instinct is that my next attempt should be to create a proxy within the Heroku server, but I am not really sure how to go about it.

Does anyone have any advice? I would really appreciate any sort of pointing in the right direction as I have been attempting to resolve this issue for the last couple of months.

you can google for cors proxy and use a free one.

1 Like

Hi! Do you know of any free ones that allow you to download images through them and work with Unity? I have been unable to find one so far.

yes, google them
there are cites listing them, and you can use ANY of them

Solution to those who may stumble across this in the future: In the end, what I ended up doing was including a proxy page within my own website that Unity then referenced. I will include the code as well as a detailed explanation from another Unity forums user:

website-side:
Downloaded the following packages from npm and added the following code to app.js:

const axios = require('axios');
const bodyParser = require('body-parser');
const cors = require('cors');

// Middleware
app.use(bodyParser.json());
app.use(cors());

app.get('/proxy-google-drive', async (req, res) => {
  const { googleDriveLink } = req.query;

  if (!googleDriveLink) {
      return res.status(400).json({ error: 'Google Drive link is required' });
  }

  try {
      // Proxy the request to the Google Drive link
      const response = await axios.get(googleDriveLink, {
          responseType: 'stream', // Ensure response is streamed
          headers: {
              'User-Agent': 'UnityPlayer/5.3.5f1',
              'Accept': '*/*'
          }
      });

      // Pipe the response back to the Unity WebGL client
      response.data.pipe(res);
  } catch (error) {
      console.error('Error proxying Google Drive link:', error.message);
      res.status(500).json({ error: 'Internal server error' });
  }
});

Unity-side:
Added the following code when changing the object texture to the google drive image link:

IEnumerator ProxyGoogleDriveLink(string googleDriveLink)
    {
         string expressServerURL = "https://langenheim-a07134ab155c.herokuapp.com";

        // URL encode the Google Drive link
        string encodedLink = UnityWebRequest.EscapeURL(googleDriveLink);

        // Create the request URL with the encoded Google Drive link
        string requestURL = expressServerURL + "/proxy-google-drive?googleDriveLink=" + encodedLink;

        // Create a UnityWebRequest to GET the data
        using (UnityWebRequest www = UnityWebRequest.Get(requestURL))
        {
            // Set the download handler to handle texture data
            DownloadHandlerTexture downloadHandlerTexture = new DownloadHandlerTexture();
            www.downloadHandler = downloadHandlerTexture;

            // Send the request
            yield return www.SendWebRequest();

            // Check for errors
            if (www.result != UnityWebRequest.Result.Success)
            {
                Debug.LogError("Error: " + www.error);
            }
            else
            {
                // Get the downloaded texture
                Texture2D texture = downloadHandlerTexture.texture;
               
                // Assign the texture to the object's material
                this.gameObject.GetComponent<Renderer>().material.mainTexture = texture;
            }
        }
    }

explanation by user alexsuvorov:
"When accessing a resource served from another domain, CORS headers don’t prevent the content from being downloaded, but they can prevent the content from being accessed from the page code in the browser. Regarding images specifically, CORS headers normally don’t prevent an image from being downloaded from another domain, or displayed on the page using an tag, or even rendered on a (assuming that you are not accessing the internal data of this canvas). However if the domain which is hosting the image doesn’t provide appropriate CORS headers, you will not be able to access the pixel data of the image. This means that normally you can get around the CORS limitations when downloading and displaying the image if you just want to display it without accessing the raw image data. For example it could be displayed on top of your game in a separate HTML element. However, I would assume that you would like to use the downloaded image in a more advanced way, like for example create a texture from this image and put it on a mesh. In this scenario there is no way to download and access the image directly from another server which doesn’t provide appropriate CORS headers.

Nevertheless, as already mentioned, CORS only applies to the browser, and it doesn’t prevent the image from being downloaded from any other software, like for example from a php or a nodejs process which is running on your server. This means that you can make a request to your server, informing it to first download the image from the original server, and then serve it as a response. In this case the image will actually come from your domain and not from the original one, and you can append all the necessary headers to the response if needed. So, technically speaking, this is not really “getting around” the CORS, but rather just re-hosting the requested images from your own server. But in many scenarios this scheme can still be useful.

This can be implemented for example as a simple php page that accepts the url of the remote resource as an argument, i.e. https:///getimage.php?url=https%3A%2F%2F%2Fimage.jpg. Php code then realizes that the https:///image.jpg image was requested, downloads it and serves to the client. Depending on the server setup you can perform streaming of the downloaded data, where it can be served to the client while being downloaded from the original server without delay, or you can cache the downloaded image on your server for future requests, or both. You can totally do this from a nodejs server as well. But the meaning is the same: first the image goes from the original server to your server (where it can be optionally cached) and then it goes from your server to the client browser where the game is currently running. The most appropriate implementation will depend on your specific server setup."

Hope this helps anyone who may be in my position in the future!