AV's Blog

Resolving a CORS Error in HTTP Calls Using Request

August 21, 2018

Back in June, I participated in a hackathon called ATXHack4Change and got involved with a group that aims to raise awareness for air quality in Central Texas. Specifically, the group wanted to make an app that shows the ozone forecast for Austin, TX aside from having a game that calculates how much one contributes to ozone pollution. We were able to come up with this minimum viable product and got our app deployed on Heroku. I have since made updates on the front-end, but the back-end is unmodified since we submitted the app.

Incorporation of the ozone forecast was supposed to be really easy, since a widget for this already exists. Unfortunately, we ran into a problem because we couldn’t incorporate the script (shown below) that contains the widget in React, which we chose along with Node.js and Express, to create the app.

<script src='http://www.airupdate.info/jsdisplay.cfm?id=AAF0FAD6-6288-489E-9EE1-69CBB77511D5'></script>

As an aside, this widget is now available as an iframe instead of a script from the AirNow website. I’m using this iframe below:

The widget shows the ozone and the particulate matter (PM2.5) for the current day. It also uses a color code to denote the air quality forecast.

Going back to the hackathon, we had to start from scratch.

Fortunately, we figured out where the data are coming from and that there is an API that exists for getting data. The docs for the AirNow API shows how to query forecasts by zip code. After requesting a key, a query tool in the site can be used to generate the URL that one can use to get the forecast. The screenshot below shows the result, which is an array of objects, when querying the Austin, TX forecast (API Key removed).

airnow querytool

Now that we know how to use the API, how do we get this in our app (aka, make http calls to AirNow and serve it in our app)?

I originally thought of doing an axios request for the generated URL in a componentDidMount function for the component where I want to render the forecast. But I came across a Cross-Origin Resource Sharing (CORS) error.

// In the React component that's supposed to render the data:
...

async componentDidMount() {
    const url = `http://www.airnowapi.org/aq/forecast/zipCode/?format=application/json&zipCode=78750&date=2018-08-23&distance=25&API_KEY...`;
    const res = await axios.get(url);
    const { data } = res;
    console.log(data);
    // this data variable is supposed to be used in a setState function here...

    // --> results to a CORS error

I didn’t know how to fix this and still can’t find how to resolve it. Google results for searching about this problem showed that there are a lot of people that have come across this, but I couldn’t find a way to resolve our own issue.

My co-hacker, Matt Myers, on the other hand, had another way of getting this data using the request package in Node.js. We eventually came up with the following code:

// we were using an express router so it looks like this (with the root being '/api')

router.get('/data', (req, res) => {
  request(url, (error, response, body) => {
    const data = JSON.parse(body)
    res.json(data)
  })
})

However, that also resulted in the same CORS error.

In the end, we were able to fix it with the help of a hackathon personel, who told us to add another line to the REST code above:

router.get('/data', (req, res) => {
  request(url, (error, response, body) => {
    const data = JSON.parse(body)
    res.set('Access-control-allow-origin', '*')
    res.json(data)
  })
})

After this, we were successful in fetching the data and serving on specific routes. We actually made two https calls for two dates: the current date and the next. These can be currently accessed at the following urls: http://cleanair-atx.herokuapp.com/api/data/today and http://cleanair-atx.herokuapp.com/api/data/tomorrow. (However, we actually didn’t need to do this as one request provides reading for four succeeding days of forecasts!)

To get this data in React, we then did an axios request in the componentDidMount lifecycle of our component.

But since we’re fetching data from two routes (which, we really don’t need!) using axios, we used the .all() function:

componentDidMount() {
    axios
      .all([axios.get("/api/data/today"), axios.get("/api/data/tomorrow")])
      .then(
        axios.spread((today, tomorrow) => {
          const todayData = today.data[0];
          const tomorrowData = tomorrow.data[0];
          this.setState({
            date: todayData.DateForecast,
            categoryName: todayData.Category.Name,
            actionDay: todayData.actionDay,
            dateTomorrow: tomorrowData.DateForecast,
            categoryNameTomorrow: tomorrowData.Category.Name,
            actionDayTomorrow: tomorrowData.ActionDay
          });
        })
      );
  }

// not needed but shown for my reference in the future.

Currently, our forecast page now looks like this:

aqiforecast

Our app is currently deployed on Heroku here.

There’s a lot of room for improvement for this app, aside from removing the second http request, such as adding more days of forecast, among others.