Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 3 additions & 2 deletions bitreq/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ maintenance = { status = "experimental" }

[dependencies]
# For the json-using-serde feature:
serde = { version = "1.0.101", default-features = false, optional = true }
serde = { version = "1.0.101" }
serde_urlencoded = "0.7.1"
serde_json = { version = "1.0.0", default-features = false, features = ["std"], optional = true }

# For the proxy feature:
Expand Down Expand Up @@ -51,7 +52,7 @@ default = ["std"]
std = []

log = ["dep:log"]
json-using-serde = ["serde", "serde_json"]
json-using-serde = ["serde_json"]
proxy = ["base64", "std"]

https = ["https-rustls"]
Expand Down
5 changes: 5 additions & 0 deletions bitreq/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ pub enum Error {
#[cfg(feature = "json-using-serde")]
/// Ran into a Serde error.
SerdeJsonError(serde_json::Error),

/// Ran into a Serde Urlencode error.
SerdeUrlencodeError(serde_urlencoded::ser::Error),

/// The response body contains invalid UTF-8, so the `as_str()`
/// conversion failed.
InvalidUtf8InBody(str::Utf8Error),
Expand Down Expand Up @@ -95,6 +99,7 @@ impl fmt::Display for Error {
match self {
#[cfg(feature = "json-using-serde")]
SerdeJsonError(err) => write!(f, "{}", err),
SerdeUrlencodeError(err) => write!(f, "{}", err),
#[cfg(feature = "std")]
IoError(err) => write!(f, "{}", err),
InvalidUtf8InBody(err) => write!(f, "{}", err),
Expand Down
83 changes: 83 additions & 0 deletions bitreq/src/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,16 @@ impl Request {
self.with_header("Content-Length", format!("{}", body_length))
}

/// Add support for form url encode
pub fn with_form<T: serde::ser::Serialize>(mut self, body: &T) -> Result<Request, Error> {
self.headers
.insert("Content-Type".to_string(), "application/x-www-form-urlencoded".to_string());
match serde_urlencoded::to_string(&body) {
Ok(json) => Ok(self.with_body(json)),
Err(err) => Err(Error::SerdeUrlencodeError(err)),
}
}

/// Adds given key and value as query parameter to request url
/// (resource).
///
Expand Down Expand Up @@ -711,3 +721,76 @@ mod encoding_tests {
assert_eq!(&req.url.path_and_query, "/?%C3%B3w%C3%B2=what%27s%20this?%20%F0%9F%91%80");
}
}

#[cfg(all(test, feature = "std"))]
mod form_tests {
use alloc::collections::BTreeMap;

use super::post;

#[test]
fn test_with_form_sets_content_type() {
let mut form_data = BTreeMap::new();
form_data.insert("key", "value");

let req =
post("http://www.example.org").with_form(&form_data).expect("form encoding failed");
assert_eq!(
req.headers.get("Content-Type"),
Some(&"application/x-www-form-urlencoded".to_string())
);
}

#[test]
fn test_with_form_sets_content_length() {
let mut form_data = BTreeMap::new();
form_data.insert("key", "value");

let req =
post("http://www.example.org").with_form(&form_data).expect("form encoding failed");
// "key=value" is 9 bytes
assert_eq!(req.headers.get("Content-Length"), Some(&"9".to_string()));
}

#[test]
fn test_with_form_encodes_body() {
let mut form_data = BTreeMap::new();
form_data.insert("name", "test");
form_data.insert("value", "42");

let req =
post("http://www.example.org").with_form(&form_data).expect("form encoding failed");
let body = req.body.expect("body should be set");
let body_str = String::from_utf8(body).expect("body should be valid UTF-8");
// BTreeMap provides ordered iteration
assert_eq!(body_str, "name=test&value=42");
}

#[test]
fn test_with_form_encodes_special_characters() {
let mut form_data = BTreeMap::new();
form_data.insert("message", "hello world");
form_data.insert("special", "a&b=c");

let req =
post("http://www.example.org").with_form(&form_data).expect("form encoding failed");
let body = req.body.expect("body should be set");
let body_str = String::from_utf8(body).expect("body should be valid UTF-8");
// Spaces are encoded as + and special chars are percent-encoded
assert!(
body_str.contains("message=hello+world") || body_str.contains("message=hello%20world")
);
assert!(body_str.contains("special=a%26b%3Dc"));
}

#[test]
fn test_with_form_empty() {
let form_data: BTreeMap<&str, &str> = BTreeMap::new();

let req =
post("http://www.example.org").with_form(&form_data).expect("form encoding failed");
let body = req.body.expect("body should be set");
assert!(body.is_empty());
assert_eq!(req.headers.get("Content-Length"), Some(&"0".to_string()));
}
}
38 changes: 38 additions & 0 deletions bitreq/tests/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,44 @@ async fn test_json_using_serde() {
assert_eq!(actual_json, original_json);
}

#[tokio::test]
async fn test_with_form() {
use std::collections::HashMap;

setup();
let mut form_data = HashMap::new();
form_data.insert("name", "test");
form_data.insert("value", "42");

let response = make_request(
bitreq::post(url("/echo")).with_form(&form_data).expect("form encoding failed"),
)
.await;
let body = response.as_str().expect("response body should be valid UTF-8");
// Form data is URL encoded, order may vary due to HashMap
assert!(body.contains("name=test"));
assert!(body.contains("value=42"));
}

#[tokio::test]
async fn test_with_form_special_chars() {
use std::collections::HashMap;

setup();
let mut form_data = HashMap::new();
form_data.insert("message", "hello world");
form_data.insert("special", "a&b=c");

let response = make_request(
bitreq::post(url("/echo")).with_form(&form_data).expect("form encoding failed"),
)
.await;
let body = response.as_str().expect("response body should be valid UTF-8");
// Special characters should be URL encoded
assert!(body.contains("message=hello+world") || body.contains("message=hello%20world"));
assert!(body.contains("special=a%26b%3Dc"));
}

#[tokio::test]
async fn test_timeout_too_low() {
setup();
Expand Down
Loading