Merge branch 'master' into release
Some checks failed
Release / publish (map[name:poem path:poem registryName:poem]) (push) Has been cancelled
Release / publish (map[name:poem-derive path:poem-derive registryName:poem-derive]) (push) Has been cancelled
Release / publish (map[name:poem-grpc path:poem-grpc registryName:poem-grpc]) (push) Has been cancelled
Release / publish (map[name:poem-grpc-build path:poem-grpc-build registryName:poem-grpc-build]) (push) Has been cancelled
Release / publish (map[name:poem-lambda path:poem-lambda registryName:poem-lambda]) (push) Has been cancelled
Release / publish (map[name:poem-mcpserver path:poem-mcpserver registryName:poem-mcpserver]) (push) Has been cancelled
Release / publish (map[name:poem-mcpserver-macros path:poem-mcpserver-macros registryName:poem-mcpserver-macros]) (push) Has been cancelled
Release / publish (map[name:poem-openapi path:poem-openapi registryName:poem-openapi]) (push) Has been cancelled
Release / publish (map[name:poem-openapi-derive path:poem-openapi-derive registryName:poem-openapi-derive]) (push) Has been cancelled

This commit is contained in:
Sunli
2025-07-28 12:38:16 +08:00
71 changed files with 767 additions and 271 deletions

View File

@@ -48,10 +48,10 @@ jobs:
- 5432:5432
options: -e POSTGRES_PASSWORD=123456 -e POSTGRES_DB=test_poem_sessions
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
# Use nightly Rust to check the format
- uses: actions-rs/toolchain@v1
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: nightly
components: rustfmt, clippy
@@ -59,7 +59,7 @@ jobs:
- name: Check Format
run: cargo fmt --all -- --check
# Switch to stable Rust
- uses: actions-rs/toolchain@v1
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
components: rustfmt, clippy
@@ -87,7 +87,7 @@ jobs:
uses: arduino/setup-protoc@v1
# Use nightly Rust to check the format
- uses: actions-rs/toolchain@v1
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: nightly
components: rustfmt, clippy
@@ -95,7 +95,7 @@ jobs:
run: cargo fmt --all -- --check
working-directory: examples
# Switch to stable Rust
- uses: actions-rs/toolchain@v1
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
components: rustfmt, clippy

View File

@@ -10,6 +10,7 @@ members = [
"poem-grpc",
"poem-mcpserver",
"poem-mcpserver-macros",
"poem-worker",
]
[workspace.package]
@@ -22,8 +23,8 @@ repository = "https://github.com/poem-web/poem"
rust-version = "1.85"
[workspace.dependencies]
poem = { path = "poem", version = "3.1.11", default-features = false }
poem-derive = { path = "poem-derive", version = "3.1.11" }
poem = { path = "poem", version = "3.1.12", default-features = false }
poem-derive = { path = "poem-derive", version = "3.1.12" }
poem-openapi-derive = { path = "poem-openapi-derive", version = "5.1.15" }
poem-grpc-build = { path = "poem-grpc-build", version = "0.5.6" }
poem-mcpserver-macros = { path = "poem-mcpserver-macros", version = "0.2.4" }
@@ -34,7 +35,7 @@ quote = "1.0.9"
syn = { version = "2.0" }
tokio = "1.39.1"
serde_json = "1.0.68"
sonic-rs = "0.3.5"
sonic-rs = "0.5.1"
serde = { version = "1.0.130", features = ["derive"] }
thiserror = "2.0"
regex = "1.5.5"
@@ -56,7 +57,7 @@ async-stream = "0.3.6"
tokio-util = "0.7.14"
rand = "0.9.0"
time = "0.3.39"
schemars = "0.9"
schemars = "1.0"
# rustls, update together
rustls = "0.23"

View File

@@ -1,6 +1,7 @@
[workspace]
resolver = "2"
members = ["poem/*", "openapi/*", "grpc/*", "mcpserver/*"]
exclude = ["poem/worker-hello-world"]
[workspace.package]
version = "0.1.0"
@@ -14,6 +15,7 @@ poem-openapi = { path = "../poem-openapi", features = ["swagger-ui"] }
poem-lambda = { path = "../poem-lambda" }
poem-grpc-build = { path = "../poem-grpc-build" }
poem-mcpserver = { path = "../poem-mcpserver" }
poem-worker = { path = "../poem-worker" }
tokio = "1.17.0"
tracing-subscriber = { version = "0.3.9", features = ["env-filter"] }

View File

@@ -6,7 +6,7 @@ edition = "2021"
[dependencies]
poem-mcpserver = { workspace = true, features = ["streamable-http"] }
serde = { version = "1.0.219", features = ["derive"] }
schemars = "0.9"
schemars = "1.0"
poem = { workspace = true, features = ["sse"] }
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "sync"] }
tracing-subscriber.workspace = true

View File

@@ -6,5 +6,5 @@ edition = "2021"
[dependencies]
poem-mcpserver.workspace = true
serde = { version = "1.0.219", features = ["derive"] }
schemars = "0.9"
schemars = "1.0"
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "sync"] }

View File

@@ -21,7 +21,7 @@ use tokio::{spawn, time::sleep};
#[handler]
fn hello(Path(name): Path<String>) -> String {
format!("hello: {}", name)
format!("hello: {name}")
}
#[tokio::main]
@@ -55,7 +55,7 @@ async fn main() -> Result<(), std::io::Error> {
{
Ok(result) => result.rustls_key,
Err(err) => {
eprintln!("failed to issue certificate: {}", err);
eprintln!("failed to issue certificate: {err}");
sleep(Duration::from_secs(60 * 5)).await;
continue;
}

View File

@@ -8,10 +8,13 @@ publish.workspace = true
poem = { workspace = true, features = ["opentelemetry"] }
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
tracing-subscriber.workspace = true
opentelemetry = { version = "0.29.0", features = ["metrics"] }
opentelemetry_sdk = { version = "0.29.0", features = ["rt-tokio"] }
opentelemetry-http = { version = "0.29.0" }
opentelemetry-otlp = { version = "0.29.0", default-features = false, features = ["trace", "grpc-tonic"] }
opentelemetry = { version = "0.30.0", features = ["metrics"] }
opentelemetry_sdk = { version = "0.30.0", features = ["rt-tokio"] }
opentelemetry-http = { version = "0.30.0" }
opentelemetry-otlp = { version = "0.30.0", default-features = false, features = [
"trace",
"grpc-tonic",
] }
reqwest = "0.12"
[[bin]]

View File

@@ -8,7 +8,7 @@ publish.workspace = true
poem = { workspace = true, features = ["redis-session"] }
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
tracing-subscriber.workspace = true
redis = { version = "0.31", features = [
redis = { version = "0.32", features = [
"aio",
"tokio-comp",
"connection-manager",

View File

@@ -0,0 +1,4 @@
target
node_modules
.wrangler
build

View File

@@ -0,0 +1,17 @@
[package]
name = "examples-worker-hello-world"
version = "0.1.0"
edition = "2021"
[workspace]
[package.metadata.release]
release = false
[lib]
crate-type = ["cdylib"]
[dependencies]
worker = { version = "0.6.0" }
poem = { path = "../../../poem", default-features = false }
poem-worker = { path = "../../../poem-worker" }

View File

@@ -0,0 +1,6 @@
# Poem with Cloudflare worker
## Prequirement
- [worker-build](https://github.com/cloudflare/workers-rs/tree/main/worker-build)
- [wranger](https://developers.cloudflare.com/workers/wrangler/install-and-update/)

View File

@@ -0,0 +1,15 @@
use poem::{get, handler, web::Path, Route};
use poem_worker::{CloudflareProperties, Server};
use worker::event;
#[handler]
fn hello(Path(name): Path<String>, _cf: CloudflareProperties) -> String {
format!("hello: {}", name)
}
#[event(start)]
fn start() {
let app = Route::new().at("/hello/:name", get(hello));
Server::new().run(app);
}

View File

@@ -0,0 +1,6 @@
name = "worker-hello-world"
main = "build/worker/shim.mjs"
compatibility_date = "2025-07-20"
[build]
command = "worker-build --release"

View File

@@ -1,6 +1,6 @@
[package]
name = "poem-derive"
version = "3.1.11"
version = "3.1.12"
authors.workspace = true
edition.workspace = true
license.workspace = true

View File

@@ -1,6 +1,6 @@
[package]
name = "poem-grpc-build"
version = "0.5.6"
version = "0.5.7"
authors.workspace = true
edition.workspace = true
license.workspace = true

View File

@@ -1,6 +1,6 @@
[package]
name = "poem-grpc"
version = "0.5.6"
version = "0.5.7"
authors.workspace = true
edition.workspace = true
license.workspace = true

View File

@@ -1,5 +1,5 @@
use std::{
io::{Error, ErrorKind, Result},
io::{Error, Result},
marker::PhantomData,
};
@@ -54,8 +54,7 @@ where
fn encode(&mut self, item: Self::Item, buf: &mut BytesMut) -> Result<()> {
let mut ser = serde_json::Serializer::new(buf.writer());
item.serialize(&mut ser)
.map_err(|err| Error::new(ErrorKind::Other, err))
item.serialize(&mut ser).map_err(Error::other)
}
}
@@ -67,7 +66,7 @@ impl<U: DeserializeOwned + Send + 'static> Decoder for JsonDecoder<U> {
fn decode(&mut self, buf: &[u8]) -> Result<Self::Item> {
let mut de = serde_json::Deserializer::from_slice(buf);
U::deserialize(&mut de).map_err(|err| Error::new(ErrorKind::Other, err))
U::deserialize(&mut de).map_err(Error::other)
}
}
@@ -116,7 +115,7 @@ where
fn encode(&mut self, item: Self::Item, buf: &mut BytesMut) -> Result<()> {
let mut ser = serde_json::Serializer::new(buf.writer());
item.serialize(i64string_serializer::I64ToStringSerializer(&mut ser))
.map_err(|err| Error::new(ErrorKind::Other, err))
.map_err(Error::other)
}
}
@@ -129,6 +128,6 @@ impl<U: DeserializeOwned + Send + 'static> Decoder for JsonI64ToStringDecoder<U>
fn decode(&mut self, buf: &[u8]) -> Result<Self::Item> {
let mut de = serde_json::Deserializer::from_slice(buf);
U::deserialize(i64string_deserializer::I64ToStringDeserializer(&mut de))
.map_err(|err| Error::new(ErrorKind::Other, err))
.map_err(Error::other)
}
}

View File

@@ -46,7 +46,7 @@ pub trait Codec: Default {
/// Returns whether the specified content type is supported
#[inline]
fn check_content_type(&self, ct: &str) -> bool {
Self::CONTENT_TYPES.iter().any(|value| *value == ct)
Self::CONTENT_TYPES.contains(&ct)
}
/// Create the encoder

View File

@@ -1,5 +1,5 @@
use std::{
io::{Error, ErrorKind, Result},
io::{Error, Result},
marker::PhantomData,
};
@@ -49,9 +49,7 @@ where
type Item = T;
fn encode(&mut self, message: Self::Item, buf: &mut BytesMut) -> Result<()> {
message
.encode(buf)
.map_err(|err| Error::new(ErrorKind::Other, err))
message.encode(buf).map_err(Error::other)
}
}
@@ -65,6 +63,6 @@ where
type Item = U;
fn decode(&mut self, buf: &[u8]) -> Result<Self::Item> {
U::decode(buf).map_err(|err| Error::new(ErrorKind::Other, err))
U::decode(buf).map_err(Error::other)
}
}

View File

@@ -20,7 +20,7 @@ use tower_service::Service;
pub(crate) enum MaybeHttpsStream {
TcpStream(TokioIo<TcpStream>),
TlsStream {
stream: TokioIo<TlsStream<TcpStream>>,
stream: Box<TokioIo<TlsStream<TcpStream>>>,
is_http2: bool,
},
}
@@ -131,13 +131,13 @@ async fn do_connect(
.unwrap_or_else(|| if scheme == Scheme::HTTPS { 443 } else { 80 });
if scheme == Scheme::HTTP {
let stream = TcpStream::connect(format!("{}:{}", host, port)).await?;
let stream = TcpStream::connect(format!("{host}:{port}")).await?;
Ok(MaybeHttpsStream::TcpStream(TokioIo::new(stream)))
} else if scheme == Scheme::HTTPS {
let mut tls_config = tls_config.unwrap_or_else(default_tls_config);
tls_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
let connector = TlsConnector::from(Arc::new(tls_config));
let stream = TcpStream::connect(format!("{}:{}", host, port)).await?;
let stream = TcpStream::connect(format!("{host}:{port}")).await?;
let domain = host.try_into().map_err(IoError::other)?;
let mut is_http2 = false;
let stream = connector
@@ -146,7 +146,7 @@ async fn do_connect(
})
.await?;
Ok(MaybeHttpsStream::TlsStream {
stream: TokioIo::new(stream),
stream: Box::new(TokioIo::new(stream)),
is_http2,
})
} else {

View File

@@ -95,7 +95,7 @@ impl proto::Health for HealthService {
Ok(Response::new(Streaming::new(async_stream::try_stream! {
while let Some(service_status) = stream.next().await {
let res = service_status.get(&service_name);
let status = res.ok_or_else(|| Status::new(Code::NotFound).with_message(format!("service `{}` not found", service_name)))?
let status = res.ok_or_else(|| Status::new(Code::NotFound).with_message(format!("service `{service_name}` not found")))?
.to_proto()
.into();
yield proto::HealthCheckResponse { status };

View File

@@ -1,6 +1,6 @@
[package]
name = "poem-lambda"
version = "5.1.3"
version = "5.1.4"
authors.workspace = true
edition.workspace = true
license.workspace = true

View File

@@ -7,7 +7,7 @@
#![cfg_attr(docsrs, feature(doc_cfg))]
#![warn(missing_docs)]
use std::{io::ErrorKind, ops::Deref, sync::Arc};
use std::{ops::Deref, sync::Arc};
pub use lambda_http::lambda_runtime::Error;
use lambda_http::{
@@ -73,7 +73,7 @@ pub async fn run(ep: impl IntoEndpoint) -> Result<(), Error> {
let data = body
.into_vec()
.await
.map_err(|_| std::io::Error::new(ErrorKind::Other, "invalid request"))?;
.map_err(|_| std::io::Error::other("invalid request"))?;
let mut lambda_resp = poem::http::Response::new(if data.is_empty() {
LambdaBody::Empty
} else {

View File

@@ -1,6 +1,6 @@
[package]
name = "poem-mcpserver-macros"
version = "0.2.4"
version = "0.2.5"
authors.workspace = true
edition.workspace = true
license.workspace = true

View File

@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
# [Unreleased]
- bump `schemars` to 1.0
# [0.2.4] 20250-06-06
- bump `schemars` to 0.9

View File

@@ -1,6 +1,6 @@
[package]
name = "poem-mcpserver"
version = "0.2.4"
version = "0.2.5"
authors.workspace = true
edition.workspace = true
license.workspace = true
@@ -28,7 +28,7 @@ schemars.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
time = { workspace = true, features = ["macros", "formatting", "parsing"] }
tokio = { workspace = true, features = ["io-std", "io-util", "rt"] }
tokio = { workspace = true, features = ["io-std", "io-util", "rt", "net"] }
poem = { workspace = true, features = ["sse"], optional = true }
rand.workspace = true
tokio-stream.workspace = true

View File

@@ -32,7 +32,7 @@
[dependencies]
poem-mcpserver.workspace = "*"
serde = { version = "1.0", features = ["derive"] }
schemars = "0.8.22"
schemars = "1.0"
```
```rust

View File

@@ -86,8 +86,7 @@ impl Tools for NoTools {
#[inline]
async fn call(&mut self, name: &str, _arguments: Value) -> Result<ToolsCallResponse, RpcError> {
Err(RpcError::method_not_found(format!(
"tool '{}' not found",
name
"tool '{name}' not found"
)))
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "poem-openapi-derive"
version = "5.1.15"
version = "5.1.16"
authors.workspace = true
edition.workspace = true
license.workspace = true

View File

@@ -302,13 +302,13 @@ fn generate_operation(
.or(ignore_case)
.or(api_args.ignore_case)
.unwrap_or(false);
let extract_param_name = is_path
.then(|| {
let n = format!("param{path_param_count}");
path_param_count += 1;
n
})
.unwrap_or_else(|| param_name.clone());
let extract_param_name = if is_path {
let n = format!("param{path_param_count}");
path_param_count += 1;
n
} else {
param_name.clone()
};
use_args.push(pname.clone());
if !hidden {

View File

@@ -1,6 +1,6 @@
[package]
name = "poem-openapi"
version = "5.1.15"
version = "5.1.16"
authors.workspace = true
edition.workspace = true
license.workspace = true

View File

@@ -6,6 +6,7 @@ use poem::{
use crate::{auth::BearerAuthorization, error::AuthorizationError};
/// Used to extract the token68 from the request.
#[derive(Debug)]
pub struct Bearer {
/// token
pub token: String,

View File

@@ -2,7 +2,7 @@ fn normalize_path(path: &str) -> String {
if path.is_empty() {
"/".to_string()
} else if !path.starts_with('/') {
format!("/{}", path)
format!("/{path}")
} else {
path.to_string()
}

View File

@@ -588,18 +588,18 @@ impl PartialEq for MetaTag {
impl Eq for MetaTag {}
impl PartialOrd for MetaTag {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.name.cmp(other.name))
}
}
impl Ord for MetaTag {
fn cmp(&self, other: &Self) -> Ordering {
self.name.cmp(other.name)
}
}
impl PartialOrd for MetaTag {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Hash for MetaTag {
fn hash<H: Hasher>(&self, state: &mut H) {
self.name.hash(state);

View File

@@ -104,7 +104,7 @@ mod tests {
for case in invalid_cases {
let result = Duration::parse_from_json(Some(Value::String(case.to_string())));
dbg!(&result);
assert!(result.is_err(), "Should have failed for: {}", case);
assert!(result.is_err(), "Should have failed for: {case}");
}
}

View File

@@ -6,7 +6,7 @@ use std::{
use poem::web::Field as PoemField;
use tokio::{
fs::File,
io::{AsyncRead, AsyncReadExt, AsyncSeek, Error as IoError, ErrorKind},
io::{AsyncRead, AsyncReadExt, AsyncSeek, Error as IoError},
};
use crate::{
@@ -64,13 +64,8 @@ impl Upload {
/// Consumes this body object to return a [`String`] that contains all data.
pub async fn into_string(self) -> Result<String, IoError> {
String::from_utf8(
self.into_vec()
.await
.map_err(|err| IoError::new(ErrorKind::Other, err))?
.to_vec(),
)
.map_err(|err| IoError::new(ErrorKind::Other, err))
String::from_utf8(self.into_vec().await.map_err(IoError::other)?.to_vec())
.map_err(IoError::other)
}
/// Consumes this body object to return a reader.

View File

@@ -26,9 +26,9 @@ pub use unique_items::UniqueItems;
use crate::registry::MetaSchema;
/// Represents a validator for validate the input value.
/// Represents a validator to validate the input value.
pub trait Validator<T>: Display {
/// Check the value is valid.
/// Checks if the value is valid.
fn check(&self, value: &T) -> bool;
}

View File

@@ -199,7 +199,7 @@ fn field_deprecated() {
}
let meta = get_meta::<Obj>();
assert_eq!(meta.properties[0].1.unwrap_inline().deprecated, true);
assert!(meta.properties[0].1.unwrap_inline().deprecated);
}
#[test]

27
poem-worker/Cargo.toml Normal file
View File

@@ -0,0 +1,27 @@
[package]
name = "poem-worker"
version = "0.1.0"
authors.workspace = true
edition.workspace = true
license.workspace = true
documentation.workspace = true
homepage.workspace = true
repository.workspace = true
rust-version.workspace = true
[dependencies]
serde = { workspace = true }
worker = { version = "0.6.0", features = ["http"] }
bytes = { workspace = true }
http = { workspace = true }
http-body = "1.0.1"
http-body-util = "0.1.0"
async-trait = "0.1.88"
poem = { workspace = true, default-features = false }
tokio = { workspace = true }
[features]
queue = ["worker/queue"]
d1 = ["worker/d1"]

50
poem-worker/src/body.rs Normal file
View File

@@ -0,0 +1,50 @@
use std::{
io,
pin::Pin,
task::{Context, Poll},
};
use http_body::{Body, Frame, SizeHint};
pub struct WorkerBody(pub(crate) worker::Body);
impl Body for WorkerBody {
type Data = bytes::Bytes;
type Error = io::Error;
#[inline]
fn poll_frame(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
let body = self.get_mut();
let inner = Pin::new(&mut body.0);
let res = inner.poll_frame(cx);
match res {
Poll::Pending => Poll::Pending,
Poll::Ready(None) => Poll::Ready(None),
Poll::Ready(Some(Ok(r))) => Poll::Ready(Some(Ok(r))),
Poll::Ready(Some(Err(e))) => match e {
worker::Error::Io(e) => Poll::Ready(Some(Err(io::Error::other(e)))),
_ => Poll::Ready(Some(Err(io::Error::other(e)))),
},
}
}
fn size_hint(&self) -> SizeHint {
self.0.size_hint()
}
fn is_end_stream(&self) -> bool {
self.0.is_end_stream()
}
}
pub fn build_worker_body(body: poem::Body) -> Result<worker::Body, worker::Error> {
let stream = body.into_bytes_stream();
worker::Body::from_stream(stream)
}

View File

@@ -0,0 +1,93 @@
use http::StatusCode;
use poem::{FromRequest, Request, RequestBody};
use serde::de::DeserializeOwned;
use worker::{Cf, TlsClientAuth};
pub struct CloudflareProperties(Cf);
impl CloudflareProperties {
pub fn colo(&self) -> String {
self.0.colo()
}
pub fn asn(&self) -> Option<u32> {
self.0.asn()
}
pub fn as_organization(&self) -> Option<String> {
self.0.as_organization()
}
pub fn country(&self) -> Option<String> {
self.0.country()
}
pub fn http_protocol(&self) -> String {
self.0.http_protocol()
}
pub fn tls_cipher(&self) -> String {
self.0.tls_cipher()
}
pub fn tls_client_auth(&self) -> Option<TlsClientAuth> {
self.0.tls_client_auth()
}
pub fn tls_version(&self) -> String {
self.0.tls_version()
}
pub fn city(&self) -> Option<String> {
self.0.city()
}
pub fn continent(&self) -> Option<String> {
self.0.continent()
}
pub fn coordinates(&self) -> Option<(f32, f32)> {
self.0.coordinates()
}
pub fn postal_code(&self) -> Option<String> {
self.0.postal_code()
}
pub fn metro_code(&self) -> Option<String> {
self.0.metro_code()
}
pub fn region(&self) -> Option<String> {
self.0.region()
}
pub fn region_code(&self) -> Option<String> {
self.0.region_code()
}
pub fn timezone_name(&self) -> String {
self.0.timezone_name()
}
pub fn is_eu_country(&self) -> bool {
self.0.is_eu_country()
}
pub fn host_metadata<T: DeserializeOwned>(&self) -> Result<Option<T>, worker::Error> {
self.0.host_metadata::<T>()
}
}
impl<'a> FromRequest<'a> for CloudflareProperties {
async fn from_request(req: &'a Request, _body: &mut RequestBody) -> Result<Self, poem::Error> {
let cf = req.data::<Cf>().ok_or_else(|| {
poem::Error::from_string(
"failed to get incoming cloudflare properties",
StatusCode::BAD_REQUEST,
)
})?;
Ok(CloudflareProperties(cf.clone()))
}
}

View File

@@ -0,0 +1,34 @@
use std::sync::Arc;
use http::StatusCode;
use poem::{FromRequest, Request, RequestBody};
#[derive(Clone)]
pub struct Context(Arc<worker::Context>);
impl Context {
pub fn new(ctx: worker::Context) -> Self {
Self(Arc::new(ctx))
}
pub fn wait_until<F>(&self, future: F)
where
F: Future<Output = ()> + 'static,
{
self.0.wait_until(future);
}
pub fn pass_through_on_exception(&self) {
self.0.pass_through_on_exception();
}
}
impl<'a> FromRequest<'a> for Context {
async fn from_request(req: &'a Request, _body: &mut RequestBody) -> poem::Result<Self> {
let ctx = req.data::<Context>().ok_or_else(|| {
poem::Error::from_string("failed to get incoming context", StatusCode::BAD_REQUEST)
})?;
Ok(ctx.clone())
}
}

90
poem-worker/src/env.rs Normal file
View File

@@ -0,0 +1,90 @@
use std::sync::Arc;
use http::StatusCode;
use poem::{FromRequest, Request, RequestBody};
use serde::de::DeserializeOwned;
use worker::{
Ai, AnalyticsEngineDataset, Bucket, DynamicDispatcher, EnvBinding, Fetcher, Hyperdrive,
ObjectNamespace, Secret, Var, kv::KvStore,
};
#[derive(Clone)]
pub struct Env(pub(crate) Arc<worker::Env>);
impl Env {
pub fn new(env: worker::Env) -> Self {
Self(Arc::new(env))
}
pub fn get_binding<T: EnvBinding>(&self, name: &str) -> worker::Result<T> {
self.0.get_binding(name)
}
pub fn ai(&self, binding: &str) -> worker::Result<Ai> {
self.0.ai(binding)
}
pub fn analytics_engine(&self, binding: &str) -> worker::Result<AnalyticsEngineDataset> {
self.0.analytics_engine(binding)
}
pub fn secret(&self, binding: &str) -> worker::Result<Secret> {
self.0.secret(binding)
}
pub fn var(&self, binding: &str) -> worker::Result<Var> {
self.0.var(binding)
}
pub fn object_var<T: DeserializeOwned>(&self, binding: &str) -> worker::Result<T> {
self.0.object_var(binding)
}
pub fn kv(&self, binding: &str) -> worker::Result<KvStore> {
self.0.kv(binding)
}
pub fn durable_object(&self, binding: &str) -> worker::Result<ObjectNamespace> {
self.0.durable_object(binding)
}
pub fn dynamic_dispatcher(&self, binding: &str) -> worker::Result<DynamicDispatcher> {
self.0.dynamic_dispatcher(binding)
}
pub fn service(&self, binding: &str) -> worker::Result<Fetcher> {
self.0.service(binding)
}
#[cfg(feature = "queue")]
pub fn queue(&self, binding: &str) -> worker::Result<worker::Queue> {
self.0.queue(binding)
}
pub fn bucket(&self, binding: &str) -> worker::Result<Bucket> {
self.0.bucket(binding)
}
#[cfg(feature = "d1")]
pub fn d1(&self, binding: &str) -> worker::Result<worker::D1Database> {
self.0.d1(binding)
}
pub fn assets(&self, binding: &str) -> worker::Result<Fetcher> {
self.0.assets(binding)
}
pub fn hyperdrive(&self, binding: &str) -> worker::Result<Hyperdrive> {
self.0.hyperdrive(binding)
}
}
impl<'a> FromRequest<'a> for Env {
async fn from_request(req: &'a Request, _body: &mut RequestBody) -> poem::Result<Self> {
let env = req.data::<Env>().ok_or_else(|| {
poem::Error::from_string("failed to get incoming env", StatusCode::BAD_REQUEST)
})?;
Ok(env.clone())
}
}

14
poem-worker/src/lib.rs Normal file
View File

@@ -0,0 +1,14 @@
pub(crate) mod body;
pub(crate) mod req;
mod cloudflare;
pub use cloudflare::*;
mod env;
pub use env::*;
mod context;
pub use context::*;
mod server;
pub use server::*;

61
poem-worker/src/req.rs Normal file
View File

@@ -0,0 +1,61 @@
use std::net::{IpAddr, SocketAddr};
use http::uri::Scheme;
use http_body_util::combinators::BoxBody;
use poem::{
Request,
web::{LocalAddr, RemoteAddr},
};
use worker::{HttpRequest, Result};
pub fn build_poem_req(req: HttpRequest) -> Result<poem::Request> {
let headers = req.headers();
let local_addr = if let Some(client_ip) = headers.get("cf-connecting-ip") {
let client_ip = client_ip
.to_str()
.map_err(|e| worker::Error::RustError(format!("{e}")))?;
let ip_addr = client_ip
.parse::<IpAddr>()
.map_err(|e| worker::Error::RustError(format!("{e}")))?;
let addr = SocketAddr::new(ip_addr, 0);
LocalAddr(poem::Addr::SocketAddr(addr))
} else {
LocalAddr::default()
};
let remote_addr = RemoteAddr(poem::Addr::Custom("worker", "".into()));
let scheme = Scheme::HTTPS;
let (parts, body) = req.into_parts();
let body = crate::body::WorkerBody(body);
let boxed_body = BoxBody::new(body);
let body = poem::Body::from(boxed_body);
let request_parts = poem::RequestParts::from((parts, local_addr, remote_addr, scheme));
Ok(Request::from_parts(request_parts, body))
}
pub fn build_worker_resp(resp: poem::Response) -> Result<worker::HttpResponse> {
let (parts, body) = resp.into_parts();
let body = crate::body::build_worker_body(body)?;
let mut builder = http::Response::builder()
.status(parts.status)
.version(parts.version)
.extension(parts.extensions);
for (key, value) in parts.headers {
if let Some(key) = key {
builder = builder.header(key, value);
}
}
let resp = builder.body(body)?;
Ok(resp)
}

67
poem-worker/src/server.rs Normal file
View File

@@ -0,0 +1,67 @@
use async_trait::async_trait;
use poem::endpoint::Endpoint;
use tokio::sync::OnceCell;
#[worker::event(fetch)]
pub async fn fetch(
request: worker::Request,
_env: worker::Env,
_ctx: worker::Context,
) -> Result<worker::Response, worker::Error> {
let cf = request.cf().cloned();
let http_req = worker::HttpRequest::try_from(request)?;
let mut poem_req = crate::req::build_poem_req(http_req)?;
if let Some(cf) = cf {
poem_req.set_data(cf);
}
poem_req.set_data(crate::Env::new(_env));
// TODO: handle error
let app = SERVER_INSTANCE.get().unwrap();
let resp = app.get_poem_response(poem_req).await;
let worker_resp = crate::req::build_worker_resp(resp)?;
let resp = worker::Response::try_from(worker_resp)?;
Ok(resp)
}
#[async_trait]
trait GetResponseInner: Send + Sync + 'static {
async fn get_poem_response(&self, req: poem::Request) -> poem::Response;
}
#[async_trait]
impl<E: Endpoint + Send + Sync + 'static> GetResponseInner for E {
async fn get_poem_response(&self, req: poem::Request) -> poem::Response {
self.get_response(req).await
}
}
pub struct Server {}
type BoxedGetResponseInner = Box<dyn GetResponseInner>;
static SERVER_INSTANCE: OnceCell<BoxedGetResponseInner> = OnceCell::const_new();
impl Default for Server {
fn default() -> Self {
Self::new()
}
}
impl Server {
pub fn new() -> Self {
Self {}
}
pub fn run(&self, app: impl Endpoint + 'static) {
SERVER_INSTANCE
.set(Box::new(app))
.map_err(|_| "Server instance can only be set once".to_string())
.unwrap();
}
}

View File

@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
# [3.1.12] 2025-07-28
- Bump `tokio-tungstenite` to `0.27`
- Bump `opentelemetry`
- Bump `x509-parser` to `0.17`
# [3.1.11] 2025-06-06
- bump `tokio-tungstenite` to `0.26`

View File

@@ -1,6 +1,6 @@
[package]
name = "poem"
version = "3.1.11"
version = "3.1.12"
authors.workspace = true
edition.workspace = true
license.workspace = true
@@ -21,7 +21,13 @@ categories = [
[features]
default = ["server"]
server = ["tokio/rt", "tokio/net", "hyper/server"]
server = [
"tokio/rt",
"tokio/net",
"hyper/server",
"hyper-util/server-auto",
"hyper-util/tokio",
]
websocket = ["tokio/rt", "tokio-tungstenite", "base64"]
multipart = ["multer"]
rustls = ["server", "tokio-rustls", "rustls-pemfile"]
@@ -76,9 +82,9 @@ bytes.workspace = true
futures-util = { workspace = true, features = ["sink"] }
http.workspace = true
hyper = { version = "1.0.0", features = ["http1", "http2"] }
hyper-util = { version = "0.1.6", features = ["server-auto", "tokio"] }
hyper-util = { version = "0.1.16", features = ["tokio"] }
http-body-util = "0.1.0"
tokio = { workspace = true, features = ["sync", "time", "macros", "net"] }
tokio = { workspace = true, features = ["sync", "time", "macros"] }
tokio-util = { workspace = true, features = ["io"] }
serde.workspace = true
sonic-rs = { workspace = true, optional = true }
@@ -99,7 +105,7 @@ sync_wrapper = { version = "1.0.0", features = ["futures"] }
# Non-feature optional dependencies
multer = { version = "3.0.0", features = ["tokio"], optional = true }
tokio-tungstenite = { version = "0.26.2", optional = true }
tokio-tungstenite = { version = "0.27", optional = true }
tokio-rustls = { workspace = true, optional = true }
rustls-pemfile = { version = "2.0.0", optional = true }
async-compression = { version = "0.4.0", optional = true, features = [
@@ -119,7 +125,7 @@ chrono = { workspace = true, optional = true, default-features = false, features
time = { version = "0.3", optional = true }
mime_guess = { version = "2.0.3", optional = true }
rand = { version = "0.9.0", optional = true }
redis = { version = "0.31", optional = true, features = [
redis = { version = "0.32", optional = true, features = [
"aio",
"tokio-comp",
"connection-manager",
@@ -131,13 +137,13 @@ libcookie = { package = "cookie", version = "0.18", features = [
"key-expansion",
"secure",
], optional = true }
opentelemetry-http = { version = "0.29.0", optional = true }
opentelemetry-http = { version = "0.30", optional = true }
opentelemetry-semantic-conventions = { version = "0.30.0", optional = true, features = [
"semconv_experimental",
] }
opentelemetry-prometheus = { version = "0.29.0", optional = true }
opentelemetry-prometheus = { version = "0.29.1", optional = true }
libprometheus = { package = "prometheus", version = "0.14.0", optional = true }
libopentelemetry = { package = "opentelemetry", version = "0.29.0", features = [
libopentelemetry = { package = "opentelemetry", version = "0.30", features = [
"metrics",
], optional = true }
libtempfile = { package = "tempfile", version = "3.2.0", optional = true }
@@ -157,7 +163,7 @@ intl-memoizer = { version = "0.5.1", optional = true }
ring = { version = "0.17.14", optional = true }
reqwest = { workspace = true, features = ["json"], optional = true }
rcgen = { version = "0.12.0", optional = true }
x509-parser = { version = "0.16.0", optional = true }
x509-parser = { version = "0.17.0", optional = true }
tokio-metrics = { version = "0.4", optional = true }
rust-embed = { version = "8.0", optional = true }
hex = { version = "0.4", optional = true }

View File

@@ -1,6 +1,6 @@
use std::{
fmt::{Debug, Formatter},
io::{Error as IoError, ErrorKind},
io::Error as IoError,
pin::Pin,
task::Poll,
};
@@ -175,7 +175,7 @@ impl Body {
.0
.collect()
.await
.map_err(|err| ReadBodyError::Io(IoError::new(ErrorKind::Other, err)))?
.map_err(|err| ReadBodyError::Io(IoError::other(err)))?
.to_bytes())
}

View File

@@ -92,17 +92,17 @@ impl<E: RustEmbed + Send + Sync> Endpoint for EmbeddedFilesEndpoint<E> {
if path.is_empty() && !original_end_with_slash {
Ok(Response::builder()
.status(StatusCode::FOUND)
.header(LOCATION, format!("{}/", original_path))
.header(LOCATION, format!("{original_path}/"))
.finish())
} else if original_end_with_slash {
let path = format!("{}index.html", path);
let path = format!("{path}index.html");
EmbeddedFileEndpoint::<E>::new(&path).call(req).await
} else if E::get(path).is_some() {
EmbeddedFileEndpoint::<E>::new(path).call(req).await
} else if E::get(&format!("{}/index.html", path)).is_some() {
} else if E::get(&format!("{path}/index.html")).is_some() {
Ok(Response::builder()
.status(StatusCode::FOUND)
.header(LOCATION, format!("{}/", original_path))
.header(LOCATION, format!("{original_path}/"))
.finish())
} else {
EmbeddedFileEndpoint::<E>::new(path).call(req).await

View File

@@ -208,7 +208,7 @@ pub trait DynEndpoint: Send + Sync {
type Output: IntoResponse;
/// Get the response to the request.
fn call(&self, req: Request) -> BoxFuture<Result<Self::Output>>;
fn call(&self, req: Request) -> BoxFuture<'_, Result<Self::Output>>;
}
/// A [`Endpoint`] wrapper used to implement [`DynEndpoint`].
@@ -221,7 +221,7 @@ where
type Output = E::Output;
#[inline]
fn call(&self, req: Request) -> BoxFuture<Result<Self::Output>> {
fn call(&self, req: Request) -> BoxFuture<'_, Result<Self::Output>> {
self.0.call(req).boxed()
}
}

View File

@@ -30,7 +30,7 @@ pub trait ResponseError {
/// The status code of this error.
fn status(&self) -> StatusCode;
/// Convert this error to a HTTP response.
/// Converts this error to an HTTP response.
fn as_response(&self) -> Response
where
Self: StdError + Send + Sync + 'static,
@@ -103,7 +103,7 @@ impl AsResponse {
/// }
/// ```
///
/// # Create you own error type
/// # Create your own error type
///
/// ```
/// use poem::{Endpoint, Request, Result, error::ResponseError, handler, http::StatusCode};

View File

@@ -222,7 +222,10 @@ impl I18NResources {
pub struct I18NBundle(SmallVec<[Arc<FluentBundle>; 8]>);
impl I18NBundle {
fn message(&self, id: impl AsRef<str>) -> Result<(&FluentBundle, FluentMessage), I18NError> {
fn message(
&self,
id: impl AsRef<str>,
) -> Result<(&'_ FluentBundle, FluentMessage<'_>), I18NError> {
let id = id.as_ref();
for bundle in &self.0 {
if let Some(message) = bundle.get_message(id) {

View File

@@ -34,7 +34,7 @@
//! # Endpoint
//!
//! The [`Endpoint`] trait represents a type that can handle HTTP requests, and
//! it returns the `Result<T: IntoResponse, Error>` type.
//! it returns a `Result<T: IntoResponse, Error>` type.
//!
//! The [`handler`] macro is used to convert a function into an endpoint.
//!

View File

@@ -1,6 +1,6 @@
use std::{
collections::HashSet,
io::{Error as IoError, ErrorKind, Result as IoResult},
io::{Error as IoError, Result as IoResult},
path::PathBuf,
};
@@ -77,14 +77,12 @@ impl AutoCertBuilder {
/// Consumes this builder and returns a [`AutoCert`] object.
pub fn build(self) -> IoResult<AutoCert> {
let directory_url = self.directory_url.parse().map_err(|err| {
IoError::new(ErrorKind::Other, format!("invalid directory url: {err}"))
})?;
let directory_url = self
.directory_url
.parse()
.map_err(|err| IoError::other(format!("invalid directory url: {err}")))?;
if self.domains.is_empty() {
return Err(IoError::new(
ErrorKind::Other,
"at least one domain name is expected",
));
return Err(IoError::other("at least one domain name is expected"));
}
let mut cache_key = None;

View File

@@ -1,5 +1,5 @@
use std::{
io::{Error as IoError, ErrorKind, Result as IoResult},
io::{Error as IoError, Result as IoResult},
sync::Arc,
};
@@ -172,12 +172,7 @@ impl AcmeClient {
Ok(resp
.bytes()
.await
.map_err(|err| {
IoError::new(
ErrorKind::Other,
format!("failed to download certificate: {err}"),
)
})?
.map_err(|err| IoError::other(format!("failed to download certificate: {err}")))?
.to_vec())
}
}
@@ -185,20 +180,23 @@ impl AcmeClient {
async fn get_directory(client: &Client, directory_url: &str) -> IoResult<Directory> {
tracing::debug!("loading directory");
let resp = client.get(directory_url).send().await.map_err(|err| {
IoError::new(ErrorKind::Other, format!("failed to load directory: {err}"))
})?;
let resp = client
.get(directory_url)
.send()
.await
.map_err(|err| IoError::other(format!("failed to load directory: {err}")))?;
if !resp.status().is_success() {
return Err(IoError::new(
ErrorKind::Other,
format!("failed to load directory: status = {}", resp.status()),
));
return Err(IoError::other(format!(
"failed to load directory: status = {}",
resp.status()
)));
}
let directory = resp.json::<Directory>().await.map_err(|err| {
IoError::new(ErrorKind::Other, format!("failed to load directory: {err}"))
})?;
let directory = resp
.json::<Directory>()
.await
.map_err(|err| IoError::other(format!("failed to load directory: {err}")))?;
tracing::debug!(
new_nonce = ?directory.new_nonce,
@@ -216,13 +214,13 @@ async fn get_nonce(client: &Client, directory: &Directory) -> IoResult<String> {
.get(&directory.new_nonce)
.send()
.await
.map_err(|err| IoError::new(ErrorKind::Other, format!("failed to get nonce: {err}")))?;
.map_err(|err| IoError::other(format!("failed to get nonce: {err}")))?;
if !resp.status().is_success() {
return Err(IoError::new(
ErrorKind::Other,
format!("failed to load directory: status = {}", resp.status()),
));
return Err(IoError::other(format!(
"failed to load directory: status = {}",
resp.status()
)));
}
let nonce = resp
@@ -263,7 +261,7 @@ async fn create_acme_account(
.get("location")
.and_then(|value| value.to_str().ok())
.map(ToString::to_string)
.ok_or_else(|| IoError::new(ErrorKind::Other, "unable to get account id"))?;
.ok_or_else(|| IoError::other("unable to get account id"))?;
tracing::debug!(kid = kid.as_str(), "account created");
Ok(kid)

View File

@@ -1,4 +1,4 @@
use std::io::{Error as IoError, ErrorKind, Result as IoResult};
use std::io::{Error as IoError, Result as IoResult};
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
use reqwest::{Client, Response};
@@ -33,13 +33,11 @@ impl<'a> Protected<'a> {
url,
};
#[cfg(not(feature = "sonic-rs"))]
let protected = serde_json::to_vec(&protected).map_err(|err| {
IoError::new(ErrorKind::Other, format!("failed to encode jwt: {err}"))
})?;
let protected = serde_json::to_vec(&protected)
.map_err(|err| IoError::other(format!("failed to encode jwt: {err}")))?;
#[cfg(feature = "sonic-rs")]
let protected = sonic_rs::to_vec(&protected).map_err(|err| {
IoError::new(ErrorKind::Other, format!("failed to encode jwt: {err}"))
})?;
let protected = sonic_rs::to_vec(&protected)
.map_err(|err| IoError::other(format!("failed to encode jwt: {err}")))?;
Ok(URL_SAFE_NO_PAD.encode(protected))
}
}
@@ -84,13 +82,11 @@ impl Jwk {
y: &self.y,
};
#[cfg(not(feature = "sonic-rs"))]
let json = serde_json::to_vec(&jwk_thumb).map_err(|err| {
IoError::new(ErrorKind::Other, format!("failed to encode jwt: {err}"))
})?;
let json = serde_json::to_vec(&jwk_thumb)
.map_err(|err| IoError::other(format!("failed to encode jwt: {err}")))?;
#[cfg(feature = "sonic-rs")]
let json = sonic_rs::to_vec(&jwk_thumb).map_err(|err| {
IoError::new(ErrorKind::Other, format!("failed to encode jwt: {err}"))
})?;
let json = sonic_rs::to_vec(&jwk_thumb)
.map_err(|err| IoError::other(format!("failed to encode jwt: {err}")))?;
let hash = sha256(json);
Ok(URL_SAFE_NO_PAD.encode(hash))
}
@@ -123,13 +119,11 @@ pub(crate) async fn request(
let payload = match payload {
Some(payload) => {
#[cfg(not(feature = "sonic-rs"))]
let res = serde_json::to_vec(&payload).map_err(|err| {
IoError::new(ErrorKind::Other, format!("failed to encode payload: {err}"))
})?;
let res = serde_json::to_vec(&payload)
.map_err(|err| IoError::other(format!("failed to encode payload: {err}")))?;
#[cfg(feature = "sonic-rs")]
let res = sonic_rs::to_vec(&payload).map_err(|err| {
IoError::new(ErrorKind::Other, format!("failed to encode payload: {err}"))
})?;
let res = sonic_rs::to_vec(&payload)
.map_err(|err| IoError::other(format!("failed to encode payload: {err}")))?;
res
}
None => Vec::new(),
@@ -150,18 +144,13 @@ pub(crate) async fn request(
})
.send()
.await
.map_err(|err| {
IoError::new(
ErrorKind::Other,
format!("failed to send http request: {err}"),
)
})?;
.map_err(|err| IoError::other(format!("failed to send http request: {err}")))?;
if !resp.status().is_success() {
return Err(IoError::new(
ErrorKind::Other,
format!("unexpected status code: status = {}", resp.status()),
));
return Err(IoError::other(format!(
"unexpected status code: status = {}",
resp.status()
)));
}
Ok(resp)
}
@@ -183,22 +172,20 @@ where
let data = resp
.text()
.await
.map_err(|_| IoError::new(ErrorKind::Other, "failed to read response"))?;
.map_err(|_| IoError::other("failed to read response"))?;
#[cfg(not(feature = "sonic-rs"))]
{
serde_json::from_str(&data)
.map_err(|err| IoError::new(ErrorKind::Other, format!("bad response: {err}")))
serde_json::from_str(&data).map_err(|err| IoError::other(format!("bad response: {err}")))
}
#[cfg(feature = "sonic-rs")]
{
sonic_rs::from_str(&data)
.map_err(|err| IoError::new(ErrorKind::Other, format!("bad response: {err}")))
sonic_rs::from_str(&data).map_err(|err| IoError::other(format!("bad response: {err}")))
}
}
pub(crate) fn key_authorization(key: &KeyPair, token: &str) -> IoResult<String> {
let jwk = Jwk::new(key);
let key_authorization = format!("{}.{}", token, jwk.thumb_sha256_base64()?);
let key_authorization = format!("{token}.{}", jwk.thumb_sha256_base64()?);
Ok(key_authorization)
}

View File

@@ -1,4 +1,4 @@
use std::io::{Error as IoError, ErrorKind, Result as IoResult};
use std::io::{Error as IoError, Result as IoResult};
use ring::{
rand::SystemRandom,
@@ -12,14 +12,14 @@ impl KeyPair {
let rng = SystemRandom::new();
EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, pkcs8.as_ref(), &rng)
.map(KeyPair)
.map_err(|_| IoError::new(ErrorKind::Other, "failed to load key pair"))
.map_err(|_| IoError::other("failed to load key pair"))
}
fn generate_pkcs8() -> IoResult<impl AsRef<[u8]>> {
let alg = &ECDSA_P256_SHA256_FIXED_SIGNING;
let rng = SystemRandom::new();
EcdsaKeyPair::generate_pkcs8(alg, &rng)
.map_err(|_| IoError::new(ErrorKind::Other, "failed to generate acme key pair"))
.map_err(|_| IoError::other("failed to generate acme key pair"))
}
pub(crate) fn generate() -> IoResult<Self> {
@@ -29,7 +29,7 @@ impl KeyPair {
pub(crate) fn sign(&self, message: impl AsRef<[u8]>) -> IoResult<Signature> {
self.0
.sign(&SystemRandom::new(), message.as_ref())
.map_err(|_| IoError::new(ErrorKind::Other, "failed to sign message"))
.map_err(|_| IoError::other("failed to sign message"))
}
pub(crate) fn public_key(&self) -> &[u8] {

View File

@@ -1,5 +1,5 @@
use std::{
io::{Error as IoError, ErrorKind, Result as IoResult},
io::{Error as IoError, Result as IoResult},
sync::{Arc, Weak},
time::{Duration, UNIX_EPOCH},
};
@@ -113,11 +113,11 @@ impl<T: Listener> Listener for AutoCertListener<T> {
if let Some(cache_cert) = &self.auto_cert.cache_cert {
match rustls_pemfile::certs(&mut cache_cert.as_slice())
.collect::<Result<_, _>>()
.map_err(|err| IoError::new(ErrorKind::Other, format!("invalid pem: {err}")))
.map_err(|err| IoError::other(format!("invalid pem: {err}")))
{
Ok(c) => certs = Some(c),
Err(err) => {
tracing::warn!("failed to parse cached tls certificates: {}", err)
tracing::warn!("failed to parse cached tls certificates: {err}")
}
};
}
@@ -128,7 +128,7 @@ impl<T: Listener> Listener for AutoCertListener<T> {
{
Ok(k) => key = k.into_iter().next(),
Err(err) => {
tracing::warn!("failed to parse cached private key: {}", err)
tracing::warn!("failed to parse cached private key: {err}")
}
};
}
@@ -230,14 +230,14 @@ fn gen_acme_cert(domain: &str, acme_hash: &[u8]) -> IoResult<CertifiedKey> {
params.alg = &PKCS_ECDSA_P256_SHA256;
params.custom_extensions = vec![CustomExtension::new_acme_identifier(acme_hash)];
let cert = Certificate::from_params(params)
.map_err(|_| IoError::new(ErrorKind::Other, "failed to generate acme certificate"))?;
.map_err(|_| IoError::other("failed to generate acme certificate"))?;
let key = any_ecdsa_type(&PrivateKeyDer::Pkcs8(
cert.serialize_private_key_der().into(),
))
.unwrap();
Ok(CertifiedKey::new(
vec![CertificateDer::from(cert.serialize_der().map_err(
|_| IoError::new(ErrorKind::Other, "failed to serialize acme certificate"),
|_| IoError::other("failed to serialize acme certificate"),
)?)],
key,
))
@@ -310,17 +310,14 @@ pub async fn issue_cert<T: AsRef<str>>(
.trigger_challenge(&resp.identifier.value, challenge_type, &challenge.url)
.await?;
} else if resp.status == "invalid" {
return Err(IoError::new(
ErrorKind::Other,
format!(
"unable to authorize `{}`: {}",
resp.identifier.value,
resp.error
.as_ref()
.map(|problem| &*problem.detail)
.unwrap_or("unknown")
),
));
return Err(IoError::other(format!(
"unable to authorize `{}`: {}",
resp.identifier.value,
resp.error
.as_ref()
.map(|problem| &*problem.detail)
.unwrap_or("unknown")
)));
}
}
@@ -333,10 +330,7 @@ pub async fn issue_cert<T: AsRef<str>>(
}
if !valid {
return Err(IoError::new(
ErrorKind::Other,
"authorization failed too many times",
));
return Err(IoError::other("authorization failed too many times"));
}
// send csr
@@ -348,62 +342,49 @@ pub async fn issue_cert<T: AsRef<str>>(
);
params.distinguished_name = DistinguishedName::new();
params.alg = &PKCS_ECDSA_P256_SHA256;
let cert = Certificate::from_params(params).map_err(|err| {
IoError::new(
ErrorKind::Other,
format!("failed create certificate request: {err}"),
)
})?;
let cert = Certificate::from_params(params)
.map_err(|err| IoError::other(format!("failed create certificate request: {err}")))?;
let pk = any_ecdsa_type(&PrivateKeyDer::Pkcs8(
cert.serialize_private_key_der().into(),
))
.unwrap();
let csr = cert.serialize_request_der().map_err(|err| {
IoError::new(
ErrorKind::Other,
format!("failed to serialize request der {err}"),
)
})?;
let csr = cert
.serialize_request_der()
.map_err(|err| IoError::other(format!("failed to serialize request der {err}")))?;
let order_resp = client.send_csr(&order_resp.finalize, &csr).await?;
if order_resp.status == "invalid" {
return Err(IoError::new(
ErrorKind::Other,
format!(
"failed to request certificate: {}",
order_resp
.error
.as_ref()
.map(|problem| &*problem.detail)
.unwrap_or("unknown")
),
));
return Err(IoError::other(format!(
"failed to request certificate: {}",
order_resp
.error
.as_ref()
.map(|problem| &*problem.detail)
.unwrap_or("unknown")
)));
}
if order_resp.status != "valid" {
return Err(IoError::new(
ErrorKind::Other,
format!(
"failed to request certificate: unexpected status `{}`",
order_resp.status
),
));
return Err(IoError::other(format!(
"failed to request certificate: unexpected status `{}`",
order_resp.status
)));
}
// download certificate
let acme_cert_pem = client
.obtain_certificate(order_resp.certificate.as_ref().ok_or_else(|| {
IoError::new(
ErrorKind::Other,
"invalid response: missing `certificate` url",
)
})?)
.obtain_certificate(
order_resp
.certificate
.as_ref()
.ok_or_else(|| IoError::other("invalid response: missing `certificate` url"))?,
)
.await?;
let pkey_pem = cert.serialize_private_key_pem();
let cert_chain = rustls_pemfile::certs(&mut acme_cert_pem.as_slice())
.collect::<Result<_, _>>()
.map_err(|err| IoError::new(ErrorKind::Other, format!("invalid pem: {err}")))?;
.map_err(|err| IoError::other(format!("invalid pem: {err}")))?;
let cert_key = CertifiedKey::new(cert_chain, pk);
tracing::debug!("certificate obtained");

View File

@@ -1,6 +1,6 @@
use std::{
fmt::{self, Display, Formatter},
io::{Error as IoError, ErrorKind, Result as IoResult},
io::{Error as IoError, Result as IoResult},
};
use serde::{Deserialize, Serialize};
@@ -101,9 +101,7 @@ impl FetchAuthorizationResponse {
self.challenges
.iter()
.find(|c| c.ty == ty.to_string())
.ok_or_else(|| {
IoError::new(ErrorKind::Other, format!("unable to find `{ty}` challenge"))
})
.ok_or_else(|| IoError::other(format!("unable to find `{ty}` challenge")))
}
}

View File

@@ -107,7 +107,7 @@ pub trait DynAcceptor: Send {
/// This function will yield once a new TCP connection is established. When
/// established, the corresponding IO stream and the remote peers
/// address will be returned.
fn accept(&mut self) -> BoxFuture<IoResult<(BoxIo, LocalAddr, RemoteAddr, Scheme)>>;
fn accept(&mut self) -> BoxFuture<'_, IoResult<(BoxIo, LocalAddr, RemoteAddr, Scheme)>>;
}
/// A [`Acceptor`] wrapper used to implement [`DynAcceptor`].
@@ -120,7 +120,7 @@ impl<A: Acceptor> DynAcceptor for ToDynAcceptor<A> {
}
#[inline]
fn accept(&mut self) -> BoxFuture<IoResult<(BoxIo, LocalAddr, RemoteAddr, Scheme)>> {
fn accept(&mut self) -> BoxFuture<'_, IoResult<(BoxIo, LocalAddr, RemoteAddr, Scheme)>> {
async move {
let (io, local_addr, remote_addr, scheme) = self.0.accept().await?;
let io = BoxIo::new(io);

View File

@@ -3,7 +3,7 @@ use futures_util::{
stream::{BoxStream, Chain, Pending},
};
use http::uri::Scheme;
use tokio::io::{Error as IoError, ErrorKind, Result as IoResult};
use tokio::io::{Error as IoError, Result as IoResult};
use tokio_native_tls::{TlsStream, native_tls::Identity};
use crate::{
@@ -49,9 +49,9 @@ impl NativeTlsConfig {
fn create_acceptor(&self) -> IoResult<tokio_native_tls::native_tls::TlsAcceptor> {
let identity = Identity::from_pkcs12(&self.pkcs12, &self.password)
.map_err(|err| IoError::new(ErrorKind::Other, err.to_string()))?;
.map_err(|err| IoError::other(err.to_string()))?;
tokio_native_tls::native_tls::TlsAcceptor::new(identity)
.map_err(|err| IoError::new(ErrorKind::Other, err.to_string()))
.map_err(|err| IoError::other(err.to_string()))
}
}
@@ -71,7 +71,7 @@ impl IntoTlsConfigStream<NativeTlsConfig> for NativeTlsConfig {
fn into_stream(self) -> IoResult<Self::Stream> {
let _ = Identity::from_pkcs12(&self.pkcs12, &self.password)
.map_err(|err| IoError::new(ErrorKind::Other, err.to_string()))?;
.map_err(|err| IoError::other(err.to_string()))?;
Ok(futures_util::stream::once(futures_util::future::ready(
self,
)))
@@ -170,9 +170,9 @@ where
let (stream, local_addr, remote_addr, _) = res?;
let tls_acceptor = match &self.current_tls_acceptor {
Some(tls_acceptor) => tls_acceptor.clone(),
None => return Err(IoError::new(ErrorKind::Other, "no valid tls config.")),
None => return Err(IoError::other("no valid tls config.")),
};
let fut = async move { tls_acceptor.accept(stream).map_err(|err| IoError::new(ErrorKind::Other, err.to_string())).await };
let fut = async move { tls_acceptor.accept(stream).map_err(|err| IoError::other(err.to_string())).await };
let stream = HandshakeStream::new(fut);
return Ok((stream, local_addr, remote_addr, Scheme::HTTPS));
}

View File

@@ -13,7 +13,7 @@ use openssl::{
ssl::{Ssl, SslAcceptor, SslAcceptorBuilder, SslFiletype, SslMethod, SslRef},
x509::X509,
};
use tokio::io::{Error as IoError, ErrorKind, Result as IoResult};
use tokio::io::{Error as IoError, Result as IoResult};
use tokio_openssl::SslStream;
use tokio_util::either::Either;
@@ -76,7 +76,7 @@ impl OpensslTlsConfig {
builder.set_certificate(
certs
.next()
.ok_or_else(|| IoError::new(ErrorKind::Other, "no leaf certificate"))?
.ok_or_else(|| IoError::other("no leaf certificate"))?
.as_ref(),
)?;
certs.try_for_each(|cert| builder.add_extra_chain_cert(cert))?;
@@ -215,16 +215,16 @@ where
let (stream, local_addr, remote_addr, _) = res?;
let tls_acceptor = match &self.current_tls_acceptor {
Some(tls_acceptor) => tls_acceptor.clone(),
None => return Err(IoError::new(ErrorKind::Other, "no valid tls config.")),
None => return Err(IoError::other("no valid tls config.")),
};
let fut = async move {
let ssl = Ssl::new(tls_acceptor.context()).map_err(|err|
IoError::new(ErrorKind::Other, err.to_string()))?;
IoError::other(err.to_string()))?;
let mut tls_stream = SslStream::new(ssl, stream).map_err(|err|
IoError::new(ErrorKind::Other, err.to_string()))?;
IoError::other(err.to_string()))?;
use std::pin::Pin;
Pin::new(&mut tls_stream).accept().await.map_err(|err|
IoError::new(ErrorKind::Other, err.to_string()))?;
IoError::other(err.to_string()))?;
Ok(tls_stream) };
let stream = HandshakeStream::new(fut);
return Ok((stream, local_addr, remote_addr, Scheme::HTTPS));

View File

@@ -6,7 +6,7 @@ use futures_util::{
};
use http::uri::Scheme;
use rustls_pemfile::Item;
use tokio::io::{Error as IoError, ErrorKind, Result as IoResult};
use tokio::io::{Error as IoError, Result as IoResult};
use tokio_rustls::{
rustls::{
ConfigBuilder, DEFAULT_VERSIONS, RootCertStore, ServerConfig, WantsVerifier,
@@ -71,7 +71,7 @@ impl RustlsCertificate {
fn create_certificate_key(&self) -> IoResult<CertifiedKey> {
let cert = rustls_pemfile::certs(&mut self.cert.as_slice())
.collect::<Result<_, _>>()
.map_err(|_| IoError::new(ErrorKind::Other, "failed to parse tls certificates"))?;
.map_err(|_| IoError::other("failed to parse tls certificates"))?;
let mut key_reader = self.key.as_slice();
let priv_key = loop {
match rustls_pemfile::read_one(&mut key_reader)? {
@@ -79,17 +79,14 @@ impl RustlsCertificate {
Some(Item::Pkcs8Key(key)) => break key.into(),
Some(Item::Sec1Key(key)) => break key.into(),
None => {
return Err(IoError::new(
ErrorKind::Other,
"failed to parse tls private keys",
));
return Err(IoError::other("failed to parse tls private keys"));
}
_ => continue,
}
};
let key = any_supported_type(&priv_key)
.map_err(|_| IoError::new(ErrorKind::Other, "invalid private key"))?;
let key =
any_supported_type(&priv_key).map_err(|_| IoError::other("invalid private key"))?;
Ok(CertifiedKey {
cert,
@@ -282,10 +279,10 @@ fn read_trust_anchor(mut trust_anchor: &[u8]) -> IoResult<RootCertStore> {
let mut store = RootCertStore::empty();
let ders = rustls_pemfile::certs(&mut trust_anchor);
for der in ders {
let der = der.map_err(|err| IoError::new(ErrorKind::Other, err.to_string()))?;
let der = der.map_err(|err| IoError::other(err.to_string()))?;
store
.add(der)
.map_err(|err| IoError::new(ErrorKind::Other, err.to_string()))?;
.map_err(|err| IoError::other(err.to_string()))?;
}
Ok(store)
}
@@ -405,7 +402,7 @@ where
let (stream, local_addr, remote_addr, _) = res?;
let tls_acceptor = match &self.current_tls_acceptor {
Some(tls_acceptor) => tls_acceptor,
None => return Err(IoError::new(ErrorKind::Other, "no valid tls config.")),
None => return Err(IoError::other("no valid tls config.")),
};
let stream = HandshakeStream::new(tls_acceptor.accept(stream));

View File

@@ -10,6 +10,7 @@ use std::{
use http::uri::Scheme;
use http_body_util::BodyExt;
use hyper::{body::Incoming, rt::Write as _};
use hyper_util::rt::TokioIo;
use parking_lot::Mutex;
use serde::de::DeserializeOwned;
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
@@ -75,6 +76,42 @@ pub struct RequestParts {
pub(crate) state: RequestState,
}
impl From<(http::request::Parts, LocalAddr, RemoteAddr, Scheme)> for RequestParts {
fn from(
(parts, local_addr, remote_addr, scheme): (
http::request::Parts,
LocalAddr,
RemoteAddr,
Scheme,
),
) -> Self {
let mut parts = parts;
let on_upgrade = Mutex::new(
parts
.extensions
.remove::<hyper::upgrade::OnUpgrade>()
.map(|fut| OnUpgrade { fut }),
);
Self {
method: parts.method,
uri: parts.uri.clone(),
version: parts.version,
headers: parts.headers,
extensions: parts.extensions,
state: RequestState {
local_addr,
remote_addr,
scheme,
original_uri: parts.uri,
match_params: Default::default(),
#[cfg(feature = "cookie")]
cookie_jar: None,
on_upgrade,
},
}
}
}
impl Debug for RequestParts {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("RequestParts")
@@ -489,7 +526,7 @@ impl AsyncRead for Upgraded {
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
Pin::new(&mut hyper_util::rt::TokioIo::new(self.project().stream)).poll_read(cx, buf)
Pin::new(&mut TokioIo::new(self.project().stream)).poll_read(cx, buf)
}
}

View File

@@ -505,7 +505,7 @@ impl<T> RadixTree<T> {
}
}
pub(crate) fn matches(&self, path: &str) -> Option<Matches<T>> {
pub(crate) fn matches(&self, path: &str) -> Option<Matches<'_, T>> {
if path.is_empty() {
return None;
}

View File

@@ -6,7 +6,7 @@ use serde_json::{Map, Value};
pub struct TestJson(Value);
impl TestJson {
/// Returns a reference the value.
/// Returns a reference to the value.
#[inline]
pub fn value(&self) -> TestJsonValue<'_> {
TestJsonValue(&self.0)
@@ -133,17 +133,17 @@ impl<'a> TestJsonValue<'a> {
self.array().iter().map(|value| value.string()).collect()
}
/// Asserts that the value is an array and return `TestJsonArray`.
/// Asserts that the value is an array and returns `TestJsonArray`.
pub fn array(&self) -> TestJsonArray<'a> {
TestJsonArray(self.0.as_array().expect("array"))
}
/// Asserts that the value is an object and return `TestJsonArray`.
/// Asserts that the value is an object and returns `TestJsonArray`.
pub fn object(&self) -> TestJsonObject<'a> {
TestJsonObject(self.0.as_object().expect("object"))
}
/// Asserts that the value is an object array and return
/// Asserts that the value is an object array and returns
/// `Vec<TestJsonObject>`.
pub fn object_array(&self) -> Vec<TestJsonObject<'a>> {
self.array().iter().map(|value| value.object()).collect()
@@ -198,7 +198,7 @@ impl<'a> TestJsonArray<'a> {
}
/// Returns the element at index `idx`, or `None` if the element does not
/// exists exists.
/// exist.
pub fn get_opt(&self, idx: usize) -> Option<TestJsonValue<'a>> {
self.0.get(idx).map(TestJsonValue)
}
@@ -208,7 +208,7 @@ impl<'a> TestJsonArray<'a> {
self.0.iter().map(TestJsonValue)
}
/// Asserts the array length is equals to `len`.
/// Asserts the array length is equal to `len`.
#[track_caller]
pub fn assert_len(&self, len: usize) {
assert_eq!(self.len(), len);
@@ -264,7 +264,7 @@ impl<'a> TestJsonObject<'a> {
}
/// Returns the element corresponding to the `name`, or `None` if the
/// element does not exists exists.
/// element does not exist.
pub fn get_opt(&self, name: impl AsRef<str>) -> Option<TestJsonValue<'a>> {
self.0.get(name.as_ref()).map(TestJsonValue)
}
@@ -274,7 +274,7 @@ impl<'a> TestJsonObject<'a> {
self.0.iter().map(|(k, v)| (k, TestJsonValue(v)))
}
/// Asserts the object length is equals to `len`.
/// Asserts the object length is equal to `len`.
#[track_caller]
pub fn assert_len(&self, len: usize) {
assert_eq!(self.len(), len);

View File

@@ -485,7 +485,7 @@ impl CookieJar {
/// Similar to the `private_with_key` function, but using the key specified
/// by the `CookieJarManager::with_key`.
pub fn private(&self) -> PrivateCookieJar {
pub fn private(&self) -> PrivateCookieJar<'_> {
self.private_with_key(
self.key
.as_ref()
@@ -532,7 +532,7 @@ impl CookieJar {
/// Similar to the `signed_with_key` function, but using the key specified
/// by the `CookieJarManager::with_key`.
pub fn signed(&self) -> SignedCookieJar {
pub fn signed(&self) -> SignedCookieJar<'_> {
self.signed_with_key(
self.key
.as_ref()

View File

@@ -93,8 +93,7 @@ impl Field {
/// Consume this field to return a reader.
pub fn into_async_read(self) -> impl AsyncRead + Send {
tokio_util::io::StreamReader::new(
self.0
.map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err.to_string())),
self.0.map_err(|err| std::io::Error::other(err.to_string())),
)
}
}

View File

@@ -1,4 +1,4 @@
use std::io::{Error as IoError, ErrorKind};
use std::io::Error as IoError;
use tokio_tungstenite::tungstenite::{handshake::derive_accept_key, protocol::CloseFrame};
@@ -15,7 +15,7 @@ pub(crate) fn tungstenite_error_to_io_error(
use tokio_tungstenite::tungstenite::Error::*;
match error {
Io(err) => err,
_ => IoError::new(ErrorKind::Other, error.to_string()),
_ => IoError::other(error.to_string()),
}
}

View File

@@ -8,7 +8,7 @@ use crate::{
web::RequestBody,
};
/// JSON extractor and response.
/// YAML extractor and response.
///
/// To extract the specified type of YAML from the body, `T` must implement
/// [`serde::Deserialize`].