Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust.
Here are it's features:
- Supports HTTP/1.x and HTTP/2
- Streaming and pipelining
- Keep-alive and slow requests handling
- Client/server WebSockets support
- Transparent content compression/decompression (br, gzip, deflate)
- Powerful request routing
- Multipart streams
- Static assets
- SSL support using OpenSSL or Rustls
- Middlewares (Logger, Session, CORS, etc)
- Includes an async HTTP client
- Supports Actix actor framework
- Runs on stable Rust 1.42+
Step 1: Install Actix Web
Add the following line to your Cargo.toml file:
actix-web = "3.3.3"
Step 2: Use
use actix_web::{get, web, App, HttpServer, Responder};
#
async fn index(web::Path((id, name)): web::Path<(u32, String)>) -> impl Responder {
format!("Hello {}! id:{}", name, id)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| App::new().service(index))
.bind("127.0.0.1:8080")?
.run()
.await
}
Full Examples
Let us look at some simple but full examples:
Example 1: Hello World Actix Web
A simple hello world example in Actix Web:
main.rs
use actix_web::{middleware, web, App, HttpRequest, HttpServer};
async fn index(req: HttpRequest) -> &'static str {
println!("REQ: {:?}", req);
"Hello world!"
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
std::env::set_var("RUST_LOG", "actix_web=info");
env_logger::init();
HttpServer::new(|| {
App::new()
// enable logger
.wrap(middleware::Logger::default())
.service(web::resource("/index.html").to(|| async { "Hello world!" }))
.service(web::resource("/").to(index))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
#[cfg(test)]
mod tests {
use super::*;
use actix_web::dev::Service;
use actix_web::{http, test, web, App, Error};
#[actix_rt::test]
async fn test_index() -> Result<(), Error> {
let app = App::new().route("/", web::get().to(index));
let mut app = test::init_service(app).await;
let req = test::TestRequest::get().uri("/").to_request();
let resp = app.call(req).await.unwrap();
assert_eq!(resp.status(), http::StatusCode::OK);
let response_body = match resp.response().body().as_ref() {
Some(actix_web::body::Body::Bytes(bytes)) => bytes,
_ => panic!("Response error"),
};
assert_eq!(response_body, r##"Hello world!"##);
Ok(())
}
}
Example 2: Actix Web Basics
main.rs
use actix_files as fs;
use actix_session::{CookieSession, Session};
use actix_utils::mpsc;
use actix_web::http::{header, Method, StatusCode};
use actix_web::{
error, get, guard, middleware, web, App, Error, HttpRequest, HttpResponse,
HttpServer, Result,
};
use std::{env, io};
/// favicon handler
#
async fn favicon() -> Result<fs::NamedFile> {
Ok(fs::NamedFile::open("static/favicon.ico")?)
}
/// simple index handler
#
async fn welcome(session: Session, req: HttpRequest) -> Result<HttpResponse> {
println!("{:?}", req);
// session
let mut counter = 1;
if let Some(count) = session.get::<i32>("counter")? {
println!("SESSION value: {}", count);
counter = count + 1;
}
// set counter to session
session.set("counter", counter)?;
// response
Ok(HttpResponse::build(StatusCode::OK)
.content_type("text/html; charset=utf-8")
.body(include_str!("../static/welcome.html")))
}
/// 404 handler
async fn p404() -> Result<fs::NamedFile> {
Ok(fs::NamedFile::open("static/404.html")?.set_status_code(StatusCode::NOT_FOUND))
}
/// response body
async fn response_body(path: web::Path<String>) -> HttpResponse {
let text = format!("Hello {}!", *path);
let (tx, rx_body) = mpsc::channel();
let _ = tx.send(Ok::<_, Error>(web::Bytes::from(text)));
HttpResponse::Ok().streaming(rx_body)
}
/// handler with path parameters like /user/{name}/
async fn with_param(
req: HttpRequest,
web::Path((name,)): web::Path<(String,)>,
) -> HttpResponse {
println!("{:?}", req);
HttpResponse::Ok()
.content_type("text/plain")
.body(format!("Hello {}!", name))
}
#[actix_web::main]
async fn main() -> io::Result<()> {
env::set_var("RUST_LOG", "actix_web=debug,actix_server=info");
env_logger::init();
HttpServer::new(|| {
App::new()
// cookie session middleware
.wrap(CookieSession::signed(&[0; 32]).secure(false))
// enable logger - always register actix-web Logger middleware last
.wrap(middleware::Logger::default())
// register favicon
.service(favicon)
// register simple route, handle all methods
.service(welcome)
// with path parameters
.service(web::resource("/user/{name}").route(web::get().to(with_param)))
// async response body
.service(
web::resource("/async-body/{name}").route(web::get().to(response_body)),
)
.service(
web::resource("/test").to(|req: HttpRequest| match *req.method() {
Method::GET => HttpResponse::Ok(),
Method::POST => HttpResponse::MethodNotAllowed(),
_ => HttpResponse::NotFound(),
}),
)
.service(web::resource("/error").to(|| async {
error::InternalError::new(
io::Error::new(io::ErrorKind::Other, "test"),
StatusCode::INTERNAL_SERVER_ERROR,
)
}))
// static files
.service(fs::Files::new("/static", "static").show_files_listing())
// redirect
.service(web::resource("/").route(web::get().to(|req: HttpRequest| {
println!("{:?}", req);
HttpResponse::Found()
.header(header::LOCATION, "static/welcome.html")
.finish()
})))
// default
.default_service(
// 404 for GET request
web::resource("")
.route(web::get().to(p404))
// all requests that are not GET
.route(
web::route()
.guard(guard::Not(guard::Get()))
.to(HttpResponse::MethodNotAllowed),
),
)
})
.bind("127.0.0.1:8080")?
.run()
.await
}
welcome.html
<!DOCTYPE html>
<html>
<head>
<title>actix - basics</title>
<link rel="shortcut icon" type="image/x-icon" href="/favicon">
</head>
<body>
<h1>Welcome <img width="30" height="30" src="/static/actixLogo.png"></h1>
</body>
</html>
404.html
<!DOCTYPE html>
<html>
<head>
<title>actix - basics</title>
<link rel="shortcut icon" type="image/x-icon" href="/favicon">
</head>
<body>
<a href="/static/welcome.html">back to home</a>
<h1>404</h1>
</body>
</html>
Example 3: Actix Web Error Handling
An error handling example in Actix Web. The goal of this example is to show how to propagate a custom error type,
to a web handler that will evaluate the type of error that
was raised and return an appropriate HTTPResponse.
This example uses a 50/50 chance of returning 200 Ok, otherwise one of four possible
http errors will be chosen, each with an equal chance of being selected:
- 403 Forbidden
- 401 Unauthorized
- 500 InternalServerError
- 400 BadRequest
main.rs
use actix_web::{web, App, Error, HttpResponse, HttpServer, ResponseError};
use derive_more::Display; // naming it clearly for illustration purposes
use rand::{
distributions::{Distribution, Standard},
thread_rng, Rng,
};
#[derive(Debug, Display)]
pub enum CustomError {
#[display(fmt = "Custom Error 1")]
CustomOne,
#[display(fmt = "Custom Error 2")]
CustomTwo,
#[display(fmt = "Custom Error 3")]
CustomThree,
#[display(fmt = "Custom Error 4")]
CustomFour,
}
impl Distribution<CustomError> for Standard {
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> CustomError {
match rng.gen_range(0, 4) {
0 => CustomError::CustomOne,
1 => CustomError::CustomTwo,
2 => CustomError::CustomThree,
_ => CustomError::CustomFour,
}
}
}
/// Actix web uses ResponseError
for conversion of errors to a response
impl ResponseError for CustomError {
fn error_response(&self) -> HttpResponse {
match self {
CustomError::CustomOne => {
println!("do some stuff related to CustomOne error");
HttpResponse::Forbidden().finish()
}
CustomError::CustomTwo => {
println!("do some stuff related to CustomTwo error");
HttpResponse::Unauthorized().finish()
}
CustomError::CustomThree => {
println!("do some stuff related to CustomThree error");
HttpResponse::InternalServerError().finish()
}
_ => {
println!("do some stuff related to CustomFour error");
HttpResponse::BadRequest().finish()
}
}
}
}
/// randomly returns either () or one of the 4 CustomError variants
async fn do_something_random() -> Result<(), CustomError> {
let mut rng = thread_rng();
// 20% chance that () will be returned by this function
if rng.gen_bool(2.0 / 10.0) {
Ok(())
} else {
Err(rand::random::<CustomError>())
}
}
async fn do_something() -> Result<HttpResponse, Error> {
do_something_random().await?;
Ok(HttpResponse::Ok().body("Nothing interesting happened. Try again."))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
std::env::set_var("RUST_LOG", "actix_web=info");
env_logger::init();
HttpServer::new(move || {
App::new()
.service(web::resource("/something").route(web::get().to(do_something)))
})
.bind("127.0.0.1:8088")?
.run()
.await
}
Example 4: Actix Web HTTP Proxy
Cargo.toml
[dependencies]
actix-web = { version = "3", features = ["openssl"] }
clap = "2.33"
url = "2.0"
main.rs
use std::net::ToSocketAddrs;
use actix_web::client::Client;
use actix_web::{middleware, web, App, Error, HttpRequest, HttpResponse, HttpServer};
use clap::{value_t, Arg};
use url::Url;
async fn forward(
req: HttpRequest,
payload: web::Payload,
url: web::Data<Url>,
client: web::Data<Client>,
) -> Result<HttpResponse, Error> {
let mut new_url = url.get_ref().clone();
new_url.set_path(req.uri().path());
new_url.set_query(req.uri().query());
// TODO: This forwarded implementation is incomplete as it only handles the inofficial
// X-Forwarded-For header but not the official Forwarded one.
let forwarded_req = client
.request_from(new_url.as_str(), req.head())
.no_decompress();
let forwarded_req = if let Some(addr) = req.head().peer_addr {
forwarded_req.header("x-forwarded-for", format!("{}", addr.ip()))
} else {
forwarded_req
};
let res = forwarded_req
.send_stream(payload)
.await
.map_err(Error::from)?;
let mut client_resp = HttpResponse::build(res.status());
// Remove Connection
as per
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection#Directives
for (header_name, header_value) in
res.headers().iter().filter(|(h, _)| *h != "connection")
{
client_resp.header(header_name.clone(), header_value.clone());
}
Ok(client_resp.streaming(res))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let matches = clap::App::new("HTTP Proxy")
.arg(
Arg::with_name("listen_addr")
.takes_value(true)
.value_name("LISTEN ADDR")
.index(1)
.required(true),
)
.arg(
Arg::with_name("listen_port")
.takes_value(true)
.value_name("LISTEN PORT")
.index(2)
.required(true),
)
.arg(
Arg::with_name("forward_addr")
.takes_value(true)
.value_name("FWD ADDR")
.index(3)
.required(true),
)
.arg(
Arg::with_name("forward_port")
.takes_value(true)
.value_name("FWD PORT")
.index(4)
.required(true),
)
.get_matches();
let listen_addr = matches.value_of("listen_addr").unwrap();
let listen_port = value_t!(matches, "listen_port", u16).unwrap_or_else(|e| e.exit());
let forwarded_addr = matches.value_of("forward_addr").unwrap();
let forwarded_port =
value_t!(matches, "forward_port", u16).unwrap_or_else(|e| e.exit());
let forward_url = Url::parse(&format!(
"http://{}",
(forwarded_addr, forwarded_port)
.to_socket_addrs()
.unwrap()
.next()
.unwrap()
))
.unwrap();
HttpServer::new(move || {
App::new()
.data(Client::new())
.data(forward_url.clone())
.wrap(middleware::Logger::default())
.default_service(web::route().to(forward))
})
.bind((listen_addr, listen_port))?
.system_exit()
.run()
.await
}
Example 5: Actix Web JSON Validation Example
This is a contrived example intended to illustrate actix-web features.
Imagine that you have a process that involves 3 steps. The steps here
are dumb in that they do nothing other than call an httpbin endpoint that returns the json that was posted to it. The intent here is to illustrate how to chain these steps together as futures and return a final result in a response.
Actix-web features illustrated here include:
- handling json input param
- validating user-submitted parameters using the 'validator' crate
- actix-web client features:
- POSTing json body
- chaining futures into a single response used by an async endpoint
Cargo.toml
[dependencies]
actix-web = { version = "3", features = ["openssl"] }
env_logger = "0.8"
futures = "0.3.1"
serde = { version = "1.0.43", features = ["derive"] }
serde_json = "1.0.16"
validator = "0.10"
validator_derive = "0.10"
main.rs
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::io;
use actix_web::{
client::Client,
error::ErrorBadRequest,
web::{self, BytesMut},
App, Error, HttpResponse, HttpServer,
};
use futures::StreamExt;
use validator::Validate;
use validator_derive::Validate;
#[derive(Debug, Validate, Deserialize, Serialize)]
struct SomeData {
#[validate(length(min = 1, max = 1000000))]
id: String,
#[validate(length(min = 1, max = 100))]
name: String,
}
#[derive(Debug, Deserialize)]
struct HttpBinResponse {
args: HashMap<String, String>,
data: String,
files: HashMap<String, String>,
form: HashMap<String, String>,
headers: HashMap<String, String>,
json: SomeData,
origin: String,
url: String,
}
/// validate data, post json to httpbin, get it back in the response body, return deserialized
async fn step_x(data: SomeData, client: &Client) -> Result<SomeData, Error> {
// validate data
data.validate().map_err(ErrorBadRequest)?;
let mut res = client
.post("https://httpbin.org/post")
.send_json(&data)
.await
.map_err(Error::from)?; // <- convert SendRequestError to an Error
let mut body = BytesMut::new();
while let Some(chunk) = res.next().await {
body.extend_from_slice(&chunk?);
}
let body: HttpBinResponse = serde_json::from_slice(&body).unwrap();
Ok(body.json)
}
async fn create_something(
some_data: web::Json<SomeData>,
client: web::Data<Client>,
) -> Result<HttpResponse, Error> {
let some_data_2 = step_x(some_data.into_inner(), &client).await?;
let some_data_3 = step_x(some_data_2, &client).await?;
let d = step_x(some_data_3, &client).await?;
Ok(HttpResponse::Ok()
.content_type("application/json")
.body(serde_json::to_string(&d).unwrap()))
}
#[actix_web::main]
async fn main() -> io::Result<()> {
std::env::set_var("RUST_LOG", "actix_web=info");
env_logger::init();
let endpoint = "127.0.0.1:8080";
println!("Starting server at: {:?}", endpoint);
HttpServer::new(|| {
App::new()
.data(Client::default())
.service(web::resource("/something").route(web::post().to(create_something)))
})
.bind(endpoint)?
.run()
.await
}
Example 6: Actix Web run in Thread
Cargo.toml
[dependencies]
actix-web = "3"
env_logger = "0.8"
main.rs
use std::sync::mpsc;
use std::{thread, time};
use actix_web::{dev::Server, middleware, rt, web, App, HttpRequest, HttpServer};
async fn index(req: HttpRequest) -> &'static str {
println!("REQ: {:?}", req);
"Hello world!"
}
fn run_app(tx: mpsc::Sender<Server>) -> std::io::Result<()> {
let mut sys = rt::System::new("test");
// srv is server controller type, dev::Server
let srv = HttpServer::new(|| {
App::new()
// enable logger
.wrap(middleware::Logger::default())
.service(web::resource("/index.html").to(|| async { "Hello world!" }))
.service(web::resource("/").to(index))
})
.bind("127.0.0.1:8080")?
.run();
// send server controller to main thread
let _ = tx.send(srv.clone());
// run future
sys.block_on(srv)
}
fn main() {
std::env::set_var("RUST_LOG", "actix_web=info,actix_server=trace");
env_logger::init();
let (tx, rx) = mpsc::channel();
println!("START SERVER");
thread::spawn(move || {
let _ = run_app(tx);
});
let srv = rx.recv().unwrap();
println!("WAITING 10 SECONDS");
thread::sleep(time::Duration::from_secs(10));
println!("STOPPING SERVER");
// init stop server and wait until server gracefully exit
rt::System::new("").block_on(srv.stop(true));
}
Example 7: Actix shutdown Server
Cargo.toml
[dependencies]
actix-web = "3"
env_logger = "0.8"
futures = "0.3"
tokio = { version = "0.2", features = ["signal"] }
main.rs
use actix_web::{get, middleware, post, web, App, HttpResponse, HttpServer};
use futures::executor;
use std::{sync::mpsc, thread};
#
async fn hello() -> &'static str {
"Hello world!"
}
#[post("/stop")]
async fn stop(stopper: web::Data<mpsc::Sender<()>>) -> HttpResponse {
// make request that sends message through the Sender
stopper.send(()).unwrap();
HttpResponse::NoContent().finish()
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
std::env::set_var("RUST_LOG", "actix_server=debug,actix_web=debug");
env_logger::init();
// create a channel
let (tx, rx) = mpsc::channel::<()>();
let bind = "127.0.0.1:8080";
// start server as normal but don't .await after .run() yet
let server = HttpServer::new(move || {
// give the server a Sender in .data
App::new()
.data(tx.clone())
.wrap(middleware::Logger::default())
.service(hello)
.service(stop)
})
.bind(&bind)?
.run();
// clone the Server handle
let srv = server.clone();
thread::spawn(move || {
// wait for shutdown signal
rx.recv().unwrap();
// stop server gracefully
executor::block_on(srv.stop(true))
});
// run server
server.await
}
Example 8: Actix Web State Example
Application may have multiple data objects that are shared across
all handlers within same Application.
For global shared state, we wrap our state in a actix_web::web::Data
and move it into
the factory closure. The closure is called once-per-thread, and we clone our state
and attach to each instance of the App
with .app_data(state.clone())
.
For thread-local state, we construct our state within the factory closure and attach to
the app with .data(state)
.
We retrieve our app state within our handlers with a state: Data<...>
argument.
By default, actix-web
runs one App
per logical cpu core.
When running on cores, we see that the example will increment counter1
(global state via
Mutex) and counter3
(global state via Atomic variable) each time the endpoint is called,
but only appear to increment counter2
every Nth time on average (thread-local state). This
is because the workload is being shared equally among cores.
Cargo.toml
[dependencies]
actix-web = "3"
env_logger = "0.8"
main.rs
use std::cell::Cell;
use std::io;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Mutex;
use actix_web::{middleware, web, App, HttpRequest, HttpResponse, HttpServer};
/// simple handle
async fn index(
counter1: web::Data<Mutex<usize>>,
counter2: web::Data<Cell<u32>>,
counter3: web::Data<AtomicUsize>,
req: HttpRequest,
) -> HttpResponse {
println!("{:?}", req);
// Increment the counters
*counter1.lock().unwrap() += 1;
counter2.set(counter2.get() + 1);
counter3.fetch_add(1, Ordering::SeqCst);
let body = format!(
"global mutex counter: {}, local counter: {}, global atomic counter: {}",
*counter1.lock().unwrap(),
counter2.get(),
counter3.load(Ordering::SeqCst),
);
HttpResponse::Ok().body(body)
}
#[actix_web::main]
async fn main() -> io::Result<()> {
std::env::set_var("RUST_LOG", "actix_web=info");
env_logger::init();
// Create some global state prior to building the server
#[allow(clippy::mutex_atomic)] // it's intentional.
let counter1 = web::Data::new(Mutex::new(0usize));
let counter3 = web::Data::new(AtomicUsize::new(0usize));
// move is necessary to give closure below ownership of counter1
HttpServer::new(move || {
// Create some thread-local state
let counter2 = Cell::new(0u32);
App::new()
.app_data(counter1.clone()) // add shared state
.app_data(counter3.clone()) // add shared state
.data(counter2) // add thread-local state
// enable logger
.wrap(middleware::Logger::default())
// register simple handler
.service(web::resource("/").to(index))
})
.bind("127.0.0.1:8080")?
.run()
.await
}