mirror of
https://github.com/tokio-rs/axum.git
synced 2026-08-31 00:00:10 +02:00
Merge branch 'main' into separate-nesting-opaque-services
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
||||
msrv = "1.54"
|
||||
msrv = "1.56"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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,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.
|
||||
///
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 },]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) }),
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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,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.
|
||||
///
|
||||
|
||||
@@ -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
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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,7 +1,7 @@
|
||||
[package]
|
||||
name = "example-async-graphql"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -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,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,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,7 +1,7 @@
|
||||
[package]
|
||||
name = "example-form"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "example-global-404-handler"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "example-graceful-shutdown"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "example-hello-world"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "example-http-proxy"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "example-jwt"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "example-key-value-store"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "example-low-level-rustls"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -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,7 +1,7 @@
|
||||
[package]
|
||||
name = "example-multipart-form"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "example-oauth"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "example-print-request-response"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -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,7 +1,7 @@
|
||||
[package]
|
||||
name = "example-readme"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "example-rest-grpc-multiplex"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "example-reverse-proxy"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
axum = { path = "../../axum" }
|
||||
|
||||
@@ -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,7 +1,7 @@
|
||||
[package]
|
||||
name = "example-sessions"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "example-sqlx-postgres"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "example-sse"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
||||
+13
-10
@@ -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,7 +1,7 @@
|
||||
[package]
|
||||
name = "example-static-file-server"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "example-stream-to-file"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "example-templates"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "example-testing"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "example-tls-rustls"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -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,7 +1,7 @@
|
||||
[package]
|
||||
name = "example-todos"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "example-tokio-postgres"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "example-tracing-aka-logging"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "example-unix-domain-socket"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
name = "example-validator"
|
||||
publish = false
|
||||
version = "0.1.0"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "example-versioning"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "example-websockets"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
@@ -0,0 +1 @@
|
||||
// intentionally left empty
|
||||
Reference in New Issue
Block a user