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:
1 parent
8421c3aef0
commit
f2b1c6d97f
424 files changed
+17284
-6624
No files matched your search
@@ -1 +0,0 @@
|
||||
target
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"rust-analyzer.cargo.features": "all",
|
||||
"rust-analyzer.check.features": "all",
|
||||
"rust-analyzer.cargo.extraArgs": [
|
||||
"--tests"
|
||||
],
|
||||
}
|
||||
@@ -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"]
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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()))
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
// }
|
||||
// }
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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::*;
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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.
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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) {}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user