mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 06:44:17 +00:00
generalize error redirect behavior across integrations
This commit is contained in:
parent
88fee243a8
commit
1ad7ee8a03
14 changed files with 159 additions and 157 deletions
|
@ -22,7 +22,6 @@ parking_lot = "0.12.1"
|
|||
regex = "1.7.0"
|
||||
tracing = "0.1.37"
|
||||
tokio = { version = "1", features = ["rt", "fs"] }
|
||||
url = "2.5.0"
|
||||
|
||||
[features]
|
||||
nonce = ["leptos/nonce"]
|
||||
|
|
|
@ -6,10 +6,7 @@
|
|||
//! [`examples`](https://github.com/leptos-rs/leptos/tree/main/examples)
|
||||
//! directory in the Leptos repository.
|
||||
|
||||
use actix_http::{
|
||||
body::MessageBody,
|
||||
header::{HeaderName, HeaderValue, ACCEPT, LOCATION, REFERER},
|
||||
};
|
||||
use actix_http::header::{HeaderName, HeaderValue, ACCEPT};
|
||||
use actix_web::{
|
||||
body::BoxBody,
|
||||
dev::{ServiceFactory, ServiceRequest},
|
||||
|
@ -28,24 +25,15 @@ use leptos_meta::*;
|
|||
use leptos_router::*;
|
||||
use parking_lot::RwLock;
|
||||
use regex::Regex;
|
||||
use server_fn::{
|
||||
error::{
|
||||
NoCustomError, ServerFnErrorSerde, ServerFnUrlError,
|
||||
SERVER_FN_ERROR_HEADER,
|
||||
},
|
||||
redirect::REDIRECT_HEADER,
|
||||
request::actix::ActixRequest,
|
||||
};
|
||||
use server_fn::{redirect::REDIRECT_HEADER, request::actix::ActixRequest};
|
||||
use std::{
|
||||
fmt::{Debug, Display},
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
str::FromStr,
|
||||
sync::Arc,
|
||||
};
|
||||
#[cfg(debug_assertions)]
|
||||
use tracing::instrument;
|
||||
use url::Url;
|
||||
/// This struct lets you define headers and override the status of the Response from an Element or a Server Function
|
||||
/// Typically contained inside of a ResponseOptions. Setting this is useful for cookies and custom responses.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
|
@ -253,82 +241,12 @@ pub fn handle_server_fns_with_context(
|
|||
let res_parts = ResponseOptions::default();
|
||||
provide_context(res_parts.clone());
|
||||
|
||||
let accepts_html = req
|
||||
.headers()
|
||||
.get(ACCEPT)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|v| v.contains("text/html"))
|
||||
.unwrap_or(false);
|
||||
let referrer = req.headers().get(REFERER).cloned();
|
||||
|
||||
let mut res = service
|
||||
.0
|
||||
.run(ActixRequest::from((req, payload)))
|
||||
.await
|
||||
.take();
|
||||
|
||||
// it it accepts text/html (i.e., is a plain form post) and doesn't already have a
|
||||
// Location set, then redirect to to Referer
|
||||
if accepts_html {
|
||||
if let Some(mut referrer) = referrer {
|
||||
let location = res.headers().get(LOCATION);
|
||||
if location.is_none() {
|
||||
let is_error = res.status()
|
||||
== StatusCode::INTERNAL_SERVER_ERROR;
|
||||
if is_error {
|
||||
if let Some(Ok(path)) = res
|
||||
.headers()
|
||||
.get(SERVER_FN_ERROR_HEADER)
|
||||
.map(|n| n.to_str().map(|n| n.to_owned()))
|
||||
{
|
||||
let (headers, body) = res.into_parts();
|
||||
if let Ok(body) = body.try_into_bytes() {
|
||||
if let Ok(body) =
|
||||
String::from_utf8(body.to_vec())
|
||||
{
|
||||
// TODO allow other kinds?
|
||||
let err: ServerFnError<
|
||||
NoCustomError,
|
||||
> = ServerFnErrorSerde::de(&body);
|
||||
if let Ok(referrer_str) =
|
||||
referrer.to_str()
|
||||
{
|
||||
let mut modified =
|
||||
Url::parse(referrer_str)
|
||||
.expect(
|
||||
"couldn't parse \
|
||||
URL from Referer \
|
||||
header.",
|
||||
);
|
||||
modified
|
||||
.query_pairs_mut()
|
||||
.append_pair(
|
||||
"__path",
|
||||
&path
|
||||
)
|
||||
.append_pair(
|
||||
"__err",
|
||||
&ServerFnErrorSerde::ser(&err).unwrap_or_default()
|
||||
);
|
||||
let modified =
|
||||
HeaderValue::from_str(
|
||||
modified.as_ref(),
|
||||
);
|
||||
if let Ok(header) = modified {
|
||||
referrer = header;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
res = headers.set_body(BoxBody::new(""))
|
||||
}
|
||||
};
|
||||
*res.status_mut() = StatusCode::FOUND;
|
||||
res.headers_mut().insert(LOCATION, referrer);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Override StatusCode if it was set in a Resource or Element
|
||||
if let Some(status) = res_parts.0.read().status {
|
||||
*res.status_mut() = status;
|
||||
|
|
|
@ -20,7 +20,7 @@ typed-builder = "0.18"
|
|||
typed-builder-macro = "0.18"
|
||||
serde = { version = "1", optional = true }
|
||||
serde_json = { version = "1", optional = true }
|
||||
server_fn = { workspace = true, features = ["browser"] }
|
||||
server_fn = { workspace = true, features = ["browser", "url", "cbor"] }
|
||||
web-sys = { version = "0.3.63", features = [
|
||||
"ShadowRoot",
|
||||
"ShadowRootInit",
|
||||
|
|
|
@ -3,10 +3,7 @@ use leptos_reactive::{
|
|||
batch, create_rw_signal, is_suppressing_resource_load, signal_prelude::*,
|
||||
spawn_local, store_value, use_context, ReadSignal, RwSignal, StoredValue,
|
||||
};
|
||||
use server_fn::{
|
||||
error::{NoCustomError, ServerFnUrlError},
|
||||
ServerFn, ServerFnError,
|
||||
};
|
||||
use server_fn::{error::ServerFnUrlError, ServerFn, ServerFnError};
|
||||
use std::{cell::Cell, future::Future, pin::Pin, rc::Rc};
|
||||
|
||||
/// An action synchronizes an imperative `async` call to the synchronous reactive system.
|
||||
|
|
|
@ -64,9 +64,10 @@ reqwest = { version = "0.11", default-features = false, optional = true, feature
|
|||
"multipart",
|
||||
"stream",
|
||||
] }
|
||||
url = "2"
|
||||
|
||||
[features]
|
||||
default = ["url", "json", "cbor"]
|
||||
default = [ "json", "cbor"]
|
||||
actix = ["ssr", "dep:actix-web", "dep:send_wrapper"]
|
||||
axum = [
|
||||
"ssr",
|
||||
|
|
|
@ -7,6 +7,7 @@ use std::{
|
|||
sync::Arc,
|
||||
};
|
||||
use thiserror::Error;
|
||||
use url::Url;
|
||||
|
||||
/// A custom header that can be used to indicate a server function returned an error.
|
||||
pub const SERVER_FN_ERROR_HEADER: &'static str = "serverfnerror";
|
||||
|
@ -403,46 +404,21 @@ impl<CustErr> From<ServerFnError<CustErr>> for ServerFnErrorErr<CustErr> {
|
|||
}
|
||||
}
|
||||
|
||||
/// TODO: Write Documentation
|
||||
/// Associates a particular server function error with the server function
|
||||
/// found at a particular path.
|
||||
///
|
||||
/// This can be used to pass an error from the server back to the client
|
||||
/// without JavaScript/WASM supported, by encoding it in the URL as a qurey string.
|
||||
/// This is useful for progressive enhancement.
|
||||
#[derive(Debug)]
|
||||
pub struct ServerFnUrlError<CustErr> {
|
||||
path: String,
|
||||
error: ServerFnError<CustErr>,
|
||||
}
|
||||
|
||||
impl<CustErr> FromStr for ServerFnUrlError<CustErr>
|
||||
where
|
||||
CustErr: FromStr + Display,
|
||||
{
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.split_once('|') {
|
||||
None => Err(()),
|
||||
Some((path, error)) => {
|
||||
let error = ServerFnError::<CustErr>::de(error);
|
||||
Ok(ServerFnUrlError {
|
||||
path: path.to_string(),
|
||||
error,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<CustErr> Display for ServerFnUrlError<CustErr>
|
||||
where
|
||||
CustErr: FromStr + Display,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}|", self.path)?;
|
||||
write!(f, "{}", &self.error.ser()?)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<CustErr> ServerFnUrlError<CustErr> {
|
||||
/// TODO: Write Documentation
|
||||
/// Creates a new structure associating the server function at some path
|
||||
/// with a particular error.
|
||||
pub fn new(path: impl Display, error: ServerFnError<CustErr>) -> Self {
|
||||
Self {
|
||||
path: path.to_string(),
|
||||
|
@ -450,15 +426,30 @@ impl<CustErr> ServerFnUrlError<CustErr> {
|
|||
}
|
||||
}
|
||||
|
||||
/// TODO: Write documentation
|
||||
/// The error itself.
|
||||
pub fn error(&self) -> &ServerFnError<CustErr> {
|
||||
&self.error
|
||||
}
|
||||
|
||||
/// TODO: Add docs
|
||||
/// The path of the server function that generated this error.
|
||||
pub fn path(&self) -> &str {
|
||||
&self.path
|
||||
}
|
||||
|
||||
/// Adds an encoded form of this server function error to the given base URL.
|
||||
pub fn to_url(&self, base: &str) -> Result<Url, url::ParseError>
|
||||
where
|
||||
CustErr: FromStr + Display,
|
||||
{
|
||||
let mut url = Url::parse(base)?;
|
||||
url.query_pairs_mut()
|
||||
.append_pair("__path", &self.path)
|
||||
.append_pair(
|
||||
"__err",
|
||||
&ServerFnErrorSerde::ser(&self.error).unwrap_or_default(),
|
||||
);
|
||||
Ok(url)
|
||||
}
|
||||
}
|
||||
|
||||
impl<CustErr> From<ServerFnUrlError<CustErr>> for ServerFnError<CustErr> {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
#![forbid(unsafe_code)]
|
||||
// uncomment this if you want to feel pain
|
||||
#![deny(missing_docs)]
|
||||
|
||||
//! # Server Functions
|
||||
|
@ -126,7 +125,7 @@ use codec::{Encoding, FromReq, FromRes, IntoReq, IntoRes};
|
|||
pub use const_format;
|
||||
use dashmap::DashMap;
|
||||
pub use error::ServerFnError;
|
||||
use error::ServerFnErrorSerde;
|
||||
use error::{ServerFnErrorSerde, ServerFnUrlError};
|
||||
use http::Method;
|
||||
use middleware::{Layer, Service};
|
||||
use once_cell::sync::Lazy;
|
||||
|
@ -236,10 +235,42 @@ where
|
|||
fn run_on_server(
|
||||
req: Self::ServerRequest,
|
||||
) -> impl Future<Output = Self::ServerResponse> + Send {
|
||||
async {
|
||||
Self::execute_on_server(req).await.unwrap_or_else(|e| {
|
||||
Self::ServerResponse::error_response(Self::PATH, e)
|
||||
})
|
||||
// Server functions can either be called by a real Client,
|
||||
// or directly by an HTML <form>. If they're accessed by a <form>, default to
|
||||
// redirecting back to the Referer.
|
||||
let accepts_html = req
|
||||
.accepts()
|
||||
.map(|n| n.contains("text/html"))
|
||||
.unwrap_or(false);
|
||||
let mut referer = req.referer().as_deref().map(ToOwned::to_owned);
|
||||
|
||||
async move {
|
||||
let (mut res, err) = Self::execute_on_server(req)
|
||||
.await
|
||||
.map(|res| (res, None))
|
||||
.unwrap_or_else(|e| {
|
||||
(
|
||||
Self::ServerResponse::error_response(Self::PATH, &e),
|
||||
Some(e),
|
||||
)
|
||||
});
|
||||
|
||||
// if it accepts HTML, we'll redirect to the Referer
|
||||
if accepts_html {
|
||||
// if it had an error, encode that error in the URL
|
||||
if let Some(err) = err {
|
||||
if let Ok(url) = ServerFnUrlError::new(Self::PATH, err)
|
||||
.to_url(referer.as_deref().unwrap_or("/"))
|
||||
{
|
||||
referer = Some(url.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// set the status code and Location header
|
||||
res.redirect(referer.as_deref().unwrap_or("/"));
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@ mod axum {
|
|||
Box::pin(async move {
|
||||
inner.await.unwrap_or_else(|e| {
|
||||
let err = ServerFnError::from(e);
|
||||
Response::<Body>::error_response(&path, err)
|
||||
Response::<Body>::error_response(&path, &err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -131,7 +131,7 @@ mod actix {
|
|||
Box::pin(async move {
|
||||
inner.await.unwrap_or_else(|e| {
|
||||
let err = ServerFnError::new(e);
|
||||
ActixResponse::error_response(&path, err).take()
|
||||
ActixResponse::error_response(&path, &err).take()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -152,7 +152,7 @@ mod actix {
|
|||
Box::pin(async move {
|
||||
ActixResponse::from(inner.await.unwrap_or_else(|e| {
|
||||
let err = ServerFnError::new(e);
|
||||
ActixResponse::error_response(&path, err).take()
|
||||
ActixResponse::error_response(&path, &err).take()
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ use actix_web::{web::Payload, HttpRequest};
|
|||
use bytes::Bytes;
|
||||
use futures::Stream;
|
||||
use send_wrapper::SendWrapper;
|
||||
use std::future::Future;
|
||||
use std::{borrow::Cow, future::Future};
|
||||
|
||||
/// A wrapped Actix request.
|
||||
///
|
||||
|
@ -17,6 +17,14 @@ impl ActixRequest {
|
|||
pub fn take(self) -> (HttpRequest, Payload) {
|
||||
self.0.take()
|
||||
}
|
||||
|
||||
fn header(&self, name: &str) -> Option<Cow<'_, str>> {
|
||||
self.0
|
||||
.0
|
||||
.headers()
|
||||
.get(name)
|
||||
.map(|h| String::from_utf8_lossy(h.as_bytes()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(HttpRequest, Payload)> for ActixRequest {
|
||||
|
@ -30,12 +38,16 @@ impl<CustErr> Req<CustErr> for ActixRequest {
|
|||
self.0 .0.uri().query()
|
||||
}
|
||||
|
||||
fn to_content_type(&self) -> Option<String> {
|
||||
self.0
|
||||
.0
|
||||
.headers()
|
||||
.get("Content-Type")
|
||||
.map(|h| String::from_utf8_lossy(h.as_bytes()).to_string())
|
||||
fn to_content_type(&self) -> Option<Cow<'_, str>> {
|
||||
self.header("Content-Type")
|
||||
}
|
||||
|
||||
fn accepts(&self) -> Option<Cow<'_, str>> {
|
||||
self.header("Accept")
|
||||
}
|
||||
|
||||
fn referer(&self) -> Option<Cow<'_, str>> {
|
||||
self.header("Referer")
|
||||
}
|
||||
|
||||
fn try_into_bytes(
|
||||
|
|
|
@ -1,18 +1,34 @@
|
|||
use crate::{error::ServerFnError, request::Req};
|
||||
use axum::body::{Body, Bytes};
|
||||
use futures::{Stream, StreamExt};
|
||||
use http::{header::CONTENT_TYPE, Request};
|
||||
use http::{
|
||||
header::{ACCEPT, CONTENT_TYPE, REFERER},
|
||||
Request,
|
||||
};
|
||||
use http_body_util::BodyExt;
|
||||
use std::borrow::Cow;
|
||||
|
||||
impl<CustErr> Req<CustErr> for Request<Body> {
|
||||
fn as_query(&self) -> Option<&str> {
|
||||
self.uri().query()
|
||||
}
|
||||
|
||||
fn to_content_type(&self) -> Option<String> {
|
||||
fn to_content_type(&self) -> Option<Cow<'_, str>> {
|
||||
self.headers()
|
||||
.get(CONTENT_TYPE)
|
||||
.map(|h| String::from_utf8_lossy(h.as_bytes()).to_string())
|
||||
.map(|h| String::from_utf8_lossy(h.as_bytes()))
|
||||
}
|
||||
|
||||
fn accepts(&self) -> Option<Cow<'_, str>> {
|
||||
self.headers()
|
||||
.get(ACCEPT)
|
||||
.map(|h| String::from_utf8_lossy(h.as_bytes()))
|
||||
}
|
||||
|
||||
fn referer(&self) -> Option<Cow<'_, str>> {
|
||||
self.headers()
|
||||
.get(REFERER)
|
||||
.map(|h| String::from_utf8_lossy(h.as_bytes()))
|
||||
}
|
||||
|
||||
async fn try_into_bytes(self) -> Result<Bytes, ServerFnError<CustErr>> {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::error::ServerFnError;
|
||||
use bytes::Bytes;
|
||||
use futures::Stream;
|
||||
use std::future::Future;
|
||||
use std::{borrow::Cow, future::Future};
|
||||
|
||||
/// Request types for Actix.
|
||||
#[cfg(any(feature = "actix", doc))]
|
||||
|
@ -73,7 +73,13 @@ where
|
|||
fn as_query(&self) -> Option<&str>;
|
||||
|
||||
/// Returns the `Content-Type` header, if any.
|
||||
fn to_content_type(&self) -> Option<String>;
|
||||
fn to_content_type(&self) -> Option<Cow<'_, str>>;
|
||||
|
||||
/// Returns the `Accepts` header, if any.
|
||||
fn accepts(&self) -> Option<Cow<'_, str>>;
|
||||
|
||||
/// Returns the `Referer` header, if any.
|
||||
fn referer(&self) -> Option<Cow<'_, str>>;
|
||||
|
||||
/// Attempts to extract the body of the request into [`Bytes`].
|
||||
fn try_into_bytes(
|
||||
|
@ -103,10 +109,17 @@ impl<CustErr> Req<CustErr> for BrowserMockReq {
|
|||
unreachable!()
|
||||
}
|
||||
|
||||
fn to_content_type(&self) -> Option<String> {
|
||||
fn to_content_type(&self) -> Option<Cow<'_, str>> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn accepts(&self) -> Option<Cow<'_, str>> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn referer(&self) -> Option<Cow<'_, str>> {
|
||||
unreachable!()
|
||||
}
|
||||
async fn try_into_bytes(self) -> Result<Bytes, ServerFnError<CustErr>> {
|
||||
unreachable!()
|
||||
}
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
use super::Res;
|
||||
use crate::error::{
|
||||
ServerFnError, ServerFnErrorErr, ServerFnErrorSerde, ServerFnUrlError,
|
||||
SERVER_FN_ERROR_HEADER,
|
||||
ServerFnError, ServerFnErrorErr, ServerFnErrorSerde, SERVER_FN_ERROR_HEADER,
|
||||
};
|
||||
use actix_web::{
|
||||
http::{header, StatusCode},
|
||||
http::{
|
||||
header,
|
||||
header::{HeaderValue, LOCATION},
|
||||
StatusCode,
|
||||
},
|
||||
HttpResponse,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
|
@ -77,11 +80,18 @@ where
|
|||
)))
|
||||
}
|
||||
|
||||
fn error_response(path: &str, err: ServerFnError<CustErr>) -> Self {
|
||||
fn error_response(path: &str, err: &ServerFnError<CustErr>) -> Self {
|
||||
ActixResponse(SendWrapper::new(
|
||||
HttpResponse::build(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.append_header((SERVER_FN_ERROR_HEADER, path))
|
||||
.body(err.ser().unwrap_or_else(|_| err.to_string())),
|
||||
))
|
||||
}
|
||||
|
||||
fn redirect(&mut self, path: &str) {
|
||||
if let Ok(path) = HeaderValue::from_str(path) {
|
||||
*self.0.status_mut() = StatusCode::FOUND;
|
||||
self.0.headers_mut().insert(LOCATION, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ use crate::error::{ServerFnError, ServerFnErrorErr};
|
|||
use axum::body::Body;
|
||||
use bytes::Bytes;
|
||||
use futures::{Stream, StreamExt};
|
||||
use http::Response;
|
||||
use http::{header, HeaderValue, Response, StatusCode};
|
||||
use std::fmt::{Debug, Display};
|
||||
|
||||
impl<CustErr> Res<CustErr> for Response<Body>
|
||||
|
@ -50,10 +50,17 @@ where
|
|||
.map_err(|e| ServerFnError::Response(e.to_string()))
|
||||
}
|
||||
|
||||
fn error_response(path: &str, err: ServerFnError<CustErr>) -> Self {
|
||||
fn error_response(path: &str, err: &ServerFnError<CustErr>) -> Self {
|
||||
Response::builder()
|
||||
.status(http::StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.body(Body::from(err.to_string()))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn redirect(&mut self, path: &str) {
|
||||
if let Ok(path) = HeaderValue::from_str(path) {
|
||||
self.headers_mut().insert(header::LOCATION, path);
|
||||
*self.status_mut() = StatusCode::FOUND;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,7 +42,10 @@ where
|
|||
) -> Result<Self, ServerFnError<CustErr>>;
|
||||
|
||||
/// Converts an error into a response, with a `500` status code and the error text as its body.
|
||||
fn error_response(path: &str, err: ServerFnError<CustErr>) -> Self;
|
||||
fn error_response(path: &str, err: &ServerFnError<CustErr>) -> Self;
|
||||
|
||||
/// Redirect the response by setting a 302 code and Location header.
|
||||
fn redirect(&mut self, path: &str);
|
||||
}
|
||||
|
||||
/// Represents the response as received by the client.
|
||||
|
@ -101,7 +104,7 @@ impl<CustErr> Res<CustErr> for BrowserMockRes {
|
|||
unreachable!()
|
||||
}
|
||||
|
||||
fn error_response(_path: &str, _err: ServerFnError<CustErr>) -> Self {
|
||||
fn error_response(_path: &str, _err: &ServerFnError<CustErr>) -> Self {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
|
@ -109,6 +112,10 @@ impl<CustErr> Res<CustErr> for BrowserMockRes {
|
|||
_content_type: &str,
|
||||
_data: impl Stream<Item = Result<Bytes, ServerFnError<CustErr>>>,
|
||||
) -> Result<Self, ServerFnError<CustErr>> {
|
||||
todo!()
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn redirect(&mut self, _path: &str) {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue