Serving complete web page with HTML, CSS and JS files from backend

I’m having trouble delivering a full website to the browser from my Rust backend. When I serve just the HTML file, everything works perfectly:

async fn serve_main_page() -> impl Responder {
    let page_content = include_str!("../assets/web/index.html");
    HttpResponse::Ok().content_type("text/html").body(page_content)
}

But when I attempt to include CSS styling as well, only the stylesheet appears in the browser:

async fn serve_main_page() -> impl Responder {
    let page_content = include_str!("../assets/web/index.html");
    HttpResponse::Ok().content_type("text/html").body(page_content);
    HttpResponse::Ok()
        .content_type("text/css")
        .body(include_str!("../assets/web/styles.css"))
}

What’s the proper way to serve all three file types (HTML, CSS, JavaScript) together so the client gets a complete working webpage? I’m clearly missing something about how HTTP responses work.

The Problem:

You’re trying to send multiple HTTP responses from a single Rust function, which is not how HTTP works. Your current code attempts to send both an HTML response and a CSS response within the same function, but only the first response (HttpResponse::Ok().content_type("text/html").body(page_content)) is actually sent to the browser. The second response is ignored.

:thinking: Understanding the “Why” (The Root Cause):

HTTP is a request-response protocol. Each HTTP request from a client (your browser) expects exactly one response from the server (your Rust backend). Your code violates this fundamental principle by attempting to send two responses sequentially within a single function. The server processes the first response and completes the function before the second response is ever executed. Therefore, the browser only receives the HTML, resulting in unstyled content.

:gear: Step-by-Step Guide:

Step 1: Separate Routes for Static Assets:

The most efficient solution is to create separate route handlers for your HTML, CSS, and JavaScript files. This aligns with how HTTP requests and responses function and prevents the conflict of sending multiple responses from a single handler. Here’s how you should refactor your Rust code using the actix-web framework. Note that actix-files is highly recommended, rather than manually handling static files like this.

use actix_web::{get, App, HttpResponse, Responder, HttpServer};

// Route for HTML
#[get("/")]
async fn serve_html() -> impl Responder {
    HttpResponse::Ok()
        .content_type("text/html")
        .body(include_str!("../assets/web/index.html"))
}

#[get("/styles.css")]
async fn serve_css() -> impl Responder {
    HttpResponse::Ok()
        .content_type("text/css")
        .body(include_str!("../assets/web/styles.css"))
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(serve_html)
            .service(serve_css)
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

Step 2: Update Your HTML File:

Modify your index.html file to correctly reference the CSS stylesheet using the new route:

<!DOCTYPE html>
<html>
<head>
    <title>My Webpage</title>
    <link rel="stylesheet" href="/styles.css">
</head>
<body>
    <!-- Your HTML content here -->
</body>
</html>

Repeat this step to include any JavaScript files with <script src="/script.js"></script>, adjusting the path as needed.

:mag: Common Pitfalls & What to Check Next:

  • File Paths: Double-check the paths in both your Rust code (include_str!("../assets/web/styles.css")) and your HTML (href="/styles.css"). Incorrect paths are a frequent source of errors. Use absolute paths during development for clarity.
  • Server Configuration: Ensure your server is running correctly and that the port (8080 in this example) is accessible.
  • Browser Cache: Clear your browser’s cache to ensure it’s not loading an outdated version of the CSS file.
  • Actix-files: Consider using the actix-files middleware for simpler static file serving. It handles MIME types and caching more efficiently.

:speech_balloon: Still running into issues? Share your (sanitized) config files, the exact command you ran, and any other relevant details. The community is here to help!

Simple problem - HTTP works on one request, one response. Your second HttpResponse gets ignored because the function already returned.

Don’t create separate handlers for each static file. That’s a mess. Managing routes for every CSS file, JS bundle, and image becomes a nightmare as your project grows.

I use Latenode workflows to automate this. One automation detects requests and serves files based on URL path and extension. Handles MIME type detection automatically.

No more writing handlers for /styles.css, /script.js, /favicon.ico and everything else. The workflow reads the request path, grabs the file from your assets folder, sets headers, and sends it back.

Works for development and production. Handles caching headers too, so static assets load faster.

You can’t send two responses from one handler - HTTP doesn’t work that way. Each request gets exactly one response back. Your second HttpResponse::Ok() call never runs because the function already returned after the first one. Your HTML file needs to make separate requests for CSS and JS. Put <link rel="stylesheet" href="/styles.css"> and <script src="/script.js"></script> in your index.html. Then create individual route handlers for each file type. I made this same mistake when I started with web backends. The browser automatically loads all the referenced assets once it gets the HTML response. You just need route handlers for those asset URLs.

You can’t return two HTTP responses from one function. Once that first HttpResponse::Ok() runs, the function’s done - your CSS response never happens.

Don’t try cramming everything into one response. Set up your HTML to reference external files the normal way: <link rel="stylesheet" href="/css/styles.css"> and <script src="/js/main.js"></script>. The browser will automatically request these files when it loads your HTML.

Make separate route handlers for each asset or just use static file serving. I made this exact mistake when I started with web servers - trying to bundle everything breaks how browsers and HTTP actually work. Browsers expect multiple requests to build a page.

you can’t send 2 responses from one function - that’s your issue. HTTP works on request-response pairs, so each browser request gets exactly one response back. your HTML should reference the css using link tags, then the browser will automatically request those files from separate routes. just set up a /styles.css route handler to serve your css file separately and you’re good to go.

Each HTTP request gets exactly one response, so your second HttpResponse never fires. You need separate endpoints for different file types. I hit this same wall building my first Rust web server. Instead of manually handling each static file, I switched to actix-files middleware. It serves all your static assets automatically with proper MIME types. Add this to Cargo.toml: actix-files = "0.6". Then set it up: use actix_files as fs; App::new().service(fs::Files::new("/static", "./assets/web").show_files_listing()). Now your HTML can reference assets like <link rel="stylesheet" href="/static/styles.css">. This handles caching headers and content types for you - saved me hours of debugging compared to writing individual handlers for every asset.

This topic was automatically closed 6 hours after the last reply. New replies are no longer allowed.