Merge branch 'main' into separate-nesting-opaque-services

This commit is contained in:
David Pedersen
2022-06-29 21:21:29 +02:00
85 changed files with 747 additions and 220 deletions
+1 -1
View File
@@ -1 +1 @@
msrv = "1.54"
msrv = "1.56"
+24 -5
View File
@@ -2,6 +2,7 @@ name: CI
env:
CARGO_TERM_COLOR: always
MSRV: 1.56.0
on:
push:
@@ -88,8 +89,8 @@ jobs:
command: test
args: --all --all-features --all-targets
# some examples doesn't support 1.54 (such as async-graphql)
# so we only test axum itself on 1.54
# some examples doesn't support our MSRV (such as async-graphql)
# so we only test axum itself on our MSRV
test-msrv:
needs: check
runs-on: ubuntu-latest
@@ -97,10 +98,21 @@ jobs:
- uses: actions/checkout@master
- uses: actions-rs/toolchain@v1
with:
toolchain: 1.54
toolchain: ${{ env.MSRV }}
override: true
profile: minimal
- name: "install Rust nightly"
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
profile: minimal
- uses: Swatinem/rust-cache@v1
- name: Select minimal versions
uses: actions-rs/cargo@v1
with:
command: update
args: -Z minimal-versions
toolchain: nightly
- name: Run tests
uses: actions-rs/cargo@v1
with:
@@ -109,8 +121,12 @@ jobs:
-p axum
-p axum-extra
-p axum-core
--all-features --all-targets
# the compiler errors are different on 1.54 which makes
-p internal-minimal-versions
--all-features
--all-targets
--locked
toolchain: ${{ env.MSRV }}
# the compiler errors are different on our MSRV which makes
# the trybuild tests in axum-macros fail, so just run the doc
# tests
- name: Run axum-macros doc tests
@@ -119,8 +135,11 @@ jobs:
command: test
args: >
-p axum-macros
-p internal-minimal-versions
--doc
--all-features
--locked
toolchain: ${{ env.MSRV }}
test-docs:
needs: check
+5
View File
@@ -4,4 +4,9 @@ members = [
"axum-core",
"axum-extra",
"axum-macros",
# internal crate used to bump the minimum versions we
# get for some dependencies which otherwise wouldn't build
# with `cargo +nightly update -Z minimal-versions`
"internal-minimal-versions",
]
+3 -1
View File
@@ -10,7 +10,7 @@ If your project isn't listed here and you would like it to be, please feel free
- [axum-flash](https://crates.io/crates/axum-flash): One-time notifications (aka flash messages) for axum.
- [axum-msgpack](https://crates.io/crates/axum-msgpack): MessagePack Extractors for axum.
- [axum-sqlx-tx](https://crates.io/crates/axum-sqlx-tx): Request-bound [SQLx](https://github.com/launchbadge/sqlx#readme) transactions with automatic commit/rollback based on response.
- [aliri_tower](https://crates.io/crates/aliri_tower): JWT validation and OAuth2 scopes checking middleware.
- [aliri_axum](https://docs.rs/aliri_axum) and [aliri_tower](https://docs.rs/aliri_tower): JWT validation middleware and OAuth2 scopes enforcing extractors.
- [ezsockets](https://github.com/gbaranski/ezsockets): Easy to use WebSocket library that integrates with Axum.
- [axum_database_sessions](https://github.com/AscendingCreations/AxumSessions): Database persistent sessions like pythons flask_sessionstore for Axum.
- [axum_sessions_auth](https://github.com/AscendingCreations/AxumSessionsAuth): Persistant session based user login with rights management for Axum.
@@ -18,6 +18,7 @@ If your project isn't listed here and you would like it to be, please feel free
- [shuttle](https://github.com/getsynth/shuttle): A serverless platform built for Rust. Now with axum support.
- [axum-tungstenite](https://github.com/davidpdrsn/axum-tungstenite): WebSocket connections for axum directly using tungstenite
- [axum-jrpc](https://github.com/0xdeafbeef/axum-jrpc): Json-rpc extractor for axum
- [axum-tracing-opentelemetry](https://crates.io/crates/axum-tracing-opentelemetry): Middlewares and tools to integrate axum + tracing + opentelemetry
## Project showcase
@@ -35,6 +36,7 @@ If your project isn't listed here and you would like it to be, please feel free
- [CLOMonitor](https://clomonitor.io) ([repository](https://github.com/cncf/clomonitor)): Checks open source projects repositories to verify they meet certain best practices.
- [Pinging.net](https://www.pinging.net) ([repository](https://github.com/benhansenslc/pinging)): A new way to check and monitor your internet connection.
- [wastebin](https://github.com/matze/wastebin): A minimalist pastebin service.
- [sandbox_axum_observability](https://github.com/davidB/sandbox_axum_observability) A Sandbox/showcase project to experiment axum and observability (tracing, opentelemetry, jaeger, grafana tempo,...)
[Realworld]: https://github.com/gothinkster/realworld
[SQLx]: https://github.com/launchbadge/sqlx
+6
View File
@@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- None.
# 0.2.6 (18. June, 2022)
- **change:** axum-core's MSRV is now 1.56 ([#1098])
[#1098]: https://github.com/tokio-rs/axum/pull/1098
# 0.2.5 (08. June, 2022)
- **added:** Automatically handle `http_body::LengthLimitError` in `FailedToBufferBody` and map
+2 -2
View File
@@ -1,14 +1,14 @@
[package]
categories = ["asynchronous", "network-programming", "web-programming"]
description = "Core types and traits for axum"
edition = "2018"
edition = "2021"
homepage = "https://github.com/tokio-rs/axum"
keywords = ["http", "web", "framework"]
license = "MIT"
name = "axum-core"
readme = "README.md"
repository = "https://github.com/tokio-rs/axum"
version = "0.2.5" # remember to also bump the version that axum depends on
version = "0.2.6" # remember to also bump the version that axum depends on
[dependencies]
async-trait = "0.1"
+1 -1
View File
@@ -14,7 +14,7 @@ This crate uses `#![forbid(unsafe_code)]` to ensure everything is implemented in
## Minimum supported Rust version
axum-core's MSRV is 1.54.
axum-core's MSRV is 1.56.
## Getting Help
+1 -1
View File
@@ -1,6 +1,6 @@
use super::{IntoResponse, IntoResponseParts, Response, ResponseParts, TryIntoHeaderError};
use http::header::{HeaderName, HeaderValue};
use std::{convert::TryInto, fmt};
use std::fmt;
/// Append headers to a response.
///
+1 -1
View File
@@ -11,7 +11,7 @@ use http_body::{
};
use std::{
borrow::Cow,
convert::{Infallible, TryInto},
convert::Infallible,
fmt,
pin::Pin,
task::{Context, Poll},
@@ -3,10 +3,7 @@ use http::{
header::{HeaderMap, HeaderName, HeaderValue},
Extensions, StatusCode,
};
use std::{
convert::{Infallible, TryInto},
fmt,
};
use std::{convert::Infallible, fmt};
/// Trait for adding headers and extensions to a response.
///
+8
View File
@@ -9,6 +9,14 @@ and this project adheres to [Semantic Versioning].
- None.
# 0.3.5 (27. June, 2022)
- **added:** Add `JsonLines` for streaming newline delimited JSON ([#1093])
- **change:** axum's MSRV is now 1.56 ([#1098])
[#1093]: https://github.com/tokio-rs/axum/pull/1093
[#1098]: https://github.com/tokio-rs/axum/pull/1098
# 0.3.4 (08. June, 2022)
- **fixed:** Use `impl IntoResponse` less in docs ([#1049])
+6 -2
View File
@@ -1,14 +1,14 @@
[package]
categories = ["asynchronous", "network-programming", "web-programming"]
description = "Extra utilities for axum"
edition = "2018"
edition = "2021"
homepage = "https://github.com/tokio-rs/axum"
keywords = ["http", "web", "framework"]
license = "MIT"
name = "axum-extra"
readme = "README.md"
repository = "https://github.com/tokio-rs/axum"
version = "0.3.4"
version = "0.3.5"
[features]
default = []
@@ -19,6 +19,7 @@ cookie-private = ["cookie", "cookie-lib/private"]
cookie-signed = ["cookie", "cookie-lib/signed"]
erased-json = ["serde_json", "serde"]
form = ["serde", "serde_html_form"]
json-lines = ["serde_json", "serde", "tokio-util/io", "tokio-stream/io-util"]
query = ["serde", "serde_html_form"]
spa = ["tower-http/fs"]
typed-routing = ["axum-macros", "serde", "percent-encoding"]
@@ -26,6 +27,7 @@ typed-routing = ["axum-macros", "serde", "percent-encoding"]
[dependencies]
axum = { path = "../axum", version = "0.5", default-features = false }
bytes = "1.1.0"
futures-util = { version = "0.3", default-features = false, features = ["alloc"] }
http = "0.2"
mime = "0.3"
pin-project-lite = "0.2"
@@ -42,10 +44,12 @@ percent-encoding = { version = "2.1", optional = true }
serde = { version = "1.0", optional = true }
serde_html_form = { version = "0.1", optional = true }
serde_json = { version = "1.0.71", optional = true }
tokio-stream = { version = "0.1.9", optional = true }
tokio-util = { version = "0.7", optional = true }
[dev-dependencies]
axum = { path = "../axum", version = "0.5", features = ["headers"] }
futures = "0.3"
hyper = "0.14"
reqwest = { version = "0.11", default-features = false, features = ["json", "stream", "multipart"] }
serde = { version = "1.0", features = ["derive"] }
+1 -1
View File
@@ -14,7 +14,7 @@ This crate uses `#![forbid(unsafe_code)]` to ensure everything is implemented in
## Minimum supported Rust version
axum-extra's MSRV is 1.54.
axum-extra's MSRV is 1.56.
## Getting Help
+4
View File
@@ -27,3 +27,7 @@ pub use self::form::Form;
#[cfg(feature = "query")]
pub use self::query::Query;
#[cfg(feature = "json-lines")]
#[doc(no_inline)]
pub use crate::json_lines::JsonLines;
+286
View File
@@ -0,0 +1,286 @@
//! Newline delimited JSON extractor and response.
use axum::{
async_trait,
body::{HttpBody, StreamBody},
extract::{rejection::BodyAlreadyExtracted, FromRequest, RequestParts},
response::{IntoResponse, Response},
BoxError,
};
use bytes::{BufMut, Bytes, BytesMut};
use futures_util::stream::{BoxStream, Stream, TryStream, TryStreamExt};
use pin_project_lite::pin_project;
use serde::{de::DeserializeOwned, Serialize};
use std::{
io::{self, Write},
marker::PhantomData,
pin::Pin,
task::{Context, Poll},
};
use tokio::io::AsyncBufReadExt;
use tokio_stream::wrappers::LinesStream;
use tokio_util::io::StreamReader;
pin_project! {
/// A stream of newline delimited JSON.
///
/// This can be used both as an extractor and as a response.
///
/// # As extractor
///
/// ```rust
/// use axum_extra::json_lines::JsonLines;
/// use futures::stream::StreamExt;
///
/// async fn handler(mut stream: JsonLines<serde_json::Value>) {
/// while let Some(value) = stream.next().await {
/// // ...
/// }
/// }
/// ```
///
/// # As response
///
/// ```rust
/// use axum::{BoxError, response::{IntoResponse, Response}};
/// use axum_extra::json_lines::JsonLines;
/// use futures::stream::Stream;
///
/// fn stream_of_values() -> impl Stream<Item = Result<serde_json::Value, BoxError>> {
/// # futures::stream::empty()
/// }
///
/// async fn handler() -> Response {
/// JsonLines::new(stream_of_values()).into_response()
/// }
/// ```
// we use `AsExtractor` as the default because you're more likely to name this type if its used
// as an extractor
pub struct JsonLines<S, T = AsExtractor> {
#[pin]
inner: Inner<S>,
_marker: PhantomData<T>,
}
}
pin_project! {
#[project = InnerProj]
enum Inner<S> {
Response {
#[pin]
stream: S,
},
Extractor {
#[pin]
stream: BoxStream<'static, Result<S, axum::Error>>,
},
}
}
/// Maker type used to prove that an `JsonLines` was constructed via `FromRequest`.
#[derive(Debug)]
#[non_exhaustive]
pub struct AsExtractor;
/// Maker type used to prove that an `JsonLines` was constructed via `JsonLines::new`.
#[derive(Debug)]
#[non_exhaustive]
pub struct AsResponse;
impl<S> JsonLines<S, AsResponse> {
/// Create a new `JsonLines` from a stream of items.
pub fn new(stream: S) -> Self {
Self {
inner: Inner::Response { stream },
_marker: PhantomData,
}
}
}
#[async_trait]
impl<B, T> FromRequest<B> for JsonLines<T, AsExtractor>
where
B: HttpBody + Send + 'static,
B::Data: Into<Bytes>,
B::Error: Into<BoxError>,
T: DeserializeOwned,
{
type Rejection = BodyAlreadyExtracted;
async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
// `Stream::lines` isn't a thing so we have to convert it into an `AsyncRead`
// so we can call `AsyncRead::lines` and then convert it back to a `Stream`
let body = req.take_body().ok_or_else(BodyAlreadyExtracted::default)?;
let body = BodyStream { body };
let stream = body
.map_ok(Into::into)
.map_err(|err| io::Error::new(io::ErrorKind::Other, err));
let read = StreamReader::new(stream);
let lines_stream = LinesStream::new(read.lines());
let deserialized_stream =
lines_stream
.map_err(axum::Error::new)
.and_then(|value| async move {
serde_json::from_str::<T>(&value).map_err(axum::Error::new)
});
Ok(Self {
inner: Inner::Extractor {
stream: Box::pin(deserialized_stream),
},
_marker: PhantomData,
})
}
}
// like `axum::extract::BodyStream` except it doesn't box the inner body
// we don't need that since we box the final stream in `Inner::Extractor`
pin_project! {
struct BodyStream<B> {
#[pin]
body: B,
}
}
impl<B> Stream for BodyStream<B>
where
B: HttpBody + Send + 'static,
{
type Item = Result<B::Data, B::Error>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
self.project().body.poll_data(cx)
}
}
impl<T> Stream for JsonLines<T, AsExtractor> {
type Item = Result<T, axum::Error>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match self.project().inner.project() {
InnerProj::Extractor { stream } => stream.poll_next(cx),
// `JsonLines<_, AsExtractor>` can only be constructed via `FromRequest`
// which doesn't use this variant
InnerProj::Response { .. } => unreachable!(),
}
}
}
impl<S> IntoResponse for JsonLines<S, AsResponse>
where
S: TryStream + Send + 'static,
S::Ok: Serialize + Send,
S::Error: Into<BoxError>,
{
fn into_response(self) -> Response {
let inner = match self.inner {
Inner::Response { stream } => stream,
// `JsonLines<_, AsResponse>` can only be constructed via `JsonLines::new`
// which doesn't use this variant
Inner::Extractor { .. } => unreachable!(),
};
let stream = inner.map_err(Into::into).and_then(|value| async move {
let mut buf = BytesMut::new().writer();
serde_json::to_writer(&mut buf, &value)?;
buf.write_all(b"\n")?;
Ok::<_, BoxError>(buf.into_inner().freeze())
});
let stream = StreamBody::new(stream);
// there is no consensus around mime type yet
// https://github.com/wardi/jsonlines/issues/36
stream.into_response()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_helpers::*;
use axum::{
routing::{get, post},
Router,
};
use futures_util::StreamExt;
use http::StatusCode;
use serde::Deserialize;
use std::{convert::Infallible, error::Error};
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
struct User {
id: i32,
}
#[tokio::test]
async fn extractor() {
let app = Router::new().route(
"/",
post(|mut stream: JsonLines<User>| async move {
assert_eq!(stream.next().await.unwrap().unwrap(), User { id: 1 });
assert_eq!(stream.next().await.unwrap().unwrap(), User { id: 2 });
assert_eq!(stream.next().await.unwrap().unwrap(), User { id: 3 });
// sources are downcastable to `serde_json::Error`
let err = stream.next().await.unwrap().unwrap_err();
let _: &serde_json::Error = err
.source()
.unwrap()
.downcast_ref::<serde_json::Error>()
.unwrap();
}),
);
let client = TestClient::new(app);
let res = client
.post("/")
.body(
vec![
"{\"id\":1}",
"{\"id\":2}",
"{\"id\":3}",
// to trigger an error for source downcasting
"{\"id\":false}",
]
.join("\n"),
)
.send()
.await;
assert_eq!(res.status(), StatusCode::OK);
}
#[tokio::test]
async fn response() {
let app = Router::new().route(
"/",
get(|| async {
let values = futures_util::stream::iter(vec![
Ok::<_, Infallible>(User { id: 1 }),
Ok::<_, Infallible>(User { id: 2 }),
Ok::<_, Infallible>(User { id: 3 }),
]);
JsonLines::new(values)
}),
);
let client = TestClient::new(app);
let res = client.get("/").send().await;
let values = res
.text()
.await
.lines()
.map(|line| serde_json::from_str::<User>(line).unwrap())
.collect::<Vec<_>>();
assert_eq!(
values,
vec![User { id: 1 }, User { id: 2 }, User { id: 3 },]
);
}
}
+4
View File
@@ -15,6 +15,7 @@
//! `cookie-signed` | Enables the `SignedCookieJar` extractor | No
//! `erased-json` | Enables the `ErasedJson` response | No
//! `form` | Enables the `Form` extractor | No
//! `json-lines` | Enables the `json-lines` extractor and response | No
//! `query` | Enables the `Query` extractor | No
//! `spa` | Enables the `Spa` router | No
//! `typed-routing` | Enables the `TypedPath` routing utilities | No
@@ -67,6 +68,9 @@ pub mod extract;
pub mod response;
pub mod routing;
#[cfg(feature = "json-lines")]
pub mod json_lines;
#[cfg(feature = "typed-routing")]
#[doc(hidden)]
pub mod __private {
+4
View File
@@ -5,3 +5,7 @@ mod erased_json;
#[cfg(feature = "erased-json")]
pub use erased_json::ErasedJson;
#[cfg(feature = "json-lines")]
#[doc(no_inline)]
pub use crate::json_lines::JsonLines;
+1
View File
@@ -204,6 +204,7 @@ mod tests {
.edit(|Path(id): Path<u64>| async move { format!("users#edit id={}", id) })
.update(|Path(id): Path<u64>| async move { format!("users#update id={}", id) })
.destroy(|Path(id): Path<u64>| async move { format!("users#destroy id={}", id) })
// TODO(david): figure out if we need `nest_service` + `nest` methods on `Resource`
.nest(Router::new().route(
"/tweets",
get(|Path(id): Path<u64>| async move { format!("users#tweets id={}", id) }),
+8
View File
@@ -9,6 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- None.
# 0.2.3 (27. June, 2022)
- **change:** axum-macros's MSRV is now 1.56 ([#1098])
- **fixed:** Silence "unnecessary use of `to_string`" lint for `#[derive(TypedPath)]` ([#1117])
[#1098]: https://github.com/tokio-rs/axum/pull/1098
[#1117]: https://github.com/tokio-rs/axum/pull/1117
# 0.2.2 (18. May, 2022)
- **added:** In `debug_handler`, check if `Request` is used as non-final extractor ([#1035])
+3 -3
View File
@@ -1,14 +1,14 @@
[package]
categories = ["asynchronous", "network-programming", "web-programming"]
description = "Macros for axum"
edition = "2018"
edition = "2021"
homepage = "https://github.com/tokio-rs/axum"
keywords = ["axum"]
license = "MIT"
name = "axum-macros"
readme = "README.md"
repository = "https://github.com/tokio-rs/axum"
version = "0.2.2"
version = "0.2.3"
[lib]
proc-macro = true
@@ -26,4 +26,4 @@ rustversion = "1.0"
serde = { version = "1.0", features = ["derive"] }
syn = { version = "1.0", features = ["full", "extra-traits"] }
tokio = { version = "1.0", features = ["full"] }
trybuild = "1.0"
trybuild = "1.0.63"
+1 -1
View File
@@ -14,7 +14,7 @@ This crate uses `#![forbid(unsafe_code)]` to ensure everything is implemented in
## Minimum supported Rust version
axum-macros's MSRV is 1.54.
axum-macros's MSRV is 1.56.
## Getting Help
+14 -2
View File
@@ -104,12 +104,18 @@ fn expand_named_fields(
let display_impl = quote_spanned! {path.span()=>
#[automatically_derived]
impl ::std::fmt::Display for #ident {
#[allow(clippy::unnecessary_to_owned)]
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
let Self { #(#captures,)* } = self;
write!(
f,
#format_str,
#(#captures = ::axum_extra::__private::utf8_percent_encode(&#captures.to_string(), ::axum_extra::__private::PATH_SEGMENT)),*
#(
#captures = ::axum_extra::__private::utf8_percent_encode(
&#captures.to_string(),
::axum_extra::__private::PATH_SEGMENT,
)
),*
)
}
}
@@ -200,12 +206,18 @@ fn expand_unnamed_fields(
let display_impl = quote_spanned! {path.span()=>
#[automatically_derived]
impl ::std::fmt::Display for #ident {
#[allow(clippy::unnecessary_to_owned)]
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
let Self { #(#destructure_self)* } = self;
write!(
f,
#format_str,
#(#captures = ::axum_extra::__private::utf8_percent_encode(&#captures.to_string(), ::axum_extra::__private::PATH_SEGMENT)),*
#(
#captures = ::axum_extra::__private::utf8_percent_encode(
&#captures.to_string(),
::axum_extra::__private::PATH_SEGMENT,
)
),*
)
}
}
+34 -4
View File
@@ -7,13 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
# Unreleased
- **added:** Support resolving host name via `Forwarded` header in `Host`
extractor ([#1078])
- **breaking:** Remove `extractor_middleware` which was previously deprecated.
Use `axum::middleware::from_extractor` instead ([#1077])
- **breaking:** Allow `Error: Into<Infallible>` for `Route::{layer, route_layer}` ([#924])
- **breaking:** `MethodRouter` now panics on overlapping routes ([#1102])
- **breaking:** `Router::nest` now only accepts `Router`s. Use
`Router::nest_service` to nest opaque services
- **added:** Add `Router::nest_service` for nesting opaque services. Use this to
nest services like `tower::services::ServeDir`
- **breaking:** The route `/foo/` no longer matches `/foo/*rest`. If you want
- **breaking:** The request `/foo/` no longer matches `/foo/*rest`. If you want
to match `/foo/` you have to add a route specifically for that
- **breaking:** Path params for wildcard routes no longer include the prefix
`/`. e.g. `/foo.js` will match `/*filepath` with a value of `foo.js`, _not_
@@ -21,7 +23,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **fixed:** Routes like `/foo` and `/*rest` are no longer considered
overlapping. `/foo` will take priority
[#1077]: https://github.com/tokio-rs/axum/pull/1077
[#1102]: https://github.com/tokio-rs/axum/pull/1102
[#924]: https://github.com/tokio-rs/axum/pull/924
# 0.5.10 (28. June, 2022)
- **fixed:** Make `Router` cheaper to clone ([#1123])
- **fixed:** Fix possible panic when doing trailing slash redirect ([#1124])
[#1123]: https://github.com/tokio-rs/axum/pull/1123
[#1124]: https://github.com/tokio-rs/axum/pull/1124
# 0.5.9 (20. June, 2022)
- **fixed:** Fix compile error when the `headers` is enabled and the `form`
feature is disabled ([#1107])
[#1107]: https://github.com/tokio-rs/axum/pull/1107
# 0.5.8 (18. June, 2022)
- **added:** Support resolving host name via `Forwarded` header in `Host`
extractor ([#1078])
- **added:** Implement `IntoResponse` for `Form` ([#1095])
- **change:** axum's MSRV is now 1.56 ([#1098])
[#1078]: https://github.com/tokio-rs/axum/pull/1078
[#1095]: https://github.com/tokio-rs/axum/pull/1095
[#1098]: https://github.com/tokio-rs/axum/pull/1098
# 0.5.7 (08. June, 2022)
@@ -403,7 +433,7 @@ Yanked, as it contained an accidental breaking change.
`Router`.
- **added:** Add `Handler::into_make_service_with_connect_info` for serving a
handler without a `Router`, and storing info about the incoming connection.
- **breaking:** axum's minimum supported rust version is now 1.54
- **breaking:** axum's minimum supported rust version is now 1.56
- Routing:
- Big internal refactoring of routing leading to several improvements ([#363])
- **added:** Wildcard routes like `.route("/api/users/*rest", service)` are now supported.
+6 -8
View File
@@ -1,9 +1,9 @@
[package]
name = "axum"
version = "0.5.7"
version = "0.5.10"
categories = ["asynchronous", "network-programming", "web-programming"]
description = "Web framework that focuses on ergonomics and modularity"
edition = "2018"
edition = "2021"
homepage = "https://github.com/tokio-rs/axum"
keywords = ["http", "web", "framework"]
license = "MIT"
@@ -25,7 +25,7 @@ ws = ["tokio-tungstenite", "sha-1", "base64"]
[dependencies]
async-trait = "0.1.43"
axum-core = { path = "../axum-core", version = "0.2.5" }
axum-core = { path = "../axum-core", version = "0.2.6" }
bitflags = "1.0"
bytes = "1.0"
futures-util = { version = "0.3", default-features = false, features = ["alloc"] }
@@ -33,9 +33,7 @@ http = "0.2.5"
http-body = "0.4.4"
hyper = { version = "0.14.14", features = ["server", "tcp", "stream"] }
itoa = "1.0.1"
# TODO(david): cannot ship until matchit has released a new version but we can
# start making changes
matchit = { git = "https://github.com/ibraheemdev/matchit", branch = "catchall-revamp" }
matchit = "0.6"
memchr = "2.4.1"
mime = "0.3.16"
percent-encoding = "2.1"
@@ -50,7 +48,7 @@ tower-service = "0.3"
# optional dependencies
base64 = { version = "0.13", optional = true }
headers = { version = "0.3", optional = true }
headers = { version = "0.3.7", optional = true }
multer = { version = "2.0.0", optional = true }
serde_json = { version = "1.0", features = ["raw_value"], optional = true }
serde_urlencoded = { version = "0.7", optional = true }
@@ -62,7 +60,7 @@ anyhow = "1.0"
futures = "0.3"
quickcheck = "1.0"
quickcheck_macros = "1.0"
reqwest = { version = "0.11", default-features = false, features = ["json", "stream", "multipart"] }
reqwest = { version = "0.11.11", default-features = false, features = ["json", "stream", "multipart"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1.6.1", features = ["macros", "rt", "rt-multi-thread", "net", "test-util"] }
+1 -1
View File
@@ -111,7 +111,7 @@ This crate uses `#![forbid(unsafe_code)]` to ensure everything is implemented in
## Minimum supported Rust version
axum's MSRV is 1.54.
axum's MSRV is 1.56.
## Examples
+7
View File
@@ -1,5 +1,12 @@
Error handling model and utilities
# Table of contents
- [axum's error handling model](#axums-error-handling-model)
- [Routing to fallible services](#routing-to-fallible-services)
- [Applying fallible middleware](#applying-fallible-middleware)
- [Running extractors for error handling](#running-extractors-for-error-handling)
# axum's error handling model
axum is based on [`tower::Service`] which bundles errors through its associated
+15
View File
@@ -1,5 +1,20 @@
Types and traits for extracting data from requests.
# Table of contents
- [Intro](#intro)
- [Common extractors](#common-extractors)
- [Applying multiple extractors](#applying-multiple-extractors)
- [Be careful when extracting `Request`](#be-careful-when-extracting-request)
- [Optional extractors](#optional-extractors)
- [Customizing extractor responses](#customizing-extractor-responses)
- [Accessing inner errors](#accessing-inner-errors)
- [Defining custom extractors](#defining-custom-extractors)
- [Accessing other extractors in `FromRequest` implementations](#accessing-other-extractors-in-fromrequest-implementations)
- [Request body extractors](#request-body-extractors)
# Intro
A handler function is an async function that takes any number of
"extractors" as arguments. An extractor is a type that implements
[`FromRequest`](crate::extract::FromRequest).
+11
View File
@@ -1,3 +1,14 @@
# Table of contents
- [Intro](#intro)
- [Applying middleware](#applying-middleware)
- [Commonly used middleware](#commonly-used-middleware)
- [Writing middleware](#writing-middleware)
- [Routing to services/middleware and backpressure](#routing-to-servicesmiddleware-and-backpressure)
- [Sharing state between handlers and middleware](#sharing-state-between-handlers-and-middleware)
# Intro
axum is unique in that it doesn't have its own bespoke middleware system and
instead integrates with [`tower`]. This means the ecosystem of [`tower`] and
[`tower-http`] middleware all work with axum.
+5
View File
@@ -1,5 +1,10 @@
Types and traits for generating responses.
# Table of contents
- [Building responses](#building-responses)
- [Returning different response types](#returning-different-response-types)
# Building responses
Anything that implements [`IntoResponse`] can be returned from a handler. axum
-16
View File
@@ -1,16 +0,0 @@
//! Convert an extractor into a middleware.
//!
//! See [`extractor_middleware`] for more details.
use crate::middleware::from_extractor;
pub use crate::middleware::{
future::FromExtractorResponseFuture as ResponseFuture, FromExtractor as ExtractorMiddleware,
FromExtractorLayer as ExtractorMiddlewareLayer,
};
/// Convert an extractor into a middleware.
#[deprecated(note = "Please use `axum::middleware::from_extractor` instead")]
pub fn extractor_middleware<E>() -> ExtractorMiddlewareLayer<E> {
from_extractor()
}
+2 -7
View File
@@ -4,7 +4,6 @@ use http::header;
use rejection::*;
pub mod connect_info;
pub mod extractor_middleware;
pub mod path;
pub mod rejection;
@@ -24,7 +23,6 @@ pub use axum_core::extract::{FromRequest, RequestParts};
pub use self::{
connect_info::ConnectInfo,
content_length_limit::ContentLengthLimit,
extractor_middleware::extractor_middleware,
host::Host,
path::Path,
raw_query::RawQuery,
@@ -39,11 +37,8 @@ pub use crate::Json;
pub use crate::Extension;
#[cfg(feature = "form")]
mod form;
#[cfg(feature = "form")]
#[doc(inline)]
pub use self::form::Form;
#[doc(no_inline)]
pub use crate::form::Form;
#[cfg(feature = "matched-path")]
mod matched_path;
+48 -21
View File
@@ -1,24 +1,23 @@
use super::{has_content_type, rejection::*, FromRequest, RequestParts};
use crate::body::{Bytes, HttpBody};
use crate::extract::{has_content_type, rejection::*, FromRequest, RequestParts};
use crate::BoxError;
use async_trait::async_trait;
use http::Method;
use axum_core::response::{IntoResponse, Response};
use http::header::CONTENT_TYPE;
use http::{Method, StatusCode};
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::ops::Deref;
/// Extractor that deserializes `application/x-www-form-urlencoded` requests
/// into some type.
/// URL encoded extractor and response.
///
/// `T` is expected to implement [`serde::Deserialize`].
/// # As extractor
///
/// # Example
/// If used as an extractor `Form` will deserialize `application/x-www-form-urlencoded` request
/// bodies into some target type via [`serde::Deserialize`].
///
/// ```rust,no_run
/// use axum::{
/// extract::Form,
/// routing::post,
/// Router,
/// };
/// ```rust
/// use axum::Form;
/// use serde::Deserialize;
///
/// #[derive(Deserialize)]
@@ -27,19 +26,31 @@ use std::ops::Deref;
/// password: String,
/// }
///
/// async fn accept_form(form: Form<SignUp>) {
/// let sign_up: SignUp = form.0;
///
/// async fn accept_form(Form(sign_up): Form<SignUp>) {
/// // ...
/// }
///
/// let app = Router::new().route("/sign_up", post(accept_form));
/// # async {
/// # axum::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap();
/// # };
/// ```
///
/// Note that `Content-Type: multipart/form-data` requests are not supported.
/// Note that `Content-Type: multipart/form-data` requests are not supported. Use [`Multipart`]
/// instead.
///
/// # As response
///
/// ```rust
/// use axum::Form;
/// use serde::Serialize;
///
/// #[derive(Serialize)]
/// struct Payload {
/// value: String,
/// }
///
/// async fn handler() -> Form<Payload> {
/// Form(Payload { value: "foo".to_owned() })
/// }
/// ```
///
/// [`Multipart`]: crate::extract::Multipart
#[cfg_attr(docsrs, doc(cfg(feature = "form")))]
#[derive(Debug, Clone, Copy, Default)]
pub struct Form<T>(pub T);
@@ -74,6 +85,22 @@ where
}
}
impl<T> IntoResponse for Form<T>
where
T: Serialize,
{
fn into_response(self) -> Response {
match serde_urlencoded::to_string(&self.0) {
Ok(body) => (
[(CONTENT_TYPE, mime::APPLICATION_WWW_FORM_URLENCODED.as_ref())],
body,
)
.into_response(),
Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response(),
}
}
}
impl<T> Deref for Form<T> {
type Target = T;
+6
View File
@@ -395,6 +395,8 @@
pub(crate) mod macros;
mod extension;
#[cfg(feature = "form")]
mod form;
#[cfg(feature = "json")]
mod json;
#[cfg(feature = "headers")]
@@ -434,5 +436,9 @@ pub use self::routing::Router;
#[cfg(feature = "headers")]
pub use self::typed_header::TypedHeader;
#[doc(inline)]
#[cfg(feature = "form")]
pub use self::form::Form;
#[doc(inline)]
pub use axum_core::{BoxError, Error};
+4
View File
@@ -15,6 +15,10 @@ pub use crate::Json;
#[cfg(feature = "headers")]
pub use crate::TypedHeader;
#[cfg(feature = "form")]
#[doc(no_inline)]
pub use crate::form::Form;
#[doc(no_inline)]
pub use crate::Extension;
-1
View File
@@ -1,6 +1,5 @@
use axum_core::response::{IntoResponse, Response};
use http::{header::LOCATION, HeaderValue, StatusCode};
use std::convert::TryFrom;
/// Response that redirects the request to another location.
///
+67 -33
View File
@@ -912,6 +912,32 @@ impl<ReqBody, E> MethodRouter<ReqBody, E> {
S: Service<Request<ReqBody>, Response = Response, Error = E> + Clone + Send + 'static,
S::Future: Send + 'static,
{
macro_rules! set_service {
(
$filter:ident,
$svc:ident,
$allow_header:ident,
[
$(
($out:ident, $variant:ident, [$($method:literal),+])
),+
$(,)?
]
) => {
$(
if $filter.contains(MethodFilter::$variant) {
if $out.is_some() {
panic!("Overlapping method route. Cannot add two method routes that both handle `{}`", stringify!($variant))
}
$out = $svc.clone();
$(
append_allow_header(&mut $allow_header, $method);
)+
}
)+
}
}
// written with a pattern match like this to ensure we update all fields
let Self {
mut get,
@@ -927,39 +953,21 @@ impl<ReqBody, E> MethodRouter<ReqBody, E> {
_request_body: _,
} = self;
let svc = Some(Route::new(svc));
if filter.contains(MethodFilter::GET) {
get = svc.clone();
append_allow_header(&mut allow_header, "GET");
append_allow_header(&mut allow_header, "HEAD");
}
if filter.contains(MethodFilter::HEAD) {
append_allow_header(&mut allow_header, "HEAD");
head = svc.clone();
}
if filter.contains(MethodFilter::DELETE) {
append_allow_header(&mut allow_header, "DELETE");
delete = svc.clone();
}
if filter.contains(MethodFilter::OPTIONS) {
append_allow_header(&mut allow_header, "OPTIONS");
options = svc.clone();
}
if filter.contains(MethodFilter::PATCH) {
append_allow_header(&mut allow_header, "PATCH");
patch = svc.clone();
}
if filter.contains(MethodFilter::POST) {
append_allow_header(&mut allow_header, "POST");
post = svc.clone();
}
if filter.contains(MethodFilter::PUT) {
append_allow_header(&mut allow_header, "PUT");
put = svc.clone();
}
if filter.contains(MethodFilter::TRACE) {
append_allow_header(&mut allow_header, "TRACE");
trace = svc;
}
set_service!(
filter,
svc,
allow_header,
[
(get, GET, ["GET", "HEAD"]),
(head, HEAD, ["HEAD"]),
(delete, DELETE, ["DELETE"]),
(options, OPTIONS, ["OPTIONS"]),
(patch, PATCH, ["PATCH"]),
(post, POST, ["POST"]),
(put, PUT, ["PUT"]),
(trace, TRACE, ["TRACE"]),
]
);
Self {
get,
head,
@@ -1294,6 +1302,32 @@ mod tests {
assert_eq!(headers[ALLOW], "GET,POST");
}
#[tokio::test]
#[should_panic(
expected = "Overlapping method route. Cannot add two method routes that both handle `GET`"
)]
async fn handler_overlaps() {
let _: MethodRouter = get(ok).get(ok);
}
#[tokio::test]
#[should_panic(
expected = "Overlapping method route. Cannot add two method routes that both handle `POST`"
)]
async fn service_overlaps() {
let _: MethodRouter = post_service(ok.into_service()).post_service(ok.into_service());
}
#[tokio::test]
async fn get_head_does_not_overlap() {
let _: MethodRouter = get(ok).head(ok);
}
#[tokio::test]
async fn head_get_does_not_overlap() {
let _: MethodRouter = head(ok).get(ok);
}
async fn call<S>(method: Method, svc: &mut S) -> (StatusCode, HeaderMap, String)
where
S: Service<Request<Body>, Response = Response, Error = Infallible>,
+52 -32
View File
@@ -64,7 +64,7 @@ impl RouteId {
/// The router type for composing handlers and services.
pub struct Router<B = Body> {
routes: HashMap<RouteId, Endpoint<B>>,
node: Node,
node: Arc<Node>,
fallback: Fallback<B>,
}
@@ -72,7 +72,7 @@ impl<B> Clone for Router<B> {
fn clone(&self) -> Self {
Self {
routes: self.routes.clone(),
node: self.node.clone(),
node: Arc::clone(&self.node),
fallback: self.fallback.clone(),
}
}
@@ -157,9 +157,12 @@ where
Err(service) => Endpoint::Route(Route::new(service)),
};
if let Err(err) = self.node.insert(path, id) {
let mut node =
Arc::try_unwrap(Arc::clone(&self.node)).unwrap_or_else(|node| (*node).clone());
if let Err(err) = node.insert(path, id) {
panic!("Invalid route: {}", err);
}
self.node = Arc::new(node);
self.routes.insert(id, service);
@@ -190,12 +193,12 @@ where
panic!("Cannot nest `Router`s that has a fallback");
}
for (id, nested_path) in node.route_id_to_path {
let route = routes.remove(&id).unwrap();
let full_path: Cow<str> = if &*nested_path == "/" {
for (id, nested_path) in &node.route_id_to_path {
let route = routes.remove(id).unwrap();
let full_path: Cow<str> = if &**nested_path == "/" {
path.into()
} else if path == "/" {
(&*nested_path).into()
(&**nested_path).into()
} else if let Some(path) = path.strip_suffix('/') {
format!("{}{}", path, nested_path).into()
} else {
@@ -289,15 +292,15 @@ where
pub fn layer<L, NewReqBody, NewResBody>(self, layer: L) -> Router<NewReqBody>
where
L: Layer<Route<B>>,
L::Service: Service<Request<NewReqBody>, Response = Response<NewResBody>, Error = Infallible>
+ Clone
+ Send
+ 'static,
L::Service:
Service<Request<NewReqBody>, Response = Response<NewResBody>> + Clone + Send + 'static,
<L::Service as Service<Request<NewReqBody>>>::Error: Into<Infallible> + 'static,
<L::Service as Service<Request<NewReqBody>>>::Future: Send + 'static,
NewResBody: HttpBody<Data = Bytes> + Send + 'static,
NewResBody::Error: Into<BoxError>,
{
let layer = ServiceBuilder::new()
.map_err(Into::into)
.layer(MapResponseBodyLayer::new(boxed))
.layer(layer)
.into_inner();
@@ -329,15 +332,14 @@ where
pub fn route_layer<L, NewResBody>(self, layer: L) -> Self
where
L: Layer<Route<B>>,
L::Service: Service<Request<B>, Response = Response<NewResBody>, Error = Infallible>
+ Clone
+ Send
+ 'static,
L::Service: Service<Request<B>, Response = Response<NewResBody>> + Clone + Send + 'static,
<L::Service as Service<Request<B>>>::Error: Into<Infallible> + 'static,
<L::Service as Service<Request<B>>>::Future: Send + 'static,
NewResBody: HttpBody<Data = Bytes> + Send + 'static,
NewResBody::Error: Into<BoxError>,
{
let layer = ServiceBuilder::new()
.map_err(Into::into)
.layer(MapResponseBodyLayer::new(boxed))
.layer(layer)
.into_inner();
@@ -466,29 +468,35 @@ where
match self.node.at(&path) {
Ok(match_) => self.call_route(match_, req),
Err(MatchError::MissingTrailingSlash) => {
let new_uri = replace_trailing_slash(req.uri(), &format!("{}/", &path));
Err(err) => {
let mut fallback = match &self.fallback {
Fallback::Default(inner) => inner.clone(),
Fallback::Custom(inner) => inner.clone(),
};
RouteFuture::from_response(
Redirect::permanent(&new_uri.to_string()).into_response(),
)
}
Err(MatchError::ExtraTrailingSlash) => {
let new_uri = replace_trailing_slash(req.uri(), path.strip_suffix('/').unwrap());
let new_uri = match err {
MatchError::MissingTrailingSlash => {
replace_path(req.uri(), &format!("{}/", &path))
}
MatchError::ExtraTrailingSlash => {
replace_path(req.uri(), path.strip_suffix('/').unwrap())
}
MatchError::NotFound => None,
};
RouteFuture::from_response(
Redirect::permanent(&new_uri.to_string()).into_response(),
)
if let Some(new_uri) = new_uri {
RouteFuture::from_response(
Redirect::permanent(&new_uri.to_string()).into_response(),
)
} else {
fallback.call(req)
}
}
Err(MatchError::NotFound) => match &self.fallback {
Fallback::Default(inner) => inner.clone().call(req),
Fallback::Custom(inner) => inner.clone().call(req),
},
}
}
}
fn replace_trailing_slash(uri: &Uri, new_path: &str) -> Uri {
fn replace_path(uri: &Uri, new_path: &str) -> Option<Uri> {
let mut new_path_and_query = new_path.to_owned();
if let Some(query) = uri.query() {
new_path_and_query.push('?');
@@ -498,7 +506,7 @@ fn replace_trailing_slash(uri: &Uri, new_path: &str) -> Uri {
let mut parts = uri.clone().into_parts();
parts.path_and_query = Some(new_path_and_query.parse().unwrap());
Uri::from_parts(parts).unwrap()
Uri::from_parts(parts).ok()
}
/// Wrapper around `matchit::Router` that supports merging two `Router`s.
@@ -601,7 +609,19 @@ impl<B> fmt::Debug for Endpoint<B> {
}
#[test]
#[allow(warnings)]
fn traits() {
use crate::test_helpers::*;
assert_send::<Router<()>>();
}
// https://github.com/tokio-rs/axum/issues/1122
#[test]
fn test_replace_trailing_slash() {
let uri = "api.ipify.org:80".parse::<Uri>().unwrap();
assert!(uri.scheme().is_none());
assert_eq!(uri.authority(), Some(&"api.ipify.org:80".parse().unwrap()));
assert!(uri.path_and_query().is_none());
replace_path(&uri, "/foo");
}
+1 -1
View File
@@ -138,7 +138,7 @@ where
let a = a.map(Some).chain(std::iter::repeat_with(|| None));
let b = b.map(Some).chain(std::iter::repeat_with(|| None));
a.zip(b)
// use `map_while` when its stable in our MSRV
// use `map_while` when its stable in our MSRV (1.57)
.take_while(|(a, b)| a.is_some() || b.is_some())
.filter_map(|(a, b)| match (a, b) {
(Some(a), Some(b)) => Some(Item::Both(a, b)),
+1 -4
View File
@@ -5,10 +5,7 @@ use http::{
Request, StatusCode,
};
use hyper::{Body, Server};
use std::{
convert::TryFrom,
net::{SocketAddr, TcpListener},
};
use std::net::{SocketAddr, TcpListener};
use tower::make::Shared;
use tower_service::Service;
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "example-async-graphql"
version = "0.1.0"
edition = "2018"
edition = "2021"
publish = false
[dependencies]
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "example-chat"
version = "0.1.0"
edition = "2018"
edition = "2021"
publish = false
[dependencies]
@@ -1,7 +1,7 @@
[package]
name = "example-consume-body-in-extractor-or-middleware"
version = "0.1.0"
edition = "2018"
edition = "2021"
publish = false
[dependencies]
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "example-cors"
version = "0.1.0"
edition = "2018"
edition = "2021"
publish = false
[dependencies]
@@ -1,7 +1,7 @@
[package]
name = "example-customize-extractor-error"
version = "0.1.0"
edition = "2018"
edition = "2021"
publish = false
[dependencies]
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "example-customize-path-rejection"
version = "0.1.0"
edition = "2018"
edition = "2021"
publish = false
[dependencies]
@@ -1,7 +1,7 @@
[package]
name = "example-error-handling-and-dependency-injection"
version = "0.1.0"
edition = "2018"
edition = "2021"
publish = false
[dependencies]
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "example-form"
version = "0.1.0"
edition = "2018"
edition = "2021"
publish = false
[dependencies]
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "example-global-404-handler"
version = "0.1.0"
edition = "2018"
edition = "2021"
publish = false
[dependencies]
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "example-graceful-shutdown"
version = "0.1.0"
edition = "2018"
edition = "2021"
publish = false
[dependencies]
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "example-hello-world"
version = "0.1.0"
edition = "2018"
edition = "2021"
publish = false
[dependencies]
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "example-http-proxy"
version = "0.1.0"
edition = "2018"
edition = "2021"
publish = false
[dependencies]
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "example-jwt"
version = "0.1.0"
edition = "2018"
edition = "2021"
publish = false
[dependencies]
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "example-key-value-store"
version = "0.1.0"
edition = "2018"
edition = "2021"
publish = false
[dependencies]
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "example-low-level-rustls"
version = "0.1.0"
edition = "2018"
edition = "2021"
publish = false
[dependencies]
+15 -4
View File
@@ -11,7 +11,14 @@ use hyper::server::{
conn::{AddrIncoming, Http},
};
use rustls_pemfile::{certs, pkcs8_private_keys};
use std::{fs::File, io::BufReader, net::SocketAddr, pin::Pin, sync::Arc};
use std::{
fs::File,
io::BufReader,
net::SocketAddr,
path::{Path, PathBuf},
pin::Pin,
sync::Arc,
};
use tokio::net::TcpListener;
use tokio_rustls::{
rustls::{Certificate, PrivateKey, ServerConfig},
@@ -30,8 +37,12 @@ async fn main() {
.init();
let rustls_config = rustls_server_config(
"examples/tls-rustls/self_signed_certs/key.pem",
"examples/tls-rustls/self_signed_certs/cert.pem",
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("self_signed_certs")
.join("key.pem"),
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("self_signed_certs")
.join("cert.pem"),
);
let acceptor = TlsAcceptor::from(rustls_config);
@@ -65,7 +76,7 @@ async fn handler(ConnectInfo(addr): ConnectInfo<SocketAddr>) -> String {
addr.to_string()
}
fn rustls_server_config(key: &str, cert: &str) -> Arc<ServerConfig> {
fn rustls_server_config(key: impl AsRef<Path>, cert: impl AsRef<Path>) -> Arc<ServerConfig> {
let mut key_reader = BufReader::new(File::open(key).unwrap());
let mut cert_reader = BufReader::new(File::open(cert).unwrap());
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "example-multipart-form"
version = "0.1.0"
edition = "2018"
edition = "2021"
publish = false
[dependencies]
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "example-oauth"
version = "0.1.0"
edition = "2018"
edition = "2021"
publish = false
[dependencies]
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "example-print-request-response"
version = "0.1.0"
edition = "2018"
edition = "2021"
publish = false
[dependencies]
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "example-prometheus-metrics"
version = "0.1.0"
edition = "2018"
edition = "2021"
publish = false
[dependencies]
@@ -1,7 +1,7 @@
[package]
name = "example-query-params-with-empty-strings"
version = "0.1.0"
edition = "2018"
edition = "2021"
publish = false
[dependencies]
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "example-readme"
version = "0.1.0"
edition = "2018"
edition = "2021"
publish = false
[dependencies]
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "example-rest-grpc-multiplex"
version = "0.1.0"
edition = "2018"
edition = "2021"
publish = false
[dependencies]
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "example-reverse-proxy"
version = "0.1.0"
edition = "2018"
edition = "2021"
[dependencies]
axum = { path = "../../axum" }
+1 -1
View File
@@ -14,7 +14,7 @@ use axum::{
Router,
};
use hyper::{client::HttpConnector, Body};
use std::{convert::TryFrom, net::SocketAddr};
use std::net::SocketAddr;
type Client = hyper::client::Client<HttpConnector, Body>;
@@ -1,7 +1,7 @@
[package]
name = "example-routes-and-handlers-close-together"
version = "0.1.0"
edition = "2018"
edition = "2021"
publish = false
[dependencies]
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "example-sessions"
version = "0.1.0"
edition = "2018"
edition = "2021"
publish = false
[dependencies]
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "example-sqlx-postgres"
version = "0.1.0"
edition = "2018"
edition = "2021"
publish = false
[dependencies]
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "example-sse"
version = "0.1.0"
edition = "2018"
edition = "2021"
publish = false
[dependencies]
+13 -10
View File
@@ -12,7 +12,7 @@ use axum::{
Router,
};
use futures::stream::{self, Stream};
use std::{convert::Infallible, net::SocketAddr, time::Duration};
use std::{convert::Infallible, net::SocketAddr, path::PathBuf, time::Duration};
use tokio_stream::StreamExt as _;
use tower_http::{services::ServeDir, trace::TraceLayer};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
@@ -27,14 +27,17 @@ async fn main() {
.with(tracing_subscriber::fmt::layer())
.init();
let static_files_service =
get_service(ServeDir::new("examples/sse/assets").append_index_html_on_directories(true))
.handle_error(|error: std::io::Error| async move {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Unhandled internal error: {}", error),
)
});
let assets_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("assets");
let static_files_service = get_service(
ServeDir::new(assets_dir).append_index_html_on_directories(true),
)
.handle_error(|error: std::io::Error| async move {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Unhandled internal error: {}", error),
)
});
// build our application with a route
let app = Router::new()
@@ -59,7 +62,7 @@ async fn sse_handler(
// A `Stream` that repeats an event every second
let stream = stream::repeat_with(|| Event::default().data("hi!"))
.map(Ok)
.throttle(Duration::from_secs(10));
.throttle(Duration::from_secs(1));
Sse::new(stream).keep_alive(
axum::response::sse::KeepAlive::new()
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "example-static-file-server"
version = "0.1.0"
edition = "2018"
edition = "2021"
publish = false
[dependencies]
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "example-stream-to-file"
version = "0.1.0"
edition = "2018"
edition = "2021"
publish = false
[dependencies]
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "example-templates"
version = "0.1.0"
edition = "2018"
edition = "2021"
publish = false
[dependencies]
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "example-testing"
version = "0.1.0"
edition = "2018"
edition = "2021"
publish = false
[dependencies]
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "example-tls-rustls"
version = "0.1.0"
edition = "2018"
edition = "2021"
publish = false
[dependencies]
+7 -3
View File
@@ -6,7 +6,7 @@
use axum::{routing::get, Router};
use axum_server::tls_rustls::RustlsConfig;
use std::net::SocketAddr;
use std::{net::SocketAddr, path::PathBuf};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
#[tokio::main]
@@ -19,8 +19,12 @@ async fn main() {
.init();
let config = RustlsConfig::from_pem_file(
"examples/tls-rustls/self_signed_certs/cert.pem",
"examples/tls-rustls/self_signed_certs/key.pem",
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("self_signed_certs")
.join("cert.pem"),
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("self_signed_certs")
.join("key.pem"),
)
.await
.unwrap();
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "example-todos"
version = "0.1.0"
edition = "2018"
edition = "2021"
publish = false
[dependencies]
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "example-tokio-postgres"
version = "0.1.0"
edition = "2018"
edition = "2021"
publish = false
[dependencies]
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "example-tracing-aka-logging"
version = "0.1.0"
edition = "2018"
edition = "2021"
publish = false
[dependencies]
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "example-unix-domain-socket"
version = "0.1.0"
edition = "2018"
edition = "2021"
publish = false
[dependencies]
+1 -1
View File
@@ -1,5 +1,5 @@
[package]
edition = "2018"
edition = "2021"
name = "example-validator"
publish = false
version = "0.1.0"
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "example-versioning"
version = "0.1.0"
edition = "2018"
edition = "2021"
publish = false
[dependencies]
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "example-websockets"
version = "0.1.0"
edition = "2018"
edition = "2021"
publish = false
[dependencies]
+10 -10
View File
@@ -16,7 +16,7 @@ use axum::{
routing::{get, get_service},
Router,
};
use std::net::SocketAddr;
use std::{net::SocketAddr, path::PathBuf};
use tower_http::{
services::ServeDir,
trace::{DefaultMakeSpan, TraceLayer},
@@ -33,18 +33,18 @@ async fn main() {
.with(tracing_subscriber::fmt::layer())
.init();
let assets_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("assets");
// build our application with some routes
let app = Router::new()
.fallback(
get_service(
ServeDir::new("examples/websockets/assets").append_index_html_on_directories(true),
)
.handle_error(|error: std::io::Error| async move {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Unhandled internal error: {}", error),
)
}),
get_service(ServeDir::new(assets_dir).append_index_html_on_directories(true))
.handle_error(|error: std::io::Error| async move {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Unhandled internal error: {}", error),
)
}),
)
// routes are matched from bottom to top, so we have to put `nest` at the
// top since it matches all routes
+16
View File
@@ -0,0 +1,16 @@
[package]
name = "internal-minimal-versions"
version = "0.1.0"
edition = "2021"
publish = false
# these dependencies don't build if installed with `cargo +nightly update -Z
# minimal-versions` so we add them here to make sure we get a version that
# does build
#
# this only matters for axum's CI
[dependencies]
crc32fast = "1.3.2"
gcc = "0.3.55"
time = "0.3.9"
tungstenite = "0.17.2"
+1
View File
@@ -0,0 +1 @@
// intentionally left empty