Angular dev-server with Rails on different ports and CORS policy with passing credentials
Frontend development with modern JS framework is usually done using dev-server. With it, we don’t have to compile JS each time and after saving code changes, it happens automatically and page is refreshed with changes reflected.
But to be able to use it, we have to run a dev-server on some port which might be different from the backend app’s port. But when request, done by page’s code is trying to access different origin (domain, scheme, or port), browser will block the request, because of CORS policy. To fix this issue we will need to add some headers on backend app.
First, lets start Angular dev-server with:
ng serve** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **
it will run on port localhost:4200 by default.
Let’s also run Rails app:
rails server
Puma starting in single mode...
* Version 3.12.0 (ruby 2.5.3-p105), codename: Llamas in Pajamas
* Min threads: 0, max threads: 16
* Environment: development
* Listening on tcp://0.0.0.0:3000
Use Ctrl-C to stop
which will start Puma on localhost:3000.
Now the problem: if we try to make request with JS to backend API, it will fail with error in browser console:
this.http
.get('http://localhost:3000/my_api/')
.subscribe((response: any) => {
console.log(response);
resolve(true);
}, () => {})Access to XMLHttpRequest at ‘http://localhost:3000/my_api/' from origin ‘http://localhost:4200' has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.
It happens, because of the browser’s CORS (Cross-Origin Resource Sharing) policy. For security reasons, browsers restrict cross-origin HTTP requests initiated from scripts and app can only request resources from the same origin the application was loaded from unless the response from other origins includes the right CORS headers.
To fix that, we need our server to add some special response headers. Before the actual request to the server, the browser is doing a ‘preflight’ request to check those headers and continues to actual request if correct headers are set.
In Rails, usually ‘rack-cors’ gem is used for that. Basically it’s just adding headers like:
Access-Control-Allow-Methods: *
Access-Control-Allow-Origin: *
to server response. Adding gem:
gem ‘rack-cors’
Also, we need to place this config to config/enviroments/development.rb, because we will probably need this only for development environment:
config.middleware.insert_before 0, Rack::Cors do
allow do
origins '*'
resource '*', headers: :any, methods: [:get, :post, :patch, :put]
end
end
After restarting Rails server, requests will start working.
But the next problem starts when we need to pass credentials data within request if the backend requires some authentication stored in page’s cookies.
Adding credentials to a query can be done with ‘credentials: ‘include’ if native JS fetch is used, or by using ‘withCredentials: true’ in Angular.
After we change JS to:
this.http.get('http://localhost:3000/my_api/',
{ withCredentials: true })
.subscribe((response: any) => {
console.log(response);
resolve(true);
}, () => {})
following error will occur:
Access to XMLHttpRequest at ‘http://localhost:3000/my_api/' from origin ‘http://localhost:4200' has been blocked by CORS policy: The value of the ‘Access-Control-Allow-Origin’ header in the response must not be the wildcard ‘*’ when the request’s credentials mode is ‘include’. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.
That happens because browser requires to set the list of allowed origins and refuses to send credentials to just ‘any (*)’ origin.
To fix it we need to change Rails side as follows:
config.middleware.insert_before 0, Rack::Cors do
allow do
origins 'http://localhost:4200'
resource '*', headers: :any, methods: [:get, :post, :patch, :put], credentials: true
end
end
Adding ‘credentials: true’ will make Rails set ‘Access-Control-Allow-Credentials’ header in response to true and with allowing sending credentials from specific origin (localhost:4200) request will start working again.