use crate::error::{FetchError, Result, TypeError};
use crate::{AbortSignal, Headers, ReadableStream};
use url::Url;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RequestMode {
SameOrigin,
Cors,
NoCors,
Navigate,
}
impl Default for RequestMode {
fn default() -> Self {
Self::Cors
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RequestCredentials {
Omit,
SameOrigin,
Include,
}
impl Default for RequestCredentials {
fn default() -> Self {
Self::SameOrigin
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RequestCache {
Default,
NoStore,
Reload,
NoCache,
ForceCache,
OnlyIfCached,
}
impl Default for RequestCache {
fn default() -> Self {
Self::Default
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RequestRedirect {
Follow,
Error,
Manual,
}
impl Default for RequestRedirect {
fn default() -> Self {
Self::Follow
}
}
#[derive(Debug, Clone, Default)]
pub struct RequestInit {
pub method: Option<String>,
pub headers: Option<Headers>,
pub body: Option<ReadableStream>,
pub mode: Option<RequestMode>,
pub credentials: Option<RequestCredentials>,
pub cache: Option<RequestCache>,
pub redirect: Option<RequestRedirect>,
pub referrer: Option<String>,
pub referrer_policy: Option<String>,
pub integrity: Option<String>,
pub keepalive: Option<bool>,
pub signal: Option<AbortSignal>,
}
impl RequestInit {
pub fn new() -> Self {
Self::default()
}
}
#[derive(Debug, Clone)]
pub struct Request {
url: Url,
method: String,
headers: Headers,
body: Option<ReadableStream>,
mode: RequestMode,
credentials: RequestCredentials,
cache: RequestCache,
redirect: RequestRedirect,
referrer: String,
referrer_policy: String,
integrity: String,
keepalive: bool,
signal: Option<AbortSignal>,
}
impl Request {
pub fn new(input: &str, init: Option<RequestInit>) -> Result<Self> {
let url = Url::parse(input)?;
let init = init.unwrap_or_default();
let method = init.method.unwrap_or_else(|| "GET".to_string());
let method = Self::normalize_method(&method)?;
if matches!(method.as_str(), "GET" | "HEAD") && init.body.is_some() {
return Err(FetchError::Type(TypeError::new(
"Request with GET/HEAD method cannot have body",
)));
}
let mut headers = init.headers.unwrap_or_default();
if let Some(ref body) = init.body {
if let (Ok(None), Some(content_type)) =
(headers.get("content-type"), body.get_content_type())
{
headers.set("content-type", content_type)?;
}
}
Ok(Self {
url,
method,
headers,
body: init.body,
mode: init.mode.unwrap_or_default(),
credentials: init.credentials.unwrap_or_default(),
cache: init.cache.unwrap_or_default(),
redirect: init.redirect.unwrap_or_default(),
referrer: init.referrer.unwrap_or_else(|| "about:client".to_string()),
referrer_policy: init.referrer_policy.unwrap_or_default(),
integrity: init.integrity.unwrap_or_default(),
keepalive: init.keepalive.unwrap_or(false),
signal: init.signal,
})
}
pub fn url(&self) -> &str {
self.url.as_str()
}
pub fn method(&self) -> &str {
&self.method
}
pub fn headers(&self) -> &Headers {
&self.headers
}
pub fn body(&self) -> Option<&ReadableStream> {
self.body.as_ref()
}
pub fn body_used(&self) -> bool {
self.body.as_ref().is_some_and(|b| b.is_used())
}
pub fn mode(&self) -> RequestMode {
self.mode
}
pub fn credentials(&self) -> RequestCredentials {
self.credentials
}
pub fn cache(&self) -> RequestCache {
self.cache
}
pub fn redirect(&self) -> RequestRedirect {
self.redirect
}
pub fn referrer(&self) -> &str {
&self.referrer
}
pub fn referrer_policy(&self) -> &str {
&self.referrer_policy
}
pub fn integrity(&self) -> &str {
&self.integrity
}
pub fn keepalive(&self) -> bool {
self.keepalive
}
pub fn signal(&self) -> Option<&AbortSignal> {
self.signal.as_ref()
}
pub fn clone_request(&self) -> Result<Self> {
if self.body_used() {
return Err(FetchError::Type(TypeError::new(
"Cannot clone a request with a used body",
)));
}
Ok(Clone::clone(self))
}
pub async fn array_buffer(self) -> Result<bytes::Bytes> {
match self.body {
Some(body) => body.array_buffer().await,
None => Ok(bytes::Bytes::new()),
}
}
pub async fn blob(self) -> Result<bytes::Bytes> {
self.array_buffer().await
}
pub async fn form_data(self) -> Result<String> {
match self.body {
Some(body) => body.form_data().await,
None => Ok(String::new()),
}
}
pub async fn json<T: serde::de::DeserializeOwned>(self) -> Result<T> {
match self.body {
Some(body) => body.json().await,
None => Err(FetchError::Type(TypeError::new(
"Unexpected end of JSON input",
))),
}
}
pub async fn text(self) -> Result<String> {
match self.body {
Some(body) => body.text().await,
None => Ok(String::new()),
}
}
fn normalize_method(method: &str) -> Result<String> {
if method.is_empty() {
return Err(FetchError::Type(TypeError::new("Invalid method")));
}
for byte in method.bytes() {
if !matches!(byte, b'!' | b'#'..=b'\'' | b'*' | b'+' | b'-' | b'.' | b'0'..=b'9' | b'A'..=b'Z' | b'^'..=b'z' | b'|' | b'~')
{
return Err(FetchError::Type(TypeError::new("Invalid method")));
}
}
let upper = method.to_ascii_uppercase();
match upper.as_str() {
"GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "OPTIONS" | "PATCH" => Ok(upper),
_ => Ok(method.to_string()), }
}
pub(crate) fn get_url(&self) -> &Url {
&self.url
}
pub(crate) fn take_body(&mut self) -> Option<ReadableStream> {
self.body.take()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_request_creation() {
let request = Request::new("https://example.com", None).unwrap();
assert_eq!(request.url(), "https://example.com/");
assert_eq!(request.method(), "GET");
assert_eq!(request.mode(), RequestMode::Cors);
assert_eq!(request.credentials(), RequestCredentials::SameOrigin);
assert_eq!(request.cache(), RequestCache::Default);
assert_eq!(request.redirect(), RequestRedirect::Follow);
assert!(!request.keepalive());
assert!(!request.body_used());
}
#[test]
fn test_request_with_init() {
let mut headers = Headers::new();
headers.set("x-test", "value").unwrap();
let mut init = RequestInit::new();
init.method = Some("POST".to_string());
init.headers = Some(headers);
init.body = Some(ReadableStream::from_text("test body"));
init.mode = Some(RequestMode::SameOrigin);
init.credentials = Some(RequestCredentials::Include);
let request = Request::new("https://example.com", Some(init)).unwrap();
assert_eq!(request.method(), "POST");
assert_eq!(request.mode(), RequestMode::SameOrigin);
assert_eq!(request.credentials(), RequestCredentials::Include);
assert!(request.headers().has("x-test").unwrap());
assert!(request.body().is_some());
}
#[test]
fn test_request_method_validation() {
assert!(Request::new(
"https://example.com",
Some({
let mut init = RequestInit::new();
init.method = Some("GET".to_string());
init
})
)
.is_ok());
assert!(Request::new(
"https://example.com",
Some({
let mut init = RequestInit::new();
init.method = Some("POST".to_string());
init
})
)
.is_ok());
assert!(Request::new(
"https://example.com",
Some({
let mut init = RequestInit::new();
init.method = Some("CUSTOM".to_string());
init
})
)
.is_ok());
assert!(Request::new(
"https://example.com",
Some({
let mut init = RequestInit::new();
init.method = Some("".to_string());
init
})
)
.is_err());
assert!(Request::new(
"https://example.com",
Some({
let mut init = RequestInit::new();
init.method = Some("GET".to_string());
init.body = Some(ReadableStream::from_text("body"));
init
})
)
.is_err());
}
#[test]
fn test_request_url_validation() {
assert!(Request::new("https://example.com", None).is_ok());
assert!(Request::new("http://localhost:8080/path", None).is_ok());
assert!(Request::new("not-a-url", None).is_err());
assert!(Request::new("", None).is_err());
}
#[test]
fn test_request_defaults() {
let init = RequestInit::new();
assert!(init.method.is_none());
assert!(init.headers.is_none());
assert!(init.body.is_none());
assert!(init.mode.is_none());
assert!(init.credentials.is_none());
assert!(init.cache.is_none());
assert!(init.redirect.is_none());
assert!(init.referrer.is_none());
assert!(init.referrer_policy.is_none());
assert!(init.integrity.is_none());
assert!(init.keepalive.is_none());
assert!(init.signal.is_none());
}
#[test]
fn test_request_enum_defaults() {
assert_eq!(RequestMode::default(), RequestMode::Cors);
assert_eq!(
RequestCredentials::default(),
RequestCredentials::SameOrigin
);
assert_eq!(RequestCache::default(), RequestCache::Default);
assert_eq!(RequestRedirect::default(), RequestRedirect::Follow);
}
#[tokio::test]
async fn test_request_body_methods() {
let request = Request::new(
"https://example.com",
Some({
let mut init = RequestInit::new();
init.method = Some("POST".to_string());
init.body = Some(ReadableStream::from_text("test body"));
init
}),
)
.unwrap();
let text = request.text().await.unwrap();
assert_eq!(text, "test body");
}
#[tokio::test]
async fn test_request_json_body() {
let data = serde_json::json!({"key": "value"});
let request = Request::new(
"https://example.com",
Some({
let mut init = RequestInit::new();
init.method = Some("POST".to_string());
init.body = Some(ReadableStream::from_json(&data));
init
}),
)
.unwrap();
let parsed: serde_json::Value = request.json().await.unwrap();
assert_eq!(parsed["key"], "value");
}
#[test]
fn test_method_normalization() {
let request = Request::new(
"https://example.com",
Some({
let mut init = RequestInit::new();
init.method = Some("get".to_string());
init
}),
)
.unwrap();
assert_eq!(request.method(), "GET");
let request = Request::new(
"https://example.com",
Some({
let mut init = RequestInit::new();
init.method = Some("custom".to_string());
init
}),
)
.unwrap();
assert_eq!(request.method(), "custom");
}
#[test]
fn test_content_type_auto_set() {
let request = Request::new(
"https://example.com",
Some({
let mut init = RequestInit::new();
init.method = Some("POST".to_string());
init.body = Some(ReadableStream::from_json(&serde_json::json!({})));
init
}),
)
.unwrap();
assert_eq!(
request.headers().get("content-type").unwrap().unwrap(),
"application/json"
);
}
#[test]
fn test_request_clone() {
let request = Request::new("https://example.com", None).unwrap();
let cloned = request.clone_request().unwrap();
assert_eq!(request.url(), cloned.url());
assert_eq!(request.method(), cloned.method());
}
}