Skip to content

Commit

Permalink
Add struct Headers to allow duplicated headers
Browse files Browse the repository at this point in the history
  • Loading branch information
emilk committed Jan 17, 2024
1 parent 1563852 commit 9476eb8
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 57 deletions.
21 changes: 4 additions & 17 deletions ehttp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ pub async fn fetch_async(request: Request) -> Result<Response> {
}

mod types;
pub use types::{Error, PartialResponse, Request, Response, Result};
pub use types::{Error, Headers, PartialResponse, Request, Response, Result};

#[cfg(not(target_arch = "wasm32"))]
mod native;
Expand All @@ -88,20 +88,7 @@ pub use web::spawn_future;
#[cfg(feature = "streaming")]
pub mod streaming;

/// Helper for constructing [`Request::headers`].
/// ```
/// use ehttp::Request;
/// let request = Request {
/// headers: ehttp::headers(&[
/// ("Accept", "*/*"),
/// ("Content-Type", "text/plain; charset=utf-8"),
/// ]),
/// ..Request::get("https://www.example.com")
/// };
/// ```
pub fn headers(headers: &[(&str, &str)]) -> std::collections::BTreeMap<String, String> {
headers
.iter()
.map(|e| (e.0.to_owned(), e.1.to_owned()))
.collect()
#[deprecated = "Use ehttp::Headers::new"]
pub fn headers(headers: &[(&str, &str)]) -> Headers {
Headers::new(headers)
}
13 changes: 5 additions & 8 deletions ehttp/src/native.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use std::collections::BTreeMap;

use crate::{Request, Response};

#[cfg(feature = "native-async")]
Expand Down Expand Up @@ -27,8 +25,8 @@ use async_channel::{Receiver, Sender};
pub fn fetch_blocking(request: &Request) -> crate::Result<Response> {
let mut req = ureq::request(&request.method, &request.url);

for header in &request.headers {
req = req.set(header.0, header.1);
for (k, v) in &request.headers {
req = req.set(k, v);
}

let resp = if request.body.is_empty() {
Expand All @@ -46,11 +44,10 @@ pub fn fetch_blocking(request: &Request) -> crate::Result<Response> {
let url = resp.get_url().to_owned();
let status = resp.status();
let status_text = resp.status_text().to_owned();
let mut headers = BTreeMap::new();
let mut headers = crate::Headers::default();
for key in &resp.headers_names() {
if let Some(value) = resp.header(key) {
// lowercase for easy lookup
headers.insert(key.to_ascii_lowercase(), value.to_owned());
headers.insert(key, value.to_owned());
}
}

Expand All @@ -70,8 +67,8 @@ pub fn fetch_blocking(request: &Request) -> crate::Result<Response> {
ok,
status,
status_text,
bytes,
headers,
bytes,
};
Ok(response)
}
Expand Down
8 changes: 3 additions & 5 deletions ehttp/src/streaming/native.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use std::collections::BTreeMap;
use std::ops::ControlFlow;

use crate::Request;
Expand All @@ -12,8 +11,8 @@ pub fn fetch_streaming_blocking(
) {
let mut req = ureq::request(&request.method, &request.url);

for header in &request.headers {
req = req.set(header.0, header.1);
for (k, v) in &request.headers {
req = req.set(k, v);
}

let resp = if request.body.is_empty() {
Expand All @@ -34,10 +33,9 @@ pub fn fetch_streaming_blocking(
let url = resp.get_url().to_owned();
let status = resp.status();
let status_text = resp.status_text().to_owned();
let mut headers = BTreeMap::new();
let mut headers = crate::Headers::default();
for key in &resp.headers_names() {
if let Some(value) = resp.header(key) {
// lowercase for easy lookup
headers.insert(key.to_ascii_lowercase(), value.to_owned());
}
}
Expand Down
124 changes: 106 additions & 18 deletions ehttp/src/types.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,82 @@
use std::collections::BTreeMap;
/// Headers in a [`Request`] or [`Response`].
///
/// Note that the same header key can appear twice.
#[derive(Clone, Debug, Default)]
pub struct Headers {
/// Name-value pairs.
pub headers: Vec<(String, String)>,
}

impl Headers {
/// ```
/// use ehttp::Request;
/// let request = Request {
/// headers: ehttp::Headers::new(&[
/// ("Accept", "*/*"),
/// ("Content-Type", "text/plain; charset=utf-8"),
/// ]),
/// ..Request::get("https://www.example.com")
/// };
/// ```
pub fn new(headers: &[(&str, &str)]) -> Self {
Self {
headers: headers
.iter()
.map(|e| (e.0.to_owned(), e.1.to_owned()))
.collect(),
}
}

/// Will add the key/value pair to the headers.
///
/// If the key already exists, it will also be kept,
/// so the same key can appear twice.
pub fn insert(&mut self, key: impl ToString, value: impl ToString) {
self.headers.push((key.to_string(), value.to_string()));
}

/// Get the value of the first header with the given key.
///
/// The lookup is case-insentive.
pub fn get(&self, key: &str) -> Option<&str> {
let key = key.to_string().to_lowercase();
self.headers
.iter()
.find(|(k, _)| k.to_lowercase() == key)
.map(|(_, v)| v.as_str())
}

/// Get all the values that match the given key.
///
/// The lookup is case-insentive.
pub fn get_all(&self, key: &str) -> impl Iterator<Item = &str> {
let key = key.to_string().to_lowercase();
self.headers
.iter()
.filter(move |(k, _)| k.to_lowercase() == key)
.map(|(_, v)| v.as_str())
}
}

impl IntoIterator for Headers {
type Item = (String, String);
type IntoIter = std::vec::IntoIter<Self::Item>;

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

impl<'h> IntoIterator for &'h Headers {
type Item = &'h (String, String);
type IntoIter = std::slice::Iter<'h, (String, String)>;

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

// ----------------------------------------------------------------------------

/// A simple HTTP request.
#[derive(Clone, Debug)]
Expand All @@ -13,7 +91,7 @@ pub struct Request {
pub body: Vec<u8>,

/// ("Accept", "*/*"), …
pub headers: BTreeMap<String, String>,
pub headers: Headers,
}

impl Request {
Expand All @@ -24,7 +102,7 @@ impl Request {
method: "GET".to_owned(),
url: url.to_string(),
body: vec![],
headers: crate::headers(&[("Accept", "*/*")]),
headers: Headers::new(&[("Accept", "*/*")]),
}
}

Expand All @@ -35,7 +113,7 @@ impl Request {
method: "HEAD".to_owned(),
url: url.to_string(),
body: vec![],
headers: crate::headers(&[("Accept", "*/*")]),
headers: Headers::new(&[("Accept", "*/*")]),
}
}

Expand All @@ -46,7 +124,7 @@ impl Request {
method: "POST".to_owned(),
url: url.to_string(),
body,
headers: crate::headers(&[
headers: Headers::new(&[
("Accept", "*/*"),
("Content-Type", "text/plain; charset=utf-8"),
]),
Expand All @@ -55,7 +133,7 @@ impl Request {
}

/// Response from a completed HTTP request.
#[derive(Clone, Eq, PartialEq)]
#[derive(Clone)]
pub struct Response {
/// The URL we ended up at. This can differ from the request url when we have followed redirects.
pub url: String,
Expand All @@ -69,8 +147,8 @@ pub struct Response {
/// Status text (e.g. "File not found" for status code `404`).
pub status_text: String,

/// The returned headers. All header names are lower-case.
pub headers: BTreeMap<String, String>,
/// The returned headers.
pub headers: Headers,

/// The raw bytes of the response body.
pub bytes: Vec<u8>,
Expand All @@ -81,26 +159,36 @@ impl Response {
std::str::from_utf8(&self.bytes).ok()
}

/// Convenience for getting the `content-type` header.
pub fn content_type(&self) -> Option<&str> {
self.headers.get("content-type").map(|s| s.as_str())
self.headers.get("content-type")
}
}

impl std::fmt::Debug for Response {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
url,
ok,
status,
status_text,
headers,
bytes,
} = self;

fmt.debug_struct("Response")
.field("url", &self.url)
.field("ok", &self.ok)
.field("status", &self.status)
.field("status_text", &self.status_text)
// .field("bytes", &self.bytes)
.field("headers", &self.headers)
.field("url", url)
.field("ok", ok)
.field("status", status)
.field("status_text", status_text)
.field("headers", headers)
.field("bytes", &format!("{} bytes", bytes.len()))
.finish_non_exhaustive()
}
}

/// An HTTP response status line and headers used for the [`streaming`](crate::streaming) API.
#[derive(Clone, Debug, Eq, PartialEq)]
#[derive(Clone, Debug)]
pub struct PartialResponse {
/// The URL we ended up at. This can differ from the request url when we have followed redirects.
pub url: String,
Expand All @@ -114,8 +202,8 @@ pub struct PartialResponse {
/// Status text (e.g. "File not found" for status code `404`).
pub status_text: String,

/// The returned headers. All header names are lower-case.
pub headers: BTreeMap<String, String>,
/// The returned headers.
pub headers: Headers,
}

impl PartialResponse {
Expand Down
10 changes: 4 additions & 6 deletions ehttp/src/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ pub(crate) async fn fetch_base(request: &Request) -> Result<web_sys::Response, J

let js_request = web_sys::Request::new_with_str_and_init(&request.url, &opts)?;

for h in &request.headers {
js_request.headers().set(h.0, h.1)?;
for (k, v) in &request.headers {
js_request.headers().set(k, v)?;
}

let window = web_sys::window().unwrap();
Expand All @@ -72,21 +72,19 @@ pub(crate) fn get_response_base(response: &web_sys::Response) -> Result<PartialR
.expect("headers try_iter")
.expect("headers have an iterator");

let mut headers = std::collections::BTreeMap::new();
let mut headers = crate::Headers::default();
for item in js_iter {
let item = item.expect("headers iterator");
let array: js_sys::Array = item.into();
let v: Vec<JsValue> = array.to_vec();

let mut key = v[0]
let key = v[0]
.as_string()
.ok_or_else(|| JsValue::from_str("headers name"))?;
let value = v[1]
.as_string()
.ok_or_else(|| JsValue::from_str("headers value"))?;

// for easy lookup
key.make_ascii_lowercase();
headers.insert(key, value);
}

Expand Down
6 changes: 3 additions & 3 deletions example_eframe/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,9 +268,9 @@ fn response_ui(ui: &mut egui::Ui, response: &ehttp::Response) {
egui::Grid::new("response_headers")
.spacing(egui::vec2(ui.spacing().item_spacing.x * 2.0, 0.0))
.show(ui, |ui| {
for header in &response.headers {
ui.label(header.0);
ui.label(header.1);
for (k, v) in &response.headers {
ui.label(k);
ui.label(v);
ui.end_row();
}
})
Expand Down

0 comments on commit 9476eb8

Please sign in to comment.