Axum By Example (CORS)

Cover Image for Axum By Example (CORS)
Justin Bender
Justin Bender
Contents

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!

axum examples

CORS - axum examples

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

  1. 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.

  1. 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.

  1. 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.

  1. 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.

  1. 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 simple json() 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 our html returned from our function we explained above.
  • using our serve function that will use the router we created in this closure. We will use port 3000 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 our json 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 issue 849 on the https://github.com/tokio-rs/axum/issues/849 page for a better explanation.
  • using CorsLayer::new() we will connect some allows. For this we will use origin and methods.
  • using allow_origin we will check that the request we are using is coming from the URL http://localhost:3000. Inside of this function we will attach .parse::<HeaderValue>().unwrap().
  • using allow_methods we will include the one method we use here GET. 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 port 4000 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.