Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This course is archived!
While the concepts of this course are still largely applicable, it's built using an older version of Symfony (4) and React (16).

API Auth & State via AJAX

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

When we used fetch() to make an AJAX call to /reps... we were "rewarded" with this big ugly error. This tells me that fetch is probably having problems... for some reason... parsing the JSON in the response.

Let's go see what happened! Click on the Network tab in your browser tools and filter for only XHR requests. Ah, here is one for /reps that was successful. BUT! That's the wrong AJAX call: this is the AJAX call made by our old code. So... where the heck is the other /reps AJAX call that was just made by fetch()?

Click instead to filter by the "Other" tab. There it is! Why is it here? Well... because... something went wrong. Look at the response: 302. And if you look at the response headers... woh! It is a redirect to the login page, which is why you see a second request below for /login.

Let's back up. First, for some reason, authentication is failing for the API request. We'll get to that in a minute. Second, fetch() requests will normally show up under the XHR network filter. We'll see that later. But, if something goes wrong, the request may show up under "Other". Just be aware of that: it's a gotcha!

So, why the heck is authentication failing? If we go directly to /reps, it works! What's wrong with you fetch!

This, in my opinion, is one of the really cool things about fetch. Look at our controller. Ah, every endpoint requires us to be logged in! This works in our browser because our browser automatically sends the session cookie. But fetch(), on the other hand, does not automatically send any cookies when it makes a request.

What Type of Authentication to Use?

I like this because it forces you to ask yourself:

Hey, how do I want to authenticate my API requests?

API authentication is a big topic. So we're going to skip it! I'm kidding: it's too important.

One way or another, every API request that needs authentication will have some sort of authentication data attached to it - maybe a session cookie or an API token set on a header.

So... what type of authentication should you use for your API? Honestly, if you're building an API that will be consumed by your own JavaScript front-end, using session cookies is an awesome option! You don't need anything fancier. When we login, that sets a session cookie. In a moment, we'll tell fetch to send that cookie, and everything will be solved. If you want to build your login page in React and send the username and password via AJAX, that's totally fine: when your server sends back the session cookie, your browser will see it & store it. Well, as long as you use the credentials option that I'm about to show you for that AJAX call.

Of course, if you want, you can also create an API token authentication system, like JWT or OAuth. That's totally fine, but that truly is a separate topic.

Whatever you choose, when it's time to make your API call, you will attach the authentication info to the request: either by sending the session cookie or your API token as a header.

To send the session cookie, fetch has a second options argument. Add credentials set to same-origin. Thanks to this, fetch() will send cookies to any requests made back to our domain.

... lines 1 - 6
return fetch('/reps', {
credentials: 'same-origin'
})
... lines 10 - 14

Tip

The default value of credentials may change to same-origin in the future.

Ok, let's see if this fixes things! Move over and refresh. No errors! Check out the console. Yes! There is our data! Notice, the API wraps everything inside an items key. Yep, inside: the 4 rep logs, which have the same fields as the state in our app. That was no accident: when we added the static data, I made sure it looked like the real data from the API so that we could swap it out later.

Processing the fetch data via Promises

In rep_log_api, I really want my getRepLogs() API to return a Promise that contains the array of rep logs... without that items key. To do that, it's a bit weird. The .json() method returns another Promise. So, to do further processing, chain a .then() from it and, inside the callback, return data.items.

... lines 1 - 9
.then(response => {
return response.json().then((data) => data.items)
});
... lines 13 - 14

Promises on top of promises! Yaaaay! When fetch() finishes, it executes our first callback. Then, when the JSON decode finishes, it executes our second callback, where we read off the .items key. Ultimately, getRepLogs() returns a Promise object where the data is the array of rep logs. Phew!

And because the browser already refreshed while I was explaining all of the promises, yep! You can see the logged data is now the array.

componentDidMount

Awesome! Let's use this to set our initial state! First, set the initial repLogs state to an empty array. Next, copy the getRepLogs() call and remove it. Instead, create a new method called componentDidMount() and paste this there. In the callback, use this.setState() to set repLogs to data.

... lines 1 - 22
componentDidMount() {
getRepLogs()
.then((data) => {
this.setState({
repLogs: data
})
});
}
... lines 31 - 83

Before we talk about this, let's try it. Refresh! Woh! We have real data! Yes, yes, yes! We're showing the same data as our original app!

Back to the code! Until this moment, render() was the only "special", React-specific, method in our class. But there are a few other special methods called "lifecycle" methods. The componentDidMount() method is one of those: if this exists, React calls it right after our component is rendered to the DOM. And this is the best place to make any AJAX requests needed to populate your initial state.

Actually, we could have left this code in the constructor(). Because we're in a browser, they're almost the same. But, componentDidMount() is generally the recommended place.

Leave a comment!

21
Login or Register to join the conversation

Since Aug 25, 2017. The spec changed the default credentials policy to same-origin for fetch in Firefox changed since 61.0b13.

1 Reply

Hey Stephane

Thanks for the info man! probably in a couple of years all browsers will do the same

Cheers!

1 Reply

Chromium 69... no errors too.

1 Reply
Default user avatar
Default user avatar HimmelHempsted | posted 3 years ago | edited

Hi!

I'm following along but with a slightly different table structure. I have a table books, and each book can have many notes. When I try to serialize this I get the error:

"A circular reference has been detected when serializing the object of class "App\Entity\Note""

Following the docs, I have tried to update it to this:

`
$json = $this->serializer->serialize($data, 'json', [

    AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER => function ($object, $format, $context) {
            return $object->getId();
    }

]);
`

But this just leads to a 30 second time-out error. Any ideas where I'm going wrong here?

Reply

Hey @HimmelHempsted

Sorry for late answer! I think you should configure @MaxDepth() annotation, on your Entities, Not sure about you structure, you should try to add it to $books or $notes properties, try to play with it to get best result for your structure!

Cheers!

Reply

Hey Guys,

I have run into a head scratcher. If I execute a fetch call to the api, then the data I get back is the html for the Symfony profiler debug toolbar.
If I make the same call twice, then both calls have the json data that I was expecting.
If I disable the profiler, then I get the correct data.
In the logs, I can see that the single call was route matched to the profiler, but I don't know why. Have you ever run into this problem?

Reply

Hey Skylar

That's funny. How looks your controller's API endpoint?
When you get the profiler's html as response did the request fail?

Cheers!

Reply

When it fails, I look at the last 10 requests, and it is NOT in the list. But if I look the inspector tools in chrome, it shows that it was sent. I also added a random number to the query string to do cache busting in case that was a possibility.

Reply

Hmm, that's indeed weird. Which web server are you using? I believe there is something funny on your configuration

Reply

I am using the symfony server

Reply

Are you using a HTTPS connection? I don't really know what's going on but could you try starting your web server by running
bin/console server:start and try again?

Reply

So I solved my problem by replacing the Symfony Built-in server with apache using xampp. All the problems went away. So, my guess is that there must have been a timing issue using the built-in server. go figure.

Reply

Hmm, that's super weird. You should be able to run your Symfony app through Symfony web-server. Probably there is an issue related to something specific to your environment. I would try re-installing Symfony CLI but well... at least you can keep going on :)

Cheers!

Reply
Johann I. Avatar
Johann I. Avatar Johann I. | posted 4 years ago

I got an problem on this.
I don't have a "reps" request in the [other] type of the network tab, nor "login". Also the error in the console says "...at position 110", not 0.
I decided to carry on but the predicted fix (adding the authentication argument to the fetch) doesn't change anything.

Any idea ? (I noticed you often reference the course on ES6. I didn't do it, so I may have missed something)

Reply

Hey Johann I.

So, your browser is not executing any requests to "reps" endpoint? It is very likely that your application is not performing that requests on page load, i.e. on "componentDidMount" method. Double check that your app is indeed executing that request

Cheers!

Reply

After some time inactivity in browser in response got redirection to login page, how to deal with it?

Reply

Hey bartek!

It depends on what you want the behavior to be. The easiest solution would be to increase Symfony's session length, or use a "remember_me" cookie (you can even automatically turn this on) so that users are basically *not* logged out. Or, you could create a heartbeat AJAX request that is sent every 10 minutes to maintain a session with the server. Or, if you *do* want to allow people to be logged out, and you want present them with the login form, it's a bit tricker. You'll need to make sure that all of your AJAX/API calls go through a central function, and in that central function you'll call fetch(). You'll need to attach an error handler that looks for authentication failure (I would also make your authentication system return a 401 response + JSON when you need to login, at least if it detects if it's an AJAX request. How you do this depends on how you've built your authentication system) and then opens a React component to show the login page. You'll also need to "save" the original request details so that you can re-send them after success.

I know this is a very generic answer - but there are multiple ways to handle it depending on what you need :).

Cheers!

Reply

I thought about remember_me or error handler, thank you very much for your reply!

1 Reply
Assel N. Avatar

Hi! I have a question regarding this. You are saying to have a central function for all ajax request to check if user is authentificated. So authentification checking is done only when ajax call is made. What if I want also check if the session expired or not when I am not active on the page and its automatically redirects to login page? My login page is doen in React. Should I have a function to periodically check the session expired or not in may entry file App? Thanks in advance

Reply

Hey Assel N.

If you want to check if the session has expired or not at the front-end level, then yes, you will have to execute a function periodically. Now, the function doesn't have to live in the main App file, it depends on your structure but that's something you can re-arrange when the time comes.

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

While the concepts of this course are still largely applicable, it's built using an older version of Symfony (4) and React (16).

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.2.0",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/doctrine-bundle": "^1.6", // 1.9.1
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.3
        "doctrine/doctrine-fixtures-bundle": "~3.0", // 3.0.2
        "doctrine/doctrine-migrations-bundle": "^1.2", // v1.3.1
        "doctrine/orm": "^2.5", // v2.7.2
        "friendsofsymfony/jsrouting-bundle": "^2.2", // 2.2.0
        "friendsofsymfony/user-bundle": "dev-master#4125505ba6eba82ddf944378a3d636081c06da0c", // dev-master
        "sensio/framework-extra-bundle": "^5.1", // v5.2.0
        "symfony/asset": "^4.0", // v4.1.4
        "symfony/console": "^4.0", // v4.1.4
        "symfony/flex": "^1.0", // v1.17.6
        "symfony/form": "^4.0", // v4.1.4
        "symfony/framework-bundle": "^4.0", // v4.1.4
        "symfony/lts": "^4@dev", // dev-master
        "symfony/monolog-bundle": "^3.1", // v3.3.0
        "symfony/polyfill-apcu": "^1.0", // v1.9.0
        "symfony/serializer-pack": "^1.0", // v1.0.1
        "symfony/swiftmailer-bundle": "^3.1", // v3.2.3
        "symfony/twig-bundle": "^4.0", // v4.1.4
        "symfony/validator": "^4.0", // v4.1.4
        "symfony/yaml": "^4.0", // v4.1.4
        "twig/twig": "2.10.*" // v2.10.0
    },
    "require-dev": {
        "symfony/debug-pack": "^1.0", // v1.0.6
        "symfony/dotenv": "^4.0", // v4.1.4
        "symfony/maker-bundle": "^1.5", // v1.5.0
        "symfony/phpunit-bridge": "^4.0", // v4.1.4
        "symfony/web-server-bundle": "^4.0" // v4.1.4
    }
}

What JavaScript libraries does this tutorial use?

// package.json
{
    "dependencies": {
        "@babel/plugin-proposal-object-rest-spread": "^7.12.1" // 7.12.1
    },
    "devDependencies": {
        "@babel/preset-react": "^7.0.0", // 7.12.5
        "@symfony/webpack-encore": "^0.26.0", // 0.26.0
        "babel-plugin-transform-object-rest-spread": "^6.26.0", // 6.26.0
        "babel-plugin-transform-react-remove-prop-types": "^0.4.13", // 0.4.13
        "bootstrap": "3", // 3.3.7
        "copy-webpack-plugin": "^4.4.1", // 4.5.1
        "core-js": "2", // 1.2.7
        "eslint": "^4.19.1", // 4.19.1
        "eslint-plugin-react": "^7.8.2", // 7.8.2
        "font-awesome": "4", // 4.7.0
        "jquery": "^3.3.1", // 3.3.1
        "promise-polyfill": "^8.0.0", // 8.0.0
        "prop-types": "^15.6.1", // 15.6.1
        "react": "^16.3.2", // 16.4.0
        "react-dom": "^16.3.2", // 16.4.0
        "sass": "^1.29.0", // 1.29.0
        "sass-loader": "^7.0.0", // 7.3.1
        "sweetalert2": "^7.11.0", // 7.22.0
        "uuid": "^3.2.1", // 3.4.0
        "webpack-notifier": "^1.5.1", // 1.6.0
        "whatwg-fetch": "^2.0.4" // 2.0.4
    }
}
userVoice