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
@@ -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()))
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user