How to solve the client side "Access-Control-Allow-Origin" request error with your own Symfony 3 API

This error will be found and reported in the client side when someone requests with Javascript (AJAX) to an endpoint of your Symfony project, that usually (but not necessarily) is an API. In most cases this error cannot be solved in the client side, as the error is actually caused by the server which in turn is not an error but a "security measure".

This security measure is the Same-Origin policy, this policy establishes that a web browser permits scripts contained in a first web page (www.myweb.com/page1.html) to access data in a second web page (www.myweb.com/script.js), but only if both web pages have the same origin. The origin is defined as a combination of URI scheme (http:// or https:// etc), hostname (www.domain.com), and port number (tipically port 80). That means, in short words that to create a request to a website A we need to send it from the same website A, if you do it from the website B then the policy will apply and you'll find the error in the console.

This policy was someway redundant, because, what if your project needs to share some information with third party websites? To solve this issue, we use the CORS specification in our server. Cross-Origin Resource Sharing (CORS) is a specification that enables truly open access across domain-boundaries. So if you serve public content, you need to consider (someway ... you need to) using CORS to open it up for universal JavaScript/browser access. You can read more about CORS here.

If you execute an XMLHttpRequest from the browser to an endpoint of your app (https://sandbox/api) with Javascript from another website (https://fiddle.jshell.net) using the following code:

$.getJSON("https://sandbox/api", function(data){
	console.log(data);
});

You'll receive the following error message in the console:

Tipically, in PHP, you can enable CORS in your script by implementing the following header:

<?php
header("Access-Control-Allow-Origin: *");

The * means that all the domains are allowed to access the response of our script in the server. You can set as value only 1 domain, otherwise you'll create more troubles for you later, besides, if you need to add support for multiple domains, check this question on Stack Overflow.

However, as you're using symfony you're not going to do it so. Instead, you need to modify the returned response in the controller.

Solve with responses in controllers

Using the controller model, in this example we are going to use a simple controller that generates the error (has no headers) and return a simple JSON response:

<?php

namespace sandbox\mainBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;

class DefaultController extends Controller
{
    /**
     * The access point to this url will be:
     * https://sandbox/api
     */
    public function apiAction(){
        $response = new Response();
        
        $date = new \DateTime();

        $response->setContent(json_encode([
            'id' => uniqid(),
            'time' => $date->format("Y-m-d")
        ]));

        $response->headers->set('Content-Type', 'application/json');
       
        return $response;
    }
}

To solve it, we need to modify the response and add the Access-Control-Allow-Origin header:

<?php

public function apiAction(){
    $response = new Response();
    $date = new \DateTime();

    $response->setContent(json_encode([
        'id' => uniqid(),
        'time' => $date->format("Y-m-d")
    ]));

    $response->headers->set('Content-Type', 'application/json');
    // Allow all websites
    $response->headers->set('Access-Control-Allow-Origin', '*');
    // Or a predefined website
    //$response->headers->set('Access-Control-Allow-Origin', 'https://jsfiddle.net/');
    // You can set the allowed methods too, if you want
//$response->headers->set('Access-Control-Allow-Methods', 'POST, GET, PUT, DELETE, PATCH, OPTIONS');
return $response; }

The origin parameter specifies a URI that may access the resource. The browser must enforce this. For requests without credentials, the server may specify "*" as a wildcard, thereby allowing any origin to access the resource.

Solve with static files and already implemented API

But what if you handle static files instead or you have a huge already built API? For example:

1) With files: if you have a file (myfile.txt) in the web directory (in the resources folder) of your symfony project (in domain A) and you want to request that file from domain B with AJAX:

$.get("https://sandbox/resources/myfile.txt", function(data){
    console.log(data);
});

2) With an already built API: let's imagine that you have already built a Restful API using FOSRestBundle:

<?php

namespace AppBundle\Controller;

class UsersController
{
    public function copyUserAction($id) // RFC-2518
    {} // "copy_user"            [COPY] /users/{id}

    public function propfindUserPropsAction($id, $property) // RFC-2518
    {} // "propfind_user_props"  [PROPFIND] /users/{id}/props/{property}

    public function proppatchUserPropsAction($id, $property) // RFC-2518
    {} // "proppatch_user_props" [PROPPATCH] /users/{id}/props/{property}

    // AND A LOT OF FUNCTIONS MORE :(
}

You'll find the same "XMLHttpRequest cannot load" error in the console in both cases so you'll need to add the mentioned header in every response. However, modifying the response in every controller or even return the files with pure PHP instead of ngix would be counterproductive and very inefficient. Therefore, to make it in the right and easy way we are going to depend of the NelmioCorsBundle. The NelmioCorsBundle allows you to send Cross-Origin Resource Sharing headers with ACL-style per-URL configuration.

To install the NelmioCorsBundle execute the following command in composer:

composer require nelmio/cors-bundle

Or add the following line in your composer.json file and then execute composer install:

{
    "require": {
        "nelmio/cors-bundle": "^1.4"
    }
}

Then proceed to register the bundle in the AppKernel file (app/AppKernel.php) in the registerBundles method:

<?php

public function registerBundles()
{
    $bundles = [
        ///..///
        new Nelmio\CorsBundle\NelmioCorsBundle(),
        ///..///
    ];

    ///..///
}

And finally proceed to setup the required configuration make your project work (read more about the NelmioCorsBundle in the official repository in Github here).

As according to your needs and requirements of your project, you may need to read the documentation of the bundle to see which options you need to enable and modify. However, the following configuration in the config.yml file should do the trick to make the /api endpoint (and all sub-urls [api/something, api/other-endpoint]) available for access from other domains:

nelmio_cors:
        defaults:
            allow_credentials: false
            allow_origin: []
            allow_headers: []
            allow_methods: []
            expose_headers: []
            max_age: 0
            hosts: []
            origin_regex: false
        paths:
            '^/api':
                allow_origin: ['*']
                allow_headers: ['*']
                allow_methods: ['POST', 'PUT', 'GET', 'DELETE']
                max_age: 3600

The /api endpoint can be accessed from any domains and allow any type of header, you may want to filter this in your project. Don't forget to clear the cache before testing and you're ready to go!

Solve it in the client side

If you're getting this error while you're trying to access a third party API (and they probably wont solve it for a while) you can rely on a unorthodox method in order to retrieve the data from the API with Javascript easily using the cors-anywhere free service. Read more about how you can bypass the same origin policy with a XMLHttpRequest in this article.

Have fun !

Become a more social person