multiple fullstack nits/cleanups before release (#4852)
* feat: `:arg` syntax for serverfn * add better decode/encode verify message * allow catch-all query args * add rebind * implement websocket frames for `Streaming` * fix lazy init * wip... serverfnstate * wip... * wip: serverfns require Send/Sync, let's fix that * wip... * small cleanup * fix hang by actually spawning the tokio task * clippy * fix parameter catchall binding * fix lychee * doc tests
This commit is contained in:
1 parent
43373f0be7
commit
5559e463c8
27 files changed
+1395
-1097
No files matched your search
@@ -129,10 +129,14 @@ impl ClientRequest {
|
||||
}
|
||||
|
||||
/// Creates a new reqwest request builder with the method, url, and headers set from this ClientRequest
|
||||
///
|
||||
/// Using this method attaches `X-Request-Client: dioxus` header to the request.
|
||||
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());
|
||||
let mut req = client
|
||||
.request(self.method.clone(), self.url.clone())
|
||||
.header("X-Request-Client", "dioxus");
|
||||
|
||||
for (key, value) in self.headers.iter() {
|
||||
req = req.header(key, value);
|
||||
@@ -141,6 +145,7 @@ impl ClientRequest {
|
||||
req
|
||||
}
|
||||
|
||||
/// Using this method attaches `X-Request-Client-Dioxus` header to the request.
|
||||
#[cfg(feature = "web")]
|
||||
pub fn new_gloo_request(&self) -> gloo_net::http::RequestBuilder {
|
||||
let mut builder = gloo_net::http::RequestBuilder::new(
|
||||
@@ -155,6 +160,7 @@ impl ClientRequest {
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.header("X-Request-Client", "dioxus")
|
||||
.method(self.method.clone());
|
||||
|
||||
for (key, value) in self.headers.iter() {
|
||||
|
||||
@@ -7,8 +7,13 @@ use serde::{de::DeserializeOwned, Serialize};
|
||||
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>;
|
||||
fn to_bytes(data: impl Serialize) -> Option<Bytes> {
|
||||
let mut buf = Vec::new();
|
||||
Self::encode(data, &mut buf)?;
|
||||
Some(buf.into())
|
||||
}
|
||||
fn encode(data: impl Serialize, buf: &mut Vec<u8>) -> Option<usize>;
|
||||
fn decode<O: DeserializeOwned>(bytes: Bytes) -> Option<O>;
|
||||
}
|
||||
|
||||
pub struct JsonEncoding;
|
||||
@@ -19,11 +24,14 @@ impl Encoding for JsonEncoding {
|
||||
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 encode(data: impl Serialize, mut buf: &mut Vec<u8>) -> Option<usize> {
|
||||
let len = buf.len();
|
||||
serde_json::to_writer(&mut buf, &data).ok()?;
|
||||
Some(buf.len() - len)
|
||||
}
|
||||
|
||||
fn from_bytes<O: DeserializeOwned>(bytes: Bytes) -> Option<O> {
|
||||
fn decode<O: DeserializeOwned>(bytes: Bytes) -> Option<O> {
|
||||
serde_json::from_slice(&bytes).ok()
|
||||
}
|
||||
}
|
||||
@@ -36,14 +44,15 @@ impl Encoding for CborEncoding {
|
||||
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 decode<O: DeserializeOwned>(bytes: Bytes) -> Option<O> {
|
||||
ciborium::de::from_reader(bytes.as_ref()).ok()
|
||||
}
|
||||
|
||||
fn from_bytes<O: DeserializeOwned>(bytes: Bytes) -> Option<O> {
|
||||
ciborium::de::from_reader(bytes.as_ref()).ok()
|
||||
fn encode(data: impl Serialize, mut buf: &mut Vec<u8>) -> Option<usize> {
|
||||
let len = buf.len();
|
||||
ciborium::into_writer(&data, &mut buf).ok()?;
|
||||
Some(buf.len() - len)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,11 +66,14 @@ impl Encoding for PostcardEncoding {
|
||||
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 encode(data: impl Serialize, mut buf: &mut Vec<u8>) -> Option<usize> {
|
||||
let len = buf.len();
|
||||
postcard::to_io(&data, &mut buf).ok()?;
|
||||
Some(buf.len() - len)
|
||||
}
|
||||
|
||||
fn from_bytes<O: DeserializeOwned>(bytes: Bytes) -> Option<O> {
|
||||
fn decode<O: DeserializeOwned>(bytes: Bytes) -> Option<O> {
|
||||
postcard::from_bytes(bytes.as_ref()).ok()
|
||||
}
|
||||
}
|
||||
@@ -76,11 +88,13 @@ impl Encoding for MsgPackEncoding {
|
||||
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 encode(data: impl Serialize, buf: &mut Vec<u8>) -> Option<usize> {
|
||||
let len = buf.len();
|
||||
rmp_serde::encode::write(buf, &data).ok()?;
|
||||
Some(buf.len() - len)
|
||||
}
|
||||
|
||||
fn from_bytes<O: DeserializeOwned>(bytes: Bytes) -> Option<O> {
|
||||
fn decode<O: DeserializeOwned>(bytes: Bytes) -> Option<O> {
|
||||
rmp_serde::from_slice(&bytes).ok()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +88,20 @@ impl<T: Send + Sync + 'static> Lazy<T> {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a reference to the value of the `Lazy` instance. This will block the current thread if the
|
||||
/// value is not yet initialized.
|
||||
pub fn get(&self) -> &T {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Send + Sync + 'static> Default for Lazy<T> {
|
||||
@@ -100,15 +114,13 @@ 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.");
|
||||
};
|
||||
self.get()
|
||||
}
|
||||
}
|
||||
|
||||
if self.value.get().is_none() {
|
||||
self.initialize().expect("Failed to initialize lazy value");
|
||||
}
|
||||
|
||||
self.value.get().unwrap()
|
||||
impl<T: std::fmt::Debug + Send + Sync + 'static> std::fmt::Debug for Lazy<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Lazy").field("value", self.get()).finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,10 +141,16 @@ where
|
||||
{
|
||||
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();
|
||||
return std::thread::spawn(move || {
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap()
|
||||
.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`
|
||||
|
||||
@@ -107,6 +107,9 @@ pub mod payloads {
|
||||
pub mod header;
|
||||
pub use header::*;
|
||||
|
||||
pub mod query;
|
||||
pub use query::*;
|
||||
|
||||
#[cfg(feature = "ws")]
|
||||
pub mod websocket;
|
||||
#[cfg(feature = "ws")]
|
||||
|
||||
@@ -42,7 +42,7 @@ use crate::{
|
||||
use axum::response::IntoResponse;
|
||||
use axum_core::extract::{FromRequest, Request};
|
||||
use bytes::Bytes;
|
||||
use dioxus_fullstack_core::{DioxusServerState, RequestError};
|
||||
use dioxus_fullstack_core::RequestError;
|
||||
use http::StatusCode;
|
||||
use send_wrapper::SendWrapper;
|
||||
use serde::Serialize;
|
||||
@@ -172,7 +172,7 @@ pub mod req_to {
|
||||
impl<T, O, R> EncodeRequest<T, O, R> for &&&&&&&&&ServerFnEncoder<T, O>
|
||||
where
|
||||
T: 'static,
|
||||
O: FromRequest<DioxusServerState> + IntoRequest<R>,
|
||||
O: IntoRequest<R>,
|
||||
{
|
||||
type VerifyEncode = EncodeIsVerified;
|
||||
fn fetch_client(
|
||||
@@ -214,6 +214,8 @@ pub mod req_to {
|
||||
pub use decode_ok::*;
|
||||
mod decode_ok {
|
||||
|
||||
use crate::{CantDecode, DecodeIsVerified};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Convert the reqwest response into the desired type, in place.
|
||||
@@ -222,13 +224,16 @@ mod decode_ok {
|
||||
/// 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> {
|
||||
type VerifyDecode;
|
||||
fn decode_client_response(
|
||||
&self,
|
||||
res: Result<R, RequestError>,
|
||||
) -> impl Future<Output = Result<Result<T, ServerFnError>, RequestError>> + Send;
|
||||
fn verify_can_deserialize(&self) -> Self::VerifyDecode;
|
||||
}
|
||||
|
||||
impl<T: FromResponse<R>, E, R> RequestDecodeResult<T, R> for &&&ServerFnDecoder<Result<T, E>> {
|
||||
type VerifyDecode = DecodeIsVerified;
|
||||
fn decode_client_response(
|
||||
&self,
|
||||
res: Result<R, RequestError>,
|
||||
@@ -240,11 +245,15 @@ mod decode_ok {
|
||||
}
|
||||
})
|
||||
}
|
||||
fn verify_can_deserialize(&self) -> Self::VerifyDecode {
|
||||
DecodeIsVerified
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: DeserializeOwned, E> RequestDecodeResult<T, ClientResponse>
|
||||
for &&ServerFnDecoder<Result<T, E>>
|
||||
{
|
||||
type VerifyDecode = DecodeIsVerified;
|
||||
fn decode_client_response(
|
||||
&self,
|
||||
res: Result<ClientResponse, RequestError>,
|
||||
@@ -304,6 +313,24 @@ mod decode_ok {
|
||||
}
|
||||
})
|
||||
}
|
||||
fn verify_can_deserialize(&self) -> Self::VerifyDecode {
|
||||
DecodeIsVerified
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, R, E> RequestDecodeResult<T, R> for &ServerFnDecoder<Result<T, E>> {
|
||||
type VerifyDecode = CantDecode;
|
||||
|
||||
fn decode_client_response(
|
||||
&self,
|
||||
_res: Result<R, RequestError>,
|
||||
) -> impl Future<Output = Result<Result<T, ServerFnError>, RequestError>> + Send {
|
||||
async move { unimplemented!() }
|
||||
}
|
||||
|
||||
fn verify_can_deserialize(&self) -> Self::VerifyDecode {
|
||||
CantDecode
|
||||
}
|
||||
}
|
||||
|
||||
pub trait RequestDecodeErr<T, E> {
|
||||
@@ -460,33 +487,34 @@ pub use req_from::*;
|
||||
pub mod req_from {
|
||||
use super::*;
|
||||
use axum::{extract::FromRequestParts, response::Response};
|
||||
use dioxus_fullstack_core::FullstackContext;
|
||||
|
||||
pub trait ExtractRequest<In, Out, H, M = ()> {
|
||||
fn extract_axum(
|
||||
&self,
|
||||
state: DioxusServerState,
|
||||
state: FullstackContext,
|
||||
request: Request,
|
||||
map: fn(In) -> Out,
|
||||
) -> impl Future<Output = Result<(H, Out), Response>> + 'static;
|
||||
) -> impl Future<Output = Result<(Out, H), 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,
|
||||
H: FromRequest<FullstackContext, M> + 'static,
|
||||
{
|
||||
fn extract_axum(
|
||||
&self,
|
||||
state: DioxusServerState,
|
||||
state: FullstackContext,
|
||||
request: Request,
|
||||
_map: fn(In) -> (),
|
||||
) -> impl Future<Output = Result<(H, ()), Response>> + 'static {
|
||||
) -> impl Future<Output = Result<((), H), Response>> + 'static {
|
||||
async move {
|
||||
H::from_request(request, &state)
|
||||
.await
|
||||
.map_err(|e| e.into_response())
|
||||
.map(|out| (out, ()))
|
||||
.map(|out| ((), out))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -496,14 +524,14 @@ pub mod req_from {
|
||||
where
|
||||
In: DeserializeOwned + 'static,
|
||||
Out: 'static,
|
||||
H: FromRequestParts<DioxusServerState>,
|
||||
H: FromRequestParts<FullstackContext>,
|
||||
{
|
||||
fn extract_axum(
|
||||
&self,
|
||||
_state: DioxusServerState,
|
||||
_state: FullstackContext,
|
||||
request: Request,
|
||||
map: fn(In) -> Out,
|
||||
) -> impl Future<Output = Result<(H, Out), Response>> + 'static {
|
||||
) -> impl Future<Output = Result<(Out, H), Response>> + 'static {
|
||||
async move {
|
||||
let (mut parts, body) = request.into_parts();
|
||||
let headers = H::from_request_parts(&mut parts, &_state)
|
||||
@@ -525,7 +553,7 @@ pub mod req_from {
|
||||
.map_err(|e| ServerFnError::from(e).into_response())
|
||||
.unwrap();
|
||||
|
||||
Ok((headers, out))
|
||||
Ok((out, headers))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -533,15 +561,15 @@ pub mod req_from {
|
||||
/// 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>,
|
||||
Out: FromRequest<FullstackContext, M> + 'static,
|
||||
H: FromRequestParts<FullstackContext>,
|
||||
{
|
||||
fn extract_axum(
|
||||
&self,
|
||||
state: DioxusServerState,
|
||||
state: FullstackContext,
|
||||
request: Request,
|
||||
_map: fn(In) -> Out,
|
||||
) -> impl Future<Output = Result<(H, Out), Response>> + 'static {
|
||||
) -> impl Future<Output = Result<(Out, H), Response>> + 'static {
|
||||
async move {
|
||||
let (mut parts, body) = request.into_parts();
|
||||
let headers = H::from_request_parts(&mut parts, &state)
|
||||
@@ -554,7 +582,7 @@ pub mod req_from {
|
||||
.await
|
||||
.map_err(|e| e.into_response());
|
||||
|
||||
res.map(|out| (headers, out))
|
||||
res.map(|out| (out, headers))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -616,7 +644,7 @@ mod resp {
|
||||
|
||||
#[allow(clippy::result_large_err)]
|
||||
pub trait MakeAxumError<E> {
|
||||
fn make_axum_error(self, result: Result<Response, E>) -> Result<Response, Response>;
|
||||
fn make_axum_error(self, result: Result<Response, E>) -> Response;
|
||||
}
|
||||
|
||||
/// Get the status code from the error type if possible.
|
||||
@@ -639,9 +667,9 @@ mod resp {
|
||||
where
|
||||
E: AsStatusCode + From<ServerFnError> + Serialize + DeserializeOwned + Display,
|
||||
{
|
||||
fn make_axum_error(self, result: Result<Response, E>) -> Result<Response, Response> {
|
||||
fn make_axum_error(self, result: Result<Response, E>) -> Response {
|
||||
match result {
|
||||
Ok(res) => Ok(res),
|
||||
Ok(res) => res,
|
||||
Err(err) => {
|
||||
let status_code = err.as_status_code();
|
||||
let err = ErrorPayload {
|
||||
@@ -656,19 +684,16 @@ mod resp {
|
||||
HeaderValue::from_static("application/json"),
|
||||
);
|
||||
*resp.status_mut() = status_code;
|
||||
Err(resp)
|
||||
resp
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> MakeAxumError<CapturedError> for &&ServerFnDecoder<Result<T, CapturedError>> {
|
||||
fn make_axum_error(
|
||||
self,
|
||||
result: Result<Response, CapturedError>,
|
||||
) -> Result<Response, Response> {
|
||||
fn make_axum_error(self, result: Result<Response, CapturedError>) -> Response {
|
||||
match result {
|
||||
Ok(res) => Ok(res),
|
||||
Ok(res) => res,
|
||||
|
||||
// Optimize the case where we have sole ownership of the error
|
||||
Err(errr) if errr._strong_count() == 1 => {
|
||||
@@ -721,19 +746,16 @@ mod resp {
|
||||
HeaderValue::from_static("application/json"),
|
||||
);
|
||||
*resp.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
|
||||
Err(resp)
|
||||
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> {
|
||||
fn make_axum_error(self, result: Result<Response, anyhow::Error>) -> Response {
|
||||
match result {
|
||||
Ok(res) => Ok(res),
|
||||
Ok(res) => res,
|
||||
Err(errr) => {
|
||||
// The `WithHttpError` trait emits ServerFnErrors so we can downcast them here
|
||||
// to create richer responses.
|
||||
@@ -775,19 +797,16 @@ mod resp {
|
||||
HeaderValue::from_static("application/json"),
|
||||
);
|
||||
*resp.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
|
||||
Err(resp)
|
||||
resp
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> MakeAxumError<StatusCode> for &&ServerFnDecoder<Result<T, StatusCode>> {
|
||||
fn make_axum_error(
|
||||
self,
|
||||
result: Result<Response, StatusCode>,
|
||||
) -> Result<Response, Response> {
|
||||
fn make_axum_error(self, result: Result<Response, StatusCode>) -> Response {
|
||||
match result {
|
||||
Ok(resp) => Ok(resp),
|
||||
Ok(resp) => resp,
|
||||
Err(status) => {
|
||||
let body = serde_json::to_string(&ErrorPayload::<()> {
|
||||
code: status.as_u16(),
|
||||
@@ -801,19 +820,16 @@ mod resp {
|
||||
HeaderValue::from_static("application/json"),
|
||||
);
|
||||
*resp.status_mut() = status;
|
||||
Err(resp)
|
||||
resp
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> MakeAxumError<HttpError> for &ServerFnDecoder<Result<T, HttpError>> {
|
||||
fn make_axum_error(
|
||||
self,
|
||||
result: Result<Response, HttpError>,
|
||||
) -> Result<Response, Response> {
|
||||
fn make_axum_error(self, result: Result<Response, HttpError>) -> Response {
|
||||
match result {
|
||||
Ok(resp) => Ok(resp),
|
||||
Ok(resp) => resp,
|
||||
Err(http_err) => {
|
||||
let body = serde_json::to_string(&ErrorPayload::<()> {
|
||||
code: http_err.status.as_u16(),
|
||||
@@ -829,7 +845,7 @@ mod resp {
|
||||
HeaderValue::from_static("application/json"),
|
||||
);
|
||||
*resp.status_mut() = http_err.status;
|
||||
Err(resp)
|
||||
resp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ impl<T: From<String>> FromResponse for Html<T> {
|
||||
|
||||
impl<T> IntoRequest for Json<T>
|
||||
where
|
||||
T: Serialize + 'static,
|
||||
T: Serialize + 'static + DeserializeOwned,
|
||||
{
|
||||
fn into_request(self, request: ClientRequest) -> impl Future<Output = ClientResult> + 'static {
|
||||
async move { request.send_json(&self.0).await }
|
||||
|
||||
@@ -4,7 +4,7 @@ pub use axum::extract::Form;
|
||||
|
||||
impl<T> IntoRequest for Form<T>
|
||||
where
|
||||
T: Serialize + 'static,
|
||||
T: Serialize + 'static + DeserializeOwned,
|
||||
{
|
||||
fn into_request(self, req: ClientRequest) -> impl Future<Output = ClientResult> + 'static {
|
||||
async move { req.send_form(&self.0).await }
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use crate::ServerFnError;
|
||||
use axum::extract::FromRequestParts;
|
||||
use http::request::Parts;
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
/// An extractor that deserializes query parameters into the given type `T`.
|
||||
///
|
||||
/// This uses `serde_qs` under the hood to support complex query parameter structures.
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct Query<T>(pub T);
|
||||
|
||||
impl<T, S> FromRequestParts<S> for Query<T>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = ServerFnError;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||
let inner: T = serde_qs::from_str(parts.uri.query().unwrap_or_default())
|
||||
.map_err(|e| ServerFnError::Deserialization(e.to_string()))?;
|
||||
Ok(Self(inner))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for Query<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
@@ -9,18 +9,83 @@ use axum_core::response::IntoResponse;
|
||||
use bytes::Bytes;
|
||||
use dioxus_fullstack_core::{HttpError, RequestError};
|
||||
use futures::{Stream, StreamExt};
|
||||
#[cfg(feature = "server")]
|
||||
use futures_channel::mpsc::UnboundedSender;
|
||||
use headers::{ContentType, Header};
|
||||
use send_wrapper::SendWrapper;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::{future::Future, marker::PhantomData, pin::Pin};
|
||||
|
||||
/// A stream of text data.
|
||||
///
|
||||
/// # Chunking
|
||||
///
|
||||
/// Note that strings sent by the server might not arrive in the same chunking as they were sent.
|
||||
///
|
||||
/// This is because the underlying transport layer (HTTP/2 or HTTP/3) may choose to split or combine
|
||||
/// chunks for efficiency.
|
||||
///
|
||||
/// If you need to preserve individual string boundaries, consider using `ChunkedTextStream` or another
|
||||
/// encoding that preserves chunk boundaries.
|
||||
pub type TextStream = Streaming<String>;
|
||||
|
||||
/// A stream of binary data.
|
||||
///
|
||||
/// # Chunking
|
||||
///
|
||||
/// Note that bytes sent by the server might not arrive in the same chunking as they were sent.
|
||||
/// This is because the underlying transport layer (HTTP/2 or HTTP/3) may choose to split or combine
|
||||
/// chunks for efficiency.
|
||||
///
|
||||
/// If you need to preserve individual byte boundaries, consider using `ChunkedByteStream` or another
|
||||
/// encoding that preserves chunk boundaries.
|
||||
pub type ByteStream = Streaming<Bytes>;
|
||||
|
||||
/// A stream of JSON-encoded data.
|
||||
///
|
||||
/// # Chunking
|
||||
///
|
||||
/// Normally, it's not possible to stream JSON over HTTP because browsers are free to re-chunk
|
||||
/// data as they see fit. However, this implementation manually frames each JSON as if it were an unmasked
|
||||
/// websocket message.
|
||||
///
|
||||
/// If you need to send a stream of JSON data without framing, consider using TextStream instead and
|
||||
/// manually handling JSON buffering.
|
||||
pub type JsonStream<T> = Streaming<T, JsonEncoding>;
|
||||
|
||||
/// A stream of Cbor-encoded data.
|
||||
///
|
||||
/// # Chunking
|
||||
///
|
||||
/// Normally, it's not possible to stream JSON over HTTP because browsers are free to re-chunk
|
||||
/// data as they see fit. However, this implementation manually frames each item as if it were an unmasked
|
||||
/// websocket message.
|
||||
pub type CborStream<T> = Streaming<T, CborEncoding>;
|
||||
|
||||
/// A stream of manually chunked binary data.
|
||||
///
|
||||
/// This encoding preserves chunk boundaries by framing each chunk with its length, using Websocket
|
||||
/// Framing.
|
||||
pub type ChunkedByteStream = Streaming<Bytes, CborEncoding>;
|
||||
|
||||
/// A stream of manually chunked text data.
|
||||
///
|
||||
/// This encoding preserves chunk boundaries by framing each chunk with its length, using Websocket
|
||||
/// Framing.
|
||||
pub type ChunkedTextStream = Streaming<String, CborEncoding>;
|
||||
|
||||
/// A streaming payload.
|
||||
///
|
||||
/// ## Frames and Chunking
|
||||
///
|
||||
/// The streaming payload sends and receives data in discrete chunks or "frames". The size is converted
|
||||
/// to hex and sent before each chunk, followed by a CRLF, the chunk data, and another CRLF.
|
||||
///
|
||||
/// This mimics actual HTTP chunked transfer encoding, but allows us to define our own framing
|
||||
/// protocol on top of it.
|
||||
///
|
||||
/// Arbitrary bytes can be encoded between these frames, but the frames do come with some overhead.
|
||||
///
|
||||
/// ## Browser Support for Streaming Input
|
||||
///
|
||||
/// Browser fetch requests do not currently support full request duplexing, which
|
||||
@@ -58,9 +123,9 @@ impl<T: 'static + Send, E> Streaming<T, E> {
|
||||
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>>,
|
||||
output_stream: Box::pin(futures::stream::empty()) as _,
|
||||
encoding: PhantomData,
|
||||
}
|
||||
}
|
||||
@@ -69,9 +134,7 @@ impl<T: 'static + Send, E> Streaming<T, E> {
|
||||
///
|
||||
/// 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
|
||||
pub fn spawn<F>(callback: impl FnOnce(UnboundedSender<T>) -> F + Send + 'static) -> Self
|
||||
where
|
||||
F: Future<Output = ()> + 'static,
|
||||
T: Send,
|
||||
@@ -92,6 +155,17 @@ impl<T: 'static + Send, E> Streaming<T, E> {
|
||||
pub fn into_inner(self) -> impl Stream<Item = Result<T, StreamingError>> + Send {
|
||||
self.input_stream
|
||||
}
|
||||
|
||||
/// Creates a streaming payload from an existing stream of bytes.
|
||||
///
|
||||
/// This uses the internal framing mechanism to decode the stream into items of type `T`.
|
||||
fn from_bytes(stream: impl Stream<Item = Result<T, StreamingError>> + Send + 'static) -> Self {
|
||||
Self {
|
||||
input_stream: Box::pin(stream),
|
||||
output_stream: Box::pin(futures::stream::empty()) as _,
|
||||
encoding: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, U> From<S> for TextStream
|
||||
@@ -100,11 +174,7 @@ where
|
||||
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,
|
||||
}
|
||||
Self::new(value.map(|data| data.into()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,11 +199,7 @@ where
|
||||
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,
|
||||
}
|
||||
Self::from_bytes(value.map(|data| Ok(data.into())))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +224,7 @@ impl IntoResponse for Streaming<Bytes> {
|
||||
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) {
|
||||
Ok(res) => match encode_stream_frame::<T, E>(res) {
|
||||
Some(bytes) => Ok(bytes),
|
||||
None => Err(StreamingError::Failed),
|
||||
},
|
||||
@@ -218,7 +284,7 @@ impl<T: DeserializeOwned + Serialize + 'static + Send, E: Encoding> FromResponse
|
||||
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) {
|
||||
Ok(bytes) => match decode_stream_frame::<T, E>(bytes) {
|
||||
Some(res) => Ok(res),
|
||||
None => Err(StreamingError::Decoding),
|
||||
},
|
||||
@@ -330,7 +396,7 @@ impl<T: DeserializeOwned + Serialize + 'static + Send, E: Encoding, S> FromReque
|
||||
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) {
|
||||
Ok(bytes) => match decode_stream_frame::<T, E>(bytes) {
|
||||
Some(res) => Ok(res),
|
||||
None => Err(StreamingError::Decoding),
|
||||
},
|
||||
@@ -380,11 +446,11 @@ impl<T: DeserializeOwned + Serialize + 'static + Send, E: Encoding> IntoRequest
|
||||
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))
|
||||
}),
|
||||
)
|
||||
.send_body_stream(self.input_stream.map(|r| {
|
||||
r.and_then(|item| {
|
||||
encode_stream_frame::<T, E>(item).ok_or(StreamingError::Failed)
|
||||
})
|
||||
}))
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -403,3 +469,113 @@ impl<T, E: Encoding> std::fmt::Debug for Streaming<T, E> {
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// This function encodes a single frame of a streaming payload using the specified encoding.
|
||||
///
|
||||
/// The resulting `Bytes` object is encoded as a websocket frame, so you can send it over a streaming
|
||||
/// HTTP response or even a websocket connection.
|
||||
///
|
||||
/// Note that the packet is not masked, as it is assumed to be sent over a trusted connection.
|
||||
pub fn encode_stream_frame<T: Serialize, E: Encoding>(data: T) -> Option<Bytes> {
|
||||
// We use full advantage of `BytesMut` here, writing a maximally full frame and then shrinking it
|
||||
// down to size at the end.
|
||||
//
|
||||
// Also note we don't do any masking over this data since it's not going over an untrusted
|
||||
// network like a websocket would.
|
||||
//
|
||||
// We allocate 10 extra bytes to account for framing overhead, which we'll shrink after
|
||||
let mut bytes = vec![0u8; 10];
|
||||
|
||||
E::encode(data, &mut bytes)?;
|
||||
|
||||
let len = (bytes.len() - 10) as u64;
|
||||
let opcode = 0x82; // FIN + binary opcode
|
||||
|
||||
// Write the header directly into the allocated space.
|
||||
let offset = if len <= 125 {
|
||||
bytes[8] = opcode;
|
||||
bytes[9] = len as u8;
|
||||
8
|
||||
} else if len <= u16::MAX as u64 {
|
||||
bytes[6] = opcode;
|
||||
bytes[7] = 126;
|
||||
let len_bytes = (len as u16).to_be_bytes();
|
||||
bytes[8] = len_bytes[0];
|
||||
bytes[9] = len_bytes[1];
|
||||
6
|
||||
} else {
|
||||
bytes[0] = opcode;
|
||||
bytes[1] = 127;
|
||||
bytes[2..10].copy_from_slice(&len.to_be_bytes());
|
||||
0
|
||||
};
|
||||
|
||||
// Shrink down to the actual used size - is zero copy!
|
||||
Some(Bytes::from(bytes).slice(offset..))
|
||||
}
|
||||
|
||||
/// Decode a websocket-framed streaming payload produced by [`encode_stream_frame`].
|
||||
///
|
||||
/// This function returns `None` if the frame is invalid or cannot be decoded.
|
||||
///
|
||||
/// It cannot handle masked frames, as those are not produced by our encoding function.
|
||||
pub fn decode_stream_frame<T, E>(frame: Bytes) -> Option<T>
|
||||
where
|
||||
E: Encoding,
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
let data = frame.as_ref();
|
||||
|
||||
if data.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let first = data[0];
|
||||
let second = data[1];
|
||||
|
||||
// Require FIN with binary opcode and no RSV bits
|
||||
let fin = first & 0x80 != 0;
|
||||
let opcode = first & 0x0F;
|
||||
let rsv = first & 0x70;
|
||||
if !fin || opcode != 0x02 || rsv != 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Mask bit must be zero for our framing
|
||||
if second & 0x80 != 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut offset = 2usize;
|
||||
let mut payload_len = (second & 0x7F) as usize;
|
||||
|
||||
if payload_len == 126 {
|
||||
if data.len() < offset + 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
payload_len = u16::from_be_bytes([data[offset], data[offset + 1]]) as usize;
|
||||
offset += 2;
|
||||
} else if payload_len == 127 {
|
||||
if data.len() < offset + 8 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut len_bytes = [0u8; 8];
|
||||
len_bytes.copy_from_slice(&data[offset..offset + 8]);
|
||||
let len_u64 = u64::from_be_bytes(len_bytes);
|
||||
|
||||
if len_u64 > usize::MAX as u64 {
|
||||
return None;
|
||||
}
|
||||
|
||||
payload_len = len_u64 as usize;
|
||||
offset += 8;
|
||||
}
|
||||
|
||||
if data.len() < offset + payload_len {
|
||||
return None;
|
||||
}
|
||||
|
||||
E::decode(frame.slice(offset..offset + payload_len))
|
||||
}
|
||||
@@ -307,11 +307,11 @@ impl<I, O, E> Websocket<I, O, E> {
|
||||
match msg {
|
||||
Message::Text(text) => {
|
||||
let e: O =
|
||||
E::from_bytes(text.into()).ok_or_else(WebsocketError::deserialization)?;
|
||||
E::decode(text.into()).ok_or_else(WebsocketError::deserialization)?;
|
||||
return Ok(e);
|
||||
}
|
||||
Message::Binary(bytes) => {
|
||||
let e: O = E::from_bytes(bytes).ok_or_else(WebsocketError::deserialization)?;
|
||||
let e: O = E::decode(bytes).ok_or_else(WebsocketError::deserialization)?;
|
||||
return Ok(e);
|
||||
}
|
||||
Message::Close { code, reason } => {
|
||||
@@ -707,13 +707,12 @@ impl<In: DeserializeOwned, Out: Serialize, E: Encoding> TypedWebsocket<In, Out,
|
||||
match res {
|
||||
Ok(res) => match res {
|
||||
AxumMessage::Text(utf8_bytes) => {
|
||||
let e: In = E::from_bytes(utf8_bytes.into())
|
||||
let e: In = E::decode(utf8_bytes.into())
|
||||
.ok_or_else(WebsocketError::deserialization)?;
|
||||
return Ok(e);
|
||||
}
|
||||
AxumMessage::Binary(bytes) => {
|
||||
let e: In =
|
||||
E::from_bytes(bytes).ok_or_else(WebsocketError::deserialization)?;
|
||||
let e: In = E::decode(bytes).ok_or_else(WebsocketError::deserialization)?;
|
||||
return Ok(e);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ pub trait IntoRequest<R = ClientResponse>: Sized {
|
||||
|
||||
impl<A, R> IntoRequest<R> for (A,)
|
||||
where
|
||||
A: IntoRequest<R> + 'static,
|
||||
A: IntoRequest<R> + 'static + Send,
|
||||
{
|
||||
fn into_request(
|
||||
self,
|
||||
@@ -157,9 +157,20 @@ 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."
|
||||
)]
|
||||
#[diagnostic::on_unimplemented(message = r#"❌ Invalid Arguments to ServerFn ❌
|
||||
|
||||
The arguments to the server function must be either:
|
||||
|
||||
- a single `impl FromRequest + IntoRequest` argument
|
||||
- or multiple `DeserializeOwned` arguments.
|
||||
|
||||
Did you forget to implement `IntoRequest` or `Deserialize` for one of the arguments?
|
||||
|
||||
`IntoRequest` is a trait that allows payloads to be sent to the server function.
|
||||
|
||||
> See https://dioxuslabs.com/learn/0.7/essentials/fullstack/server_functions for more details.
|
||||
|
||||
"#)]
|
||||
pub trait AssertCanEncode {}
|
||||
|
||||
pub struct CantEncode;
|
||||
@@ -167,5 +178,27 @@ pub struct CantEncode;
|
||||
pub struct EncodeIsVerified;
|
||||
impl AssertCanEncode for EncodeIsVerified {}
|
||||
|
||||
#[diagnostic::on_unimplemented(message = r#"❌ Invalid return type from ServerFn ❌
|
||||
|
||||
The arguments to the server function must be either:
|
||||
|
||||
- a single `impl FromResponse` return type
|
||||
- a single `impl Serialize + DeserializedOwned` return type
|
||||
|
||||
Did you forget to implement `FromResponse` or `DeserializeOwned` for one of the arguments?
|
||||
|
||||
`FromResponse` is a trait that allows payloads to be decoded from the server function response.
|
||||
|
||||
> See https://dioxuslabs.com/learn/0.7/essentials/fullstack/server_functions for more details.
|
||||
|
||||
"#)]
|
||||
pub trait AssertCanDecode {}
|
||||
pub struct CantDecode;
|
||||
pub struct DecodeIsVerified;
|
||||
impl AssertCanDecode for DecodeIsVerified {}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn assert_can_encode(_t: impl AssertCanEncode) {}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn assert_can_decode(_t: impl AssertCanDecode) {}
|
||||
Reference in new issue
Block a user