Percent decode automatically in extract::Path (#272)

* Percent decode automatically in `extract::Path`

Fixes https://github.com/tokio-rs/axum/issues/261

* return an error if path param contains invalid utf-8

* Mention automatic decoding in the docs

* Update changelog: This is a breaking change

* cleanup

* fix tests
This commit is contained in:
David Pedersen
2021-10-02 14:04:29 +00:00
committed by GitHub
parent 2c2bcd7754
commit afabded385
7 changed files with 156 additions and 58 deletions
+29 -31
View File
@@ -1,5 +1,4 @@
use crate::routing::UrlParams;
use crate::util::ByteStr;
use crate::util::{ByteStr, PercentDecodedByteStr};
use serde::{
de::{self, DeserializeSeed, EnumAccess, Error, MapAccess, SeqAccess, VariantAccess, Visitor},
forward_to_deserialize_any, Deserializer,
@@ -53,20 +52,20 @@ macro_rules! parse_single_value {
where
V: Visitor<'de>,
{
if self.url_params.0.len() != 1 {
if self.url_params.len() != 1 {
return Err(PathDeserializerError::custom(
format!(
"wrong number of parameters: {} expected 1",
self.url_params.0.len()
self.url_params.len()
)
.as_str(),
));
}
let value = self.url_params.0[0].1.parse().map_err(|_| {
let value = self.url_params[0].1.parse().map_err(|_| {
PathDeserializerError::custom(format!(
"can not parse `{:?}` to a `{}`",
self.url_params.0[0].1.as_str(),
self.url_params[0].1.as_str(),
$tp
))
})?;
@@ -76,12 +75,12 @@ macro_rules! parse_single_value {
}
pub(crate) struct PathDeserializer<'de> {
url_params: &'de UrlParams,
url_params: &'de [(ByteStr, PercentDecodedByteStr)],
}
impl<'de> PathDeserializer<'de> {
#[inline]
pub(crate) fn new(url_params: &'de UrlParams) -> Self {
pub(crate) fn new(url_params: &'de [(ByteStr, PercentDecodedByteStr)]) -> Self {
PathDeserializer { url_params }
}
}
@@ -114,13 +113,13 @@ impl<'de> Deserializer<'de> for PathDeserializer<'de> {
where
V: Visitor<'de>,
{
if self.url_params.0.len() != 1 {
if self.url_params.len() != 1 {
return Err(PathDeserializerError::custom(format!(
"wrong number of parameters: {} expected 1",
self.url_params.0.len()
self.url_params.len()
)));
}
visitor.visit_str(&self.url_params.0[0].1)
visitor.visit_str(&self.url_params[0].1)
}
fn deserialize_unit<V>(self, visitor: V) -> Result<V::Value, Self::Error>
@@ -157,7 +156,7 @@ impl<'de> Deserializer<'de> for PathDeserializer<'de> {
V: Visitor<'de>,
{
visitor.visit_seq(SeqDeserializer {
params: &self.url_params.0,
params: self.url_params,
})
}
@@ -165,18 +164,18 @@ impl<'de> Deserializer<'de> for PathDeserializer<'de> {
where
V: Visitor<'de>,
{
if self.url_params.0.len() < len {
if self.url_params.len() < len {
return Err(PathDeserializerError::custom(
format!(
"wrong number of parameters: {} expected {}",
self.url_params.0.len(),
self.url_params.len(),
len
)
.as_str(),
));
}
visitor.visit_seq(SeqDeserializer {
params: &self.url_params.0,
params: self.url_params,
})
}
@@ -189,18 +188,18 @@ impl<'de> Deserializer<'de> for PathDeserializer<'de> {
where
V: Visitor<'de>,
{
if self.url_params.0.len() < len {
if self.url_params.len() < len {
return Err(PathDeserializerError::custom(
format!(
"wrong number of parameters: {} expected {}",
self.url_params.0.len(),
self.url_params.len(),
len
)
.as_str(),
));
}
visitor.visit_seq(SeqDeserializer {
params: &self.url_params.0,
params: self.url_params,
})
}
@@ -209,7 +208,7 @@ impl<'de> Deserializer<'de> for PathDeserializer<'de> {
V: Visitor<'de>,
{
visitor.visit_map(MapDeserializer {
params: &self.url_params.0,
params: self.url_params,
value: None,
})
}
@@ -235,21 +234,21 @@ impl<'de> Deserializer<'de> for PathDeserializer<'de> {
where
V: Visitor<'de>,
{
if self.url_params.0.len() != 1 {
if self.url_params.len() != 1 {
return Err(PathDeserializerError::custom(format!(
"wrong number of parameters: {} expected 1",
self.url_params.0.len()
self.url_params.len()
)));
}
visitor.visit_enum(EnumDeserializer {
value: &self.url_params.0[0].1,
value: &self.url_params[0].1,
})
}
}
struct MapDeserializer<'de> {
params: &'de [(ByteStr, ByteStr)],
params: &'de [(ByteStr, PercentDecodedByteStr)],
value: Option<&'de str>,
}
@@ -519,7 +518,7 @@ impl<'de> VariantAccess<'de> for UnitVariant {
}
struct SeqDeserializer<'de> {
params: &'de [(ByteStr, ByteStr)],
params: &'de [(ByteStr, PercentDecodedByteStr)],
}
impl<'de> SeqAccess<'de> for SeqDeserializer<'de> {
@@ -561,18 +560,16 @@ mod tests {
a: i32,
}
fn create_url_params<I, K, V>(values: I) -> UrlParams
fn create_url_params<I, K, V>(values: I) -> Vec<(ByteStr, PercentDecodedByteStr)>
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<str>,
V: AsRef<str>,
{
UrlParams(
values
.into_iter()
.map(|(k, v)| (ByteStr::new(k), ByteStr::new(v)))
.collect(),
)
values
.into_iter()
.map(|(k, v)| (ByteStr::new(k), PercentDecodedByteStr::new(v).unwrap()))
.collect()
}
macro_rules! check_single_value {
@@ -601,6 +598,7 @@ mod tests {
check_single_value!(f32, "123", 123.0);
check_single_value!(f64, "123", 123.0);
check_single_value!(String, "abc", "abc");
check_single_value!(String, "one%20two", "one two");
check_single_value!(char, "a", 'a');
let url_params = create_url_params(vec![("a", "B")]);
+44 -9
View File
@@ -1,14 +1,24 @@
mod de;
use super::{rejection::*, FromRequest};
use crate::{extract::RequestParts, routing::UrlParams};
use crate::{
extract::RequestParts,
routing::{InvalidUtf8InPathParam, UrlParams},
};
use async_trait::async_trait;
use serde::de::DeserializeOwned;
use std::ops::{Deref, DerefMut};
use std::{
borrow::Cow,
ops::{Deref, DerefMut},
};
/// Extractor that will get captures from the URL and parse them using
/// [`serde`].
///
/// Any percent encoded parameters will be automatically decoded. The decoded
/// parameters must be valid UTF-8, otherwise `Path` will fail and return a `400
/// Bad Request` response.
///
/// # Example
///
/// ```rust,no_run
@@ -140,20 +150,45 @@ where
{
type Rejection = PathParamsRejection;
#[allow(warnings)]
async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
const EMPTY_URL_PARAMS: &UrlParams = &UrlParams(Vec::new());
let url_params = if let Some(params) = req
let params = match req
.extensions_mut()
.and_then(|ext| ext.get::<Option<UrlParams>>())
{
params.as_ref().unwrap_or(EMPTY_URL_PARAMS)
} else {
return Err(MissingRouteParams.into());
Some(Some(UrlParams(Ok(params)))) => Cow::Borrowed(params),
Some(Some(UrlParams(Err(InvalidUtf8InPathParam { key })))) => {
return Err(InvalidPathParam::new(key.as_str()).into())
}
Some(None) => Cow::Owned(Vec::new()),
None => {
return Err(MissingRouteParams.into());
}
};
T::deserialize(de::PathDeserializer::new(url_params))
T::deserialize(de::PathDeserializer::new(&*params))
.map_err(|err| PathParamsRejection::InvalidPathParam(InvalidPathParam::new(err.0)))
.map(Path)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::*;
use crate::{handler::get, Router};
#[tokio::test]
async fn percent_decoding() {
let app = Router::new().route(
"/:key",
get(|Path(param): Path<String>| async move { param }),
);
let client = TestClient::new(app);
let res = client.get("/one%20two").send().await;
assert_eq!(res.text().await, "one two");
}
}
+1 -1
View File
@@ -107,7 +107,7 @@ define_rejection! {
/// Rejection type for [`Path`](super::Path) if the capture route
/// param didn't have the expected type.
#[derive(Debug)]
pub struct InvalidPathParam(String);
pub struct InvalidPathParam(pub(crate) String);
impl InvalidPathParam {
pub(super) fn new(err: impl Into<String>) -> Self {