mirror of
https://github.com/tokio-rs/axum.git
synced 2026-08-29 00:00:18 +02:00
feat: default to charset=utf-8 for text content type (#554)
* feat: default to charset=utf-8 for text content type * added changelog && fix comment * fix workflow
This commit is contained in:
@@ -15,6 +15,7 @@ erased-json = ["serde", "serde_json"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = { path = "../axum", version = "0.3" }
|
axum = { path = "../axum", version = "0.3" }
|
||||||
|
mime = "0.3"
|
||||||
tower-service = "0.3"
|
tower-service = "0.3"
|
||||||
|
|
||||||
# optional dependencies
|
# optional dependencies
|
||||||
|
|||||||
@@ -45,23 +45,22 @@ impl IntoResponse for ErasedJson {
|
|||||||
type BodyError = Infallible;
|
type BodyError = Infallible;
|
||||||
|
|
||||||
fn into_response(self) -> Response<Self::Body> {
|
fn into_response(self) -> Response<Self::Body> {
|
||||||
#[allow(clippy::declare_interior_mutable_const)]
|
|
||||||
const APPLICATION_JSON: HeaderValue = HeaderValue::from_static("application/json");
|
|
||||||
|
|
||||||
let bytes = match self.0 {
|
let bytes = match self.0 {
|
||||||
Ok(res) => res,
|
Ok(res) => res,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return Response::builder()
|
return Response::builder()
|
||||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
.header(header::CONTENT_TYPE, "text/plain")
|
.header(header::CONTENT_TYPE, mime::TEXT_PLAIN_UTF_8.as_ref())
|
||||||
.body(Full::from(err.to_string()))
|
.body(Full::from(err.to_string()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut res = Response::new(Full::from(bytes));
|
let mut res = Response::new(Full::from(bytes));
|
||||||
res.headers_mut()
|
res.headers_mut().insert(
|
||||||
.insert(header::CONTENT_TYPE, APPLICATION_JSON);
|
header::CONTENT_TYPE,
|
||||||
|
HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()),
|
||||||
|
);
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,11 +33,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- **breaking:** `Router::nest` will panic if the nested router has a fallback.
|
- **breaking:** `Router::nest` will panic if the nested router has a fallback.
|
||||||
Previously it would be silently discarded ([#529])
|
Previously it would be silently discarded ([#529])
|
||||||
- Update WebSockets to use tokio-tungstenite 0.16 ([#525])
|
- Update WebSockets to use tokio-tungstenite 0.16 ([#525])
|
||||||
|
- **added:** Default to return `charset=utf-8` for text content type. ([#554])
|
||||||
|
|
||||||
[#525]: https://github.com/tokio-rs/axum/pull/525
|
[#525]: https://github.com/tokio-rs/axum/pull/525
|
||||||
[#527]: https://github.com/tokio-rs/axum/pull/527
|
[#527]: https://github.com/tokio-rs/axum/pull/527
|
||||||
[#529]: https://github.com/tokio-rs/axum/pull/529
|
[#529]: https://github.com/tokio-rs/axum/pull/529
|
||||||
[#534]: https://github.com/tokio-rs/axum/pull/534
|
[#534]: https://github.com/tokio-rs/axum/pull/534
|
||||||
|
[#554]: https://github.com/tokio-rs/axum/pull/554
|
||||||
|
|
||||||
# 0.3.3 (13. November, 2021)
|
# 0.3.3 (13. November, 2021)
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -14,8 +14,8 @@ repository = "https://github.com/tokio-rs/axum"
|
|||||||
default = ["http1", "json", "tower-log"]
|
default = ["http1", "json", "tower-log"]
|
||||||
http1 = ["hyper/http1"]
|
http1 = ["hyper/http1"]
|
||||||
http2 = ["hyper/http2"]
|
http2 = ["hyper/http2"]
|
||||||
json = ["serde_json", "mime"]
|
json = ["serde_json"]
|
||||||
multipart = ["multer", "mime"]
|
multipart = ["multer"]
|
||||||
tower-log = ["tower/log"]
|
tower-log = ["tower/log"]
|
||||||
ws = ["tokio-tungstenite", "sha-1", "base64"]
|
ws = ["tokio-tungstenite", "sha-1", "base64"]
|
||||||
|
|
||||||
@@ -26,6 +26,7 @@ bytes = "1.0"
|
|||||||
futures-util = { version = "0.3", default-features = false, features = ["alloc"] }
|
futures-util = { version = "0.3", default-features = false, features = ["alloc"] }
|
||||||
http = "0.2.5"
|
http = "0.2.5"
|
||||||
http-body = "0.4.4"
|
http-body = "0.4.4"
|
||||||
|
mime = "0.3.16"
|
||||||
hyper = { version = "0.14.14", features = ["server", "tcp", "stream"] }
|
hyper = { version = "0.14.14", features = ["server", "tcp", "stream"] }
|
||||||
matchit = "0.4.4"
|
matchit = "0.4.4"
|
||||||
percent-encoding = "2.1"
|
percent-encoding = "2.1"
|
||||||
@@ -43,7 +44,6 @@ tower-service = "0.3"
|
|||||||
# optional dependencies
|
# optional dependencies
|
||||||
base64 = { optional = true, version = "0.13" }
|
base64 = { optional = true, version = "0.13" }
|
||||||
headers = { optional = true, version = "0.3" }
|
headers = { optional = true, version = "0.3" }
|
||||||
mime = { optional = true, version = "0.3" }
|
|
||||||
multer = { optional = true, version = "2.0.0" }
|
multer = { optional = true, version = "2.0.0" }
|
||||||
serde_json = { version = "1.0", optional = true, features = ["raw_value"] }
|
serde_json = { version = "1.0", optional = true, features = ["raw_value"] }
|
||||||
sha-1 = { optional = true, version = "0.9.6" }
|
sha-1 = { optional = true, version = "0.9.6" }
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ async fn plain_text() -> &'static str {
|
|||||||
"foo"
|
"foo"
|
||||||
}
|
}
|
||||||
|
|
||||||
// String works too and will get a `text/plain` content-type
|
// String works too and will get a `text/plain; charset=utf-8` content-type
|
||||||
async fn plain_text_string(uri: Uri) -> String {
|
async fn plain_text_string(uri: Uri) -> String {
|
||||||
format!("Hi from {}", uri.path())
|
format!("Hi from {}", uri.path())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ where
|
|||||||
.map_err(FailedToDeserializeQueryString::new::<T, _>)?;
|
.map_err(FailedToDeserializeQueryString::new::<T, _>)?;
|
||||||
Ok(Form(value))
|
Ok(Form(value))
|
||||||
} else {
|
} else {
|
||||||
if !has_content_type(req, "application/x-www-form-urlencoded")? {
|
if !has_content_type(req, &mime::APPLICATION_WWW_FORM_URLENCODED)? {
|
||||||
return Err(InvalidFormContentType.into());
|
return Err(InvalidFormContentType.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@ mod tests {
|
|||||||
.method(Method::POST)
|
.method(Method::POST)
|
||||||
.header(
|
.header(
|
||||||
http::header::CONTENT_TYPE,
|
http::header::CONTENT_TYPE,
|
||||||
"application/x-www-form-urlencoded",
|
mime::APPLICATION_WWW_FORM_URLENCODED.as_ref(),
|
||||||
)
|
)
|
||||||
.body(http_body::Full::<bytes::Bytes>::new(
|
.body(http_body::Full::<bytes::Bytes>::new(
|
||||||
serde_urlencoded::to_string(&value).unwrap().into(),
|
serde_urlencoded::to_string(&value).unwrap().into(),
|
||||||
@@ -182,7 +182,7 @@ mod tests {
|
|||||||
Request::builder()
|
Request::builder()
|
||||||
.uri("http://example.com/test")
|
.uri("http://example.com/test")
|
||||||
.method(Method::POST)
|
.method(Method::POST)
|
||||||
.header(http::header::CONTENT_TYPE, "application/json")
|
.header(http::header::CONTENT_TYPE, mime::APPLICATION_JSON.as_ref())
|
||||||
.body(http_body::Full::<bytes::Bytes>::new(
|
.body(http_body::Full::<bytes::Bytes>::new(
|
||||||
serde_urlencoded::to_string(&Pagination {
|
serde_urlencoded::to_string(&Pagination {
|
||||||
size: Some(10),
|
size: Some(10),
|
||||||
|
|||||||
@@ -332,7 +332,7 @@ where
|
|||||||
|
|
||||||
pub(crate) fn has_content_type<B>(
|
pub(crate) fn has_content_type<B>(
|
||||||
req: &RequestParts<B>,
|
req: &RequestParts<B>,
|
||||||
expected_content_type: &str,
|
expected_content_type: &mime::Mime,
|
||||||
) -> Result<bool, HeadersAlreadyExtracted> {
|
) -> Result<bool, HeadersAlreadyExtracted> {
|
||||||
let content_type = if let Some(content_type) = req
|
let content_type = if let Some(content_type) = req
|
||||||
.headers()
|
.headers()
|
||||||
@@ -350,7 +350,7 @@ pub(crate) fn has_content_type<B>(
|
|||||||
return Ok(false);
|
return Ok(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(content_type.starts_with(expected_content_type))
|
Ok(content_type.starts_with(expected_content_type.as_ref()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn take_body<B>(req: &mut RequestParts<B>) -> Result<B, BodyAlreadyExtracted> {
|
pub(crate) fn take_body<B>(req: &mut RequestParts<B>) -> Result<B, BodyAlreadyExtracted> {
|
||||||
|
|||||||
+8
-6
@@ -176,23 +176,25 @@ where
|
|||||||
type BodyError = Infallible;
|
type BodyError = Infallible;
|
||||||
|
|
||||||
fn into_response(self) -> Response<Self::Body> {
|
fn into_response(self) -> Response<Self::Body> {
|
||||||
#[allow(clippy::declare_interior_mutable_const)]
|
|
||||||
const APPLICATION_JSON: HeaderValue = HeaderValue::from_static("application/json");
|
|
||||||
|
|
||||||
let bytes = match serde_json::to_vec(&self.0) {
|
let bytes = match serde_json::to_vec(&self.0) {
|
||||||
Ok(res) => res,
|
Ok(res) => res,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return Response::builder()
|
return Response::builder()
|
||||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
.header(header::CONTENT_TYPE, "text/plain")
|
.header(
|
||||||
|
header::CONTENT_TYPE,
|
||||||
|
HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()),
|
||||||
|
)
|
||||||
.body(Full::from(err.to_string()))
|
.body(Full::from(err.to_string()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut res = Response::new(Full::from(bytes));
|
let mut res = Response::new(Full::from(bytes));
|
||||||
res.headers_mut()
|
res.headers_mut().insert(
|
||||||
.insert(header::CONTENT_TYPE, APPLICATION_JSON);
|
header::CONTENT_TYPE,
|
||||||
|
HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()),
|
||||||
|
);
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -127,7 +127,7 @@
|
|||||||
//! };
|
//! };
|
||||||
//! use serde_json::{Value, json};
|
//! use serde_json::{Value, json};
|
||||||
//!
|
//!
|
||||||
//! // `&'static str` becomes a `200 OK` with `content-type: text/plain`
|
//! // `&'static str` becomes a `200 OK` with `content-type: text/plain; charset=utf-8`
|
||||||
//! async fn plain_text() -> &'static str {
|
//! async fn plain_text() -> &'static str {
|
||||||
//! "foo"
|
//! "foo"
|
||||||
//! }
|
//! }
|
||||||
|
|||||||
+24
-19
@@ -331,26 +331,25 @@ impl IntoResponse for std::borrow::Cow<'static, str> {
|
|||||||
type BodyError = Infallible;
|
type BodyError = Infallible;
|
||||||
|
|
||||||
fn into_response(self) -> Response<Self::Body> {
|
fn into_response(self) -> Response<Self::Body> {
|
||||||
#[allow(clippy::declare_interior_mutable_const)]
|
|
||||||
const TEXT_PLAIN: HeaderValue = HeaderValue::from_static("text/plain");
|
|
||||||
|
|
||||||
let mut res = Response::new(Full::from(self));
|
let mut res = Response::new(Full::from(self));
|
||||||
res.headers_mut().insert(header::CONTENT_TYPE, TEXT_PLAIN);
|
res.headers_mut().insert(
|
||||||
|
header::CONTENT_TYPE,
|
||||||
|
HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()),
|
||||||
|
);
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::declare_interior_mutable_const)]
|
|
||||||
const APPLICATION_OCTET_STREAM: HeaderValue = HeaderValue::from_static("application/octet-stream");
|
|
||||||
|
|
||||||
impl IntoResponse for Bytes {
|
impl IntoResponse for Bytes {
|
||||||
type Body = Full<Bytes>;
|
type Body = Full<Bytes>;
|
||||||
type BodyError = Infallible;
|
type BodyError = Infallible;
|
||||||
|
|
||||||
fn into_response(self) -> Response<Self::Body> {
|
fn into_response(self) -> Response<Self::Body> {
|
||||||
let mut res = Response::new(Full::from(self));
|
let mut res = Response::new(Full::from(self));
|
||||||
res.headers_mut()
|
res.headers_mut().insert(
|
||||||
.insert(header::CONTENT_TYPE, APPLICATION_OCTET_STREAM);
|
header::CONTENT_TYPE,
|
||||||
|
HeaderValue::from_static(mime::APPLICATION_OCTET_STREAM.as_ref()),
|
||||||
|
);
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -361,8 +360,10 @@ impl IntoResponse for &'static [u8] {
|
|||||||
|
|
||||||
fn into_response(self) -> Response<Self::Body> {
|
fn into_response(self) -> Response<Self::Body> {
|
||||||
let mut res = Response::new(Full::from(self));
|
let mut res = Response::new(Full::from(self));
|
||||||
res.headers_mut()
|
res.headers_mut().insert(
|
||||||
.insert(header::CONTENT_TYPE, APPLICATION_OCTET_STREAM);
|
header::CONTENT_TYPE,
|
||||||
|
HeaderValue::from_static(mime::APPLICATION_OCTET_STREAM.as_ref()),
|
||||||
|
);
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -373,8 +374,10 @@ impl IntoResponse for Vec<u8> {
|
|||||||
|
|
||||||
fn into_response(self) -> Response<Self::Body> {
|
fn into_response(self) -> Response<Self::Body> {
|
||||||
let mut res = Response::new(Full::from(self));
|
let mut res = Response::new(Full::from(self));
|
||||||
res.headers_mut()
|
res.headers_mut().insert(
|
||||||
.insert(header::CONTENT_TYPE, APPLICATION_OCTET_STREAM);
|
header::CONTENT_TYPE,
|
||||||
|
HeaderValue::from_static(mime::APPLICATION_OCTET_STREAM.as_ref()),
|
||||||
|
);
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -385,8 +388,10 @@ impl IntoResponse for std::borrow::Cow<'static, [u8]> {
|
|||||||
|
|
||||||
fn into_response(self) -> Response<Self::Body> {
|
fn into_response(self) -> Response<Self::Body> {
|
||||||
let mut res = Response::new(Full::from(self));
|
let mut res = Response::new(Full::from(self));
|
||||||
res.headers_mut()
|
res.headers_mut().insert(
|
||||||
.insert(header::CONTENT_TYPE, APPLICATION_OCTET_STREAM);
|
header::CONTENT_TYPE,
|
||||||
|
HeaderValue::from_static(mime::APPLICATION_OCTET_STREAM.as_ref()),
|
||||||
|
);
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -468,11 +473,11 @@ where
|
|||||||
type BodyError = Infallible;
|
type BodyError = Infallible;
|
||||||
|
|
||||||
fn into_response(self) -> Response<Self::Body> {
|
fn into_response(self) -> Response<Self::Body> {
|
||||||
#[allow(clippy::declare_interior_mutable_const)]
|
|
||||||
const TEXT_HTML: HeaderValue = HeaderValue::from_static("text/html");
|
|
||||||
|
|
||||||
let mut res = Response::new(self.0.into());
|
let mut res = Response::new(self.0.into());
|
||||||
res.headers_mut().insert(header::CONTENT_TYPE, TEXT_HTML);
|
res.headers_mut().insert(
|
||||||
|
header::CONTENT_TYPE,
|
||||||
|
HeaderValue::from_static(mime::TEXT_HTML_UTF_8.as_ref()),
|
||||||
|
);
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ where
|
|||||||
};
|
};
|
||||||
|
|
||||||
Response::builder()
|
Response::builder()
|
||||||
.header(http::header::CONTENT_TYPE, "text/event-stream")
|
.header(http::header::CONTENT_TYPE, mime::TEXT_EVENT_STREAM.as_ref())
|
||||||
.header(http::header::CACHE_CONTROL, "no-cache")
|
.header(http::header::CACHE_CONTROL, "no-cache")
|
||||||
.body(body)
|
.body(body)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ publish = false
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = { path = "../../axum" }
|
axum = { path = "../../axum" }
|
||||||
|
mime = "0.3"
|
||||||
tokio = { version = "1.0", features = ["full"] }
|
tokio = { version = "1.0", features = ["full"] }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version="0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
tower-http = { version = "0.1", features = ["trace"] }
|
tower-http = { version = "0.1", features = ["trace"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
hyper = { version = "0.14", features = ["full"] }
|
hyper = { version = "0.14", features = ["full"] }
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ mod tests {
|
|||||||
Request::builder()
|
Request::builder()
|
||||||
.method(http::Method::POST)
|
.method(http::Method::POST)
|
||||||
.uri("/json")
|
.uri("/json")
|
||||||
.header(http::header::CONTENT_TYPE, "application/json")
|
.header(http::header::CONTENT_TYPE, mime::APPLICATION_JSON.as_ref())
|
||||||
.body(Body::from(
|
.body(Body::from(
|
||||||
serde_json::to_vec(&json!([1, 2, 3, 4])).unwrap(),
|
serde_json::to_vec(&json!([1, 2, 3, 4])).unwrap(),
|
||||||
))
|
))
|
||||||
|
|||||||
Reference in New Issue
Block a user