codify "platform" into resolution logic, server-fn/fullstack overhaul (#4596)

overhaul dioxus fullstack using a new implementation of server_fn

This PR is a complete rewrite of dioxus fullstack designed to improve compatibility with axum and to fix a number of compilation errors based on feature flag unification plumbing through macros.
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2025-10-03 18:26:19 -07:00
1 parent 8421c3aef0
commit f2b1c6d97f
424 files changed
+17284 -6624

No files matched your search

-1
View File
@@ -1 +0,0 @@
target
+7
View File
@@ -0,0 +1,7 @@
{
"rust-analyzer.cargo.features": "all",
"rust-analyzer.check.features": "all",
"rust-analyzer.cargo.extraArgs": [
"--tests"
],
}
+90 -82
View File
@@ -3,108 +3,116 @@ name = "dioxus-fullstack"
authors = ["Jonathan Kelley", "Evan Almloff"]
version = { workspace = true }
edition = "2021"
description = "Fullstack utilities for Dioxus: Build fullstack web, desktop, and mobile apps with a single codebase."
description = "Library for fetching resources from servers in Dioxus apps."
license = "MIT OR Apache-2.0"
repository = "https://github.com/DioxusLabs/dioxus/"
homepage = "https://dioxuslabs.com"
keywords = ["web", "desktop", "mobile", "gui", "server"]
keywords = ["web", "gui", "server"]
resolver = "2"
[dependencies]
# server functions
server_fn = { workspace = true, default-features = false }
dioxus_server_macro = { workspace = true }
# axum
axum = { workspace = true, optional = true }
tower-http = { workspace = true, optional = true, features = ["fs"] }
dioxus-core = { workspace = true }
dioxus-document = { workspace = true }
generational-box = { workspace = true }
# Dioxus + SSR
dioxus-server = { workspace = true, optional = true}
dioxus-isrg = { workspace = true, optional = true }
dioxus-router = { workspace = true, features = ["streaming"], optional = true }
dioxus-fullstack-hooks = { workspace = true }
dioxus-fullstack-protocol = { workspace = true }
# Web Integration
dioxus-web = { workspace = true, features = ["hydrate"], default-features = false, optional = true }
dioxus-interpreter-js = { workspace = true, optional = true }
# Desktop Integration
dioxus-desktop = { workspace = true, optional = true }
tracing = { workspace = true }
tracing-futures = { workspace = true, optional = true }
tokio-util = { workspace = true, features = ["rt"], optional = true }
async-trait = { workspace = true, optional = true }
serde = { workspace = true }
tokio-stream = { workspace = true, features = ["sync"], optional = true }
futures-util = { workspace = true }
# shared for both server + client
futures-channel = { workspace = true }
serde = { workspace = true, features = ["derive"] }
ciborium = { workspace = true }
base64 = { workspace = true }
rustls = { workspace = true, optional = true }
hyper-rustls = { workspace = true, optional = true }
tracing = { workspace = true }
thiserror = { workspace = true }
dioxus-fullstack-core = { workspace = true }
http = { workspace = true }
anyhow = { workspace = true }
dioxus-core = { workspace = true }
dioxus-signals = { workspace = true }
dioxus-hooks = { workspace = true }
dioxus-html = { workspace = true }
dioxus-cli-config = { workspace = true }
futures = { workspace = true, default-features = true }
bytes = { workspace = true, features = ["serde"] }
dioxus-fullstack-macro = { workspace = true }
http-body-util = "0.1.3"
futures-util = { workspace = true }
axum-core = { workspace = true }
send_wrapper = { features = ["futures"], workspace = true, default-features = true }
serde_json = { workspace = true }
pin-project = { workspace = true }
serde_qs = { workspace = true, default-features = true}
mime = "0.3.17"
reqwest = { workspace = true, features = ["json", "rustls-tls", "cookies", "stream"] }
url = { workspace = true }
serde_urlencoded = { workspace = true }
form_urlencoded = { workspace = true }
axum = { workspace = true, default-features = false, features = ["json", "form", "query", "multipart", "query"] }
http-body = { workspace = true }
headers = "0.4.1"
dioxus-asset-resolver = { workspace = true, features = ["native"] }
pin-project = { version = "1.1.10", optional = true }
thiserror = { workspace = true, optional = true }
bytes = "1.10.1"
const_format = { workspace = true, default-features = true }
const-str = { workspace = true, default-features = true }
rustversion = { workspace = true, default-features = true }
xxhash-rust = { features = ["const_xxh64"], workspace = true, default-features = true }
# rmp-serde = { version = "1.3", default-features = true }
derive_more = { version = "2.0.1", features = ["deref", "deref_mut", "display", "from"] }
# server deps
axum-extra = { workspace = true, optional = true, features = ["typed-header"] }
inventory = { workspace = true, optional = true }
tokio = { workspace = true, optional = true }
tokio-tungstenite = { workspace = true, optional = true }
tokio-stream = { workspace = true, features = ["sync"], optional = true }
tower = { workspace = true, features = ["util"], optional = true }
tower-http = { workspace = true, features = ["fs", "limit"], optional = true }
tower-layer = { version = "0.3.3", optional = true }
parking_lot = { workspace = true, features = ["send_guard"], optional = true }
web-sys = { version = "0.3.77", optional = true, features = [
"Window",
"Document",
"Element",
"HtmlDocument",
"Storage",
"console",
] }
dioxus-cli-config = { workspace = true, optional = true }
# payloads
postcard = { features = ["alloc"], optional = true, workspace = true, default-features = true }
rmp-serde = { version = "1.3", optional = true }
async-stream = "0.3.6"
dioxus-devtools = { workspace = true, optional = true }
aws-lc-rs = { version = "1.13.1", optional = true }
dioxus-history = { workspace = true }
http.workspace = true
# websocket stuff
# [target.'cfg(target_arch = "wasm32")'.dependencies]
web-sys = { version = "0.3", features = ["WebSocket", "CloseEvent", "ErrorEvent", "Event", "MessageEvent", "BinaryType", "FormData", "Response", "ReadableStream", "AbortController", "File", "ReadableStream", "HtmlFormElement", "FormData"], optional = true }
gloo-net = "0.6.0"
js-sys = { workspace = true }
wasm-bindgen = { workspace = true }
wasm-bindgen-futures = { workspace = true }
wasm-streams = "0.4.2"
content_disposition = "0.4.0"
# native websocket deps
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
async-tungstenite = { version = "0.31.0", default-features = false, features = ["futures-03-sink"], optional = true }
tungstenite = { version = "0.27", default-features = false, features = ["handshake"], optional = true }
tokio-util = { workspace = true, features = ["codec", "compat"] }
[dev-dependencies]
dioxus = { workspace = true, features = ["fullstack"] }
tokio = { workspace = true, features = ["full"] }
dioxus = { workspace = true, features = ["fullstack", "router"] }
dioxus-server = { workspace = true }
[features]
default = ["devtools", "document", "file_engine", "mounted"]
devtools = ["dioxus-web?/devtools", "dep:dioxus-devtools"]
mounted = ["dioxus-web?/mounted"]
file_engine = ["dioxus-web?/file_engine"]
document = ["dioxus-web?/document"]
web = ["dep:dioxus-web", "dep:web-sys", "dioxus-fullstack-hooks/web", "server_fn/browser", "dioxus_server_macro/browser"]
desktop = ["server_fn/reqwest", "dioxus_server_macro/reqwest"]
mobile = ["server_fn/reqwest", "dioxus_server_macro/reqwest"]
default-tls = ["server_fn/default-tls"]
rustls = ["server_fn/rustls", "dep:rustls", "dep:hyper-rustls"]
default = ["ws"]
web = [
"dep:web-sys"
]
native = []
server = [
"server-core" ,
"default-tls",
"server_fn/axum",
"dep:inventory",
"dep:tokio",
"dep:tokio-tungstenite",
"dep:tokio-stream",
"dep:async-tungstenite",
"dep:tungstenite",
"dep:tower",
"dep:tower-http",
"dep:tower-layer",
"dep:axum-extra",
"axum/ws",
"axum/tokio",
]
server-core = [
"server_fn/axum-no-default",
"dioxus_server_macro/axum",
"server_fn/reqwest",
"server_fn/ssr",
"dioxus_server_macro/reqwest",
"dioxus-fullstack-hooks/server",
"dep:dioxus-server",
"dioxus-interpreter-js",
]
aws-lc-rs = ["dep:aws-lc-rs"]
postcard = ["dep:postcard"]
msgpack = ["dep:rmp-serde"]
ws = ["dep:async-tungstenite", "dep:tungstenite"]
[package.metadata.docs.rs]
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
features = ["axum", "web", "aws-lc-rs"]
+3 -155
View File
@@ -1,157 +1,5 @@
# Dioxus Fullstack
# dioxus-fullstack
[![Crates.io][crates-badge]][crates-url]
[![MIT licensed][mit-badge]][mit-url]
[![Build Status][actions-badge]][actions-url]
[![Discord chat][discord-badge]][discord-url]
- wraps dioxus-server
- exposes rpc system
[crates-badge]: https://img.shields.io/crates/v/dioxus-fullstack.svg
[crates-url]: https://crates.io/crates/dioxus-fullstack
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/main/LICENSE-MIT
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
[discord-url]: https://discord.gg/XgGxMSkvUM
[Website](https://dioxuslabs.com) |
[Guides](https://dioxuslabs.com/learn/0.6/) |
[API Docs](https://docs.rs/dioxus-fullstack/latest/dioxus_fullstack/) |
[Chat](https://discord.gg/XgGxMSkvUM)
Fullstack utilities for the [`Dioxus`](https://dioxuslabs.com) framework.
# Features
- Integrates with the [Axum](./examples/axum-hello-world/src/main.rs) server framework with utilities for serving and rendering Dioxus applications.
- [Server functions](https://docs.rs/dioxus-fullstack/latest/dioxus_fullstack/prelude/attr.server.html) allow you to call code on the server from the client as if it were a normal function.
- Instant RSX Hot reloading with [`dioxus-hot-reload`](https://crates.io/crates/dioxus-hot-reload).
- Passing root props from the server to the client.
# Example
Full stack Dioxus in under 30 lines of code
```rust, no_run
#![allow(non_snake_case)]
use dioxus::prelude::*;
fn main() {
dioxus::launch(App);
}
#[component]
fn App() -> Element {
let mut meaning = use_signal(|| None);
rsx! {
h1 { "Meaning of life: {meaning:?}" }
button {
onclick: move |_| async move {
if let Ok(data) = get_meaning("life the universe and everything".into()).await {
meaning.set(data);
}
},
"Run a server function"
}
}
}
#[server]
async fn get_meaning(of: String) -> ServerFnResult<Option<u32>> {
Ok(of.contains("life").then(|| 42))
}
```
## Axum Integration
If you have an existing Axum router or you need more control over the server, you can use the [`DioxusRouterExt`](https://docs.rs/dioxus-fullstack/0.6.0-alpha.2/dioxus_fullstack/prelude/trait.DioxusRouterExt.html) trait to integrate with your existing Axum router.
First, make sure your `axum` dependency is optional and enabled by the server feature flag. Axum cannot be compiled to wasm, so if it is enabled by default, it will cause a compile error:
```toml
[dependencies]
dioxus = { version = "*", features = ["fullstack"] }
axum = { version = "0.8.0", optional = true }
tokio = { version = "1.0", features = ["full"], optional = true }
dioxus-cli-config = { version = "*", optional = true }
[features]
server = ["dioxus/server", "dep:axum", "dep:tokio", "dioxus-cli-config"]
web = ["dioxus/web"]
```
Then we can set up dioxus with the axum server:
```rust, no_run
#![allow(non_snake_case)]
use dioxus::prelude::*;
use dioxus_fullstack::ServeConfig;
// The entry point for the server
#[cfg(feature = "server")]
#[tokio::main]
async fn main() {
// Get the address the server should run on. If the CLI is running, the CLI proxies fullstack into the main address
// and we use the generated address the CLI gives us
let address = dioxus::cli_config::fullstack_address_or_localhost();
// Set up the axum router
let router = axum::Router::new()
// You can add a dioxus application to the router with the `serve_dioxus_application` method
// This will add a fallback route to the router that will serve your component and server functions
.serve_dioxus_application(ServeConfig::new().unwrap(), App);
// Finally, we can launch the server
let router = router.into_make_service();
let listener = tokio::net::TcpListener::bind(address).await.unwrap();
axum::serve(listener, router).await.unwrap();
}
// For any other platform, we just launch the app
#[cfg(not(feature = "server"))]
fn main() {
dioxus::launch(App);
}
#[component]
fn App() -> Element {
let mut meaning = use_signal(|| None);
rsx! {
h1 { "Meaning of life: {meaning:?}" }
button {
onclick: move |_| async move {
if let Ok(data) = get_meaning("life the universe and everything".into()).await {
meaning.set(data);
}
},
"Run a server function"
}
}
}
#[server]
async fn get_meaning(of: String) -> ServerFnResult<Option<u32>> {
Ok(of.contains("life").then(|| 42))
}
```
## Getting Started
To get started with full stack Dioxus, check out our [getting started guide](https://dioxuslabs.com/learn/0.6/getting_started), or the [full stack examples](https://github.com/DioxusLabs/dioxus/tree/master/examples).
## Contributing
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
- Join the discord and ask questions!
## License
This project is licensed under the [MIT license].
[mit license]: https://github.com/dioxuslabs/dioxus/blob/main/LICENSE-MIT
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in Dioxus by you shall be licensed as MIT without any additional
terms or conditions.
-48
View File
@@ -1,48 +0,0 @@
This method interacts with information from the current request. The request may come from:
1. The initial SSR render if this method called from a [`Component`](dioxus_core_macro::component) or a [`server`](dioxus_server) function that is called during the initial render
```rust
# use dioxus::prelude::*;
#[component]
fn PrintHtmlRequestInfo() -> Element {
// The server context only exists on the server, so we need to put it behind a server_only! config
server_only! {
// Since we are calling this from a component, the server context that is returned will be from
// the html request for ssr rendering
let context = server_context();
let request_parts = context.request_parts();
println!("headers are {:?}", request_parts.headers);
}
rsx! {}
}
```
2. A request to a [`server`](dioxus_server) function called directly from the client (either on desktop/mobile or on the web frontend after the initial render)
```rust
# use dioxus::prelude::*;
#[server]
async fn read_headers() -> ServerFnResult {
// Since we are calling this from a server function, the server context that is may be from the
// initial request or a request from the client
let context = server_context();
let request_parts = context.request_parts();
println!("headers are {:?}", request_parts.headers);
Ok(())
}
#[component]
fn CallServerFunction() -> Element {
rsx! {
button {
// If you click the button, the server function will be called and the server context will be
// from the client request
onclick: move |_| async {
_ = read_headers().await
},
"Call server function"
}
}
}
```
+601
View File
@@ -0,0 +1,601 @@
#![allow(unreachable_code)]
use crate::{reqwest_error_to_request_error, StreamingError};
use bytes::Bytes;
use dioxus_fullstack_core::RequestError;
use futures::Stream;
use futures::{TryFutureExt, TryStreamExt};
use headers::{ContentType, Header};
use http::{response::Parts, Extensions, HeaderMap, HeaderName, HeaderValue, Method, StatusCode};
use send_wrapper::SendWrapper;
use serde::{de::DeserializeOwned, Serialize};
use std::{fmt::Display, pin::Pin, prelude::rust_2024::Future, sync::OnceLock};
use url::Url;
pub static GLOBAL_REQUEST_CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
pub type ClientResult = Result<ClientResponse, RequestError>;
pub struct ClientRequest {
pub url: Url,
pub headers: HeaderMap,
pub method: Method,
pub extensions: Extensions,
}
impl ClientRequest {
/// Create a new ClientRequest with the given method, url path, and query parameters.
pub fn new(method: http::Method, path: String, params: &impl Serialize) -> Self {
Self::fetch_inner(method, path, serde_qs::to_string(params).unwrap())
}
// Shrink monomorphization bloat by moving this to its own function
fn fetch_inner(method: http::Method, path: String, query: String) -> ClientRequest {
// On wasm, this doesn't matter since we always use relative URLs when making requests anyways
let mut server_url = get_server_url();
if server_url.is_empty() {
server_url = "http://this.is.not.a.real.url:9000";
}
let url = format!(
"{server_url}{path}{params}",
params = if query.is_empty() {
"".to_string()
} else {
format!("?{}", query)
}
)
.parse()
.unwrap();
ClientRequest {
method,
url,
headers: HeaderMap::new(),
extensions: Extensions::new(),
}
}
/// Get the HTTP method of this Request.
pub fn method(&self) -> &Method {
&self.method
}
pub fn url(&self) -> &Url {
&self.url
}
/// Extend the query parameters of this request with the given serialzable struct.
///
/// This will use `serde_qs` to serialize the struct into query parameters. `serde_qs` has various
/// restrictions - make sure to read its documentation!
pub fn extend_query(mut self, query: &impl Serialize) -> Self {
let old_query = self.url.query().unwrap_or("");
let new_query = serde_qs::to_string(query).unwrap();
let combined_query = format!(
"{}{}{}",
old_query,
if old_query.is_empty() { "" } else { "&" },
new_query
);
self.url.set_query(Some(&combined_query));
self
}
/// Add a `Header` to this Request.
pub fn header(
mut self,
name: impl TryInto<HeaderName, Error = impl Display>,
value: impl TryInto<HeaderValue, Error = impl Display>,
) -> Result<Self, RequestError> {
self.headers.append(
name.try_into()
.map_err(|d| RequestError::Builder(d.to_string()))?,
value
.try_into()
.map_err(|d| RequestError::Builder(d.to_string()))?,
);
Ok(self)
}
/// Add a `Header` to this Request.
pub fn typed_header<H: Header>(mut self, header: H) -> Self {
let mut headers = vec![];
header.encode(&mut headers);
for header in headers {
self.headers.append(H::name(), header);
}
self
}
/// Creates a new reqwest client with cookies set
pub fn new_reqwest_client() -> reqwest::Client {
#[allow(unused_mut)]
let mut client = reqwest::Client::builder();
#[cfg(not(target_arch = "wasm32"))]
{
use std::sync::Arc;
use std::sync::LazyLock;
static COOKIES: LazyLock<Arc<reqwest::cookie::Jar>> =
LazyLock::new(|| Arc::new(reqwest::cookie::Jar::default()));
client = client.cookie_store(true).cookie_provider(COOKIES.clone());
}
client.build().unwrap()
}
/// Creates a new reqwest request builder with the method, url, and headers set from this ClientRequest
pub fn new_reqwest_request(&self) -> reqwest::RequestBuilder {
let client = GLOBAL_REQUEST_CLIENT.get_or_init(Self::new_reqwest_client);
let mut req = client.request(self.method.clone(), self.url.clone());
for (key, value) in self.headers.iter() {
req = req.header(key, value);
}
req
}
#[cfg(feature = "web")]
pub fn new_gloo_request(&self) -> gloo_net::http::RequestBuilder {
let mut builder =
gloo_net::http::RequestBuilder::new(self.url.path()).method(self.method.clone());
for (key, value) in self.headers.iter() {
let value = match value.to_str() {
Ok(v) => v,
Err(er) => {
tracing::error!("Error converting header {key} value: {}", er);
continue;
}
};
builder = builder.header(key.as_str(), value);
}
builder
}
pub async fn send_form(self, data: &impl Serialize) -> Result<ClientResponse, RequestError> {
// For GET and HEAD requests, we encode the form data as query parameters.
// For other request methods, we encode the form data as the request body.
if matches!(*self.method(), Method::GET | Method::HEAD) {
return self.extend_query(data).send_empty_body().await;
}
let body =
serde_urlencoded::to_string(data).map_err(|err| RequestError::Body(err.to_string()))?;
self.typed_header(ContentType::form_url_encoded())
.send_raw_bytes(body)
.await
}
/// Sends the request with an empty body.
pub async fn send_empty_body(self) -> Result<ClientResponse, RequestError> {
#[cfg(feature = "web")]
if cfg!(target_arch = "wasm32") {
return self.send_js_value(wasm_bindgen::JsValue::UNDEFINED).await;
}
#[cfg(not(target_arch = "wasm32"))]
{
let res = self
.new_reqwest_request()
.send()
.await
.map_err(reqwest_error_to_request_error)?;
return Ok(ClientResponse {
response: Box::new(res),
extensions: self.extensions,
});
}
unimplemented!()
}
pub async fn send_raw_bytes(
self,
bytes: impl Into<Bytes>,
) -> Result<ClientResponse, RequestError> {
#[cfg(feature = "web")]
if cfg!(target_arch = "wasm32") {
let bytes = bytes.into();
let uint_8_array = js_sys::Uint8Array::from(&bytes[..]);
return self.send_js_value(uint_8_array.into()).await;
}
#[cfg(not(target_arch = "wasm32"))]
{
let res = self
.new_reqwest_request()
.body(bytes.into())
.send()
.await
.map_err(reqwest_error_to_request_error)?;
return Ok(ClientResponse {
response: Box::new(res),
extensions: self.extensions,
});
}
unimplemented!()
}
/// Sends text data with the `text/plain; charset=utf-8` content type.
pub async fn send_text(
self,
text: impl Into<String> + Into<Bytes>,
) -> Result<ClientResponse, RequestError> {
self.typed_header(ContentType::text_utf8())
.send_raw_bytes(text)
.await
}
/// Sends JSON data with the `application/json` content type.
pub async fn send_json(self, json: &impl Serialize) -> Result<ClientResponse, RequestError> {
let bytes =
serde_json::to_vec(json).map_err(|e| RequestError::Serialization(e.to_string()))?;
if bytes.is_empty() || bytes == b"{}" || bytes == b"null" {
return self.send_empty_body().await;
}
self.typed_header(ContentType::json())
.send_raw_bytes(bytes)
.await
}
pub async fn send_body_stream(
self,
stream: impl Stream<Item = Result<Bytes, StreamingError>> + Send + 'static,
) -> Result<ClientResponse, RequestError> {
#[cfg(not(target_arch = "wasm32"))]
{
let res = self
.new_reqwest_request()
.body(reqwest::Body::wrap_stream(stream))
.send()
.await
.map_err(reqwest_error_to_request_error)?;
return Ok(ClientResponse {
response: Box::new(res),
extensions: self.extensions,
});
}
// On the web, we have to buffer the entire stream into a Blob before sending it,
// since the Fetch API doesn't support streaming request bodies on browsers yet.
#[cfg(feature = "web")]
{
use wasm_bindgen::JsValue;
let stream: Vec<Bytes> = stream.try_collect().await.map_err(|e| {
RequestError::Request(format!("Error collecting stream for request body: {}", e))
})?;
let uint_8_array =
js_sys::Uint8Array::new_with_length(stream.iter().map(|b| b.len() as u32).sum());
let mut offset = 0;
for chunk in stream {
uint_8_array.set(&js_sys::Uint8Array::from(&chunk[..]), offset);
offset += chunk.len() as u32;
}
return self.send_js_value(JsValue::from(uint_8_array)).await;
}
unimplemented!()
}
#[cfg(feature = "web")]
pub async fn send_js_value(
self,
value: wasm_bindgen::JsValue,
) -> Result<ClientResponse, RequestError> {
use std::str::FromStr;
let inner = self
.new_gloo_request()
.body(value)
.map_err(|e| RequestError::Request(e.to_string()))?
.send()
.await
.map_err(|e| RequestError::Request(e.to_string()))?;
let status = inner.status();
let url = inner
.url()
.parse()
.map_err(|e| RequestError::Request(format!("Error parsing response URL: {}", e)))?;
let headers = {
let mut map = HeaderMap::new();
for (key, value) in inner.headers().entries() {
if let Ok(header_value) = http::HeaderValue::from_str(&value) {
let header = HeaderName::from_str(&key).unwrap();
map.append(header, header_value);
}
}
map
};
let content_length = headers
.get(http::header::CONTENT_LENGTH)
.and_then(|val| val.to_str().ok())
.and_then(|s| s.parse::<u64>().ok());
let status = http::StatusCode::from_u16(status).unwrap_or(http::StatusCode::OK);
Ok(ClientResponse {
extensions: self.extensions,
response: Box::new(browser::WrappedGlooResponse {
inner,
headers,
status,
url,
content_length,
}),
})
}
}
// On wasm reqwest not being send/sync gets annoying, but it's not relevant since wasm is single-threaded
unsafe impl Send for ClientRequest {}
unsafe impl Sync for ClientRequest {}
/// A wrapper type over the platform's HTTP response type.
///
/// This abstracts over the inner `reqwest::Response` type and provides the original request
/// and a way to store state associated with the response.
///
/// On the web, it uses `web_sys::Response` instead of `reqwest::Response` to avoid pulling in
/// the entire `reqwest` crate and to support native browser APIs.
pub struct ClientResponse {
pub(crate) response: Box<dyn ClientResponseDriver>,
pub(crate) extensions: Extensions,
}
impl ClientResponse {
pub fn status(&self) -> StatusCode {
self.response.status()
}
pub fn headers(&self) -> &HeaderMap {
self.response.headers()
}
pub fn url(&self) -> &Url {
self.response.url()
}
pub fn content_length(&self) -> Option<u64> {
self.response.content_length()
}
pub async fn bytes(self) -> Result<Bytes, RequestError> {
self.response.bytes().await
}
pub fn bytes_stream(
self,
) -> impl futures_util::Stream<Item = Result<Bytes, StreamingError>> + 'static + Unpin + Send
{
self.response.bytes_stream()
}
pub fn extensions(&self) -> &Extensions {
&self.extensions
}
pub fn extensions_mut(&mut self) -> &mut Extensions {
&mut self.extensions
}
pub async fn json<T: DeserializeOwned>(self) -> Result<T, RequestError> {
serde_json::from_slice(&self.bytes().await?)
.map_err(|e| RequestError::Decode(e.to_string()))
}
pub async fn text(self) -> Result<String, RequestError> {
self.response.text().await
}
/// Creates the `http::response::Parts` from this response.
pub fn make_parts(&self) -> Parts {
let mut response = http::response::Response::builder().status(self.response.status());
response = response.version(self.response.version());
for (key, value) in self.response.headers().iter() {
response = response.header(key, value);
}
let (parts, _) = response.body(()).unwrap().into_parts();
parts
}
/// Consumes the response, returning the head and a stream of the body.
pub fn into_parts(self) -> (Parts, impl Stream<Item = Result<Bytes, StreamingError>>) {
(self.make_parts(), self.bytes_stream())
}
}
/// Set the root server URL that all server function paths are relative to for the client.
///
/// If this is not set, it defaults to the origin.
pub fn set_server_url(url: &'static str) {
ROOT_URL.set(url).unwrap();
}
/// Returns the root server URL for all server functions.
pub fn get_server_url() -> &'static str {
ROOT_URL.get().copied().unwrap_or("")
}
static ROOT_URL: OnceLock<&'static str> = OnceLock::new();
pub trait ClientResponseDriver {
fn status(&self) -> StatusCode;
fn headers(&self) -> &HeaderMap;
fn url(&self) -> &Url;
fn version(&self) -> http::Version {
http::Version::HTTP_2
}
fn content_length(&self) -> Option<u64>;
fn bytes(self: Box<Self>) -> Pin<Box<dyn Future<Output = Result<Bytes, RequestError>> + Send>>;
fn bytes_stream(
self: Box<Self>,
) -> Pin<Box<dyn Stream<Item = Result<Bytes, StreamingError>> + 'static + Unpin + Send>>;
fn text(self: Box<Self>) -> Pin<Box<dyn Future<Output = Result<String, RequestError>> + Send>>;
}
mod native {
use futures::Stream;
use super::*;
impl ClientResponseDriver for reqwest::Response {
fn status(&self) -> http::StatusCode {
reqwest::Response::status(self)
}
fn version(&self) -> http::Version {
#[cfg(target_arch = "wasm32")]
{
return http::Version::HTTP_2;
}
reqwest::Response::version(self)
}
fn headers(&self) -> &http::HeaderMap {
reqwest::Response::headers(self)
}
fn url(&self) -> &url::Url {
reqwest::Response::url(self)
}
fn content_length(&self) -> Option<u64> {
reqwest::Response::content_length(self)
}
fn bytes(
self: Box<Self>,
) -> Pin<Box<dyn Future<Output = Result<Bytes, RequestError>> + Send>> {
Box::pin(SendWrapper::new(async move {
reqwest::Response::bytes(*self)
.map_err(reqwest_error_to_request_error)
.await
}))
}
fn bytes_stream(
self: Box<Self>,
) -> Pin<Box<dyn Stream<Item = Result<Bytes, StreamingError>> + 'static + Unpin + Send>>
{
Box::pin(SendWrapper::new(
reqwest::Response::bytes_stream(*self).map_err(|_| StreamingError::Failed),
))
}
fn text(
self: Box<Self>,
) -> Pin<Box<dyn Future<Output = Result<String, RequestError>> + Send>> {
Box::pin(SendWrapper::new(async move {
reqwest::Response::text(*self)
.map_err(reqwest_error_to_request_error)
.await
}))
}
}
}
#[cfg(feature = "web")]
mod browser {
use crate::{ClientResponseDriver, StreamingError};
use bytes::Bytes;
use dioxus_fullstack_core::RequestError;
use futures::{Stream, StreamExt};
use http::{HeaderMap, StatusCode};
use js_sys::Uint8Array;
use send_wrapper::SendWrapper;
use std::{pin::Pin, prelude::rust_2024::Future};
use wasm_bindgen::JsCast;
pub(crate) struct WrappedGlooResponse {
pub(crate) inner: gloo_net::http::Response,
pub(crate) headers: HeaderMap,
pub(crate) status: StatusCode,
pub(crate) url: url::Url,
pub(crate) content_length: Option<u64>,
}
impl ClientResponseDriver for WrappedGlooResponse {
fn status(&self) -> StatusCode {
self.status
}
fn headers(&self) -> &HeaderMap {
&self.headers
}
fn url(&self) -> &url::Url {
&self.url
}
fn content_length(&self) -> Option<u64> {
self.content_length
}
fn bytes(
self: Box<Self>,
) -> Pin<Box<dyn Future<Output = Result<Bytes, RequestError>> + Send>> {
Box::pin(SendWrapper::new(async move {
let bytes = self.inner.binary().await.unwrap();
Ok(bytes.into())
}))
}
fn bytes_stream(
self: Box<Self>,
) -> Pin<Box<dyn Stream<Item = Result<Bytes, StreamingError>> + 'static + Unpin + Send>>
{
Box::pin(SendWrapper::new(
wasm_streams::ReadableStream::from_raw(self.inner.body().unwrap())
.into_stream()
.map(|chunk| {
Ok(chunk
.unwrap()
.dyn_into::<Uint8Array>()
.unwrap()
.to_vec()
.into())
}),
))
}
fn text(
self: Box<Self>,
) -> Pin<Box<dyn Future<Output = Result<String, RequestError>> + Send>> {
Box::pin(SendWrapper::new(async move {
self.inner
.text()
.await
.map_err(|e| RequestError::Request(e.to_string()))
}))
}
}
}
+106
View File
@@ -0,0 +1,106 @@
use bytes::Bytes;
use serde::{de::DeserializeOwned, Serialize};
/// A trait for encoding and decoding data.
///
/// This takes an owned self to make it easier for zero-copy encodings.
pub trait Encoding {
fn content_type() -> &'static str;
fn stream_content_type() -> &'static str;
fn to_bytes(data: impl Serialize) -> Option<Bytes>;
fn from_bytes<O: DeserializeOwned>(bytes: Bytes) -> Option<O>;
}
pub struct JsonEncoding;
impl Encoding for JsonEncoding {
fn content_type() -> &'static str {
"application/json"
}
fn stream_content_type() -> &'static str {
"application/stream+json"
}
fn to_bytes(data: impl Serialize) -> Option<Bytes> {
serde_json::to_vec(&data).ok().map(Into::into)
}
fn from_bytes<O: DeserializeOwned>(bytes: Bytes) -> Option<O> {
serde_json::from_slice(&bytes).ok()
}
}
pub struct CborEncoding;
impl Encoding for CborEncoding {
fn content_type() -> &'static str {
"application/cbor"
}
fn stream_content_type() -> &'static str {
"application/stream+cbor"
}
fn to_bytes(data: impl Serialize) -> Option<Bytes> {
let mut buf = Vec::new();
ciborium::into_writer(&data, &mut buf).ok()?;
Some(buf.into())
}
fn from_bytes<O: DeserializeOwned>(bytes: Bytes) -> Option<O> {
ciborium::de::from_reader(bytes.as_ref()).ok()
}
}
#[cfg(feature = "postcard")]
pub struct PostcardEncoding;
#[cfg(feature = "postcard")]
impl Encoding for PostcardEncoding {
fn content_type() -> &'static str {
"application/postcard"
}
fn stream_content_type() -> &'static str {
"application/stream+postcard"
}
fn to_bytes(data: impl Serialize) -> Option<Bytes> {
postcard::to_allocvec(&data).ok().map(Into::into)
}
fn from_bytes<O: DeserializeOwned>(bytes: Bytes) -> Option<O> {
postcard::from_bytes(bytes.as_ref()).ok()
}
}
#[cfg(feature = "msgpack")]
pub struct MsgPackEncoding;
#[cfg(feature = "msgpack")]
impl Encoding for MsgPackEncoding {
fn content_type() -> &'static str {
"application/msgpack"
}
fn stream_content_type() -> &'static str {
"application/stream+msgpack"
}
fn to_bytes(data: impl Serialize) -> Option<Bytes> {
rmp_serde::to_vec(&data).ok().map(Into::into)
}
fn from_bytes<O: DeserializeOwned>(bytes: Bytes) -> Option<O> {
rmp_serde::from_slice(&bytes).ok()
}
}
// todo: ... add rkyv support
// pub struct RkyvEncoding;
// impl Encoding for RkyvEncoding {
// fn content_type() -> &'static str {
// "application/rkyv"
// }
// fn stream_content_type() -> &'static str {
// "application/stream+rkyv"
// }
// fn to_bytes(data: impl Serialize) -> Option<Bytes> {
// let mut buf = rkyv::ser::Serializer::new(rkyv::ser::AllocSerializer::new());
// rkyv::ser::Serializer::serialize(&mut buf, &data).ok()?;
// Some(Bytes::from(buf.into_inner()))
// }
// fn from_bytes<O: DeserializeOwned>(bytes: Bytes) -> Option<O> {
// let archived = unsafe { rkyv::archived_root::<O>(&bytes) };
// rkyv::Deserialize::deserialize(archived).ok()
// }
// }
-222
View File
@@ -1,222 +0,0 @@
use std::{
error::Error,
fmt::{Debug, Display},
str::FromStr,
};
use dioxus_core::{CapturedError, RenderError};
use serde::{de::DeserializeOwned, Serialize};
use server_fn::{
codec::JsonEncoding,
error::{FromServerFnError, ServerFnErrorErr},
};
/// A default result type for server functions, which can either be successful or contain an error. The [`ServerFnResult`] type
/// is a convenient alias for a `Result` type that uses [`ServerFnError`] as the error type.
///
/// # Example
/// ```rust
/// use dioxus::prelude::*;
///
/// #[server]
/// async fn parse_number(number: String) -> ServerFnResult<f32> {
/// let parsed_number: f32 = number.parse()?;
/// Ok(parsed_number)
/// }
/// ```
pub type ServerFnResult<T = (), E = String> = std::result::Result<T, ServerFnError<E>>;
/// An error type for server functions. This may either be an error that occurred while running the server
/// function logic, or an error that occurred while communicating with the server inside the server function crate.
///
/// ## Usage
///
/// You can use the [`ServerFnError`] type in the Error type of your server function result or use the [`ServerFnResult`]
/// type as the return type of your server function. When you call the server function, you can handle the error directly
/// or convert it into a [`CapturedError`] to throw into the nearest [`ErrorBoundary`](dioxus_core::ErrorBoundary).
///
/// ```rust
/// use dioxus::prelude::*;
///
/// #[server]
/// async fn parse_number(number: String) -> ServerFnResult<f32> {
/// // You can convert any error type into the `ServerFnError` with the `?` operator
/// let parsed_number: f32 = number.parse()?;
/// Ok(parsed_number)
/// }
///
/// #[component]
/// fn ParseNumberServer() -> Element {
/// let mut number = use_signal(|| "42".to_string());
/// let mut parsed = use_signal(|| None);
///
/// rsx! {
/// input {
/// value: "{number}",
/// oninput: move |e| number.set(e.value()),
/// }
/// button {
/// onclick: move |_| async move {
/// // Call the server function to parse the number
/// // If the result is Ok, continue running the closure, otherwise bubble up the
/// // error to the nearest error boundary with `?`
/// let result = parse_number(number()).await?;
/// parsed.set(Some(result));
/// Ok(())
/// },
/// "Parse Number"
/// }
/// if let Some(value) = parsed() {
/// p { "Parsed number: {value}" }
/// } else {
/// p { "No number parsed yet." }
/// }
/// }
/// }
/// ```
///
/// ## Differences from [`CapturedError`]
///
/// Both this error type and [`CapturedError`] can be used to represent boxed errors in dioxus. However, this error type
/// is more strict about the kinds of errors it can represent. [`CapturedError`] can represent any error that implements
/// the [`Error`] trait or can be converted to a string. [`CapturedError`] holds onto the type information of the error
/// and lets you downcast the error to its original type.
///
/// [`ServerFnError`] represents server function errors as [`String`]s by default without any additional type information.
/// This makes it easy to serialize the error to JSON and send it over the wire, but it means that you can't get the
/// original type information of the error back. If you need to preserve the type information of the error, you can use a
/// [custom error variant](#custom-error-variants) that holds onto the type information.
///
/// ## Custom error variants
///
/// The [`ServerFnError`] type accepts a generic type parameter `T` that is used to represent the error type used for server
/// functions. If you need to keep the type information of your error, you can create a custom error variant that implements
/// [`Serialize`] and [`DeserializeOwned`]. This allows you to serialize the error to JSON and send it over the wire,
/// while still preserving the type information.
///
/// ```rust
/// use dioxus::prelude::*;
/// use serde::{Deserialize, Serialize};
/// use std::fmt::Debug;
///
/// #[derive(Clone, Debug, Serialize, Deserialize)]
/// pub struct MyCustomError {
/// message: String,
/// code: u32,
/// }
///
/// impl MyCustomError {
/// pub fn new(message: String, code: u32) -> Self {
/// Self { message, code }
/// }
/// }
///
/// #[server]
/// async fn server_function() -> ServerFnResult<String, MyCustomError> {
/// // Return your custom error
/// Err(ServerFnError::ServerError(MyCustomError::new(
/// "An error occurred".to_string(),
/// 404,
/// )))
/// }
/// ```
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
pub enum ServerFnError<T = String> {
/// An error running the server function
ServerError(T),
/// An error communicating with the server
CommunicationError(ServerFnErrorErr),
}
impl ServerFnError {
/// Creates a new `ServerFnError` from something that implements `ToString`.
///
/// # Examples
/// ```rust
/// use dioxus::prelude::*;
/// use serde::{Serialize, Deserialize};
///
/// #[server]
/// async fn server_function() -> ServerFnResult<String> {
/// // Return your custom error
/// Err(ServerFnError::new("Something went wrong"))
/// }
/// ```
pub fn new(error: impl ToString) -> Self {
Self::ServerError(error.to_string())
}
}
impl<T> ServerFnError<T> {
/// Returns true if the error is a server error
pub fn is_server_error(&self) -> bool {
matches!(self, ServerFnError::ServerError(_))
}
/// Returns true if the error is a communication error
pub fn is_communication_error(&self) -> bool {
matches!(self, ServerFnError::CommunicationError(_))
}
/// Returns a reference to the server error if it is a server error
/// or `None` if it is a communication error.
pub fn server_error(&self) -> Option<&T> {
match self {
ServerFnError::ServerError(err) => Some(err),
ServerFnError::CommunicationError(_) => None,
}
}
/// Returns a reference to the communication error if it is a communication error
/// or `None` if it is a server error.
pub fn communication_error(&self) -> Option<&ServerFnErrorErr> {
match self {
ServerFnError::ServerError(_) => None,
ServerFnError::CommunicationError(err) => Some(err),
}
}
}
impl<T: Serialize + DeserializeOwned + Debug + 'static> FromServerFnError for ServerFnError<T> {
type Encoder = JsonEncoding;
fn from_server_fn_error(err: ServerFnErrorErr) -> Self {
Self::CommunicationError(err)
}
}
impl<T: FromStr> FromStr for ServerFnError<T> {
type Err = <T as FromStr>::Err;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
std::result::Result::Ok(Self::ServerError(T::from_str(s)?))
}
}
impl<T: Display> Display for ServerFnError<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ServerFnError::ServerError(err) => write!(f, "Server error: {err}"),
ServerFnError::CommunicationError(err) => write!(f, "Communication error: {err}"),
}
}
}
impl From<ServerFnError> for CapturedError {
fn from(error: ServerFnError) -> Self {
Self::from_display(error)
}
}
impl From<ServerFnError> for RenderError {
fn from(error: ServerFnError) -> Self {
RenderError::Aborted(CapturedError::from(error))
}
}
impl<E: Error> From<E> for ServerFnError {
fn from(error: E) -> Self {
Self::ServerError(error.to_string())
}
}
+141
View File
@@ -0,0 +1,141 @@
#![allow(clippy::needless_return)]
use std::{hint::black_box, prelude::rust_2024::Future, sync::atomic::AtomicBool};
/// `Lazy` is a thread-safe, lazily-initialized global variable.
///
/// Unlike other async once-cell implementations, accessing the value of a `Lazy` instance is synchronous
/// and done on `deref`.
///
/// This is done by offloading the async initialization to a blocking thread during the first access,
/// and then using the initialized value for all subsequent accesses.
///
/// It uses `std::sync::OnceLock` internally to ensure that the value is only initialized once.
pub struct Lazy<T> {
value: std::sync::OnceLock<T>,
started_initialization: AtomicBool,
constructor: Option<fn() -> Result<T, dioxus_core::Error>>,
_phantom: std::marker::PhantomData<T>,
}
impl<T: Send + Sync + 'static> Lazy<T> {
/// Create a new `Lazy` instance.
///
/// This internally calls `std::sync::OnceLock::new()` under the hood.
#[allow(clippy::self_named_constructors)]
pub const fn lazy() -> Self {
Self {
_phantom: std::marker::PhantomData,
constructor: None,
started_initialization: AtomicBool::new(false),
value: std::sync::OnceLock::new(),
}
}
pub const fn new<F, G, E>(constructor: F) -> Self
where
F: Fn() -> G + Copy,
G: Future<Output = Result<T, E>> + Send + 'static,
E: Into<dioxus_core::Error>,
{
if std::mem::size_of::<F>() != 0 {
panic!("The constructor function must be a zero-sized type (ZST). Consider using a function pointer or a closure without captured variables.");
}
// Prevent the constructor from being optimized out
black_box(constructor);
Self {
_phantom: std::marker::PhantomData,
value: std::sync::OnceLock::new(),
started_initialization: AtomicBool::new(false),
constructor: Some(blocking_initialize::<T, F, G, E>),
}
}
/// Set the value of the `Lazy` instance.
///
/// This should only be called once during the server setup phase, typically inside `dioxus::serve`.
/// Future calls to this method will return an error containing the provided value.
pub fn set(&self, pool: T) -> Result<(), dioxus_core::Error> {
let res = self.value.set(pool);
if res.is_err() {
return Err(anyhow::anyhow!("Lazy value is already initialized."));
}
Ok(())
}
pub fn try_set(&self, pool: T) -> Result<(), T> {
self.value.set(pool)
}
/// Initialize the value of the `Lazy` instance if it hasn't been initialized yet.
pub fn initialize(&self) -> Result<(), dioxus_core::Error> {
if let Some(constructor) = self.constructor {
// If we're already initializing this value, wait on the receiver.
if self
.started_initialization
.swap(true, std::sync::atomic::Ordering::SeqCst)
{
self.value.wait();
return Ok(());
}
// Otherwise, we need to initialize the value
self.set(constructor().unwrap())?;
}
Ok(())
}
}
impl<T: Send + Sync + 'static> Default for Lazy<T> {
fn default() -> Self {
Self::lazy()
}
}
impl<T: Send + Sync + 'static> std::ops::Deref for Lazy<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
if self.constructor.is_none() {
return self.value.get().expect("Lazy value is not initialized. Make sure to call `initialize` before dereferencing.");
};
if self.value.get().is_none() {
self.initialize().expect("Failed to initialize lazy value");
}
self.value.get().unwrap()
}
}
/// This is a small hack that allows us to staple the async initialization into a blocking context.
///
/// We call the `rust-call` method of the zero-sized constructor function. This is safe because we're
/// not actually dereferencing any unsafe data, just calling its vtable entry to get the future.
fn blocking_initialize<T, F, G, E>() -> Result<T, dioxus_core::Error>
where
T: Send + Sync + 'static,
F: Fn() -> G + Copy,
G: Future<Output = Result<T, E>> + Send + 'static,
E: Into<dioxus_core::Error>,
{
assert_eq!(std::mem::size_of::<F>(), 0, "The constructor function must be a zero-sized type (ZST). Consider using a function pointer or a closure without captured variables.");
#[cfg(feature = "server")]
{
let ptr: F = unsafe { std::mem::zeroed() };
let fut = ptr();
let rt = tokio::runtime::Handle::current();
return std::thread::spawn(move || rt.block_on(fut).map_err(|e| e.into()))
.join()
.unwrap();
}
// todo: technically we can support constructors in wasm with the same tricks inventory uses with `__wasm_call_ctors`
// the host would need to decide when to cal the ctors and when to block them.
#[cfg(not(feature = "server"))]
unimplemented!("Lazy initialization is only supported with tokio and threads enabled.")
}
+107 -24
View File
@@ -1,28 +1,111 @@
#![doc = include_str!("../README.md")]
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
#![warn(missing_docs)]
#![cfg_attr(docsrs, feature(doc_cfg))]
// #![warn(missing_docs)]
#![allow(clippy::manual_async_fn)]
#![allow(clippy::needless_return)]
pub use client::{get_server_url, set_server_url};
pub use dioxus_fullstack_core::*;
#[doc(inline)]
pub use dioxus_fullstack_macro::*;
pub use axum_core;
pub use headers;
pub use http;
pub use reqwest;
pub use serde;
/// Re-export commonly used items from axum, http, and hyper for convenience.
pub use axum::{body, extract, response, routing};
mod error;
#[doc(hidden)]
pub mod mock_client;
pub use const_format;
#[doc(hidden)]
pub use const_str;
#[doc(hidden)]
pub use xxhash_rust;
pub use dioxus_fullstack_hooks::history::provide_fullstack_history_context;
pub use crate::error::{ServerFnError, ServerFnResult};
#[doc(inline)]
pub use dioxus_fullstack_hooks::*;
#[cfg(feature = "server")]
#[doc(inline)]
pub use dioxus_server::*;
#[doc(inline)]
pub use dioxus_server_macro::*;
pub use server_fn::ServerFn as _;
#[doc(inline)]
pub use server_fn::{
self, client,
client::{get_server_url, set_server_url},
codec, server, BoxedStream, ContentType, Decodes, Encodes, Format, FormatType, ServerFn,
Websocket,
};
pub use {axum, axum_extra::TypedHeader, inventory};
#[cfg(feature = "server")]
pub(crate) mod spawn;
#[cfg(feature = "server")]
pub(crate) use spawn::*;
pub mod magic;
pub use magic::*;
pub mod request;
pub use request::*;
pub use http::StatusCode;
pub mod encoding;
pub use encoding::*;
pub mod lazy;
pub use lazy::*;
pub use http::{HeaderMap, HeaderValue, Method};
mod client;
pub use client::*;
pub use axum::extract::Json;
pub use payloads::*;
pub mod payloads {
use crate::{ClientRequest, ClientResponse, ClientResult, IntoRequest};
use crate::{FromResponse, FromResponseParts};
use axum::extract::FromRequest;
use axum::response::{IntoResponse, IntoResponseParts, ResponseParts};
use bytes::Bytes;
use dioxus_fullstack_core::ServerFnError;
use futures::Stream;
use headers::Header;
use http::{header::InvalidHeaderValue, HeaderValue};
use serde::{de::DeserializeOwned, Serialize};
use std::future::Future;
mod axum_types;
pub mod cbor;
pub use cbor::*;
pub mod form;
pub use form::*;
pub mod multipart;
pub use multipart::*;
#[cfg(feature = "postcard")]
pub mod postcard;
#[cfg(feature = "postcard")]
pub use postcard::*;
#[cfg(feature = "msgpack")]
pub mod msgpack;
#[cfg(feature = "msgpack")]
pub use msgpack::*;
pub mod text;
pub use text::*;
pub mod sse;
pub use sse::*;
pub mod stream;
pub use stream::*;
pub mod files;
pub use files::*;
pub mod header;
pub use header::*;
#[cfg(feature = "ws")]
pub mod websocket;
#[cfg(feature = "ws")]
pub use websocket::*;
}
+753
View File
@@ -0,0 +1,753 @@
//! ServerFn request magical 🧙 decoders and encoders.
//!
//! The Dioxus Server Function implementation brings a lot of *magic* to the types of endpoints we can handle.
//! Our ultimate goal is to handle *all* endpoints, even axum endpoints, with the macro.
//!
//! Unfortunately, some axum traits like `FromRequest` overlap with some of the default magic we want
//! to provide, like allowing DeserializedOwned groups.
//!
//! Our ultimate goal - to accept all axum handlers - is feasible but not fully implemented.
//!
//! Broadly, we support the following categories of handlers arguments:
//! - Handlers with a single argument that implements `FromRequest` + `IntoRequest`
//! - Handlers with multiple arguments that implement *all* `DeserializeOwned` (and thus can be deserialized from a JSON body)
//!
//! The handler error return types we support are:
//! - `Result<T, E> where E: From<ServerFnError> + Serialize + DeserializeOwned` (basically any custom `thiserror` impl)
//! - `Result<T, anyhow::Error>` where we transport the error as a string and/or through ServerFnError
//!
//! The handler return types we support are:
//! - `T where T: FromResponse`
//! - `T where T: DeserializeOwned`
//!
//! Note that FromResponse and IntoRequest are *custom* traits defined in this crate. The intention
//! is to provide "inverse" traits of the axum traits, allowing types to flow seamlessly between client and server.
//!
//! These are unfortunately in conflict with the serialization traits. Types like `Bytes` implement both
//! IntoResponse and Serialize, so what should you use?
//!
//! This module implements auto-deref specialization to allow tiering of the above cases.
//!
//! This is sadly quite "magical", but it works. Because the FromResponse traits are defined in this crate,
//! they are sealed against types that implement Deserialize/Serialize, meaning you cannot implement
//! FromResponse for a type that implements Serialize.
//!
//! This module is broken up into several parts, attempting to match how the server macro generates code:
//! - ReqwestEncoder: encodes a set of arguments into a reqwest request
use crate::{
CantEncode, ClientRequest, ClientResponse, EncodeIsVerified, FromResponse, HttpError,
IntoRequest, ServerFnError,
};
use axum::response::IntoResponse;
use axum_core::extract::{FromRequest, Request};
use bytes::Bytes;
use dioxus_fullstack_core::{DioxusServerState, RequestError};
use http::StatusCode;
use send_wrapper::SendWrapper;
use serde::Serialize;
use serde::{de::DeserializeOwned, Deserialize};
use std::fmt::Display;
use std::{marker::PhantomData, prelude::rust_2024::Future};
#[doc(hidden)]
pub struct ServerFnEncoder<In, Out>(PhantomData<fn() -> (In, Out)>);
impl<In, Out> ServerFnEncoder<In, Out> {
#[doc(hidden)]
pub fn new() -> Self {
ServerFnEncoder(PhantomData)
}
}
#[doc(hidden)]
pub struct ServerFnDecoder<Out>(PhantomData<fn() -> Out>);
impl<Out> ServerFnDecoder<Out> {
#[doc(hidden)]
pub fn new() -> Self {
ServerFnDecoder(PhantomData)
}
}
/// A response structure for a regular REST API, with a success and error case where the status is
/// encoded in the body and all fields are serializable. This lets you call fetch().await.json()
/// and get a strongly typed result.
///
/// Eventually we want to support JsonRPC which requires a different format.
///
/// We use the `___status` field to avoid conflicts with user-defined fields. Hopefully no one uses this field name!
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub enum RestEndpointPayload<T, E> {
#[serde(rename = "success")]
Success(T),
#[serde(rename = "error")]
Error(ErrorPayload<E>),
}
/// The error payload structure for REST API errors.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct ErrorPayload<E> {
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
code: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
data: Option<E>,
}
/// Convert a `RequestError` into a `ServerFnError`.
///
/// This is a separate function to avoid bringing in `reqwest` into fullstack-core.
pub fn reqwest_response_to_serverfn_err(err: reqwest::Error) -> ServerFnError {
ServerFnError::Request(reqwest_error_to_request_error(err))
}
pub fn reqwest_error_to_request_error(err: reqwest::Error) -> RequestError {
let message = err.to_string();
if err.is_timeout() {
RequestError::Timeout(message)
} else if err.is_request() {
RequestError::Request(message)
} else if err.is_body() {
RequestError::Body(message)
} else if err.is_decode() {
RequestError::Decode(message)
} else if err.is_redirect() {
RequestError::Redirect(message)
} else if let Some(status) = err.status() {
RequestError::Status(message, status.as_u16())
} else {
#[cfg(not(target_arch = "wasm32"))]
{
if err.is_connect() {
RequestError::Connect(message)
} else {
RequestError::Request(message)
}
}
#[cfg(target_arch = "wasm32")]
{
RequestError::Request(message)
}
}
}
pub use req_to::*;
pub mod req_to {
use super::*;
pub trait EncodeRequest<In, Out, R> {
type VerifyEncode;
fn fetch_client(
&self,
ctx: ClientRequest,
data: In,
map: fn(In) -> Out,
) -> impl Future<Output = Result<R, RequestError>> + 'static;
fn verify_can_serialize(&self) -> Self::VerifyEncode;
}
/// Using the deserialize path
impl<T, O> EncodeRequest<T, O, ClientResponse> for &&&&&&&&&&ServerFnEncoder<T, O>
where
T: DeserializeOwned + Serialize + 'static,
{
type VerifyEncode = EncodeIsVerified;
fn fetch_client(
&self,
ctx: ClientRequest,
data: T,
_map: fn(T) -> O,
) -> impl Future<Output = Result<ClientResponse, RequestError>> + 'static {
async move { ctx.send_json(&data).await }
}
fn verify_can_serialize(&self) -> Self::VerifyEncode {
EncodeIsVerified
}
}
/// When we use the FromRequest path, we don't need to deserialize the input type on the client,
impl<T, O, R> EncodeRequest<T, O, R> for &&&&&&&&&ServerFnEncoder<T, O>
where
T: 'static,
O: FromRequest<DioxusServerState> + IntoRequest<R>,
{
type VerifyEncode = EncodeIsVerified;
fn fetch_client(
&self,
ctx: ClientRequest,
data: T,
map: fn(T) -> O,
) -> impl Future<Output = Result<R, RequestError>> + 'static {
O::into_request(map(data), ctx)
}
fn verify_can_serialize(&self) -> Self::VerifyEncode {
EncodeIsVerified
}
}
/// The fall-through case that emits a `CantEncode` type which fails to compile when checked by the macro
impl<T, O> EncodeRequest<T, O, ClientResponse> for &ServerFnEncoder<T, O>
where
T: 'static,
{
type VerifyEncode = CantEncode;
#[allow(clippy::manual_async_fn)]
fn fetch_client(
&self,
_ctx: ClientRequest,
_data: T,
_map: fn(T) -> O,
) -> impl Future<Output = Result<ClientResponse, RequestError>> + 'static {
async move { unimplemented!() }
}
fn verify_can_serialize(&self) -> Self::VerifyEncode {
CantEncode
}
}
}
pub use decode_ok::*;
mod decode_ok {
use super::*;
/// Convert the reqwest response into the desired type, in place.
/// The point here is to prefer FromResponse types *first* and then DeserializeOwned types second.
///
/// This is because FromResponse types are more specialized and can handle things like websockets and files.
/// DeserializeOwned types are more general and can handle things like JSON responses.
pub trait RequestDecodeResult<T, R> {
fn decode_client_response(
&self,
res: Result<R, RequestError>,
) -> impl Future<Output = Result<Result<T, ServerFnError>, RequestError>> + Send;
}
impl<T: FromResponse<R>, E, R> RequestDecodeResult<T, R> for &&&ServerFnDecoder<Result<T, E>> {
fn decode_client_response(
&self,
res: Result<R, RequestError>,
) -> impl Future<Output = Result<Result<T, ServerFnError>, RequestError>> + Send {
SendWrapper::new(async move {
match res {
Err(err) => Err(err),
Ok(res) => Ok(T::from_response(res).await),
}
})
}
}
impl<T: DeserializeOwned, E> RequestDecodeResult<T, ClientResponse>
for &&ServerFnDecoder<Result<T, E>>
{
fn decode_client_response(
&self,
res: Result<ClientResponse, RequestError>,
) -> impl Future<Output = Result<Result<T, ServerFnError>, RequestError>> + Send {
SendWrapper::new(async move {
match res {
Err(err) => Err(err),
Ok(res) => {
let status = res.status();
let bytes = res.bytes().await.unwrap();
let as_bytes = if bytes.is_empty() {
b"null".as_slice()
} else {
&bytes
};
let res = if status.is_success() {
serde_json::from_slice::<T>(as_bytes)
.map(RestEndpointPayload::Success)
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
} else {
match serde_json::from_slice::<ErrorPayload<serde_json::Value>>(
as_bytes,
) {
Ok(res) => Ok(RestEndpointPayload::Error(ErrorPayload {
message: res.message,
code: res.code,
data: res.data,
})),
Err(err) => {
if let Ok(text) = String::from_utf8(as_bytes.to_vec()) {
Ok(RestEndpointPayload::Error(ErrorPayload {
message: format!("HTTP {}: {}", status.as_u16(), text),
code: Some(status.as_u16()),
data: None,
}))
} else {
Err(ServerFnError::Deserialization(err.to_string()))
}
}
}
};
match res {
Ok(RestEndpointPayload::Success(t)) => Ok(Ok(t)),
Ok(RestEndpointPayload::Error(err)) => {
Ok(Err(ServerFnError::ServerError {
message: err.message,
details: err.data,
code: err.code,
}))
}
Err(e) => Ok(Err(e)),
}
}
}
})
}
}
pub trait RequestDecodeErr<T, E> {
fn decode_client_err(
&self,
res: Result<Result<T, ServerFnError>, RequestError>,
) -> impl Future<Output = Result<T, E>> + Send;
}
impl<T, E> RequestDecodeErr<T, E> for &&&ServerFnDecoder<Result<T, E>>
where
E: From<ServerFnError> + DeserializeOwned + Serialize,
{
fn decode_client_err(
&self,
res: Result<Result<T, ServerFnError>, RequestError>,
) -> impl Future<Output = Result<T, E>> + Send {
SendWrapper::new(async move {
match res {
Ok(Ok(res)) => Ok(res),
Ok(Err(e)) => match e {
ServerFnError::ServerError {
details,
message,
code,
} => {
// If there are "details", then we try to deserialize them into the error type.
// If there aren't, we just create a generic ServerFnError::ServerError with the message.
match details {
Some(details) => match serde_json::from_value::<E>(details) {
Ok(res) => Err(res),
Err(err) => Err(E::from(ServerFnError::Deserialization(
err.to_string(),
))),
},
None => Err(E::from(ServerFnError::ServerError {
message,
details: None,
code,
})),
}
}
err => Err(err.into()),
},
// todo: implement proper through-error conversion, instead of just ServerFnError::Request
// we should expand these cases.
Err(err) => Err(ServerFnError::from(err).into()),
}
})
}
}
/// Here we convert to ServerFnError and then into the anyhow::Error, letting the user downcast
/// from the ServerFnError if they want to.
///
/// This loses any actual type information, but is the most flexible for users.
impl<T> RequestDecodeErr<T, anyhow::Error> for &&ServerFnDecoder<Result<T, anyhow::Error>> {
fn decode_client_err(
&self,
res: Result<Result<T, ServerFnError>, RequestError>,
) -> impl Future<Output = Result<T, anyhow::Error>> + Send {
SendWrapper::new(async move {
match res {
Ok(Ok(res)) => Ok(res),
Ok(Err(e)) => Err(anyhow::Error::from(e)),
Err(err) => Err(anyhow::Error::from(err)),
}
})
}
}
/// This converts to statuscode, which can be useful but loses a lot of information.
impl<T> RequestDecodeErr<T, StatusCode> for &ServerFnDecoder<Result<T, StatusCode>> {
fn decode_client_err(
&self,
res: Result<Result<T, ServerFnError>, RequestError>,
) -> impl Future<Output = Result<T, StatusCode>> + Send {
SendWrapper::new(async move {
match res {
Ok(Ok(res)) => Ok(res),
// We do a best-effort conversion from ServerFnError to StatusCode.
Ok(Err(e)) => match e {
ServerFnError::Request(error) => {
Err(StatusCode::from_u16(error.status_code().unwrap_or(500))
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR))
}
ServerFnError::ServerError {
message: _message,
details: _details,
code,
} => Err(StatusCode::from_u16(code.unwrap_or(500))
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)),
ServerFnError::Registration(_) | ServerFnError::MiddlewareError(_) => {
Err(StatusCode::INTERNAL_SERVER_ERROR)
}
ServerFnError::Deserialization(_)
| ServerFnError::Serialization(_)
| ServerFnError::Args(_)
| ServerFnError::MissingArg(_)
| ServerFnError::StreamError(_) => Err(StatusCode::UNPROCESSABLE_ENTITY),
ServerFnError::UnsupportedRequestMethod(_) => {
Err(StatusCode::METHOD_NOT_ALLOWED)
}
ServerFnError::Response(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
},
// The reqwest error case, we try to convert the reqwest error into a status code.
Err(reqwest_err) => {
let code = reqwest_err
.status()
.unwrap_or(StatusCode::SERVICE_UNAVAILABLE);
Err(code)
}
}
})
}
}
impl<T> RequestDecodeErr<T, HttpError> for &ServerFnDecoder<Result<T, HttpError>> {
fn decode_client_err(
&self,
res: Result<Result<T, ServerFnError>, RequestError>,
) -> impl Future<Output = Result<T, HttpError>> + Send {
SendWrapper::new(async move {
match res {
Ok(Ok(res)) => Ok(res),
Ok(Err(res)) => match res {
ServerFnError::ServerError { message, code, .. } => Err(HttpError {
status: StatusCode::from_u16(code.unwrap_or(500))
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
message: Some(message),
}),
_ => HttpError::internal_server_error("Internal Server Error"),
},
Err(err) => Err(HttpError::new(
err.status().unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
err.to_string(),
)),
}
})
}
}
}
pub use req_from::*;
pub mod req_from {
use super::*;
use axum::{extract::FromRequestParts, response::Response};
pub trait ExtractRequest<In, Out, H, M = ()> {
fn extract_axum(
&self,
state: DioxusServerState,
request: Request,
map: fn(In) -> Out,
) -> impl Future<Output = Result<(H, Out), Response>> + 'static;
}
/// When you're extracting entirely on the server, we need to reject client-consuning request bodies
/// This sits above priority in the combined headers on server / body on client case.
impl<In, M, H> ExtractRequest<In, (), H, M> for &&&&&&&&&&&ServerFnEncoder<In, ()>
where
H: FromRequest<DioxusServerState, M> + 'static,
{
fn extract_axum(
&self,
state: DioxusServerState,
request: Request,
_map: fn(In) -> (),
) -> impl Future<Output = Result<(H, ()), Response>> + 'static {
async move {
H::from_request(request, &state)
.await
.map_err(|e| e.into_response())
.map(|out| (out, ()))
}
}
}
// One-arg case
impl<In, Out, H> ExtractRequest<In, Out, H> for &&&&&&&&&&ServerFnEncoder<In, Out>
where
In: DeserializeOwned + 'static,
Out: 'static,
H: FromRequestParts<DioxusServerState>,
{
fn extract_axum(
&self,
_state: DioxusServerState,
request: Request,
map: fn(In) -> Out,
) -> impl Future<Output = Result<(H, Out), Response>> + 'static {
async move {
let (mut parts, body) = request.into_parts();
let headers = H::from_request_parts(&mut parts, &_state)
.await
.map_err(|e| e.into_response())?;
let request = Request::from_parts(parts, body);
let bytes = Bytes::from_request(request, &()).await.unwrap();
let as_str = String::from_utf8_lossy(&bytes);
let bytes = if as_str.is_empty() {
"{}".as_bytes()
} else {
&bytes
};
let out = serde_json::from_slice::<In>(bytes)
.map(map)
.map_err(|e| ServerFnError::from(e).into_response())
.unwrap();
Ok((headers, out))
}
}
}
/// We skip the BodySerialize wrapper and just go for the output type directly.
impl<In, Out, M, H> ExtractRequest<In, Out, H, M> for &&&&&&&&&ServerFnEncoder<In, Out>
where
Out: FromRequest<DioxusServerState, M> + 'static,
H: FromRequestParts<DioxusServerState>,
{
fn extract_axum(
&self,
state: DioxusServerState,
request: Request,
_map: fn(In) -> Out,
) -> impl Future<Output = Result<(H, Out), Response>> + 'static {
async move {
let (mut parts, body) = request.into_parts();
let headers = H::from_request_parts(&mut parts, &state)
.await
.map_err(|e| e.into_response())?;
let request = Request::from_parts(parts, body);
let res = Out::from_request(request, &state)
.await
.map_err(|e| e.into_response());
res.map(|out| (headers, out))
}
}
}
}
pub use resp::*;
mod resp {
use crate::HttpError;
use super::*;
use axum::response::Response;
use http::HeaderValue;
/// A trait for converting the result of the Server Function into an Axum response.
///
/// This is to work around the issue where we want to return both Deserialize types and FromResponse types.
/// Stuff like websockets
///
/// We currently have an `Input` type even though it's not useful since we might want to support regular axum endpoints later.
/// For now, it's just Result<T, E> where T is either DeserializeOwned or FromResponse
pub trait MakeAxumResponse<T, E, R> {
fn make_axum_response(self, result: Result<T, E>) -> Result<Response, E>;
}
// Higher priority impl for special types like websocket/file responses that generate their own responses
// The FromResponse impl helps narrow types to those usable on the client
impl<T, E, R> MakeAxumResponse<T, E, R> for &&&&ServerFnDecoder<Result<T, E>>
where
T: FromResponse<R> + IntoResponse,
{
fn make_axum_response(self, result: Result<T, E>) -> Result<Response, E> {
result.map(|v| v.into_response())
}
}
// Lower priority impl for regular serializable types
// We try to match the encoding from the incoming request, otherwise default to JSON
impl<T, E> MakeAxumResponse<T, E, ()> for &&&ServerFnDecoder<Result<T, E>>
where
T: DeserializeOwned + Serialize,
{
fn make_axum_response(self, result: Result<T, E>) -> Result<Response, E> {
match result {
Ok(res) => {
let body = serde_json::to_string(&res).unwrap();
let mut resp = Response::new(body.into());
resp.headers_mut().insert(
http::header::CONTENT_TYPE,
HeaderValue::from_static("application/json"),
);
*resp.status_mut() = StatusCode::OK;
Ok(resp)
}
Err(err) => Err(err),
}
}
}
#[allow(clippy::result_large_err)]
pub trait MakeAxumError<E> {
fn make_axum_error(self, result: Result<Response, E>) -> Result<Response, Response>;
}
impl<T, E> MakeAxumError<E> for &&&ServerFnDecoder<Result<T, E>>
where
E: From<ServerFnError> + Serialize + DeserializeOwned + Display,
{
fn make_axum_error(self, result: Result<Response, E>) -> Result<Response, Response> {
match result {
Ok(res) => Ok(res),
Err(err) => {
let err = ErrorPayload {
code: None,
message: err.to_string(),
data: Some(err),
};
let body = serde_json::to_string(&err).unwrap();
let mut resp = Response::new(body.into());
resp.headers_mut().insert(
http::header::CONTENT_TYPE,
HeaderValue::from_static("application/json"),
);
*resp.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
Err(resp)
}
}
}
}
impl<T> MakeAxumError<anyhow::Error> for &&ServerFnDecoder<Result<T, anyhow::Error>> {
fn make_axum_error(
self,
result: Result<Response, anyhow::Error>,
) -> Result<Response, Response> {
match result {
Ok(res) => Ok(res),
Err(errr) => {
// The `WithHttpError` trait emits ServerFnErrors so we can downcast them here
// to create richer responses.
let payload = match errr.downcast::<ServerFnError>() {
Ok(ServerFnError::ServerError {
message,
code,
details,
}) => ErrorPayload {
message,
code,
data: details,
},
Ok(other) => ErrorPayload {
message: other.to_string(),
code: None,
data: None,
},
Err(err) => match err.downcast::<HttpError>() {
Ok(http_err) => ErrorPayload {
message: http_err
.message
.unwrap_or_else(|| http_err.status.to_string()),
code: Some(http_err.status.as_u16()),
data: None,
},
Err(err) => ErrorPayload {
code: None,
message: err.to_string(),
data: None,
},
},
};
let body = serde_json::to_string(&payload).unwrap();
let mut resp = Response::new(body.into());
resp.headers_mut().insert(
http::header::CONTENT_TYPE,
HeaderValue::from_static("application/json"),
);
*resp.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
Err(resp)
}
}
}
}
impl<T> MakeAxumError<StatusCode> for &&ServerFnDecoder<Result<T, StatusCode>> {
fn make_axum_error(
self,
result: Result<Response, StatusCode>,
) -> Result<Response, Response> {
match result {
Ok(resp) => Ok(resp),
Err(status) => {
let body = serde_json::to_string(&ErrorPayload::<()> {
code: Some(status.as_u16()),
message: status.to_string(),
data: None,
})
.unwrap();
let mut resp = Response::new(body.into());
resp.headers_mut().insert(
http::header::CONTENT_TYPE,
HeaderValue::from_static("application/json"),
);
*resp.status_mut() = status;
Err(resp)
}
}
}
}
impl<T> MakeAxumError<HttpError> for &ServerFnDecoder<Result<T, HttpError>> {
fn make_axum_error(
self,
result: Result<Response, HttpError>,
) -> Result<Response, Response> {
match result {
Ok(resp) => Ok(resp),
Err(http_err) => {
let body = serde_json::to_string(&ErrorPayload::<()> {
code: Some(http_err.status.as_u16()),
message: http_err
.message
.unwrap_or_else(|| http_err.status.to_string()),
data: None,
})
.unwrap();
let mut resp = Response::new(body.into());
resp.headers_mut().insert(
http::header::CONTENT_TYPE,
HeaderValue::from_static("application/json"),
);
*resp.status_mut() = http_err.status;
Err(resp)
}
}
}
}
}
-153
View File
@@ -1,153 +0,0 @@
//! A mock [`server_fn::client::Client`] implementation used when no client feature is enabled.
use std::future::Future;
use futures_util::Stream;
use server_fn::{request::ClientReq, response::ClientRes};
/// A placeholder [`server_fn::client::Client`] used when no client feature is enabled. The
/// [`server_fn::client::browser::BrowserClient`] is used on web clients, and [`server_fn::client::reqwest::ReqwestClient`]
/// is used on native clients
#[non_exhaustive]
pub struct MockServerFnClient {}
impl Default for MockServerFnClient {
fn default() -> Self {
Self::new()
}
}
impl MockServerFnClient {
/// Create a new mock server function client
pub fn new() -> Self {
Self {}
}
}
impl<Error, InputStreamError, OutputStreamError>
server_fn::client::Client<Error, InputStreamError, OutputStreamError> for MockServerFnClient
{
type Request = MockServerFnClientRequest;
type Response = MockServerFnClientResponse;
async fn send(_: Self::Request) -> Result<Self::Response, Error> {
unimplemented!()
}
#[allow(unreachable_code)]
async fn open_websocket(
_: &str,
) -> Result<
(
impl Stream<Item = Result<server_fn::Bytes, server_fn::Bytes>> + std::marker::Send + 'static,
impl futures_util::Sink<server_fn::Bytes> + std::marker::Send + 'static,
),
Error,
> {
unimplemented!()
as Result<
(
futures_util::stream::Once<futures_util::future::Pending<_>>,
futures_util::sink::Drain<server_fn::Bytes>,
),
_,
>
}
fn spawn(_: impl Future<Output = ()> + Send + 'static) {
unimplemented!()
}
}
/// A placeholder [`ClientReq`] used when no client feature is enabled.
#[non_exhaustive]
pub struct MockServerFnClientRequest {}
impl<E> ClientReq<E> for MockServerFnClientRequest {
type FormData = ();
fn try_new_req_query(_: &str, _: &str, _: &str, _: &str, _: http::Method) -> Result<Self, E> {
unimplemented!()
}
fn try_new_req_text(_: &str, _: &str, _: &str, _: String, _: http::Method) -> Result<Self, E> {
unimplemented!()
}
fn try_new_req_bytes(
_: &str,
_: &str,
_: &str,
_: bytes::Bytes,
_: http::Method,
) -> Result<Self, E> {
unimplemented!()
}
fn try_new_req_form_data(
_: &str,
_: &str,
_: &str,
_: Self::FormData,
_: http::Method,
) -> Result<Self, E> {
unimplemented!()
}
fn try_new_req_multipart(
_: &str,
_: &str,
_: Self::FormData,
_: http::Method,
) -> Result<Self, E> {
unimplemented!()
}
fn try_new_req_streaming(
_: &str,
_: &str,
_: &str,
_: impl Stream<Item = bytes::Bytes> + Send + 'static,
_: http::Method,
) -> Result<Self, E> {
unimplemented!()
}
}
/// A placeholder [`ClientRes`] used when no client feature is enabled.
pub struct MockServerFnClientResponse;
impl<E> ClientRes<E> for MockServerFnClientResponse {
async fn try_into_string(self) -> Result<String, E> {
unimplemented!()
}
async fn try_into_bytes(self) -> Result<bytes::Bytes, E> {
unimplemented!()
}
#[allow(unreachable_code)]
fn try_into_stream(
self,
) -> Result<impl Stream<Item = Result<bytes::Bytes, bytes::Bytes>> + Send + Sync + 'static, E>
{
unimplemented!() as Result<futures_util::stream::Once<futures_util::future::Pending<_>>, _>
}
fn status(&self) -> u16 {
unimplemented!()
}
fn status_text(&self) -> String {
unimplemented!()
}
fn location(&self) -> String {
unimplemented!()
}
fn has_redirect(&self) -> bool {
unimplemented!()
}
}
@@ -0,0 +1,77 @@
use super::*;
use crate::{ClientResponse, FromResponse};
pub use axum::extract::Json;
use axum::response::Html;
use dioxus_fullstack_core::{RequestError, ServerFnError};
use futures::StreamExt;
use std::future::Future;
impl<T: From<String>> FromResponse for Html<T> {
fn from_response(res: ClientResponse) -> impl Future<Output = Result<Self, ServerFnError>> {
async move {
let content = res.text().await?;
Ok(Html(content.into()))
}
}
}
impl<T> IntoRequest for Json<T>
where
T: Serialize + 'static,
{
fn into_request(self, request: ClientRequest) -> impl Future<Output = ClientResult> + 'static {
async move { request.send_json(&self.0).await }
}
}
impl<T: DeserializeOwned> FromResponse for Json<T> {
fn from_response(res: ClientResponse) -> impl Future<Output = Result<Self, ServerFnError>> {
async move {
let data = res.json::<T>().await?;
Ok(Json(data))
}
}
}
/// Implementation of `FromResponse` for `axum::response::Response`.
///
/// This allows converting a `ClientResponse` (from a client-side HTTP request)
/// into an `axum::Response` for server-side handling. The response's status,
/// headers, and body are transferred from the client response to the axum response.
impl FromResponse for axum::response::Response {
fn from_response(res: ClientResponse) -> impl Future<Output = Result<Self, ServerFnError>> {
async move {
let parts = res.make_parts();
let body = axum::body::Body::from_stream(res.bytes_stream());
let response = axum::response::Response::from_parts(parts, body);
Ok(response)
}
}
}
/// Implementation of `IntoRequest` for `axum::extract::Request`.
///
/// This allows converting an `axum::Request` (from server-side extraction)
/// into a `ClientRequest` that can be sent as an HTTP request. The request's
/// headers and body are transferred from the axum request to the client request.
impl IntoRequest for axum::extract::Request {
fn into_request(
self,
mut request: ClientRequest,
) -> impl Future<Output = Result<ClientResponse, RequestError>> + 'static {
async move {
let (parts, body) = self.into_parts();
for (key, value) in &parts.headers {
request = request.header(key, value)?;
}
request
.send_body_stream(
body.into_data_stream()
.map(|res| res.map_err(|_| StreamingError::Failed)),
)
.await
}
}
}
+160
View File
@@ -0,0 +1,160 @@
use axum::{
body::Bytes,
extract::{rejection::BytesRejection, FromRequest, Request},
http::{header, HeaderMap, HeaderValue, StatusCode},
response::{IntoResponse, Response},
};
use serde::{de::DeserializeOwned, Serialize};
/// CBOR Extractor / Response.
///
/// When used as an extractor, it can deserialize request bodies into some type that
/// implements [`serde::Deserialize`]. The request will be rejected (and a [`CborRejection`] will
/// be returned) if:
///
/// - The request doesn't have a `Content-Type: application/cbor` (or similar) header.
/// - The body doesn't contain syntactically valid CBOR.
/// - The body contains syntactically valid CBOR but it couldn't be deserialized into the target type.
/// - Buffering the request body fails.
///
/// ⚠️ Since parsing CBOR requires consuming the request body, the `Cbor` extractor must be
/// *last* if there are multiple extractors in a handler.
/// See ["the order of extractors"][order-of-extractors]
///
/// [order-of-extractors]: crate::extract#the-order-of-extractors
#[must_use]
pub struct Cbor<T>(pub T);
/// Check if the request has a valid CBOR content type header.
///
/// This function validates that the `Content-Type` header is set to `application/cbor`
/// or a compatible CBOR media type (including subtypes with `+cbor` suffix).
fn is_valid_cbor_content_type(headers: &HeaderMap) -> bool {
let Some(content_type) = headers.get(header::CONTENT_TYPE) else {
return false;
};
let Ok(content_type) = content_type.to_str() else {
return false;
};
let Ok(mime) = content_type.parse::<mime::Mime>() else {
return false;
};
let is_cbor_content_type = mime.type_() == "application"
&& (mime.subtype() == "cbor" || mime.suffix().is_some_and(|name| name == "cbor"));
is_cbor_content_type
}
impl<S, T> FromRequest<S> for Cbor<T>
where
S: Send + Sync,
T: DeserializeOwned,
{
type Rejection = CborRejection;
/// Extract a CBOR payload from the request body.
///
/// This implementation validates the content type and deserializes the CBOR data.
/// Returns a `CborRejection` if validation or deserialization fails.
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
if !is_valid_cbor_content_type(req.headers()) {
return Err(CborRejection::MissingCborContentType);
}
let bytes = Bytes::from_request(req, state).await?;
let value =
ciborium::from_reader(&bytes as &[u8]).map_err(|_| CborRejection::FailedToParseCbor)?;
Ok(Cbor(value))
}
}
impl<T> IntoResponse for Cbor<T>
where
T: Serialize,
{
/// Convert the CBOR payload into an HTTP response.
///
/// This serializes the inner value to CBOR format and sets the appropriate
/// `Content-Type: application/cbor` header. Returns a 500 Internal Server Error
/// if serialization fails.
fn into_response(self) -> Response {
let mut buf = Vec::new();
match ciborium::into_writer(&self.0, &mut buf) {
Err(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
[(
header::CONTENT_TYPE,
HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()),
)],
"Failed to serialize to CBOR".to_string(),
)
.into_response(),
Ok(()) => (
[(
header::CONTENT_TYPE,
HeaderValue::from_static("application/cbor"),
)],
buf,
)
.into_response(),
}
}
}
impl<T> From<T> for Cbor<T> {
/// Create a `Cbor<T>` from the inner value.
///
/// This is a convenience constructor that wraps any value in the `Cbor` struct.
fn from(inner: T) -> Self {
Self(inner)
}
}
impl<T> Cbor<T>
where
T: DeserializeOwned,
{
/// Construct a `Cbor<T>` from a byte slice.
///
/// This method attempts to deserialize the provided bytes as CBOR data.
/// Returns a `CborRejection` if deserialization fails.
pub fn from_bytes(bytes: &[u8]) -> Result<Self, CborRejection> {
ciborium::de::from_reader(bytes)
.map(Cbor)
.map_err(|_| CborRejection::FailedToParseCbor)
}
}
/// Rejection type for CBOR extraction failures.
///
/// This enum represents the various ways that CBOR extraction can fail.
/// It implements `IntoResponse` to provide appropriate HTTP responses for each error type.
#[derive(thiserror::Error, Debug)]
pub enum CborRejection {
/// The request is missing the required `Content-Type: application/cbor` header.
#[error("Expected request with `Content-Type: application/cbor`")]
MissingCborContentType,
/// Failed to parse the request body as valid CBOR.
#[error("Invalid CBOR data")]
FailedToParseCbor,
/// Failed to read the request body bytes.
#[error(transparent)]
BytesRejection(#[from] BytesRejection),
}
impl IntoResponse for CborRejection {
fn into_response(self) -> Response {
use CborRejection::*;
match self {
MissingCborContentType => {
(StatusCode::UNSUPPORTED_MEDIA_TYPE, self.to_string()).into_response()
}
FailedToParseCbor => (StatusCode::BAD_REQUEST, self.to_string()).into_response(),
BytesRejection(rejection) => rejection.into_response(),
}
}
}
+371
View File
@@ -0,0 +1,371 @@
use super::*;
use axum_core::extract::Request;
use dioxus_fullstack_core::RequestError;
use dioxus_html::FileData;
#[cfg(feature = "server")]
use std::path::Path;
use std::{
pin::Pin,
task::{Context, Poll},
};
/// A payload for uploading files using streams.
///
/// The `FileUpload` struct allows you to upload files by streaming their data. It can be constructed
/// from a stream of bytes and can be sent as part of an HTTP request. This is particularly useful for
/// handling large files without loading them entirely into memory.
///
/// On the web, this uses the `ReadableStream` API to stream file data.
pub struct FileStream {
data: Option<FileData>,
name: String,
size: Option<u64>,
content_type: Option<String>,
#[cfg(feature = "server")]
server_body: Option<axum_core::body::BodyDataStream>,
// For downloaded files...
#[allow(clippy::type_complexity)]
client_body: Option<Pin<Box<dyn Stream<Item = Result<Bytes, StreamingError>> + Send>>>,
}
impl FileStream {
/// Get the name of the file.
pub fn file_name(&self) -> &str {
&self.name
}
/// Get the size of the file, if known.
pub fn size(&self) -> Option<u64> {
self.size
}
/// Get the content type of the file, if available.
pub fn content_type(&self) -> Option<&str> {
self.content_type.as_deref()
}
/// Return the underlying body stream, assuming the `FileStream` was created by a server request.
#[cfg(feature = "server")]
pub fn body_mut(&mut self) -> Option<&mut axum_core::body::BodyDataStream> {
self.server_body.as_mut()
}
/// Create a new `FileStream` from a file path. This is only available on the server.
#[cfg(feature = "server")]
pub async fn from_path(file: impl AsRef<Path>) -> Result<Self, std::io::Error> {
Self::from_path_buf(file.as_ref()).await
}
#[cfg(feature = "server")]
async fn from_path_buf(file: &Path) -> Result<Self, std::io::Error> {
let metadata = file.metadata()?;
let contents = tokio::fs::File::open(&file).await?;
let mime = dioxus_asset_resolver::native::get_mime_from_ext(
file.extension().and_then(|s| s.to_str()),
);
let size = metadata.len();
let name = file
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("file")
.to_string();
// Convert the tokio file into an async byte stream
let reader_stream = tokio_util::io::ReaderStream::new(contents);
// Attempt to construct a BodyDataStream from the reader stream.
// Many axum_core versions provide a `from_stream` or similar constructor.
let body = axum_core::body::Body::from_stream(reader_stream).into_data_stream();
Ok(Self {
data: None,
name,
size: Some(size),
content_type: Some(mime.to_string()),
#[cfg(feature = "server")]
server_body: Some(body),
client_body: None,
})
}
/// Create a new `FileStream` from raw components.
///
/// This is meant to be used on the server where a file might not even exist but you still want
/// to stream it to the client as a download.
#[cfg(feature = "server")]
pub fn from_raw(
name: String,
size: Option<u64>,
content_type: String,
body: axum_core::body::BodyDataStream,
) -> Self {
Self {
data: None,
name,
size,
content_type: Some(content_type),
#[cfg(feature = "server")]
server_body: Some(body),
client_body: None,
}
}
}
impl IntoRequest for FileStream {
#[allow(unreachable_code)]
fn into_request(
self,
#[allow(unused_mut)] mut builder: ClientRequest,
) -> impl Future<Output = ClientResult> + 'static {
async move {
let Some(file_data) = self.data else {
return Err(RequestError::Request(
"FileStream has no data to send".into(),
));
};
#[cfg(feature = "web")]
if cfg!(target_arch = "wasm32") {
use js_sys::escape;
use wasm_bindgen::JsCast;
let as_file = file_data.inner().downcast_ref::<web_sys::File>().unwrap();
let as_blob = as_file.dyn_ref::<web_sys::Blob>().unwrap();
let content_type = as_blob.type_();
let content_length = as_blob.size().to_string();
let name = as_file.name();
// Set both Content-Length and X-Content-Size for compatibility with server extraction.
// In browsers, content-length is often overwritten, so we set X-Content-Size as well
// for better compatibility with dioxus-based clients.
return builder
.header("Content-Type", content_type)?
.header("Content-Length", content_length.clone())?
.header("X-Content-Size", content_length)?
.header(
"Content-Disposition",
format!("attachment; filename=\"{}\"", escape(&name)),
)?
.send_js_value(as_blob.clone().into())
.await;
}
#[cfg(not(target_arch = "wasm32"))]
{
use std::ascii::escape_default;
use futures::TryStreamExt;
let content_type = self
.content_type
.unwrap_or_else(|| "application/octet-stream".to_string());
let content_length = self.size.map(|s| s.to_string());
let name = self.name;
let stream = file_data.byte_stream().map_err(|_| StreamingError::Failed);
// Ascii escape the filename to avoid issues with special characters.
let mut chars = vec![];
for byte in name.chars() {
chars.extend(escape_default(byte as u8));
}
let filename = String::from_utf8(chars).map_err(|_| {
RequestError::Request(
"Failed to escape filename for Content-Disposition".into(),
)
});
if let Some(length) = content_length {
builder = builder.header("Content-Length", length)?;
}
if let Ok(filename) = filename {
builder = builder.header(
"Content-Disposition",
format!("attachment; filename=\"{}\"", filename),
)?;
}
return builder
.header("Content-Type", content_type)?
.send_body_stream(stream)
.await;
}
unimplemented!("FileStream::into_request is only implemented for web targets");
}
}
}
impl<S> FromRequest<S> for FileStream {
type Rejection = ServerFnError;
fn from_request(
req: Request,
_: &S,
) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
async move {
tracing::info!("Extracting FileUpload from request: {:?}", req);
let disposition = req.headers().get("Content-Disposition");
let filename = match disposition.map(|s| s.to_str()) {
Some(Ok(dis)) => {
let content = content_disposition::parse_content_disposition(dis);
let names = content.filename();
match names {
Some((_name, Some(filename))) => filename.to_string(),
Some((name, None)) => name.to_string(),
None => "file".to_string(),
}
}
_ => "file".to_string(),
};
// Content-length is unreliable, so we use `X-Content-Size` as an indicator.
// For stream requests with known bodies, the browser will still set Content-Length to 0, unfortunately.
let size = req
.headers()
.get("X-Content-Size")
.and_then(|s| s.to_str().ok())
.and_then(|s| s.parse::<u64>().ok());
let content_type = req
.headers()
.get("Content-Type")
.and_then(|s| s.to_str().ok())
.map(|s| s.to_string());
Ok(FileStream {
data: None,
name: filename,
content_type,
size,
client_body: None,
#[cfg(feature = "server")]
server_body: Some(req.into_body().into_data_stream()),
})
}
}
}
impl FromResponse for FileStream {
fn from_response(res: ClientResponse) -> impl Future<Output = Result<Self, ServerFnError>> {
async move {
// Extract filename from Content-Disposition header if present.
let name = res
.headers()
.get("Content-Disposition")
.and_then(|h| h.to_str().ok())
.and_then(|dis| {
let cd = content_disposition::parse_content_disposition(dis);
cd.filename().map(|(name, _)| name.to_string())
})
.unwrap_or_else(|| "file".to_string());
// Extract content type header
let content_type = res
.headers()
.get("Content-Type")
.and_then(|h| h.to_str().ok())
.map(|s| s.to_string());
// Prefer the response's known content length but fall back to X-Content-Size header.
let size = res.content_length().or_else(|| {
res.headers()
.get("X-Content-Size")
.and_then(|h| h.to_str().ok())
.and_then(|s| s.parse::<u64>().ok())
});
Ok(Self {
data: None,
name,
size,
content_type,
client_body: Some(Box::pin(res.bytes_stream())),
#[cfg(feature = "server")]
server_body: None,
})
}
}
}
#[cfg(feature = "server")]
impl IntoResponse for FileStream {
fn into_response(self) -> axum::response::Response {
use axum::body::Body;
let Some(body) = self.server_body else {
use dioxus_fullstack_core::HttpError;
return HttpError::new(http::StatusCode::BAD_REQUEST, "FileStream has no body")
.into_response();
};
let mut res = axum::response::Response::new(Body::from_stream(body));
// Set relevant headers if available
if let Some(content_type) = &self.content_type {
res.headers_mut()
.insert("Content-Type", content_type.parse().unwrap());
}
if let Some(size) = self.size {
res.headers_mut()
.insert("Content-Length", size.to_string().parse().unwrap());
}
res.headers_mut().insert(
"Content-Disposition",
format!("attachment; filename=\"{}\"", self.name)
.parse()
.unwrap(),
);
res
}
}
impl From<FileData> for FileStream {
fn from(value: FileData) -> Self {
Self {
name: value.name().to_string(),
content_type: value.content_type().map(|s| s.to_string()),
size: Some(value.size()),
data: Some(value),
client_body: None,
#[cfg(feature = "server")]
server_body: None,
}
}
}
impl Stream for FileStream {
type Item = Result<Bytes, StreamingError>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
// For server-side builds, poll the server_body stream if it exists.
#[cfg(feature = "server")]
if let Some(body) = self.server_body.as_mut() {
return Pin::new(body)
.poll_next(cx)
.map_err(|_| StreamingError::Failed);
}
// For client-side builds, poll the client_body stream if it exists.
if let Some(body) = self.client_body.as_mut() {
return body.as_mut().poll_next(cx);
}
// Otherwise, the stream is exhausted.
Poll::Ready(None)
}
}
impl std::fmt::Debug for FileStream {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FileStream")
.field("name", &self.name)
.field("size", &self.size)
.field("content_type", &self.content_type)
.finish()
}
}
+12
View File
@@ -0,0 +1,12 @@
use super::*;
pub use axum::extract::Form;
impl<T> IntoRequest for Form<T>
where
T: Serialize + 'static,
{
fn into_request(self, req: ClientRequest) -> impl Future<Output = ClientResult> + 'static {
async move { req.send_form(&self.0).await }
}
}
+66
View File
@@ -0,0 +1,66 @@
use super::*;
pub use headers::Cookie;
pub use headers::SetCookie;
#[derive(Clone, Debug)]
pub struct SetHeader<Data> {
data: Option<Data>,
}
impl<T: Header> SetHeader<T> {
pub fn new(
value: impl TryInto<HeaderValue, Error = InvalidHeaderValue>,
) -> Result<Self, headers::Error> {
let values = value.try_into().map_err(|_| headers::Error::invalid())?;
let res = T::decode(&mut std::iter::once(&values))?;
Ok(Self { data: Some(res) })
}
}
impl<T: Header> IntoResponseParts for SetHeader<T> {
type Error = ();
fn into_response_parts(self, res: ResponseParts) -> Result<ResponseParts, Self::Error> {
let data = self.data.expect("SetHeader must have data to set");
let mut headers = vec![];
data.encode(&mut headers);
Ok(axum::response::AppendHeaders(
headers.into_iter().map(|value| (T::name().clone(), value)),
)
.into_response_parts(res)
.unwrap())
}
}
impl<T: Header> FromResponseParts for SetHeader<T> {
fn from_response_parts(parts: &mut axum::http::response::Parts) -> Result<Self, ServerFnError> {
let Some(header) = parts.headers.remove(T::name()) else {
return Ok(SetHeader { data: None });
};
let data = T::decode(&mut std::iter::once(&header))
.map_err(|_| ServerFnError::Deserialization("Failed to decode header".into()))?;
Ok(SetHeader { data: Some(data) })
}
}
impl<T: Header> IntoResponse for SetHeader<T> {
fn into_response(self) -> axum::response::Response {
let mut values = vec![];
self.data.unwrap().encode(&mut values);
let mut response = axum::response::Response::builder();
for value in values {
response = response.header(T::name(), value);
}
response.body(axum_core::body::Body::empty()).unwrap()
}
}
+186
View File
@@ -0,0 +1,186 @@
#![forbid(unsafe_code)]
use axum::{
body::{Body, Bytes},
extract::{FromRequest, Request},
http::{header::HeaderValue, StatusCode},
response::{IntoResponse, Response},
};
use axum::{extract::rejection::BytesRejection, http, BoxError};
use derive_more::{Deref, DerefMut, From};
use serde::{de::DeserializeOwned, Serialize};
/// MessagePack Extractor / Response.
///
/// When used as an extractor, it can deserialize request bodies into some type that
/// implements [`serde::Deserialize`]. If the request body cannot be parsed, or value of the
/// `Content-Type` header does not match any of the `application/msgpack`, `application/x-msgpack`
/// or `application/*+msgpack` it will reject the request and return a `400 Bad Request` response.
///
/// When used as a response, it can serialize any type that implements [`serde::Serialize`] to
/// `MsgPack`, and will automatically set `Content-Type: application/msgpack` header.
#[derive(Debug, Clone, Copy, Default, Deref, DerefMut, From)]
pub struct MsgPack<T>(pub T);
impl<T, S> FromRequest<S> for MsgPack<T>
where
T: DeserializeOwned,
S: Send + Sync,
{
type Rejection = MsgPackRejection;
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
if !message_pack_content_type(&req) {
return Err(MsgPackRejection::MissingMsgPackContentType);
}
let bytes = Bytes::from_request(req, state).await?;
let value = rmp_serde::from_slice(&bytes)
.map_err(|e| MsgPackRejection::InvalidMsgPackBody(e.into()))?;
Ok(MsgPack(value))
}
}
impl<T> IntoResponse for MsgPack<T>
where
T: Serialize,
{
fn into_response(self) -> Response {
let bytes = match rmp_serde::encode::to_vec_named(&self.0) {
Ok(res) => res,
Err(err) => {
return Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.header("Content-Type", "text/plain")
.body(Body::new(err.to_string()))
.unwrap();
}
};
let mut res = bytes.into_response();
res.headers_mut().insert(
"Content-Type",
HeaderValue::from_static("application/msgpack"),
);
res
}
}
/// MessagePack Extractor / Response.
///
/// When used as an extractor, it can deserialize request bodies into some type that
/// implements [`serde::Deserialize`]. If the request body cannot be parsed, or value of the
/// `Content-Type` header does not match any of the `application/msgpack`, `application/x-msgpack`
/// or `application/*+msgpack` it will reject the request and return a `400 Bad Request` response.
#[derive(Debug, Clone, Copy, Default, Deref, DerefMut, From)]
pub struct MsgPackRaw<T>(pub T);
impl<T, S> FromRequest<S> for MsgPackRaw<T>
where
T: DeserializeOwned,
S: Send + Sync,
{
type Rejection = MsgPackRejection;
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
if !message_pack_content_type(&req) {
return Err(MsgPackRejection::MissingMsgPackContentType);
}
let bytes = Bytes::from_request(req, state).await?;
let value = rmp_serde::from_slice(&bytes)
.map_err(|e| MsgPackRejection::InvalidMsgPackBody(e.into()))?;
Ok(MsgPackRaw(value))
}
}
impl<T> IntoResponse for MsgPackRaw<T>
where
T: Serialize,
{
fn into_response(self) -> Response {
let bytes = match rmp_serde::encode::to_vec(&self.0) {
Ok(res) => res,
Err(err) => {
return Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.header("Content-Type", "text/plain")
.body(Body::new(err.to_string()))
.unwrap();
}
};
let mut res = bytes.into_response();
res.headers_mut().insert(
"Content-Type",
HeaderValue::from_static("application/msgpack"),
);
res
}
}
fn message_pack_content_type<B>(req: &Request<B>) -> bool {
let Some(content_type) = req.headers().get("Content-Type") else {
return false;
};
let Ok(content_type) = content_type.to_str() else {
return false;
};
match content_type {
"application/msgpack" => true,
"application/x-msgpack" => true,
ct if ct.starts_with("application/") && ct.ends_with("+msgpack") => true,
_ => false,
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum MsgPackRejection {
#[error("Failed to parse the request body as MsgPack: {0}")]
InvalidMsgPackBody(BoxError),
#[error("Expected request with `Content-Type: application/msgpack`")]
MissingMsgPackContentType,
#[error("Cannot have two request body extractors for a single handler")]
BodyAlreadyExtracted,
#[error(transparent)]
BytesRejection(#[from] BytesRejection),
}
impl IntoResponse for MsgPackRejection {
fn into_response(self) -> Response {
match self {
Self::InvalidMsgPackBody(inner) => {
let mut res = Response::new(Body::from(format!(
"Failed to parse the request body as MsgPack: {}",
inner
)));
*res.status_mut() = http::StatusCode::BAD_REQUEST;
res
}
Self::MissingMsgPackContentType => {
let mut res = Response::new(Body::from(
"Expected request with `Content-Type: application/msgpack`",
));
*res.status_mut() = http::StatusCode::BAD_REQUEST;
res
}
Self::BodyAlreadyExtracted => {
let mut res = Response::new(Body::from(
"Cannot have two request body extractors for a single handler",
));
*res.status_mut() = http::StatusCode::INTERNAL_SERVER_ERROR;
res
}
Self::BytesRejection(inner) => inner.into_response(),
}
}
}
@@ -0,0 +1,165 @@
#![allow(unreachable_code)]
use crate::{ClientRequest, ClientResponse, IntoRequest};
use axum::{
extract::{FromRequest, Request},
response::{IntoResponse, Response},
};
use dioxus_fullstack_core::RequestError;
use dioxus_html::{FormData, FormEvent};
use std::{prelude::rust_2024::Future, rc::Rc};
#[cfg(feature = "server")]
use axum::extract::multipart::{Field, MultipartError};
/// A streaming multipart form data handler.
///
/// This type makes it easy to send and receive multipart form data in a streaming fashion by directly
/// leveraging the corresponding `dioxus_html::FormData` and `axum::extract::Multipart` types.
///
/// On the client, you can create a `MultipartFormData` instance by using `.into()` on a `FormData` instance.
/// This is typically done by using the `FormEvent`'s `.data()` method.
///
/// On the server, you can extract a `MultipartFormData` instance by using it as an extractor in your handler function.
/// This gives you access to axum's `Multipart` extractor, which allows you to handle the various fields
/// and files in the multipart form data.
///
/// ## Axum Usage
///
/// Extractor that parses `multipart/form-data` requests (commonly used with file uploads).
///
/// ⚠️ Since extracting multipart form data from the request requires consuming the body, the
/// `Multipart` extractor must be *last* if there are multiple extractors in a handler.
/// See ["the order of extractors"][order-of-extractors]
///
/// [order-of-extractors]: crate::extract#the-order-of-extractors
///
/// # Large Files
///
/// For security reasons, by default, `Multipart` limits the request body size to 2MB.
/// See [`DefaultBodyLimit`][default-body-limit] for how to configure this limit.
///
/// [default-body-limit]: crate::extract::DefaultBodyLimit
pub struct MultipartFormData<T = ()> {
#[cfg(feature = "server")]
form: Option<axum::extract::Multipart>,
_client: Option<Rc<FormData>>,
_phantom: std::marker::PhantomData<T>,
}
impl MultipartFormData {
#[cfg(feature = "server")]
pub async fn next_field(&mut self) -> Result<Option<Field<'_>>, MultipartError> {
if let Some(form) = &mut self.form {
form.next_field().await
} else {
Ok(None)
}
}
}
impl<S> IntoRequest for MultipartFormData<S> {
fn into_request(
self,
_req: ClientRequest,
) -> impl Future<Output = Result<ClientResponse, RequestError>> + 'static {
async move {
// On the web, it's just easier to convert the form data into a blob and then send that
// blob as the body of the request. This handles setting the correct headers, wiring
// up file uploads as streams, and encoding the request.
#[cfg(feature = "web")]
if cfg!(target_arch = "wasm32") {
let data = self._client.clone().ok_or_else(|| {
RequestError::Builder("Failed to get FormData from event".into())
})?;
fn get_form_data(data: Rc<FormData>) -> Option<wasm_bindgen::JsValue> {
use wasm_bindgen::JsCast;
let event: &web_sys::Event = data.downcast()?;
let target = event.target()?;
let form: &web_sys::HtmlFormElement = target.dyn_ref()?;
let data: web_sys::FormData = web_sys::FormData::new_with_form(form).ok()?;
Some(data.into())
}
let js_form_data = get_form_data(data).ok_or_else(|| {
RequestError::Builder("Failed to get FormData from event".into())
})?;
return _req.send_js_value(js_form_data).await;
}
// On non-web platforms, we actually need to read the values out of the FormData
// and construct a multipart form body manually.
#[cfg(not(target_arch = "wasm32"))]
{
todo!("Properly implement multipart form data for non-web wasm32 clients");
}
unimplemented!("Non web wasm32 clients are not supported yet")
}
}
}
impl<S: Send + Sync + 'static, D> FromRequest<S> for MultipartFormData<D> {
type Rejection = Response;
#[doc = " Perform the extraction."]
fn from_request(
req: Request,
state: &S,
) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
#[cfg(feature = "server")]
return async move {
let form = axum::extract::multipart::Multipart::from_request(req, state)
.await
.map_err(|err| err.into_response())?;
Ok(MultipartFormData {
form: Some(form),
_client: None,
_phantom: std::marker::PhantomData,
})
};
#[cfg(not(feature = "server"))]
async {
use dioxus_fullstack_core::HttpError;
let _ = req;
let _ = state;
Err(HttpError::new(
http::StatusCode::INTERNAL_SERVER_ERROR,
"MultipartFormData extractor is not supported on non-server builds",
)
.into_response())
}
}
}
impl<T> From<Rc<FormData>> for MultipartFormData<T> {
fn from(_value: Rc<FormData>) -> Self {
MultipartFormData {
#[cfg(feature = "server")]
form: None,
_client: Some(_value),
_phantom: std::marker::PhantomData,
}
}
}
impl<T> From<FormEvent> for MultipartFormData<T> {
fn from(event: FormEvent) -> Self {
let data = event.data();
MultipartFormData {
#[cfg(feature = "server")]
form: None,
_client: Some(data),
_phantom: std::marker::PhantomData,
}
}
}
unsafe impl Send for MultipartFormData {}
unsafe impl Sync for MultipartFormData {}
+117
View File
@@ -0,0 +1,117 @@
use axum::{
body::Bytes,
extract::{rejection::BytesRejection, FromRequest},
http::{header, HeaderMap, StatusCode},
response::{IntoResponse, Response},
};
use postcard::{from_bytes, to_allocvec};
use serde::{de::DeserializeOwned, Serialize};
use std::future::Future;
/// Postcard Extractor / Response.
///
/// When used as an extractor, it can deserialize request bodies into some type that
/// implements [`serde::Deserialize`]. The request will be rejected (and a [`PostcardRejection`] will
/// be returned) if:
///
/// - The request doesn't have a `Content-Type: application/postcard` (or similar) header.
/// - The body doesn't contain syntactically valid Postcard.
/// - The body contains syntactically valid Postcard but it couldn't be deserialized into the target type.
/// - Buffering the request body fails.
///
/// ⚠️ Since parsing Postcard requires consuming the request body, the `Postcard` extractor must be
/// *last* if there are multiple extractors in a handler.
/// See ["the order of extractors"][order-of-extractors]
///
/// [order-of-extractors]: crate::extract#the-order-of-extractors
pub struct Postcard<T>(pub T);
#[derive(thiserror::Error, Debug)]
pub enum PostcardRejection {
#[error("Expected request with `Content-Type: application/postcard`")]
MissingPostcardContentType,
#[error(transparent)]
PostcardError(#[from] postcard::Error),
#[error(transparent)]
Bytes(#[from] BytesRejection),
}
impl IntoResponse for PostcardRejection {
fn into_response(self) -> Response {
use PostcardRejection::*;
// its often easiest to implement `IntoResponse` by calling other implementations
match self {
MissingPostcardContentType => {
(StatusCode::UNSUPPORTED_MEDIA_TYPE, self.to_string()).into_response()
}
PostcardError(err) => (StatusCode::BAD_REQUEST, err.to_string()).into_response(),
_ => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response(),
}
}
}
impl<T, S> FromRequest<S> for Postcard<T>
where
T: DeserializeOwned,
S: Send + Sync,
{
type Rejection = PostcardRejection;
fn from_request(
req: axum::extract::Request,
state: &S,
) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
async move {
if postcard_content_type(req.headers()) {
let bytes = Bytes::from_request(req, state).await?;
let value = match from_bytes(&bytes) {
Ok(value) => value,
Err(err) => return Err(PostcardRejection::PostcardError(err)),
};
Ok(Postcard(value))
} else {
Err(PostcardRejection::MissingPostcardContentType)
}
}
}
}
fn postcard_content_type(headers: &HeaderMap) -> bool {
let content_type = if let Some(content_type) = headers.get(header::CONTENT_TYPE) {
content_type
} else {
return false;
};
let content_type = if let Ok(content_type) = content_type.to_str() {
content_type
} else {
return false;
};
let mime = if let Ok(mime) = content_type.parse::<mime::Mime>() {
mime
} else {
return false;
};
let is_postcard_content_type = mime.type_() == "application"
&& (mime.subtype() == "postcard" || mime.suffix().is_some_and(|name| name == "postcard"));
is_postcard_content_type
}
impl<T> IntoResponse for Postcard<T>
where
T: Serialize,
{
fn into_response(self) -> Response {
// TODO: maybe use 128 bytes cause serde is doing something like that
match to_allocvec(&self.0) {
Ok(value) => ([(header::CONTENT_TYPE, "application/postcard")], value).into_response(),
Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response(),
}
}
}
Whitespace-only changes.
+422
View File
@@ -0,0 +1,422 @@
use crate::{ClientResponse, FromResponse, RequestError, ServerFnError};
#[cfg(feature = "server")]
use axum::{
response::sse::{Event, KeepAlive},
BoxError,
};
use futures::io::AsyncBufReadExt;
use futures::Stream;
use futures::{StreamExt, TryStreamExt};
use http::{header::CONTENT_TYPE, HeaderValue, StatusCode};
use serde::de::DeserializeOwned;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::Duration;
/// A stream of Server-Sent Events (SSE) that can be used to receive events from the server.
///
/// This type implements `Stream` for asynchronous iteration over events.
/// Events are automatically deserialized from JSON to the specified type `T`.
#[allow(clippy::type_complexity)]
pub struct ServerEvents<T> {
_marker: std::marker::PhantomData<fn() -> T>,
// The receiving end from the server
client: Option<Pin<Box<dyn Stream<Item = Result<ServerSentEvent, ServerFnError>>>>>,
#[cfg(feature = "server")]
keep_alive: Option<KeepAlive>,
// The actual SSE response to send to the client
#[cfg(feature = "server")]
sse: Option<axum::response::Sse<Pin<Box<dyn Stream<Item = Result<Event, BoxError>> + Send>>>>,
}
impl<T: DeserializeOwned> ServerEvents<T> {
/// Receives the next event from the stream, deserializing it to `T`.
///
/// Returns `None` if the stream has ended.
pub async fn recv(&mut self) -> Option<Result<T, ServerFnError>> {
let event = self.next_event().await?;
match event {
Ok(event) => {
let data: Result<T, ServerFnError> =
serde_json::from_str(&event.data).map_err(|err| {
ServerFnError::Serialization(format!(
"failed to deserialize event data: {}",
err
))
});
Some(data)
}
Err(err) => Some(Err(err)),
}
}
}
impl<T> ServerEvents<T> {
/// Receives the next raw event from the stream.
///
/// Returns `None` if the stream has ended.
pub async fn next_event(&mut self) -> Option<Result<ServerSentEvent, ServerFnError>> {
self.client.as_mut()?.next().await
}
}
impl<T: DeserializeOwned> Stream for ServerEvents<T> {
type Item = Result<T, ServerFnError>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let Some(client) = self.client.as_mut() else {
return Poll::Ready(None);
};
match client.as_mut().poll_next(cx) {
Poll::Ready(Some(Ok(event))) => {
let data = serde_json::from_str(&event.data).map_err(|err| {
ServerFnError::Serialization(format!(
"failed to deserialize event data: {}",
err
))
});
Poll::Ready(Some(data))
}
Poll::Ready(Some(Err(err))) => Poll::Ready(Some(Err(err))),
Poll::Ready(None) => Poll::Ready(None),
Poll::Pending => Poll::Pending,
}
}
}
impl<T> FromResponse for ServerEvents<T> {
async fn from_response(res: ClientResponse) -> Result<Self, ServerFnError> {
let status = res.status();
if status != StatusCode::OK {
return Err(ServerFnError::Request(RequestError::Status(
format!("Expected status 200 OK, got {}", status),
status.as_u16(),
)));
}
let content_type = res.headers().get(CONTENT_TYPE);
if content_type != Some(&HeaderValue::from_static(mime::TEXT_EVENT_STREAM.as_ref())) {
return Err(ServerFnError::Request(RequestError::Request(format!(
"Expected content type 'text/event-stream', got {:?}",
content_type
))));
}
let mut stream = res
.bytes_stream()
.map(|result| result.map_err(std::io::Error::other))
.into_async_read();
let mut line_buffer = String::new();
let mut event_buffer = EventBuffer::new();
let stream: Pin<Box<dyn Stream<Item = Result<ServerSentEvent, ServerFnError>>>> = Box::pin(
async_stream::try_stream! {
loop {
line_buffer.clear();
if stream.read_line(&mut line_buffer).await.map_err(|err| ServerFnError::StreamError(err.to_string()))? == 0 {
break;
}
let line = if let Some(line) = line_buffer.strip_suffix('\n') {
line
} else {
&line_buffer
};
// dispatch
if line.is_empty() {
if let Some(event) = event_buffer.produce_event() {
yield event;
}
continue;
}
// Parse line to split field name and value, applying proper trimming.
let (field, value) = line.split_once(':').unwrap_or((line, ""));
let value = value.strip_prefix(' ').unwrap_or(value);
// Handle fields - these are the in SSE speci.
match field {
"event" => event_buffer.set_event_type(value),
"data" => event_buffer.push_data(value),
"id" => event_buffer.set_id(value),
"retry" => {
if let Ok(millis) = value.parse() {
event_buffer.set_retry(Duration::from_millis(millis));
}
}
_ => {}
}
}
},
);
Ok(Self {
_marker: std::marker::PhantomData,
client: Some(stream),
#[cfg(feature = "server")]
keep_alive: None,
#[cfg(feature = "server")]
sse: None,
})
}
}
/// Server-Sent Event representation.
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct ServerSentEvent {
/// A string identifying the type of event described.
pub event_type: String,
/// The data field for the message.
pub data: String,
/// Last event ID value.
pub last_event_id: Option<String>,
/// Reconnection time.
pub retry: Option<Duration>,
}
/// Internal buffer used to accumulate lines of an SSE (Server-Sent Events) stream.
struct EventBuffer {
event_type: String,
data: String,
last_event_id: Option<String>,
retry: Option<Duration>,
}
impl EventBuffer {
/// Creates fresh new [`EventBuffer`].
#[allow(clippy::new_without_default)]
fn new() -> Self {
Self {
event_type: String::new(),
data: String::new(),
last_event_id: None,
retry: None,
}
}
/// Produces a [`Event`], if current state allow it.
///
/// Reset the internal state to process further data.
fn produce_event(&mut self) -> Option<ServerSentEvent> {
let event = if self.data.is_empty() {
None
} else {
Some(ServerSentEvent {
event_type: if self.event_type.is_empty() {
"message".to_string()
} else {
self.event_type.clone()
},
data: self.data.to_string(),
last_event_id: self.last_event_id.clone(),
retry: self.retry,
})
};
self.event_type.clear();
self.data.clear();
event
}
/// Set the [`Event`]'s type. Override previous value.
fn set_event_type(&mut self, event_type: &str) {
self.event_type.clear();
self.event_type.push_str(event_type);
}
/// Extends internal data with given data.
fn push_data(&mut self, data: &str) {
if !self.data.is_empty() {
self.data.push('\n');
}
self.data.push_str(data);
}
fn set_id(&mut self, id: &str) {
self.last_event_id = Some(id.to_string());
}
fn set_retry(&mut self, retry: Duration) {
self.retry = Some(retry);
}
}
#[cfg(feature = "server")]
pub use server_impl::*;
#[cfg(feature = "server")]
mod server_impl {
use super::*;
use crate::spawn_platform;
use axum::response::sse::Sse;
use axum_core::response::IntoResponse;
use futures::Future;
use futures::SinkExt;
use futures::{Sink, TryStream};
use serde::Serialize;
impl<T: 'static> ServerEvents<T> {
/// Create a `ServerEvents` from a function that is given a sender to send events to the client.
///
/// By default, we send a comment every 15 seconds to keep the connection alive.
pub fn new<F, R>(f: impl FnOnce(SseTx<T>) -> F + Send + 'static) -> Self
where
F: Future<Output = R> + 'static,
R: 'static + Send,
{
let (tx, mut rx) = futures_channel::mpsc::unbounded();
let tx = SseTx {
sender: tx,
_marker: std::marker::PhantomData,
};
// Spawn the user function in the background
spawn_platform(move || f(tx));
// Create the stream of events, mapping the incoming events to `Ok`
// If the user function ends, the stream will end and the connection will be closed
let stream = futures::stream::poll_fn(move |cx| match rx.poll_next_unpin(cx) {
std::task::Poll::Ready(Some(event)) => std::task::Poll::Ready(Some(
Ok(event) as Result<axum::response::sse::Event, BoxError>
)),
std::task::Poll::Ready(None) => std::task::Poll::Ready(None),
std::task::Poll::Pending => std::task::Poll::Pending,
});
let sse = Sse::new(stream.boxed());
Self {
_marker: std::marker::PhantomData,
client: None,
keep_alive: Some(KeepAlive::new().interval(Duration::from_secs(15))),
sse: Some(sse),
}
}
/// Create a `ServerEvents` from a `TryStream` of events.
pub fn from_stream<S>(stream: S) -> Self
where
S: TryStream<Ok = T, Error = BoxError> + Send + 'static,
T: Serialize,
{
let stream = stream.map_ok(|event| {
axum::response::sse::Event::default()
.json_data(event)
.expect("Failed to serialize SSE event")
});
let sse = axum::response::Sse::new(stream.boxed());
Self {
_marker: std::marker::PhantomData,
client: None,
keep_alive: Some(KeepAlive::new().interval(Duration::from_secs(15))),
sse: Some(sse),
}
}
/// Set the keep-alive configuration for the SSE connection.
///
/// A `None` value will disable the default `KeepAlive` of 15 seconds.
pub fn with_keep_alive(mut self, keep_alive: Option<KeepAlive>) -> Self {
self.keep_alive = keep_alive;
self
}
/// Create a `ServerEvents` from an existing Axum `Sse` response.
#[allow(clippy::type_complexity)]
pub fn from_sse(
sse: Sse<Pin<Box<dyn Stream<Item = Result<Event, BoxError>> + Send>>>,
) -> Self {
Self {
_marker: std::marker::PhantomData,
client: None,
keep_alive: None,
sse: Some(sse),
}
}
}
impl<T> IntoResponse for ServerEvents<T> {
fn into_response(self) -> axum_core::response::Response {
let mut sse = self
.sse
.expect("SSE should be initialized before using it as a response");
if let Some(keep_alive) = self.keep_alive {
sse = sse.keep_alive(keep_alive)
}
sse.into_response()
}
}
/// A transmitter for sending events to the SSE stream.
pub struct SseTx<T> {
sender: futures_channel::mpsc::UnboundedSender<axum::response::sse::Event>,
_marker: std::marker::PhantomData<fn() -> T>,
}
impl<T: Serialize> SseTx<T> {
/// Sends an event to the SSE stream.
pub async fn send(&mut self, event: T) -> anyhow::Result<()> {
let event = axum::response::sse::Event::default().json_data(event)?;
self.sender.unbounded_send(event)?;
Ok(())
}
}
impl<T> std::ops::Deref for SseTx<T> {
type Target = futures_channel::mpsc::UnboundedSender<axum::response::sse::Event>;
fn deref(&self) -> &Self::Target {
&self.sender
}
}
impl<T> std::ops::DerefMut for SseTx<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.sender
}
}
impl<T: Serialize> Sink<T> for SseTx<T> {
type Error = anyhow::Error;
fn poll_ready(
mut self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<Result<(), Self::Error>> {
self.sender.poll_ready_unpin(_cx).map_err(|e| e.into())
}
fn start_send(mut self: Pin<&mut Self>, item: T) -> Result<(), Self::Error> {
let event = axum::response::sse::Event::default().json_data(item)?;
self.sender.start_send(event).map_err(|e| e.into())
}
fn poll_flush(
mut self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<Result<(), Self::Error>> {
self.sender.poll_flush_unpin(_cx).map_err(|e| e.into())
}
fn poll_close(
mut self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<Result<(), Self::Error>> {
self.sender.poll_close_unpin(_cx).map_err(|e| e.into())
}
}
}
+405
View File
@@ -0,0 +1,405 @@
#![allow(clippy::type_complexity)]
use crate::{
CborEncoding, ClientRequest, ClientResponse, Encoding, FromResponse, IntoRequest, JsonEncoding,
ServerFnError,
};
use axum::extract::{FromRequest, Request};
use axum_core::response::IntoResponse;
use bytes::Bytes;
use dioxus_fullstack_core::{HttpError, RequestError};
use futures::{Stream, StreamExt};
use headers::{ContentType, Header};
use send_wrapper::SendWrapper;
use serde::{de::DeserializeOwned, Serialize};
use std::{future::Future, marker::PhantomData, pin::Pin};
pub type TextStream = Streaming<String>;
pub type ByteStream = Streaming<Bytes>;
pub type JsonStream<T> = Streaming<T, JsonEncoding>;
pub type CborStream<T> = Streaming<T, CborEncoding>;
/// A streaming payload.
///
/// ## Browser Support for Streaming Input
///
/// Browser fetch requests do not currently support full request duplexing, which
/// means that that they do not begin handling responses until the full request has been sent.
///
/// This means that if you use a streaming input encoding, the input stream needs to
/// end before the output will begin.
///
/// Streaming requests are only allowed over HTTP2 or HTTP3.
///
/// Also note that not all browsers support streaming bodies to servers.
pub struct Streaming<T = String, E = ()> {
output_stream: Pin<Box<dyn Stream<Item = Result<T, StreamingError>> + Send>>,
input_stream: Pin<Box<dyn Stream<Item = Result<T, StreamingError>> + Send>>,
encoding: PhantomData<E>,
}
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq, Hash)]
pub enum StreamingError {
/// The streaming request was interrupted and could not be completed.
#[error("The streaming request was interrupted")]
Interrupted,
/// The stream failed to decode a chunk - possibly due to invalid data or version mismatch.
#[error("The stream failed to decode a chunk")]
Decoding,
/// The stream failed to connect or encountered an error.
#[error("The streaming request failed")]
Failed,
}
impl<T: 'static + Send, E> Streaming<T, E> {
/// Creates a new stream from the given stream.
pub fn new(value: impl Stream<Item = T> + Send + 'static) -> Self {
// Box and pin the incoming stream and store as a trait object
Self {
output_stream: Box::pin(futures::stream::empty()) as _,
input_stream: Box::pin(value.map(|item| Ok(item)))
as Pin<Box<dyn Stream<Item = Result<T, StreamingError>> + Send>>,
encoding: PhantomData,
}
}
/// Spawns a new task that produces items for the stream.
///
/// The callback is provided an `UnboundedSender` that can be used to send items to the stream.
#[cfg(feature = "server")]
pub fn spawn<F>(
callback: impl FnOnce(futures_channel::mpsc::UnboundedSender<T>) -> F + Send + 'static,
) -> Self
where
F: Future<Output = ()> + 'static,
T: Send,
{
let (tx, rx) = futures_channel::mpsc::unbounded();
crate::spawn_platform(move || callback(tx));
Self::new(rx)
}
/// Returns the next item in the stream, or `None` if the stream has ended.
pub async fn next(&mut self) -> Option<Result<T, StreamingError>> {
self.output_stream.as_mut().next().await
}
/// Consumes the wrapper, returning the inner stream.
pub fn into_inner(self) -> impl Stream<Item = Result<T, StreamingError>> + Send {
self.input_stream
}
}
impl<S, U> From<S> for TextStream
where
S: Stream<Item = U> + Send + 'static,
U: Into<String>,
{
fn from(value: S) -> Self {
Self {
input_stream: Box::pin(value.map(|data| Ok(data.into()))),
output_stream: Box::pin(futures::stream::empty()) as _,
encoding: PhantomData,
}
}
}
impl<S, E> From<S> for ByteStream
where
S: Stream<Item = Result<Bytes, E>> + Send + 'static,
{
fn from(value: S) -> Self {
Self {
input_stream: Box::pin(value.map(|data| data.map_err(|_| StreamingError::Failed))),
output_stream: Box::pin(futures::stream::empty()) as _,
encoding: PhantomData,
}
}
}
impl<T, S, U, E> From<S> for Streaming<T, E>
where
S: Stream<Item = U> + Send + 'static,
U: Into<T>,
T: 'static + Send,
E: Encoding,
{
fn from(value: S) -> Self {
Self {
input_stream: Box::pin(value.map(|data| Ok(data.into()))),
output_stream: Box::pin(futures::stream::empty()) as _,
encoding: PhantomData,
}
}
}
impl IntoResponse for Streaming<String> {
fn into_response(self) -> axum_core::response::Response {
axum::response::Response::builder()
.header("Content-Type", "text/plain; charset=utf-8")
.body(axum::body::Body::from_stream(self.input_stream))
.unwrap()
}
}
impl IntoResponse for Streaming<Bytes> {
fn into_response(self) -> axum_core::response::Response {
axum::response::Response::builder()
.header("Content-Type", "application/octet-stream")
.body(axum::body::Body::from_stream(self.input_stream))
.unwrap()
}
}
impl<T: DeserializeOwned + Serialize + 'static, E: Encoding> IntoResponse for Streaming<T, E> {
fn into_response(self) -> axum_core::response::Response {
let res = self.input_stream.map(|r| match r {
Ok(res) => match E::to_bytes(&res) {
Some(bytes) => Ok(bytes),
None => Err(StreamingError::Failed),
},
Err(_err) => Err(StreamingError::Failed),
});
axum::response::Response::builder()
.header("Content-Type", E::stream_content_type())
.body(axum::body::Body::from_stream(res))
.unwrap()
}
}
impl FromResponse for Streaming<String> {
fn from_response(res: ClientResponse) -> impl Future<Output = Result<Self, ServerFnError>> {
SendWrapper::new(async move {
let client_stream = Box::pin(res.bytes_stream().map(|byte| match byte {
Ok(bytes) => match String::from_utf8(bytes.to_vec()) {
Ok(string) => Ok(string),
Err(_) => Err(StreamingError::Decoding),
},
Err(_) => Err(StreamingError::Failed),
}));
Ok(Self {
output_stream: client_stream,
input_stream: Box::pin(futures::stream::empty()),
encoding: PhantomData,
})
})
}
}
impl FromResponse for Streaming<Bytes> {
fn from_response(res: ClientResponse) -> impl Future<Output = Result<Self, ServerFnError>> {
async move {
let client_stream = Box::pin(SendWrapper::new(res.bytes_stream().map(
|byte| match byte {
Ok(bytes) => Ok(bytes),
Err(_) => Err(StreamingError::Failed),
},
)));
Ok(Self {
output_stream: client_stream,
input_stream: Box::pin(futures::stream::empty()),
encoding: PhantomData,
})
}
}
}
impl<T: DeserializeOwned + Serialize + 'static + Send, E: Encoding> FromResponse
for Streaming<T, E>
{
fn from_response(res: ClientResponse) -> impl Future<Output = Result<Self, ServerFnError>> {
SendWrapper::new(async move {
let client_stream = Box::pin(SendWrapper::new(res.bytes_stream().map(
|byte| match byte {
Ok(bytes) => match E::from_bytes(bytes) {
Some(res) => Ok(res),
None => Err(StreamingError::Decoding),
},
Err(_) => Err(StreamingError::Failed),
},
)));
Ok(Self {
output_stream: client_stream,
input_stream: Box::pin(futures::stream::empty()),
encoding: PhantomData,
})
})
}
}
impl<S> FromRequest<S> for Streaming<String> {
type Rejection = ServerFnError;
fn from_request(
req: Request,
_state: &S,
) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
async move {
let (parts, body) = req.into_parts();
let content_type = parts
.headers
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
if !content_type.starts_with("text/plain") {
HttpError::bad_request("Invalid content type")?;
}
let stream = body.into_data_stream();
Ok(Self {
input_stream: Box::pin(futures::stream::empty()),
output_stream: Box::pin(stream.map(|byte| match byte {
Ok(bytes) => match String::from_utf8(bytes.to_vec()) {
Ok(string) => Ok(string),
Err(_) => Err(StreamingError::Decoding),
},
Err(_) => Err(StreamingError::Failed),
})),
encoding: PhantomData,
})
}
}
}
impl<S> FromRequest<S> for ByteStream {
type Rejection = ServerFnError;
fn from_request(
req: Request,
_state: &S,
) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
async move {
let (parts, body) = req.into_parts();
let content_type = parts
.headers
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
if !content_type.starts_with("application/octet-stream") {
HttpError::bad_request("Invalid content type")?;
}
let stream = body.into_data_stream();
Ok(Self {
input_stream: Box::pin(futures::stream::empty()),
output_stream: Box::pin(stream.map(|byte| match byte {
Ok(bytes) => Ok(bytes),
Err(_) => Err(StreamingError::Failed),
})),
encoding: PhantomData,
})
}
}
}
impl<T: DeserializeOwned + Serialize + 'static + Send, E: Encoding, S> FromRequest<S>
for Streaming<T, E>
{
type Rejection = ServerFnError;
fn from_request(
req: Request,
_state: &S,
) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
async move {
let (parts, body) = req.into_parts();
let content_type = parts
.headers
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
if !content_type.starts_with(E::stream_content_type()) {
HttpError::bad_request("Invalid content type")?;
}
let stream = body.into_data_stream();
Ok(Self {
input_stream: Box::pin(futures::stream::empty()),
output_stream: Box::pin(stream.map(|byte| match byte {
Ok(bytes) => match E::from_bytes(bytes) {
Some(res) => Ok(res),
None => Err(StreamingError::Decoding),
},
Err(_) => Err(StreamingError::Failed),
})),
encoding: PhantomData,
})
}
}
}
impl IntoRequest for Streaming<String> {
fn into_request(
self,
builder: ClientRequest,
) -> impl Future<Output = Result<ClientResponse, RequestError>> + 'static {
async move {
builder
.header("Content-Type", "text/plain; charset=utf-8")?
.send_body_stream(self.input_stream.map(|e| e.map(Bytes::from)))
.await
}
}
}
impl IntoRequest for ByteStream {
fn into_request(
self,
builder: ClientRequest,
) -> impl Future<Output = Result<ClientResponse, RequestError>> + 'static {
async move {
builder
.header(ContentType::name(), "application/octet-stream")?
.send_body_stream(self.input_stream)
.await
}
}
}
impl<T: DeserializeOwned + Serialize + 'static + Send, E: Encoding> IntoRequest
for Streaming<T, E>
{
fn into_request(
self,
builder: ClientRequest,
) -> impl Future<Output = Result<ClientResponse, RequestError>> + 'static {
async move {
builder
.header("Content-Type", E::stream_content_type())?
.send_body_stream(
self.input_stream.map(|r| {
r.and_then(|item| E::to_bytes(&item).ok_or(StreamingError::Failed))
}),
)
.await
}
}
}
impl<T> std::fmt::Debug for Streaming<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("Streaming").finish()
}
}
impl<T, E: Encoding> std::fmt::Debug for Streaming<T, E> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Streaming")
.field("encoding", &std::any::type_name::<E>())
.finish()
}
}
+38
View File
@@ -0,0 +1,38 @@
use crate::{ClientResponse, FromResponse};
use axum_core::response::{IntoResponse, Response};
use dioxus_fullstack_core::ServerFnError;
use send_wrapper::SendWrapper;
use std::future::Future;
/// A simple text response type.
///
/// The `T` parameter can be anything that converts to and from `String`, such as `Rc<str>` or `String`.
///
/// Unlike `Json` or plain `String`, this uses the `text/plain` content type. The `text/plain` header
/// will be set on the request.
pub struct Text<T>(pub T);
impl<T> Text<T> {
/// Create a new text response.
pub fn new(text: T) -> Self {
Self(text)
}
}
impl<T: Into<String>> IntoResponse for Text<T> {
fn into_response(self) -> Response {
Response::builder()
.header("Content-Type", "text/plain; charset=utf-8")
.body(axum_core::body::Body::from(self.0.into()))
.unwrap()
}
}
impl<T: From<String>> FromResponse for Text<T> {
fn from_response(res: ClientResponse) -> impl Future<Output = Result<Self, ServerFnError>> {
SendWrapper::new(async move {
let text = res.text().await?;
Ok(Text::new(text.into()))
})
}
}
File diff suppressed because it is too large. Load diff
+171
View File
@@ -0,0 +1,171 @@
use dioxus_fullstack_core::{RequestError, ServerFnError};
#[cfg(feature = "server")]
use headers::Header;
use http::response::Parts;
use std::{future::Future, pin::Pin};
use crate::{ClientRequest, ClientResponse};
/// The `IntoRequest` trait allows types to be used as the body of a request to a HTTP endpoint or server function.
///
/// `IntoRequest` allows for types handle the calling of `ClientRequest::send` where the result is then
/// passed to `FromResponse` to decode the response.
///
/// You can think of the `IntoRequest` and `FromResponse` traits are "inverse" traits of the axum
/// `FromRequest` and `IntoResponse` traits. Just like a type can be decoded from a request via `FromRequest`,
/// a type can be encoded into a request via `IntoRequest`.
///
/// ## Generic State
///
/// `IntoRequest` is generic over the response type `R` which defaults to `ClientResponse`. The default
/// `ClientResponse` is the base response type that internally wraps `reqwest::Response`.
///
/// However, some responses might need state from the initial request to properly decode the response.
/// Most state can be extended via the `.extension()` method on `ClientRequest`. In some cases, like
/// websockets, the response needs to retain an initial connection from the request. Here, you can use
/// the `R` generic to specify a concrete response type. The resulting type that implements `FromResponse`
/// must also be generic over the same `R` type.
pub trait IntoRequest<R = ClientResponse>: Sized {
fn into_request(
self,
req: ClientRequest,
) -> impl Future<Output = Result<R, RequestError>> + 'static;
}
impl<A, R> IntoRequest<R> for (A,)
where
A: IntoRequest<R> + 'static,
{
fn into_request(
self,
req: ClientRequest,
) -> impl Future<Output = Result<R, RequestError>> + 'static {
A::into_request(self.0, req)
}
}
pub trait FromResponse<R = ClientResponse>: Sized {
fn from_response(res: R) -> impl Future<Output = Result<Self, ServerFnError>>;
}
impl<A> FromResponse for A
where
A: FromResponseParts,
{
fn from_response(res: ClientResponse) -> impl Future<Output = Result<Self, ServerFnError>> {
async move {
let (parts, _body) = res.into_parts();
let mut parts = parts;
A::from_response_parts(&mut parts)
}
}
}
impl<A, B> FromResponse for (A, B)
where
A: FromResponseParts,
B: FromResponse,
{
fn from_response(res: ClientResponse) -> impl Future<Output = Result<Self, ServerFnError>> {
async move {
let mut parts = res.make_parts();
let a = A::from_response_parts(&mut parts)?;
let b = B::from_response(res).await?;
Ok((a, b))
}
}
}
impl<A, B, C> FromResponse for (A, B, C)
where
A: FromResponseParts,
B: FromResponseParts,
C: FromResponse,
{
fn from_response(res: ClientResponse) -> impl Future<Output = Result<Self, ServerFnError>> {
async move {
let mut parts = res.make_parts();
let a = A::from_response_parts(&mut parts)?;
let b = B::from_response_parts(&mut parts)?;
let c = C::from_response(res).await?;
Ok((a, b, c))
}
}
}
pub trait FromResponseParts
where
Self: Sized,
{
fn from_response_parts(parts: &mut Parts) -> Result<Self, ServerFnError>;
}
#[cfg(feature = "server")]
impl<T: Header> FromResponseParts for axum_extra::TypedHeader<T> {
fn from_response_parts(parts: &mut Parts) -> Result<Self, ServerFnError> {
use headers::HeaderMapExt;
let t = parts
.headers
.typed_get::<T>()
.ok_or_else(|| ServerFnError::Serialization("Invalid header value".into()))?;
Ok(axum_extra::TypedHeader(t))
}
}
/*
todo: make the serverfns return ServerFnRequest which lets us control the future better
*/
#[pin_project::pin_project]
#[must_use = "Requests do nothing unless you `.await` them"]
pub struct ServerFnRequest<Output> {
_phantom: std::marker::PhantomData<Output>,
#[pin]
fut: Pin<Box<dyn Future<Output = Output> + Send>>,
}
impl<O> ServerFnRequest<O> {
pub fn new(res: impl Future<Output = O> + Send + 'static) -> Self {
ServerFnRequest {
_phantom: std::marker::PhantomData,
fut: Box::pin(res),
}
}
}
impl<T, E> std::future::Future for ServerFnRequest<Result<T, E>> {
type Output = Result<T, E>;
fn poll(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Self::Output> {
self.project().fut.poll(cx)
}
}
#[doc(hidden)]
#[diagnostic::on_unimplemented(
message = "The return type of a server function must be `Result<T, E>`",
note = "`T` is either `impl IntoResponse` *or* `impl Serialize`",
note = "`E` is either `From<ServerFnError> + Serialize`, `dioxus::Error` or `StatusCode`."
)]
pub trait AssertIsResult {}
impl<T, E> AssertIsResult for Result<T, E> {}
#[doc(hidden)]
pub fn assert_is_result<T: AssertIsResult>() {}
#[diagnostic::on_unimplemented(
message = "The arguments to the server function must either be a single `impl FromRequest + IntoRequest` argument, or multiple `DeserializeOwned` arguments."
)]
pub trait AssertCanEncode {}
pub struct CantEncode;
pub struct EncodeIsVerified;
impl AssertCanEncode for EncodeIsVerified {}
#[doc(hidden)]
pub fn assert_can_encode(_t: impl AssertCanEncode) {}
+31
View File
@@ -0,0 +1,31 @@
use std::future::Future;
/// Spawn a task in the background. If wasm is enabled, this will use the single threaded tokio runtime
pub(crate) fn spawn_platform<Fut>(
f: impl FnOnce() -> Fut + Send + 'static,
) -> tokio::task::JoinHandle<Fut::Output>
where
Fut: Future + 'static,
Fut::Output: Send + 'static,
{
#[cfg(not(target_arch = "wasm32"))]
{
use tokio_util::task::LocalPoolHandle;
static TASK_POOL: std::sync::OnceLock<LocalPoolHandle> = std::sync::OnceLock::new();
let pool = TASK_POOL.get_or_init(|| {
LocalPoolHandle::new(
std::thread::available_parallelism()
.map(usize::from)
.unwrap_or(1),
)
});
pool.spawn_pinned(f)
}
#[cfg(target_arch = "wasm32")]
{
tokio::task::spawn_local(f())
}
}
+344
View File
@@ -0,0 +1,344 @@
#![allow(clippy::manual_async_fn)]
#![allow(unused_variables)]
use anyhow::Result;
use axum::extract::FromRequest;
use axum::response::IntoResponse;
use axum::{response::Html, Json};
use bytes::Bytes;
use dioxus::prelude::*;
use dioxus_fullstack::{get, FileStream, ServerFnError, Text, TextStream, Websocket};
use futures::StreamExt;
use http::HeaderMap;
use http::StatusCode;
use http_body_util::BodyExt;
use serde::{Deserialize, Serialize};
use std::future::Future;
fn main() {}
mod simple_extractors {
use super::*;
/// We can extract the state and return anything thats IntoResponse
#[get("/home")]
async fn one() -> Result<String> {
Ok("hello home".to_string())
}
/// We can extract the path arg and return anything thats IntoResponse
#[get("/home/{id}")]
async fn two(id: String) -> Result<String> {
Ok(format!("hello home {}", id))
}
/// We can do basically nothing
#[get("/")]
async fn three() -> Result<()> {
Ok(())
}
/// We can do basically nothing, with args
#[get("/{one}/{two}?a&b&c")]
async fn four(one: String, two: String, a: String, b: String, c: String) -> Result<()> {
Ok(())
}
/// We can return anything that implements IntoResponse
#[get("/hello")]
async fn five() -> Result<Html<String>> {
Ok(Html("<h1>Hello!</h1>".to_string()))
}
/// We can return anything that implements IntoResponse
#[get("/hello")]
async fn six() -> Result<Json<String>> {
Ok(Json("Hello!".to_string()))
}
/// We can return our own custom `Text<T>` type for sending plain text
#[get("/hello")]
async fn six_2() -> Result<Text<String>> {
Ok(Text("Hello!".to_string()))
}
/// We can return our own custom TextStream type for sending plain text streams
#[get("/hello")]
async fn six_3() -> Result<TextStream> {
Ok(TextStream::new(futures::stream::iter(vec![
"Hello 1".to_string(),
"Hello 2".to_string(),
"Hello 3".to_string(),
])))
}
/// We can return a Result with anything that implements IntoResponse
#[get("/hello")]
async fn seven() -> Result<Bytes> {
Ok(Bytes::from_static(b"Hello!"))
}
/// We can return a Result with anything that implements IntoResponse
#[get("/hello")]
async fn eight() -> Result<Bytes, StatusCode> {
Ok(Bytes::from_static(b"Hello!"))
}
/// We can use the anyhow error type
#[get("/hello")]
async fn nine() -> Result<Bytes> {
Ok(Bytes::from_static(b"Hello!"))
}
/// We can use the ServerFnError error type
#[get("/hello")]
async fn ten() -> Result<Bytes, ServerFnError> {
Ok(Bytes::from_static(b"Hello!"))
}
/// We can use the ServerFnError error type
#[get("/hello")]
async fn elevent() -> Result<Bytes> {
Ok(Bytes::from_static(b"Hello!"))
}
/// We can use multiple args that are Deserialize
#[get("/hello")]
async fn twelve(a: i32, b: i32, c: i32) -> Result<Bytes> {
Ok(format!("Hello! {} {} {}", a, b, c).into())
}
// How should we handle generics? Doesn't make a lot of sense with distributed registration?
// I think we should just not support them for now. Reworking it will be a big change though.
//
// /// We can use generics
// #[get("/hello")]
// async fn thirteen<S: Serialize + DeserializeOwned>(a: S) -> Result<Bytes> {
// Ok(format!("Hello! {}", serde_json::to_string(&a)?).into())
// }
// /// We can use impl-style generics
// #[get("/hello")]
// async fn fourteen(a: impl Serialize + DeserializeOwned) -> Result<Bytes> {
// Ok(format!("Hello! {}", serde_json::to_string(&a)?).into())
// }
}
mod custom_serialize {
use super::*;
#[derive(Serialize, Deserialize)]
struct YourObject {
id: i32,
amount: Option<i32>,
offset: Option<i32>,
}
/// Directly return the object, and it will be serialized to JSON
#[get("/item/{id}?amount&offset")]
async fn get_item1(
id: i32,
amount: Option<i32>,
offset: Option<i32>,
) -> Result<Json<YourObject>> {
Ok(Json(YourObject { id, amount, offset }))
}
#[get("/item/{id}?amount&offset")]
async fn get_item2(
id: i32,
amount: Option<i32>,
offset: Option<i32>,
) -> Result<Json<YourObject>> {
Ok(Json(YourObject { id, amount, offset }))
}
#[get("/item/{id}?amount&offset")]
async fn get_item3(id: i32, amount: Option<i32>, offset: Option<i32>) -> Result<YourObject> {
Ok(YourObject { id, amount, offset })
}
#[get("/item/{id}?amount&offset")]
async fn get_item4(
id: i32,
amount: Option<i32>,
offset: Option<i32>,
) -> Result<YourObject, StatusCode> {
Ok(YourObject { id, amount, offset })
}
}
mod custom_types {
use axum::response::Response;
// use axum_core::response::Response;
use dioxus_fullstack::{
ClientRequest, ClientResponse, FromResponse, IntoRequest, RequestError, WebSocketOptions,
};
use super::*;
/// We can extract the path arg and return anything thats IntoResponse
#[get("/upload/image/")]
async fn streaming_file(body: FileStream) -> Result<Json<i32>> {
unimplemented!()
}
/// We can extract the path arg and return anything thats IntoResponse
#[get("/upload/image/?name&size&ftype")]
async fn streaming_file_args(
name: String,
size: usize,
ftype: String,
body: FileStream,
) -> Result<Json<i32>> {
unimplemented!()
}
#[get("/")]
async fn ws_endpoint(options: WebSocketOptions) -> Result<Websocket<String, String>> {
unimplemented!()
}
struct MyCustomPayload {}
impl FromResponse for MyCustomPayload {
fn from_response(res: ClientResponse) -> impl Future<Output = Result<Self, ServerFnError>> {
async move { Ok(MyCustomPayload {}) }
}
}
impl IntoResponse for MyCustomPayload {
fn into_response(self) -> Response {
unimplemented!()
}
}
impl<T> FromRequest<T> for MyCustomPayload {
type Rejection = ServerFnError;
#[allow(clippy::manual_async_fn)]
fn from_request(
_req: axum::extract::Request,
_state: &T,
) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
async move { Ok(MyCustomPayload {}) }
}
}
impl IntoRequest for MyCustomPayload {
fn into_request(
self,
request_builder: ClientRequest,
) -> impl Future<Output = Result<ClientResponse, RequestError>> + 'static {
async move { unimplemented!() }
}
}
#[get("/myendpoint")]
async fn my_custom_handler1(payload: MyCustomPayload) -> Result<MyCustomPayload> {
Ok(payload)
}
#[get("/myendpoint2")]
async fn my_custom_handler2(payload: MyCustomPayload) -> Result<MyCustomPayload, StatusCode> {
Ok(payload)
}
}
mod overlap {
use super::*;
#[derive(Serialize, Deserialize)]
struct MyCustomPayload {}
impl IntoResponse for MyCustomPayload {
fn into_response(self) -> axum::response::Response {
unimplemented!()
}
}
impl<T> FromRequest<T> for MyCustomPayload {
type Rejection = ServerFnError;
#[allow(clippy::manual_async_fn)]
fn from_request(
_req: axum::extract::Request,
_state: &T,
) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
async move { Ok(MyCustomPayload {}) }
}
}
/// When we have overlapping serialize + IntoResponse impls, the autoref logic will only pick Serialize
/// if IntoResponse is not available. Otherwise, IntoResponse is preferred.
#[get("/myendpoint")]
async fn my_custom_handler3(payload: MyCustomPayload) -> Result<MyCustomPayload, StatusCode> {
Ok(payload)
}
/// Same, but with the anyhow::Error path
#[get("/myendpoint")]
async fn my_custom_handler4(payload: MyCustomPayload) -> Result<MyCustomPayload> {
Ok(payload)
}
}
mod http_ext {
use dioxus::Result;
use super::*;
/// Extract requests directly for full control
#[get("/myendpoint")]
async fn my_custom_handler1(request: axum::extract::Request) -> Result<()> {
let mut data = request.into_data_stream();
while let Some(chunk) = data.next().await {
let _ = chunk.unwrap();
}
Ok(())
}
}
mod input_types {
use super::*;
#[derive(Serialize, Deserialize)]
struct CustomPayload {
name: String,
age: u32,
}
/// We can take `()` as input
#[post("/")]
async fn zero(a: (), b: (), c: ()) -> Result<()> {
Ok(())
}
/// We can take `()` as input in serde types
#[post("/")]
async fn zero_1(a: Json<()>) -> Result<()> {
Ok(())
}
/// We can take regular axum extractors as input
#[post("/")]
async fn one(data: Json<CustomPayload>) -> Result<()> {
Ok(())
}
/// We can take Deserialize types as input, and they will be deserialized from JSON
#[post("/")]
async fn two(name: String, age: u32) -> Result<()> {
Ok(())
}
/// We can take Deserialize types as input, with custom server extensions
#[post("/", headers: HeaderMap)]
async fn three(name: String) -> Result<()> {
Ok(())
}
/// We can take a regular axum-like mix with extractors and Deserialize types
#[post("/", headers: HeaderMap)]
async fn four(data: Json<CustomPayload>) -> Result<()> {
Ok(())
}
/// We can even accept string in the final position.
#[post("/")]
async fn five(age: u32, name: String) -> Result<()> {
Ok(())
}
}