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:
Jonathan Kelley authored and GitHub committed 2025-10-30 18:52:47 -07:00
1 parent 43373f0be7
commit 5559e463c8
27 files changed
+1395 -1097

No files matched your search

+7 -1
View File
@@ -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() {
+31 -17
View File
@@ -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()
}
}
+30 -12
View File
@@ -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`
+3
View File
@@ -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")]
+61 -45
View File
@@ -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 }
+1 -1
View File
@@ -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 }
+34
View File
@@ -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
}
}
+198 -22
View File
@@ -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))
}
+4 -5
View File
@@ -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);
}
+37 -4
View File
@@ -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) {}