JavaScript Fetch API

Dr. Greg Bernstein

November 8th, 2019

Modern AJAX

Readings

AJAX?

AJAX stands for Asynchronous JavaScript and XML. See Wikipedia AJAX.

With Ajax, Web applications can send data to and retrieve from a server asynchronously (in the background) without interfering with the display and behavior of the existing page.

AJAX

  • All modern web applications that work with a server use some form of AJAX.

  • The XMLHttpRequest was standardized in 2006. We will be using the modern version called fetch().

  • Note that fetch() is fairly widely supported caniuse and polyfills are available.

Not XML Anymore…

  • XML (eXtensible Markup Language) is a markup language that looks a lot like HTML but where you get to define the tags and attributes.

  • XML is significantly more complex and verbose than JSON and requires a correspondingly more complex parser to understand it.

  • Hence JSON is the primary data transfer format use with AJAX and not XML, but AJAJ (Asynchronous JavaScript and JSON) isn’t as catchy an acronym.

Using Fetch

Need a Sever

  • To try out the fetch() functionality we need a server.

  • We will use the QuizServer.js Node.js application that I wrote.

  • API for the QuizServer.js
    • GET ‘/allQs’: returns JSON of all questions in database.
    • POST ‘/addQ’: adds a multiple choice question to the database

Server Essentials

File QuizServer.js

const express = require('express');
const app = express();
app.use(express.static('public')); // For static assets
const questions = require('./questions.json');


app.get('/allQs', function (req, res) {
    res.json({questions: questions});
    }
)

app.post('/addQ', express.json(), function(req, res){
        questions.push(req.body.q);
        res.json(req.body.q);
})

Example Files

  • Bundled with the QuizServer.zip in the /public directory

  • get examples, post examples, and error examples

Simplest Get

from simpleGet1.html:

        var infoP = document.getElementById("Info");

        window.onload = function() {
            var pButton = document.getElementById("TheButton");
            pButton.addEventListener("click", getIt);
        }

        function getIt() {
            // fetch returns a promise, must use "then"
            fetch('/allQs').then(function(response) {
                infoP.innerHTML = response;
                console.log(response);
                });
        }

Running it…

The body is left as a “readable stream”.

Getting JSON

from simpleGet2.html:

        var infoP = document.getElementById("Info");
        var infoP2 = document.getElementById("Info2");

        window.onload = function() {
            var pButton = document.getElementById("TheButton");
            pButton.addEventListener("click", getIt);
        }

        function getIt() {
            fetch('/allQs').then(response => response.json())
            .then(function(data) {
                infoP.innerHTML = data;
                // Had to escape the returned string, since
                // questions had HTML tags.
                infoP2.innerHTML = escapeHtml(JSON.stringify(data));
                console.log(data);
            });
        }

Result…

Sending JSON Data

  • We will use the “POST” method

  • We will be sending JSON so we need to indicate that in a HTTP header

  • We need to specify the request message body

POST Example 1

From file sampleFetchPost.html

function postIt() {
    fetch('/addQ', {
            method: 'POST',
            headers: {
                "Content-type": "application/json"
            },
            body: JSON.stringify({
                q: q
            })
        }).then(function(response) {
            console.log('Request status code: ', response.statusText, response.status, response.type);
        });
}

POST Example 2

Getting data back. From file sampleFetchPost2.html

function postIt() {
    const addUrl = 'http://localhost:5555/addQ';
    fetch('/addQ', {
            method: 'POST',
            headers: {
                "Content-type": "application/json"
            },
            body: JSON.stringify({
                q: q
            })
        }).then(function(response) {
            console.log('Request status code: ', response.statusText, response.status, response.type);
            return response.json();
        })
        .then(function(data) {
            infoP.innerHTML = escapeHtml(JSON.stringify(data));
                console.log(data);
        });
}

Fetch Error Handling

Dealing with Errors

There are a lot of ways we can run into errors when fetching across a network:

  • Request times out, Non-existant server, Bad IP address…

  • Bad URL path (not found errors), Internal server errors…

  • And more…

Error Example 1

Bad URL path, from file errorGet1

function getIt() {
    // Bad URL path here
    fetch('/allQss').then(response => response.json())
        .then(function(data) {
        infoP.innerHTML = data;
        infoP2.innerHTML = escapeHtml(JSON.stringify(data));
        console.log(data);
        });
}

Error Example 1 Result

Handling Errors

From file errorGet2.html

function getIt() {
    // Bad URL path here
    fetch('/allQss').then(function(response) {
        if(response.ok) {
            return response.json(); // a promise
        } else {
            let info = `Status code: ${response.status}, ${response.statusText}`;
            console.log(response);
            return Promise.reject(info); //rejected promise!
        }
    }) // Returns a promise
    .then(function(data) {
        infoP.innerHTML = data;
        infoP2.innerHTML = escapeHtml(JSON.stringify(data));
        console.log(data);
        })
    .catch(function(msg){
        console.log("Something bad: " + msg);
        infoP2.innerHTML = msg;
    })
}

Example #2 Result

Note reporting of status code

Error Example #3: Bad IP

From file errorGet3.html

function getIt() {
    // Bad IP address here
    fetch('http://127.200.121.101/allQs')
    .then(function(response) {
        if(response.ok) {
            return response.json();
        } else {
            let info = `Status code: ${response.status}, ${response.statusText}`;
            console.log(response);
            return Promise.reject(info);
        }
    })
    .then(function(data) {
        infoP.innerHTML = data;
        infoP2.innerHTML = escapeHtml(JSON.stringify(data));
        console.log(data);
        })
    .catch(function(msg){
        console.log("Something bad: " + msg);
        infoP2.innerHTML = msg;
    })
}

Example #3 Result

Fetch and CORS

Cross-Origin Resource Sharing (CORS)

  • Reference MDN CORS

  • “A web application executes a cross-origin HTTP request when it requests a resource that has a different origin (domain, protocol, or port) from its own.”

CORS Diagram

MDN CORS

CORS and Fetch

MDN CORS

For security reasons, browsers restrict cross-origin HTTP requests initiated from scripts. For example, the Fetch API follows the same-origin policy. This means that a web application using those APIs can only request resources from the same origin the application was loaded from, unless the response from other origins includes the right CORS headers.

CORS Example Part 1

Two websites serving JSON data:

CORS Example Part 2

From ExternalFetch.html HTML essentials

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>External Fetch Example</title>
  <!-- Styling not shown -->
</head>
<body>
    <main>
    <!-- Introductory HTML removed -->
        <section class="fetchResults">
            <h2>Cooperating Site</h2>
            <button id="Button1">Get JSON</button>
            <p>JSON stringified information:</p>
            <p id="Info1"></p>
        </section>

        <section class="fetchResults">
            <h2>Non-Cooperating Site</h2>
            <button id="Button2">Get JSON</button>
            <p>JSON stringified information:</p>
            <p id="Info2"></p>
        </section>
    </main>
<script><!--  See next slide   --></script>
</body>
</html>

CORS Example Part 3

From ExternalFetch.html JavaScript essentials

  const el1 = 'Info1',
      el2 = 'Info2',
      url1 = 'https://cs651.grotto-networking.com/Tours',
      url2 = 'https://windsurf.grotto-networking.com/data/logs/windEvents2019.json';

  function getIt(url, el) {
    fetch(url)
    .then(response => response.json())
    .then(function(data) {
      let display = document.getElementById(el);
      display.innerHTML = escapeHtml(JSON.stringify(data));
      console.log(`Data from: ${url}:`);
      console.log(data);})
    .catch(function(err){
      console.log(`Error from: ${url}:`);
      console.log(err);});
  }

  let getIt1 = getIt.bind(null, url1, el1);
  let getIt2 = getIt.bind(null, url2, el2);

  function escapeHtml(str) { // A trick to escape HTML
      var div = document.createElement('div');
      div.appendChild(document.createTextNode(str));
      return div.innerHTML;
  }

  window.onload = function() {
          var pButton1 = document.getElementById("Button1");
          pButton1.addEventListener("click", getIt1);
    var pButton2 = document.getElementById("Button2");
          pButton2.addEventListener("click", getIt2);
      }

CORS Example Part 4

Successful CORS Fetch

CORS Example Part 5

Unsuccessful CORS Fetch

How to Fix?

  • Only the server that owns the cross-origin resource can fix this issue and only if it doesn’t cause a security issue.

  • The page making the fetch has no control over this for security purposes.

Express Example

// Pulls tour information out of a database and sends it
// as JSON. Note the added headers!

app.get('/tours', async function (req, res) {
    // Use a "projection" so we don't send them the _id field.
    let tours = await tourDB.find({}, {_id: 0});
    // To allow CORS
    res.header("Access-Control-Allow-Origin", "*");
    res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
    res.json(tours);
});