mirror of
https://github.com/rwf2/Rocket.git
synced 2025-01-09 03:02:45 +00:00
4c6562cd29
This allows responses to be sent to the client even when data is only partially read, significantly improving the experience for the client from one with a "connection closed" error to one with a proper response. The consequence is a lifetime in 'Data'. Though other non-lifetime-introducing solutions exist, the introduction of a lifetime to 'Data' is a longstanding desire as it prevents smuggling 'Data' into a longer-lived context. Use of 'Data' in that context was unspecified with various runtime consequences. The addition of a lifetime bound by the request prevents this error statically. In summary, the changes are: * Clients receive responses even when data isn't fully read. * 'Data' becomes 'Data<'r>'. 'FromData' changes accordingly. * Route 'Outcome's are strictly tied to the request lifetime. Tangentially, the invalid length form field validation error message has improved to format length in byte units if it exceeds 1024.
347 lines
10 KiB
Rust
347 lines
10 KiB
Rust
// Rocket sometimes generates mangled identifiers that activate the
|
|
// non_snake_case lint. We deny the lint in this test to ensure that
|
|
// code generation uses #[allow(non_snake_case)] in the appropriate places.
|
|
#![deny(non_snake_case)]
|
|
|
|
#[macro_use] extern crate rocket;
|
|
|
|
use std::path::PathBuf;
|
|
|
|
use rocket::request::Request;
|
|
use rocket::http::ext::Normalize;
|
|
use rocket::local::blocking::Client;
|
|
use rocket::data::{self, Data, FromData};
|
|
use rocket::http::{Status, RawStr, ContentType, uri::fmt::Path};
|
|
|
|
// Use all of the code generation available at once.
|
|
|
|
#[derive(FromForm, UriDisplayQuery)]
|
|
struct Inner<'r> {
|
|
field: &'r str
|
|
}
|
|
|
|
struct Simple(String);
|
|
|
|
#[async_trait]
|
|
impl<'r> FromData<'r> for Simple {
|
|
type Error = std::io::Error;
|
|
|
|
async fn from_data(req: &'r Request<'_>, data: Data<'r>) -> data::Outcome<'r, Self> {
|
|
String::from_data(req, data).await.map(Simple)
|
|
}
|
|
}
|
|
|
|
#[post(
|
|
"/<a>/<name>/name/<path..>?sky=blue&<sky>&<query..>",
|
|
format = "json",
|
|
data = "<simple>",
|
|
rank = 138
|
|
)]
|
|
fn post1(
|
|
sky: usize,
|
|
name: &str,
|
|
a: String,
|
|
query: Inner<'_>,
|
|
path: PathBuf,
|
|
simple: Simple,
|
|
) -> String {
|
|
let string = format!("{}, {}, {}, {}, {}, {}",
|
|
sky, name, a, query.field, path.normalized_str(), simple.0);
|
|
|
|
let uri = uri!(post1(a, name, path, sky, query));
|
|
|
|
format!("({}) ({})", string, uri.to_string())
|
|
}
|
|
|
|
#[route(
|
|
POST,
|
|
uri = "/<a>/<name>/name/<path..>?sky=blue&<sky>&<query..>",
|
|
format = "json",
|
|
data = "<simple>",
|
|
rank = 138
|
|
)]
|
|
fn post2(
|
|
sky: usize,
|
|
name: &str,
|
|
a: String,
|
|
query: Inner<'_>,
|
|
path: PathBuf,
|
|
simple: Simple,
|
|
) -> String {
|
|
let string = format!("{}, {}, {}, {}, {}, {}",
|
|
sky, name, a, query.field, path.normalized_str(), simple.0);
|
|
|
|
let uri = uri!(post2(a, name, path, sky, query));
|
|
|
|
format!("({}) ({})", string, uri.to_string())
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
#[post("/<_unused_param>?<_unused_query>", data="<_unused_data>")]
|
|
fn test_unused_params(_unused_param: String, _unused_query: String, _unused_data: Data<'_>) {
|
|
}
|
|
|
|
#[test]
|
|
fn test_full_route() {
|
|
let rocket = rocket::build()
|
|
.mount("/1", routes![post1])
|
|
.mount("/2", routes![post2]);
|
|
|
|
let client = Client::debug(rocket).unwrap();
|
|
|
|
let a = RawStr::new("A%20A");
|
|
let name = RawStr::new("Bob%20McDonald");
|
|
let path = "this/path/here";
|
|
let sky = 777;
|
|
let query = "field=inside";
|
|
let simple = "data internals";
|
|
|
|
let path_part = format!("/{}/{}/name/{}", a, name, path);
|
|
let query_part = format!("?sky={}&sky=blue&{}", sky, query);
|
|
let uri = format!("{}{}", path_part, query_part);
|
|
let expected_uri = format!("{}?sky=blue&sky={}&{}", path_part, sky, query);
|
|
|
|
let response = client.post(&uri).body(simple).dispatch();
|
|
assert_eq!(response.status(), Status::NotFound);
|
|
|
|
let response = client.post(format!("/1{}", uri)).body(simple).dispatch();
|
|
assert_eq!(response.status(), Status::NotFound);
|
|
|
|
let response = client
|
|
.post(format!("/1{}", uri))
|
|
.header(ContentType::JSON)
|
|
.body(simple)
|
|
.dispatch();
|
|
|
|
assert_eq!(response.into_string().unwrap(), format!("({}, {}, {}, {}, {}, {}) ({})",
|
|
sky, name.percent_decode().unwrap(), "A A", "inside", path, simple, expected_uri));
|
|
|
|
let response = client.post(format!("/2{}", uri)).body(simple).dispatch();
|
|
assert_eq!(response.status(), Status::NotFound);
|
|
|
|
let response = client
|
|
.post(format!("/2{}", uri))
|
|
.header(ContentType::JSON)
|
|
.body(simple)
|
|
.dispatch();
|
|
|
|
assert_eq!(response.into_string().unwrap(), format!("({}, {}, {}, {}, {}, {}) ({})",
|
|
sky, name.percent_decode().unwrap(), "A A", "inside", path, simple, expected_uri));
|
|
}
|
|
|
|
mod scopes {
|
|
#![allow(dead_code)]
|
|
|
|
mod other {
|
|
#[get("/world")]
|
|
pub fn world() -> &'static str {
|
|
"Hello, world!"
|
|
}
|
|
}
|
|
|
|
#[get("/hello")]
|
|
pub fn hello() -> &'static str {
|
|
"Hello, outside world!"
|
|
}
|
|
|
|
use other::world;
|
|
|
|
fn _rocket() -> rocket::Rocket<rocket::Build> {
|
|
rocket::build().mount("/", rocket::routes![hello, world, other::world])
|
|
}
|
|
}
|
|
|
|
use rocket::form::Contextual;
|
|
|
|
#[derive(Default, Debug, PartialEq, FromForm)]
|
|
struct Filtered<'r> {
|
|
bird: Option<&'r str>,
|
|
color: Option<&'r str>,
|
|
cat: Option<&'r str>,
|
|
rest: Option<&'r str>,
|
|
}
|
|
|
|
#[get("/?bird=1&color=blue&<bird>&<color>&cat=bob&<rest..>")]
|
|
fn filtered_raw_query(bird: usize, color: &str, rest: Contextual<'_, Filtered<'_>>) -> String {
|
|
assert_ne!(bird, 1);
|
|
assert_ne!(color, "blue");
|
|
assert_eq!(rest.value.unwrap(), Filtered::default());
|
|
|
|
format!("{} - {}", bird, color)
|
|
}
|
|
|
|
#[test]
|
|
fn test_filtered_raw_query() {
|
|
let rocket = rocket::build().mount("/", routes![filtered_raw_query]);
|
|
let client = Client::debug(rocket).unwrap();
|
|
|
|
#[track_caller]
|
|
fn run(client: &Client, birds: &[&str], colors: &[&str], cats: &[&str]) -> (Status, String) {
|
|
let join = |slice: &[&str], name: &str| slice.iter()
|
|
.map(|v| format!("{}={}", name, v))
|
|
.collect::<Vec<_>>()
|
|
.join("&");
|
|
|
|
let q = format!("{}&{}&{}",
|
|
join(birds, "bird"),
|
|
join(colors, "color"),
|
|
join(cats, "cat"));
|
|
|
|
let response = client.get(format!("/?{}", q)).dispatch();
|
|
let status = response.status();
|
|
let body = response.into_string().unwrap();
|
|
|
|
(status, body)
|
|
}
|
|
|
|
let birds = &["2", "3"];
|
|
let colors = &["red", "blue", "green"];
|
|
let cats = &["bob", "bob"];
|
|
assert_eq!(run(&client, birds, colors, cats).0, Status::NotFound);
|
|
|
|
let birds = &["2", "1", "3"];
|
|
let colors = &["red", "green"];
|
|
let cats = &["bob", "bob"];
|
|
assert_eq!(run(&client, birds, colors, cats).0, Status::NotFound);
|
|
|
|
let birds = &["2", "1", "3"];
|
|
let colors = &["red", "blue", "green"];
|
|
let cats = &[];
|
|
assert_eq!(run(&client, birds, colors, cats).0, Status::NotFound);
|
|
|
|
let birds = &["2", "1", "3"];
|
|
let colors = &["red", "blue", "green"];
|
|
let cats = &["bob", "bob"];
|
|
assert_eq!(run(&client, birds, colors, cats).1, "2 - red");
|
|
|
|
let birds = &["1", "2", "1", "3"];
|
|
let colors = &["blue", "red", "blue", "green"];
|
|
let cats = &["bob"];
|
|
assert_eq!(run(&client, birds, colors, cats).1, "2 - red");
|
|
|
|
let birds = &["5", "1"];
|
|
let colors = &["blue", "orange", "red", "blue", "green"];
|
|
let cats = &["bob"];
|
|
assert_eq!(run(&client, birds, colors, cats).1, "5 - orange");
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, FromForm)]
|
|
struct Dog<'r> {
|
|
name: &'r str,
|
|
age: usize
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, FromForm)]
|
|
struct Q<'r> {
|
|
dog: Dog<'r>
|
|
}
|
|
|
|
#[get("/?<color>&color=red&<q..>")]
|
|
fn query_collection(color: Vec<&str>, q: Q<'_>) -> String {
|
|
format!("{} - {} - {}", color.join("&"), q.dog.name, q.dog.age)
|
|
}
|
|
|
|
#[get("/?<color>&color=red&<dog>")]
|
|
fn query_collection_2(color: Vec<&str>, dog: Dog<'_>) -> String {
|
|
format!("{} - {} - {}", color.join("&"), dog.name, dog.age)
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_collection() {
|
|
#[track_caller]
|
|
fn run(client: &Client, colors: &[&str], dog: &[&str]) -> (Status, String) {
|
|
let join = |slice: &[&str], prefix: &str| slice.iter()
|
|
.map(|v| format!("{}{}", prefix, v))
|
|
.collect::<Vec<_>>()
|
|
.join("&");
|
|
|
|
let q = format!("{}&{}", join(colors, "color="), join(dog, "dog."));
|
|
let response = client.get(format!("/?{}", q)).dispatch();
|
|
(response.status(), response.into_string().unwrap())
|
|
}
|
|
|
|
fn run_tests(rocket: rocket::Rocket<rocket::Build>) {
|
|
let client = Client::debug(rocket).unwrap();
|
|
|
|
let colors = &["blue", "green"];
|
|
let dog = &["name=Fido", "age=10"];
|
|
assert_eq!(run(&client, colors, dog).0, Status::NotFound);
|
|
|
|
let colors = &["red"];
|
|
let dog = &["name=Fido"];
|
|
assert_eq!(run(&client, colors, dog).0, Status::NotFound);
|
|
|
|
let colors = &["red"];
|
|
let dog = &["name=Fido", "age=2"];
|
|
assert_eq!(run(&client, colors, dog).1, " - Fido - 2");
|
|
|
|
let colors = &["red", "blue", "green"];
|
|
let dog = &["name=Fido", "age=10"];
|
|
assert_eq!(run(&client, colors, dog).1, "blue&green - Fido - 10");
|
|
|
|
let colors = &["red", "blue", "green"];
|
|
let dog = &["name=Fido", "age=10", "toy=yes"];
|
|
assert_eq!(run(&client, colors, dog).1, "blue&green - Fido - 10");
|
|
|
|
let colors = &["blue", "red", "blue"];
|
|
let dog = &["name=Fido", "age=10"];
|
|
assert_eq!(run(&client, colors, dog).1, "blue&blue - Fido - 10");
|
|
|
|
let colors = &["blue", "green", "red", "blue"];
|
|
let dog = &["name=Max+Fido", "age=10"];
|
|
assert_eq!(run(&client, colors, dog).1, "blue&green&blue - Max Fido - 10");
|
|
}
|
|
|
|
let rocket = rocket::build().mount("/", routes![query_collection]);
|
|
run_tests(rocket);
|
|
|
|
let rocket = rocket::build().mount("/", routes![query_collection_2]);
|
|
run_tests(rocket);
|
|
}
|
|
|
|
use rocket::request::FromSegments;
|
|
use rocket::http::uri::Segments;
|
|
|
|
struct PathString(String);
|
|
|
|
impl FromSegments<'_> for PathString {
|
|
type Error = std::convert::Infallible;
|
|
|
|
fn from_segments(segments: Segments<'_, Path>) -> Result<Self, Self::Error> {
|
|
Ok(PathString(segments.collect::<Vec<_>>().join("/")))
|
|
}
|
|
|
|
}
|
|
|
|
#[get("/<_>/b/<path..>", rank = 1)]
|
|
fn segments(path: PathString) -> String {
|
|
format!("nonempty+{}", path.0)
|
|
}
|
|
|
|
#[get("/<path..>", rank = 2)]
|
|
fn segments_empty(path: PathString) -> String {
|
|
format!("empty+{}", path.0)
|
|
}
|
|
|
|
#[test]
|
|
fn test_inclusive_segments() {
|
|
let rocket = rocket::build()
|
|
.mount("/", routes![segments])
|
|
.mount("/", routes![segments_empty]);
|
|
|
|
let client = Client::debug(rocket).unwrap();
|
|
let get = |uri| client.get(uri).dispatch().into_string().unwrap();
|
|
|
|
assert_eq!(get("/"), "empty+");
|
|
assert_eq!(get("//"), "empty+");
|
|
assert_eq!(get("//a/"), "empty+a");
|
|
assert_eq!(get("//a//"), "empty+a");
|
|
assert_eq!(get("//a//c/d"), "empty+a/c/d");
|
|
|
|
assert_eq!(get("//a/b"), "nonempty+");
|
|
assert_eq!(get("//a/b/c"), "nonempty+c");
|
|
assert_eq!(get("//a/b//c"), "nonempty+c");
|
|
assert_eq!(get("//a//b////c"), "nonempty+c");
|
|
assert_eq!(get("//a//b////c/d/e"), "nonempty+c/d/e");
|
|
}
|