Axum By Example (CORS)


Axum By Example (CORS)
🦀 One of the best ways to understand code is to read more of it.
I would like to take a second to welcome you to this article. I hope you're having a great day. Be safe and have fun!
What is Cross-Origin Resource Sharing (CORS)?
CORS or Cross-Origin
Resource Sharing is an HTTP-header
based mechanism that allows a server to
indicate any origins
(domain, scheme, or port) other than its own from which a
browser should permit loading resources. CORS also relies on a mechanism by
which browsers make a "preflight" request to the server hosting the cross-origin
resource, in order to check that the server will permit the actual request.
For security reasons, browsers restrict cross-origin HTTP request initiated from
scripts. For example, XMLHttpRequest
and the Fetch API
follow the
same-origin policy
. This means that a web application using those APIs can
only request resources form the same origin the application was loaded from
unless the response from other origins includes the right CORS headers.
💬 The scope of the post is not to teach you all about CORS. I would like to go over a few facts about CORS. So everyone can understand what is going on with the program we are building.
The Cross-Origin resource sharing standard works by adding new HTTP headers
that let servers describe which origins are permitted to read that information
from a web browser. CORS failures result in errors but for security reasons,
specifics about the error are not available in JavaScript. All the code knows is
that an error occurred. You must check the browser console for more details.
CORS Examples
- Same-Origin Requests (CORS Success)
Let's say you are browsing a web page on https://www.example.com
and this page
wants to load an image from https://images.example.com
. Since both the web
page and the image have the same origin https://example.com
, this is a
same-origin request, and CORS will succeed. The browser allows this because it
trusts the same origin.
- Cross-Origin Requests (CORS Failure)
Imagine you're on a web page from https://www.example.com
that wants to fetch
some data from https://api.exampleapi.com
. In this case, the request is coming
from a different origin that the target API. The browser will then check if
https://api.exampleapi.com
has explicitly allowed https://www.example.com
to
access its resources using CORS headers.
- Missing CORS Headers (CORS Failure)
Let's say you have a website on https://www.mysite.com
that wants to fetch
data form an API at https://api.exampleapi.com
. But the API server does not
include the necessary CORS headers in its responses. The lack of proper CORS
headers will cause the browser to block the request, and CORS will fail.
- Different Ports (CORS Failure)
Suppose your web page is running on https://www.example.com
and attempts to
fetch data from a server at https://api.example.com:3000
. Here, the port
number 3000 is different from the standard HTTP/HTTPS ports 80/443. If the API
server doesn't explicitly allow requests from https://www.example.com
on port
3000, CORS will fail.
- No Credentials with Credentials Required (CORS Failure)
Some APIs may require authentication credentials (like cookies or tokens) to grant access to their resources. If your web page tries to access such an API without providing the necessary credentials, CORS will fail, and the browser will block the request.
Axum CORS Example
Let's start with the Cargo.toml
file to see exactly what would be required for
this project. We will pull in a few crates from the tokio
stack.
axum
tokio
tower-http
Cargo.toml
:
[package]
name = "example-cors"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
axum = { path = "../../axum" }
tokio = { version = "1.0", features = ["full"] }
tower-http = { version = "0.4.0", features = ["cors"] }
Let's checkout the whole file. Before we break it down into smaller sections, to talk about.
main.rs
:
use axum::{
http::{HeaderValue, Method},
response::{Html, IntoResponse},
routing::get,
Json, Router,
};
use std::net::SocketAddr;
use tower_http::cors::CorsLayer;
#[tokio::main]
async fn main() {
let frontend = async {
let app = Router::new().route("/", get(html));
serve(app, 3000).await;
};
let backend = async {
let app = Router::new().route("/json", get(json)).layer(
// see https://docs.rs/tower-http/latest/tower_http/cors/index.html
// for more details
//
// pay attention that for some request types like posting content-type: application/json
// it is required to add ".allow_headers([http::header::CONTENT_TYPE])"
// or see this issue https://github.com/tokio-rs/axum/issues/849
CorsLayer::new()
.allow_origin("http://localhost:3000".parse::<HeaderValue>().unwrap())
.allow_methods([Method::GET]),
);
serve(app, 4000).await;
};
tokio::join!(frontend, backend);
}
async fn serve(app: Router, port: u16) {
let addr = SocketAddr::from(([127, 0, 0, 1], port));
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn html() -> impl IntoResponse {
Html(
r#"
<script>
fetch('http://localhost:4000/json')
.then(response => response.json())
.then(data => console.log(data));
</script>
"#,
)
}
async fn json() -> impl IntoResponse {
Json(vec!["one", "two", "three"])
}
Let's break down main.rs
We will use all of the packages that we brought into our Cargo.toml
. I
personally don't have much to say about this section. It would just be repeating
myself. So we can just look at the code it self and see what we import.
You may notice that we get the CorsLayer
from the tower_http
crate. This
doesn't live in axum
.
use axum::{
http::{HeaderValue, Method},
response::{Html, IntoResponse},
routing::get,
Json, Router,
};
use std::net::SocketAddr;
use tower_http::cors::CorsLayer;
Before we move to main. Let's checkout some of the supporting functions. We have
the html()
, json()
and serve(app: Router, port: u16)
Functions
html()
-
r#""#
is a raw string literal. This allows us to use symbols, quotes new lines without having to escape characters.
Honestly it's just a basic filler html for this project. We could do something a bit more complex, but we will keep it simple.
async fn html() -> impl IntoResponse {
Html(
r#"
<script>
fetch('http://localhost:4000/json')
.then(response => response.json())
.then(data => console.log(data));
</script>
"#,
)
}
json()
-
Very basic
Json
setup with a vector with 3 values. I'm not 100% sure the inner workings for this. For now we will just have the simplejson()
function setup.
async fn json() -> impl IntoResponse {
Json(vec!["one", "two", "three"])
}
serve()
Nothing special about this serve function. We have see it before.
- create the address using
localhost
and the input port - create a listener using the address we just created
- use
axum::serve
and start the server serving
async fn serve(app: Router, port: u16) {
let addr = SocketAddr::from([127, 0, 0, 1], port));
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
}
main()
Now it's time to look at the main function to see how the program is going to work. First let's look at the code. Then we will break it down with each little section. To not get overwhelmed, but first let's take away the basic of the tokio function.
-
we will almost always have
#[tokio::main]
. Which allows us to know this is a tokio async function. -
We will also use
tokio::join!()
which takes all of the separate apps being served and handles them.
#[tokio::main]
async fn main() {
let frontend = async {
let app = Router::new().route("/", get(html));
serve(app, 3000).await;
}
let backend = async {
let app = Router::new().route("/json", get(json)).layer(
// see https://docs.rs/tower-http/latest/tower-http/cors/index.html
// for more details
//
// pay attention that for some request types like posting content-type: application/json
// it is required to add ".allow_headers([http::header::CONTENT_TYPE])"
// or see this issue https://github.com/tokio-rs/axum/issues/849
CorsLayer::new()
.allow_origin("http://localhost:3000".parse::<HeaderValue>().unwrap())
.allow_methods([Method::GET]),
);
serve(app, 4000).await;
};
tokio::join!(frontend, backend);
}
Now we will look at the frontend
variable. Which sets up our frontend app.
- inside of an async closure
-
create a router with the path
/
. That GET's ourhtml
returned from our function we explained above. -
using our
serve
function that will use the router we created in this closure. We will use port3000
for this.
This convention is a bit different than our previous post. We are having the
server wrapped this time. Compared to using serve()
inside of the
tokio::join!(serve(app, 3000))
let frontend = async {
let app = Router::new().route("/", get(html));
serve(app, 3000).await;
}
Now we will look at the backend
variable. Which sets up our backend.
- inside of an async closure
-
create a router with the path
/json
. That GET's ourjson
returned from our function we explained above. - our
cors
layer is inside of a.layer()
connected to the.route()
-
To understand more about
cors
you can use this link. https://docs.rs/tower-http/latest/tower-http/cors/index.html -
There are some notes in this code I would like to mention. You have to pay
attention that some request types like posting
content-type: application/json
. You will have to add.allow_header([http::header::CONTENT_TYPE
. If this does not make enough sense to you. You can always checkout issue849
on thehttps://github.com/tokio-rs/axum/issues/849
page for a better explanation. -
using
CorsLayer::new()
we will connect some allows. For this we will useorigin
andmethods
. -
using
allow_origin
we will check that the request we are using is coming from the URLhttp://localhost:3000
. Inside of this function we will attach.parse::<HeaderValue>().unwrap()
. -
using
allow_methods
we will include the one method we use hereGET
. This is pretty easy to setup. We will use[Method::GET]
. Since we are only using GET requests on this function. There is no need for anything extra. -
using our
serve
function that will use the router we created in this closure. We will use port4000
for this.
let backend = async {
let app = Router::new().route("/json", get(json)).layer(
// see https://docs.rs/tower-http/latest/tower-http/cors/index.html
// for more details
//
// pay attention that for some request types like posting content-type: application/json
// it is required to add ".allow_headers([http::header::CONTENT_TYPE])"
// or see this issue https://github.com/tokio-rs/axum/issues/849
CorsLayer::new()
.allow_origin("http://localhost:3000".parse::<HeaderValue>().unwrap())
.allow_methods([Method::GET]),
);
serve(app, 4000).await;
};
Testing time! 🚀
Well now we have the cors example setup. How do we test it?
If we just use a curl request here. The code won't run. There is no JavaScript
runtime. Which is fine, but you'll notice that nothing happen with the
<script>
tags.
❯ curl localhost:3000
<script>
fetch('http://localhost:4000/json')
.then(response => response.json())
.then(data => console.log(data));
</script>
- Open up a web browser and enter in
localhost:3000
- Open the developer tools and look at the console. You will notice that the JSON value was returned, looped and printed out.
Array(3)0:
"one"1:
"two"2:
"three"
length: 3
[[Prototype]]: Array(0)
🛑 What about a failure? How about we remove the allow_methods
and see
what errors we get.
Well this seems to work either way. I'm not sure how to make this more
aggressive in stopping requests. We will have to come back to this section after
I can read more about the CorsLayer
.
🛑 What if we change the allowed URL to port 3001
and still attempt to get from
that port?
localhost/:1 Access to fetch at 'http://localhost:4000/json' from origin
'http://localhost:3000' has been blocked by CORS policy: The
'Access-Control-Allow-Origin' header has a value 'http://localhost:3001' that is
not equal to the supplied origin. Have the server send the header with a valid
value, or, if an opaque response serves your needs, set the request's mode to
'no-cors' to fetch the resource with CORS disabled.
GET http://localhost:4000/json net::ERR_FAILED 200 (OK)
🧙 I hope you learned something today. Have fun! See you next time.