Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 2 additions & 10 deletions http-cache-reqwest/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,9 +279,7 @@ pub type ReqwestStreamingError = http_cache::ClientStreamingError;
#[cfg(feature = "streaming")]
use http_cache::StreamingCacheManager;

use std::{
collections::HashMap, convert::TryInto, str::FromStr, time::SystemTime,
};
use std::{convert::TryInto, str::FromStr, time::SystemTime};

pub use http::request::Parts;
use http::{
Expand Down Expand Up @@ -443,13 +441,7 @@ impl Middleware for ReqwestMiddleware<'_> {
.run(copied_req, self.extensions)
.await
.map_err(BoxError::from)?;
let mut headers = HashMap::new();
for header in res.headers() {
headers.insert(
header.0.as_str().to_owned(),
header.1.to_str()?.to_owned(),
);
}
let headers = res.headers().into();
let url = res.url().clone();
let status = res.status().into();
let version = res.version();
Expand Down
6 changes: 3 additions & 3 deletions http-cache-surf/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,8 @@
//! ```

use std::convert::TryInto;
use std::str::FromStr;
use std::time::SystemTime;
use std::{collections::HashMap, str::FromStr};

use http::{
header::CACHE_CONTROL,
Expand All @@ -155,7 +155,7 @@ use http_cache::{
BadHeader, BoxError, CacheManager, CacheOptions, HitOrMiss, HttpResponse,
Middleware, Result, XCACHE, XCACHELOOKUP,
};
pub use http_cache::{CacheMode, HttpCache};
pub use http_cache::{CacheMode, HttpCache, HttpHeaders};
use http_cache_semantics::CachePolicy;
use http_types::{
headers::HeaderValue as HttpTypesHeaderValue,
Expand Down Expand Up @@ -258,7 +258,7 @@ impl Middleware for SurfMiddleware<'_> {
let url = self.req.url().clone();
let mut res =
self.next.run(self.req.clone().into(), self.client.clone()).await?;
let mut headers = HashMap::new();
let mut headers = HttpHeaders::new();
for header in res.iter() {
headers.insert(
header.0.as_str().to_owned(),
Expand Down
37 changes: 18 additions & 19 deletions http-cache-ureq/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -584,11 +584,14 @@ impl<'a, T: CacheManager> CachedRequestBuilder<'a, T> {
if cache_status_headers {
cached_response
.headers
.insert(XCACHE.to_string(), HitOrMiss::MISS.to_string());
cached_response.headers.insert(
XCACHELOOKUP.to_string(),
HitOrMiss::MISS.to_string(),
);
.entry(XCACHE.to_string())
.or_insert_with(Vec::new)
.push(HitOrMiss::MISS.to_string());
cached_response
.headers
.entry(XCACHELOOKUP.to_string())
.or_insert_with(Vec::new)
.push(HitOrMiss::MISS.to_string());
}

Ok(cached_response)
Expand Down Expand Up @@ -664,18 +667,9 @@ fn convert_ureq_response_to_http_response(
url: &str,
) -> Result<HttpResponse, HttpCacheError> {
let status = response.status();
let mut headers = HashMap::new();

// Copy headers
for (name, value) in response.headers() {
let value_str = value.to_str().map_err(|e| {
HttpCacheError::http(Box::new(std::io::Error::other(format!(
"Invalid header value: {}",
e
))))
})?;
headers.insert(name.as_str().to_string(), value_str.to_string());
}
let headers = response.headers().into();

// Read body using read_to_string
let body_string = response.body_mut().read_to_string().map_err(|e| {
Expand Down Expand Up @@ -708,7 +702,7 @@ fn convert_ureq_response_to_http_response(
#[derive(Debug)]
pub struct CachedResponse {
status: u16,
headers: HashMap<String, String>,
headers: HashMap<String, Vec<String>>,
body: Vec<u8>,
url: String,
cached: bool,
Expand All @@ -727,7 +721,9 @@ impl CachedResponse {

/// Get a header value
pub fn header(&self, name: &str) -> Option<&str> {
self.headers.get(name).map(|s| s.as_str())
self.headers
.get(name)
.and_then(|values| values.first().map(|s| s.as_str()))
}

/// Get all header names
Expand Down Expand Up @@ -786,7 +782,10 @@ impl CachedResponse {
let mut headers = HashMap::new();
for (name, value) in response.headers() {
let value_str = value.to_str().unwrap_or("");
headers.insert(name.as_str().to_string(), value_str.to_string());
headers
.entry(name.as_str().to_string())
.or_insert_with(Vec::new)
.push(value_str.to_string());
}

// Note: Cache headers will be added by the cache system based on cache_status_headers option
Expand All @@ -810,7 +809,7 @@ impl From<HttpResponse> for CachedResponse {
// based on the cache_status_headers option, so don't add them here
Self {
status: response.status,
headers: response.headers,
headers: response.headers.into(),
body: response.body,
url: response.url.to_string(),
cached: true,
Expand Down
183 changes: 170 additions & 13 deletions http-cache/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -424,13 +424,180 @@ fn determine_scheme(host: &str, headers: &http::HeaderMap) -> Result<String> {
}
}

/// Represents HTTP headers in either legacy or modern format
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum HttpHeaders {
/// Modern header representation - allows multiple values per key
Modern(HashMap<String, Vec<String>>),
/// Legacy header representation - kept for backward compatibility with deserialization
Legacy(HashMap<String, String>),
}

impl HttpHeaders {
/// Creates a new empty HttpHeaders in modern format
pub fn new() -> Self {
HttpHeaders::Modern(HashMap::new())
}

/// Inserts a header key-value pair
pub fn insert(&mut self, key: String, value: String) {
match self {
HttpHeaders::Legacy(legacy) => {
legacy.insert(key, value);
}
HttpHeaders::Modern(modern) => {
modern.entry(key).or_default().push(value);
}
}
}

/// Retrieves the first value for a given header key
pub fn get(&self, key: &str) -> Option<&String> {
match self {
HttpHeaders::Legacy(legacy) => legacy.get(key),
HttpHeaders::Modern(modern) => {
modern.get(key).and_then(|vals| vals.first())
}
}
}

/// Removes a header key and its associated values
pub fn remove(&mut self, key: &str) {
match self {
HttpHeaders::Legacy(legacy) => {
legacy.remove(key);
}
HttpHeaders::Modern(modern) => {
modern.remove(key);
}
}
}

/// Returns an iterator over the header key-value pairs
pub fn iter(&self) -> HttpHeadersIterator<'_> {
match self {
HttpHeaders::Legacy(legacy) => {
HttpHeadersIterator { inner: legacy.iter().collect(), index: 0 }
}
HttpHeaders::Modern(modern) => HttpHeadersIterator {
inner: modern
.iter()
.flat_map(|(k, vals)| vals.iter().map(move |v| (k, v)))
.collect(),
index: 0,
},
}
}
}

impl From<&http::HeaderMap> for HttpHeaders {
fn from(headers: &http::HeaderMap) -> Self {
let mut http_headers = HttpHeaders::new();
for (k, v) in headers.iter() {
if let Ok(value_str) = v.to_str() {
http_headers.insert(k.to_string(), value_str.to_string());
}
}
http_headers
}
}

impl From<HttpHeaders> for HashMap<String, Vec<String>> {
fn from(headers: HttpHeaders) -> Self {
match headers {
HttpHeaders::Legacy(legacy) => {
legacy.into_iter().map(|(k, v)| (k, vec![v])).collect()
}
HttpHeaders::Modern(modern) => modern,
}
}
}

impl Default for HttpHeaders {
fn default() -> Self {
HttpHeaders::new()
}
}

impl IntoIterator for HttpHeaders {
type Item = (String, String);
type IntoIter = HttpHeadersIntoIterator;

fn into_iter(self) -> Self::IntoIter {
HttpHeadersIntoIterator {
inner: match self {
HttpHeaders::Legacy(legacy) => legacy.into_iter().collect(),
HttpHeaders::Modern(modern) => modern
.into_iter()
.flat_map(|(k, vals)| {
vals.into_iter().map(move |v| (k.clone(), v))
})
.collect(),
},
index: 0,
}
}
}

/// Iterator for HttpHeaders
#[derive(Debug)]
pub struct HttpHeadersIntoIterator {
inner: Vec<(String, String)>,
index: usize,
}

impl Iterator for HttpHeadersIntoIterator {
type Item = (String, String);

fn next(&mut self) -> Option<Self::Item> {
if self.index < self.inner.len() {
let item = self.inner[self.index].clone();
self.index += 1;
Some(item)
} else {
None
}
}
}

impl<'a> IntoIterator for &'a HttpHeaders {
type Item = (&'a String, &'a String);
type IntoIter = HttpHeadersIterator<'a>;

fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}

/// Iterator for HttpHeaders references
#[derive(Debug)]
pub struct HttpHeadersIterator<'a> {
inner: Vec<(&'a String, &'a String)>,
index: usize,
}

impl<'a> Iterator for HttpHeadersIterator<'a> {
type Item = (&'a String, &'a String);

fn next(&mut self) -> Option<Self::Item> {
if self.index < self.inner.len() {
let item = self.inner[self.index];
self.index += 1;
Some(item)
} else {
None
}
}
}

/// A basic generic type that represents an HTTP response
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct HttpResponse {
/// HTTP response body
pub body: Vec<u8>,
/// HTTP response headers
pub headers: HashMap<String, String>,
pub headers: HttpHeaders,
/// HTTP response status code
pub status: u16,
/// HTTP response url
Expand Down Expand Up @@ -1175,16 +1342,6 @@ impl HttpCacheOptions {
self.create_cache_key(parts, Some(method_override))
}

/// Converts http::HeaderMap to HashMap<String, String> for HttpResponse
fn headers_to_hashmap(
headers: &http::HeaderMap,
) -> HashMap<String, String> {
headers
.iter()
.map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
.collect()
}

/// Converts HttpResponse to http::Response with the given body type
pub fn http_response_to_response<B>(
http_response: &HttpResponse,
Expand Down Expand Up @@ -1214,7 +1371,7 @@ impl HttpCacheOptions {
) -> Result<HttpResponse> {
Ok(HttpResponse {
body: vec![], // We don't need the full body for cache mode decision
headers: Self::headers_to_hashmap(&parts.headers),
headers: (&parts.headers).into(),
status: parts.status.as_u16(),
url: extract_url_from_request_parts(request_parts)?,
version: parts.version.try_into()?,
Expand Down Expand Up @@ -1536,7 +1693,7 @@ impl<T: CacheManager> HttpCache<T> {
// ENOTCACHED
let mut res = HttpResponse {
body: b"GatewayTimeout".to_vec(),
headers: HashMap::default(),
headers: HttpHeaders::default(),
status: 504,
url: middleware.url()?,
version: HttpVersion::Http11,
Expand Down
Loading