mod parsing;
use parsing::contains_ignore_ascii_case;
pub use parsing::{DecodeError, url_decode};
use crate::constants::{
HEADER_TRACE_ID, MAX_FORM_FIELDS, MAX_HEADER_VALUE_LEN, MAX_TOTAL_HEADERS_SIZE,
MAX_URL_DECODED_LEN,
};
use crate::json::{self, JsonValue};
use std::cell::OnceCell;
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Method {
Get,
Post,
Put,
Patch,
Delete,
Head,
Options,
}
impl Method {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Get => "GET",
Self::Post => "POST",
Self::Put => "PUT",
Self::Patch => "PATCH",
Self::Delete => "DELETE",
Self::Head => "HEAD",
Self::Options => "OPTIONS",
}
}
}
impl std::fmt::Display for Method {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[non_exhaustive]
pub struct Request {
method: Method,
path: String,
headers: Vec<(String, String)>,
body: Option<Vec<u8>>,
params: HashMap<String, String>,
query_cache: OnceCell<HashMap<String, Vec<String>>>,
form_cache: OnceCell<HashMap<String, Vec<String>>>,
header_index: HashMap<String, Vec<usize>>,
}
impl std::fmt::Debug for Request {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Request")
.field("method", &self.method)
.field("path", &self.path)
.field("headers", &self.headers.len())
.field("body", &self.body.as_ref().map(std::vec::Vec::len))
.field("params", &self.params)
.finish_non_exhaustive()
}
}
impl Request {
#[doc(hidden)]
#[must_use]
pub fn new(
method: Method,
path: String,
headers: Vec<(String, String)>,
body: Option<Vec<u8>>,
params: HashMap<String, String>,
) -> Self {
let mut header_index: HashMap<String, Vec<usize>> = HashMap::with_capacity(headers.len());
let mut total_headers_size: usize = 0;
let mut oversized_value_count = 0u32;
let mut total_size_exceeded = false;
for (i, (k, v)) in headers.iter().enumerate() {
let header_size = k.len().saturating_add(v.len());
total_headers_size = total_headers_size.saturating_add(header_size);
if v.len() > MAX_HEADER_VALUE_LEN {
oversized_value_count += 1;
}
if total_headers_size > MAX_TOTAL_HEADERS_SIZE && !total_size_exceeded {
total_size_exceeded = true;
}
header_index.entry(k.to_lowercase()).or_default().push(i);
}
if oversized_value_count > 0 {
crate::log_warn!(
"Header value size limit exceeded: {} header(s) exceed {} bytes (max: {} bytes)",
oversized_value_count,
MAX_HEADER_VALUE_LEN,
MAX_HEADER_VALUE_LEN
);
}
if total_size_exceeded {
crate::log_warn!(
"Total headers size limit exceeded: {} bytes (max: {} bytes)",
total_headers_size,
MAX_TOTAL_HEADERS_SIZE
);
}
Self {
method,
path,
headers,
body,
params,
query_cache: OnceCell::new(),
form_cache: OnceCell::new(),
header_index,
}
}
#[inline]
pub const fn method(&self) -> Method {
self.method
}
#[inline]
pub fn path(&self) -> &str {
&self.path
}
#[inline]
pub fn path_without_query(&self) -> &str {
self.path.split('?').next().unwrap_or(&self.path)
}
#[inline]
pub fn param(&self, name: &str) -> Option<&str> {
self.params.get(name).map(String::as_str)
}
pub fn query(&self, name: &str) -> Option<&str> {
let cache = self.query_cache.get_or_init(|| self.parse_query());
cache.get(name).and_then(|v| v.first()).map(String::as_str)
}
pub fn query_all(&self, name: &str) -> &[String] {
let cache = self.query_cache.get_or_init(|| self.parse_query());
cache.get(name).map_or(&[], Vec::as_slice)
}
pub fn header(&self, name: &str) -> Option<&str> {
let indices = if name.bytes().all(|b| !b.is_ascii_uppercase()) {
self.header_index.get(name)
} else {
self.header_index.get(&name.to_lowercase())
};
indices
.and_then(|idx| idx.first())
.and_then(|&i| self.headers.get(i))
.map(|(_, v)| v.as_str())
}
#[inline]
pub fn trace_id(&self) -> Option<&str> {
self.header(HEADER_TRACE_ID)
}
pub fn header_all(&self, name: &str) -> Vec<&str> {
let indices = if name.bytes().all(|b| !b.is_ascii_uppercase()) {
self.header_index.get(name)
} else {
self.header_index.get(&name.to_lowercase())
};
indices
.map(|idx| {
idx.iter()
.filter_map(|&i| self.headers.get(i).map(|(_, v)| v.as_str()))
.collect()
})
.unwrap_or_default()
}
#[inline]
pub fn headers(&self) -> &[(String, String)] {
&self.headers
}
#[inline]
#[must_use]
pub fn body(&self) -> Option<&[u8]> {
self.body.as_deref()
}
#[inline]
#[must_use]
pub fn text(&self) -> Option<&str> {
self.body.as_ref().and_then(|b| std::str::from_utf8(b).ok())
}
#[inline]
#[must_use]
pub fn has_body(&self) -> bool {
self.body.as_ref().is_some_and(|b| !b.is_empty())
}
#[inline]
#[must_use]
pub fn content_type(&self) -> Option<&str> {
use crate::constants::HEADER_CONTENT_TYPE;
self.header(HEADER_CONTENT_TYPE)
}
#[inline]
#[must_use]
pub fn is_json(&self) -> bool {
use crate::constants::MIME_JSON;
self.content_type()
.is_some_and(|ct| contains_ignore_ascii_case(ct, MIME_JSON))
}
#[inline]
#[must_use]
pub fn is_form(&self) -> bool {
use crate::constants::MIME_FORM_URLENCODED;
self.content_type()
.is_some_and(|ct| contains_ignore_ascii_case(ct, MIME_FORM_URLENCODED))
}
#[inline]
#[must_use]
pub fn is_html(&self) -> bool {
use crate::constants::MIME_HTML;
self.content_type()
.is_some_and(|ct| contains_ignore_ascii_case(ct, MIME_HTML))
}
pub fn accepts(&self, mime: &str) -> bool {
self.header("accept")
.is_some_and(|accept| contains_ignore_ascii_case(accept, mime))
}
#[must_use]
pub fn form(&self, name: &str) -> Option<&str> {
self.form_cache()
.get(name)
.and_then(|v| v.first())
.map(String::as_str)
}
pub fn form_all(&self, name: &str) -> &[String] {
self.form_cache().get(name).map_or(&[], Vec::as_slice)
}
#[must_use]
pub fn json_with<T>(&self, parse: impl FnOnce(&[u8]) -> Option<T>) -> Option<T> {
self.body().and_then(parse)
}
#[must_use]
pub fn json(&self) -> Option<JsonValue> {
self.json_with(json::try_parse)
}
fn form_cache(&self) -> &HashMap<String, Vec<String>> {
self.form_cache.get_or_init(|| self.parse_form())
}
fn parse_form(&self) -> HashMap<String, Vec<String>> {
let mut map: HashMap<String, Vec<String>> = HashMap::new();
if let Some(body) = self.text() {
let mut truncated = false;
let mut decode_failures = 0u32;
for pair in body.split('&') {
if map.len() >= MAX_FORM_FIELDS {
truncated = true;
break;
}
if let Some((key, value)) = pair.split_once('=') {
match (url_decode(key), url_decode(value)) {
(Ok(decoded_key), Ok(decoded_value)) => {
map.entry(decoded_key).or_default().push(decoded_value);
},
_ => {
decode_failures += 1;
},
}
} else if !pair.is_empty() {
match url_decode(pair) {
Ok(decoded_key) => {
map.entry(decoded_key).or_default().push(String::new());
},
Err(_) => {
decode_failures += 1;
},
}
}
}
if truncated {
crate::log_warn!(
"Form field limit exceeded: dropped fields after {} (max: {})",
MAX_FORM_FIELDS,
MAX_FORM_FIELDS
);
}
if decode_failures > 0 {
crate::log_warn!(
"Form field decode failed: dropped {} field(s). Check for invalid percent-encoding (e.g., %ZZ) or values exceeding {} bytes after decoding",
decode_failures,
MAX_URL_DECODED_LEN
);
}
}
map
}
fn parse_query(&self) -> HashMap<String, Vec<String>> {
let mut map: HashMap<String, Vec<String>> = HashMap::new();
let mut dropped_count = 0u32;
if let Some(query_start) = self.path.find('?') {
let query = &self.path[query_start + 1..];
for pair in query.split('&') {
if let Some((key, value)) = pair.split_once('=') {
match (url_decode(key), url_decode(value)) {
(Ok(decoded_key), Ok(decoded_value)) => {
map.entry(decoded_key).or_default().push(decoded_value);
},
_ => {
dropped_count += 1;
},
}
} else if !pair.is_empty() {
match url_decode(pair) {
Ok(decoded_key) => {
map.entry(decoded_key).or_default().push(String::new());
},
Err(_) => {
dropped_count += 1;
},
}
}
}
}
if dropped_count > 0 {
crate::log_warn!(
"Query param decode failed: dropped {} param(s). Check for invalid percent-encoding (e.g., %ZZ) or values exceeding {} bytes after decoding",
dropped_count,
MAX_URL_DECODED_LEN
);
}
map
}
}
#[cfg(test)]
mod tests;