I want to route both http://0.0.0.0/foo and http:0.0.0.0/foo/ to the same get_foo handler. However, in practice, only /foo gets routed and /foo/ 404s. I suspect I'm setting up/attaching the middleware wrong:
use axum::http::StatusCode;
use axum::{routing::{get}, Router};
use std::{net::SocketAddr};
use tower_http::normalize_path::NormalizePathLayer;
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/foo", get(get_foo))
.layer(NormalizePathLayer::trim_trailing_slash());
let port_str = std::env::var("PORT").unwrap_or("8000".to_owned());
let port = port_str.parse::<u16>().unwrap();
let addr = SocketAddr::from(([0, 0, 0, 0], port));
println!("listening on http://{}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
async fn get_foo() -> Result<String, StatusCode> {
Ok("Hello from foo.".to_owned())
}
... and accompanying Cargo.toml:
[package]
name = "axum_trailing_slash"
version = "0.1.0"
edition = "2021"
[dependencies]
axum = { version = "0.6" }
tokio = { version = "1.0", features = ["full"] }
tower = { version = "0.4" }
tower-http = { version = "0.3", features = ["normalize-path"] }
From the docs of Router::layer:
Middleware added with this method will run after routing and thus cannot be used to rewrite the request URI. See “Rewriting request URI in middleware” for more details and a workaround.
The workaround is to wrap the middleware around the entire Router (this works because Router implements Service):
//…
use axum::ServiceExt;
use tower::layer::Layer;
#[tokio::main]
async fn main() {
let app =
NormalizePathLayer::trim_trailing_slash().layer(Router::new().route("/foo", get(get_foo)));
//…
}
Related
I am trying to make a multilingual website that redirects you to the homepage in the corresponding language when accessing it's / route, but for some reason Axum is no being able to redirect, but only when setting the redirection from the / route and routing normalization is active.
use axum::{
http::{header::HeaderMap, StatusCode},
response::{IntoResponse, Redirect},
routing::get,
Router, ServiceExt,
};
use std::net::SocketAddr;
use tower::layer::Layer;
use tower_http::normalize_path::NormalizePathLayer;
#[tokio::main]
async fn main() {
let addr = SocketAddr::from(([127, 0, 0, 1], 9090));
axum::Server::bind(&addr)
.serve(
NormalizePathLayer::trim_trailing_slash()
.layer(
Router::new()
.route("/", get(redirect))
.fallback(handler_404),
)
.into_make_service(),
)
.await
.unwrap();
}
async fn redirect(_headers: HeaderMap) -> Redirect {
Redirect::temporary("/404")
}
async fn handler_404() -> impl IntoResponse {
(StatusCode::NOT_FOUND, "nothing to see here")
}
[package]
name = "minimal"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
accept-language = "2.0.0"
axum = "0.6.5"
tokio = { version = "1.25.0", features = ["full"] }
tower = "0.4.13"
tower-http = { version = "0.3.5", features = ["normalize-path"] }
use = "0.0.0"
I'm want to extract my AppState struct from HttpRequest in my handler function.
It works when I'm calling that handler through my main app instance, but doesn't work inside integration tests.
Handler function:
pub async fn handle(req: HttpRequest, user: Json<NewUser>) -> Result<HttpResponse, ShopError> {
let state = req.app_data::<Data<AppState>>().expect("app_data is empty!");
Ok(HttpResponse::Ok().finish())
}
main.rs:
#[actix_web::main]
pub async fn main() -> std::io::Result<()> {
dotenv().ok();
let chat_server = Lobby::default().start();
let state = AppState {
static_data: String::from("my_data")
};
HttpServer::new(move || {
App::new()
.app_data(Data::new(state.clone()))
.service(web::scope("/").configure(routes::router))
.app_data(Data::new(chat_server.clone()))
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
Test:
#[actix_web::test]
async fn sanity_test() {
let app = test::init_service(App::new().route("/", web::post().to(handle))).await;
let user = NewUser {
username: String::from("legit_user"),
password: String::from("123"),
};
let state = AppState {
static_data: String::from("my_data")
};
let req = test::TestRequest::post()
.app_data(Data::new(state))
.set_json(&user)
.uri("/")
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
}
Test output:
running 1 test
thread 'routes::login::tests::sanity_test' panicked at 'app_data is empty!', src/routes/login.rs:35:50
For some reason it is always None. I've tried using Data<AppState> extractor but then whole handler is not even called, again, only when testing, otherwise everything works.
Cargo.toml:
[dependencies]
diesel = { version = "1.4.4", features = ["postgres", "r2d2", "chrono"] }
diesel_migrations = "1.4.0"
chrono = { version = "0.4.19", features = ["serde"] }
dotenv = "0.15.0"
actix = "0.13.0"
actix-web = "4.1.0"
actix-web-actors = "4.1.0"
bcrypt = "0.13.0"
uuid = { version = "1.1.2", features = ["serde", "v4"] }
serde_json = "1.0.82"
serde = { version = "1.0.139", features = ["derive"] }
validator = { version = "0.16.0", features = ["derive"] }
derive_more = "0.99.17"
r2d2 = "0.8.10"
lazy_static = "1.4.0"
jsonwebtoken = "8.1.1"
I'm aware of app_data is always None in request handler thread, and it does not solve my problem since for me everything works except when testing.
From what I see in your integration test, app_data is not configured for the app instance passed to test::init_service, which is why the handler panics. When the app is configured with app_data as you have done it in main, app_data becomes available for the handler.
With the code below, the handler can access AppData in the integration test. The main difference from the original post is that the app in the integration test is configured with app_data, not the request.
use actix_web::{
HttpServer,
HttpRequest,
HttpResponse,
App,
web::{Data, Json},
post,
};
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize)]
pub struct NewUser{
username: String,
password: String,
}
#[derive(Clone)]
struct AppState{
static_data: String,
}
#[post("/")]
pub async fn handle(
req: HttpRequest,
_user: Json<NewUser>
) -> HttpResponse {
let state = req
.app_data::<Data<AppState>>()
.expect("app_data is empty!");
println!("Static data: {}", state.static_data);
HttpResponse::Ok().finish()
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let state = AppState{
static_data: "Sparta".to_string(),
};
HttpServer::new(move || {
App::new()
.app_data(Data::new(state.clone()))
.service(handle)
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
#[cfg(test)]
mod tests {
use super::*;
use actix_web::{
test,
};
#[actix_web::test]
async fn itest() {
// Set up app with test::init_service
let state = AppState {
static_data: "Sparta".to_string(),
};
let app = test::init_service(
App::new()
.app_data(Data::new(state.clone()))
.service(handle)
).await;
// Prepare request
let sample_user = NewUser{
username: "legit_user".to_string(),
password: "nosecret123".to_string(),
};
let req = test::TestRequest::post()
.set_json(&sample_user)
.uri("/")
.to_request();
// Send request and await response
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success())
}
}
The documentation for reqwest v0.9.18 shows the following example of posting a file:
let file = fs::File::open("from_a_file.txt")?;
let client = reqwest::Client::new();
let res = client.post("http://httpbin.org/post")
.body(file)
.send()?;
The latest documentation for reqwest v0.11 no longer includes this example, and trying to build it fails with the following error when calling body():
the trait `From<std::fs::File>` is not implemented for `Body`
What is the updated method for sending a file?
The specific example you're linking to, was prior to the reqwest crate using async. If you want to use that exact example, then instead of reqwest::Client, you need to use reqwest::blocking::Client. This also requires enabling the blocking feature.
To be clear, you can actually still find that example, it's just located in the docs for reqwest::blocking::RequestBuilder's body() method instead.
// reqwest = { version = "0.11", features = ["blocking"] }
use reqwest::blocking::Client;
use std::fs::File;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let file = File::open("from_a_file.txt")?;
let client = Client::new();
let res = client.post("http://httpbin.org/post")
.body(file)
.send()?;
Ok(())
}
Also check out reqwest's Form and RequestBuilder's multipart() method, as there for instance is a file() method.
If you do want to use async, then you can use FramedRead from the tokio-util crate. Along with the TryStreamExt trait, from the futures crate.
Just make sure to enable the stream feature for reqwest, and the codec feature for tokio-util.
// futures = "0.3"
use futures::stream::TryStreamExt;
// reqwest = { version = "0.11", features = ["stream"] }
use reqwest::{Body, Client};
// tokio = { version = "1.0", features = ["full"] }
use tokio::fs::File;
// tokio-util = { version = "0.6", features = ["codec"] }
use tokio_util::codec::{BytesCodec, FramedRead};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let file = File::open("from_a_file.txt").await?;
let client = reqwest::Client::new();
let res = client
.post("http://httpbin.org/post")
.body(file_to_body(file))
.send()
.await?;
Ok(())
}
fn file_to_body(file: File) -> Body {
let stream = FramedRead::new(file, BytesCodec::new());
let body = Body::wrap_stream(stream);
body
}
If you want to use multipart/form-data and you are using Tokio
already, this approach could help you.
1. Setup Dependencies
# Cargo.toml
[dependencies]
tokio = { version = "1.19", features = ["macros", "rt-multi-thread"] }
reqwest = { version = "0.11.11", features = ["stream","multipart","json"] }
tokio-util = { version = "0.7.3", features = ["codec"] }
2. Upload file using multipart/form-data
use reqwest::{multipart, Body, Client};
use tokio::fs::File;
use tokio_util::codec::{BytesCodec, FramedRead};
async fn reqwest_multipart_form(url: &str) -> anyhow::Result<String> {
let client = Client::new();
let file = File::open(".gitignore").await?;
// read file body stream
let stream = FramedRead::new(file, BytesCodec::new());
let file_body = Body::wrap_stream(stream);
//make form part of file
let some_file = multipart::Part::stream(file_body)
.file_name("gitignore.txt")
.mime_str("text/plain")?;
//create the multipart form
let form = multipart::Form::new()
.text("username", "seanmonstar")
.text("password", "secret")
.part("file", some_file);
//send request
let response = client.post(url).multipart(form).send().await?;
let result = response.text().await?;
Ok(result)
}
3. Unit Testing
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_post_form_file() {
let url = "http://httpbin.org/post?a=1&b=true";
let get_json = reqwest_multipart_form(url).await.unwrap();
println!("users: {:#?}", get_json);
}
}
the crate streamer can do that for you with feature hyper enabled:
use hyper::{Body, Request}:
let file = File::open("from_a_file.txt").unwrap();
let mut streaming = Streamer::new(file)
// optional, set the field name
// streaming.meta.set_name("txt");
// optional, set the file name
streaming.meta.set_filename("from_a_file.txt");
// length sent as a chunk, the default is 64kB if not set
streaming.meta.set_buf_len(1024 * 1024);
let body: Body = streaming.streaming();
// build a request
let request: Request<Body> = Request::post("<uri-here>").body(body).expect("failed to build a request");
streamer will stream your file in 1 Mega-bytes chunks
I am upgrading tokio-tungstenite from 0.9 to 0.10, which no longer has the peer_addr method. I am dealing with WebSocketStream. I have tried this, which would have worked before:
use tokio_tungstenite::connect_async;
fn main() {
async fn test() {
println!("Hello, world!");
let url = "wss://echo.websocket.org".to_string();
let (ws_stream, ws_response) = connect_async(url).await.unwrap();
let addr = ws_stream.peer_addr();
println!("peer addr {:?}", addr);
}
}
peer_addr() no longer exists and I can see no replacement or instructions on what to do now. I've tried searching the documentation and GitHub.
Cargo.toml:
futures = { version = "0.3" }
tungstenite = {version="0.11.0", features=["tls"] }
tokio-tungstenite = { version="0.10.1", features = ["tls"] }
tokio = {version ="0.2.21", features = [ "full" ] }
url = "2.0"
The documentation on docs.rs is incorrect. If you build the docs locally, you'll see the correct signature of connect_async:
pub async fn connect_async<R>(
request: R
) -> Result<(WebSocketStream<MaybeTlsStream<TcpStream>>, Response), Error>
where
R: IntoClientRequest + Unpin,
You need to get to the TcpStream to get access to the peer address:
use tokio_tungstenite::{connect_async, stream::Stream};
#[tokio::main]
async fn main() {
let (ws_stream, _) = connect_async("wss://echo.websocket.org").await.unwrap();
let tcp_stream = match ws_stream.get_ref() {
Stream::Plain(s) => s,
Stream::Tls(t) => t.get_ref(),
};
let addr = tcp_stream
.peer_addr()
.expect("connected streams should have a peer address");
println!("peer addr {:?}", addr);
}
Cargo.toml:
[dependencies]
tokio-tungstenite = { version = "0.10.1", features = ["tls"] }
tokio = { version = "0.2.21", features = [ "full" ] }
Note that your original Cargo.toml contains two different versions of tungstenite and you really don't want that. See also Why is a trait not implemented for a type that clearly has it implemented?.
How can I make an HTTP request from Rust? I can't seem to find anything in the core library.
I don't need to parse the output, just make a request and check the HTTP response code.
Bonus marks if someone can show me how to URL encode the query parameters on my URL!
The easiest way to make HTTP requests in Rust is with the reqwest crate:
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let resp = reqwest::blocking::get("https://httpbin.org/ip")?.text()?;
println!("{:#?}", resp);
Ok(())
}
In Cargo.toml:
[dependencies]
reqwest = { version = "0.11", features = ["blocking"] }
Async
Reqwest also supports making asynchronous HTTP requests using Tokio:
use std::error::Error;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let resp = reqwest::get("https://httpbin.org/ip")
.await?
.text()
.await?;
println!("{:#?}", resp);
Ok(())
}
In Cargo.toml:
[dependencies]
reqwest = "0.11"
tokio = { version = "1", features = ["full"] }
Hyper
Reqwest is an easy to use wrapper around Hyper, which is a popular HTTP library for Rust. You can use it directly if you need more control over managing connections. A Hyper-based example is below and is largely inspired by an example in its documentation:
use hyper::{body::HttpBody as _, Client, Uri};
use std::error::Error;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let client = Client::new();
let res = client
.get(Uri::from_static("http://httpbin.org/ip"))
.await?;
println!("status: {}", res.status());
let buf = hyper::body::to_bytes(res).await?;
println!("body: {:?}", buf);
}
In Cargo.toml:
[dependencies]
hyper = { version = "0.14", features = ["full"] }
tokio = { version = "1", features = ["full"] }
Original answer (Rust 0.6)
I believe what you're looking for is in the standard library. now in rust-http and Chris Morgan's answer is the standard way in current Rust for the foreseeable future. I'm not sure how far I can take you (and hope I'm not taking you the wrong direction!), but you'll want something like:
// Rust 0.6 -- old code
extern mod std;
use std::net_ip;
use std::uv;
fn main() {
let iotask = uv::global_loop::get();
let result = net_ip::get_addr("www.duckduckgo.com", &iotask);
io::println(fmt!("%?", result));
}
As for encoding, there are some examples in the unit tests in src/libstd/net_url.rs.
Update: This answer refers to fairly ancient history. For the current best practices, please look at Isaac Aggrey's answer instead.
I've been working on rust-http, which has become the de facto HTTP library for Rust (Servo uses it); it's far from complete and very poorly documented at present. Here's an example of making a request and doing something with the status code:
extern mod http;
use http::client::RequestWriter;
use http::method::Get;
use http::status;
use std::os;
fn main() {
let request = RequestWriter::new(Get, FromStr::from_str(os::args()[1]).unwrap());
let response = match request.read_response() {
Ok(response) => response,
Err(_request) => unreachable!(), // Uncaught condition will have failed first
};
if response.status == status::Ok {
println!("Oh goodie, I got me a 200 OK response!");
} else {
println!("That URL ain't returning 200 OK, it returned {} instead", response.status);
}
}
Run this code with a URL as the sole command-line argument and it'll check the status code! (HTTP only; no HTTPS.)
Compare with src/examples/client/client.rs for an example that does a little more.
rust-http is tracking the master branch of rust. At present it'll work in the just-released Rust 0.8, but there are likely to be breaking changes soon. Actually, no version of rust-http works on Rust 0.8—there was a breaking change which can't be worked around in privacy rules just before the release, leaving something that rust-http depends on in extra::url inaccessible. This has since been fixed, but it leaves rust-http incompatible with Rust 0.8.
As for the query string encoding matter, at present that should be done with extra::url::Query (a typedef for ~[(~str, ~str)]). Appropriate functions for conversions:
extra::url::query_to_str
extra::url::query_from_str (sorry, can't use this just at present as it's private. PR to make it public about to come. In the mean time, this link actually shouldn't work, it's only available because of https://github.com/mozilla/rust/issues/7476.)
Using curl bindings. Stick this in your Cargo.toml:
[dependencies.curl]
git = "https://github.com/carllerche/curl-rust"
...and this in the src/main.rs:
extern crate curl;
use curl::http;
fn main(){
let resp = http::handle()
.post("http://localhost:3000/login", "username=dude&password=sikrit")
.exec().unwrap();
println!("code={}; headers={}; body={}",
resp.get_code(), resp.get_headers(), resp.get_body());
}
I prefer Crates with low dependency count, so I would recommend these:
MinReq (0 deps)
use minreq;
fn main() -> Result<(), minreq::Error> {
let o = minreq::get("https://speedtest.lax.hivelocity.net").send()?;
let s = o.as_str()?;
print!("{}", s);
Ok(())
}
HTTP_Req (35 deps)
use {http_req::error, http_req::request, std::io, std::io::Write};
fn main() -> Result<(), error::Error> {
let mut a = Vec::new();
request::get("https://speedtest.lax.hivelocity.net", &mut a)?;
io::stdout().write(&a)?;
Ok(())
}
To elaborate on Isaac Aggrey's answer, here's an example of making a POST request with query parameters using the reqwest library.
Cargo.toml
[package]
name = "play_async"
version = "0.1.0"
edition = "2018"
[dependencies]
reqwest = "0.10.4"
tokio = { version = "0.2.21", features = ["macros"] }
Code
use reqwest::Client;
type Error = Box<dyn std::error::Error>;
type Result<T, E = Error> = std::result::Result<T, E>;
async fn post_greeting() -> Result<()> {
let client = Client::new();
let req = client
// or use .post, etc.
.get("https://webhook.site/1dff66fd-07ff-4cb5-9a77-681efe863747")
.header("Accepts", "application/json")
.query(&[("hello", "1"), ("world", "ABCD")]);
let res = req.send().await?;
println!("{}", res.status());
let body = res.bytes().await?;
let v = body.to_vec();
let s = String::from_utf8_lossy(&v);
println!("response: {} ", s);
Ok(())
}
#[tokio::main]
async fn main() -> Result<()> {
post_greeting().await?;
Ok(())
}
Go to https://webhook.site and create your webhook link and change the code to match. You'll see the request was received on server in realtime.
This example was originally based on Bastian Gruber's example and has been updated for modern Rust syntax and newer crate versions.
Building upon Patrik Stas' answer, if you want to do an HTTP form URL-encoded POST, here is what you have to do. In this case, it's to get an OAuth client_credentials token.
Cargo.toml
[dependencies]
reqwest = "0.10.4"
tokio = { version = "0.2.21", features = ["macros"] }
Code
use reqwest::{Client, Method};
type Error = Box<dyn std::error::Error>;
type Result<T, E = Error> = std::result::Result<T, E>;
async fn print_access_token() -> Result<()> {
let client = Client::new();
let host = "login.microsoftonline.com";
let tenant = "TENANT";
let client_id = "CLIENT_ID";
let client_secret = "CLIENT_SECRET";
let scope = "https://graph.microsoft.com/.default";
let grant_type = "client_credentials";
let url_string = format!("https://{}/{}/oauth2/v2.0/token", host, tenant);
let body = format!(
"client_id={}&client_secret={}&scope={}&grant_type={}",
client_id, client_secret, scope, grant_type,
);
let req = client.request(Method::POST, &url_string).body(body);
let res = req.send().await?;
println!("{}", res.status());
let body = res.bytes().await?;
let v = body.to_vec();
let s = String::from_utf8_lossy(&v);
println!("response: {} ", s);
Ok(())
}
#[tokio::main]
async fn main() -> Result<()> {
print_access_token().await?;
Ok(())
}
This will print something like the following.
200 OK
response: {"token_type":"Bearer","expires_in":3599,"ext_expires_in":3599,"access_token":"ACCESS_TOKEN"}
Dropping a version here that uses the surf crate (dual to the tide crate):
let res = surf::get("https://httpbin.org/get").await?;
assert_eq!(res.status(), 200);
Using hyper "0.13"
Also using hyper-tls for HTTPS support.
File Cargo.toml
hyper = "0.13"
hyper-tls = "0.4.1"
tokio = { version = "0.2", features = ["full"] }
Code
extern crate hyper;
use hyper::Client;
use hyper::body::HttpBody as _;
use tokio::io::{stdout, AsyncWriteExt as _};
use hyper_tls::HttpsConnector;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// HTTP only
// let client = Client::new();
// http or https connections
let client = Client::builder().build::<_, hyper::Body>(HttpsConnector::new());
let mut resp = client.get("https://catfact.ninja/fact".parse()?).await?;
println!("Response: {}", resp.status());
while let Some(chunk) = resp.body_mut().data().await {
stdout().write_all(&chunk?).await?;
}
Ok(())
}
Adapted from https://hyper.rs/guides/client/basic/
Simple http request with this crate: wsd
fn test() {
wsd::http::get("https://docs.rs/", |data| {
println!("status = {}, data = {}", data.status(), data.text());
});
}