Skip to content

Commit a253ca1

Browse files
committed
Add with_form method for URL-encoded form data
Add support for sending application/x-www-form-urlencoded form data in HTTP requests via a new `with_form()` method on `Request`. - Add `serde_urlencoded` dependency for form serialization - Add `SerdeUrlencodeError` variant to handle encoding failures - Add unit tests for form encoding behavior - Add integration tests for form submission
1 parent 20b58c4 commit a253ca1

File tree

4 files changed

+129
-2
lines changed

4 files changed

+129
-2
lines changed

bitreq/Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ maintenance = { status = "experimental" }
1717

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

2324
# For the proxy feature:
@@ -51,7 +52,7 @@ default = ["std"]
5152
std = []
5253

5354
log = ["dep:log"]
54-
json-using-serde = ["serde", "serde_json"]
55+
json-using-serde = ["serde_json"]
5556
proxy = ["base64", "std"]
5657

5758
https = ["https-rustls"]

bitreq/src/error.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ pub enum Error {
1212
#[cfg(feature = "json-using-serde")]
1313
/// Ran into a Serde error.
1414
SerdeJsonError(serde_json::Error),
15+
16+
/// Ran into a Serde Urlencode error.
17+
SerdeUrlencodeError(serde_urlencoded::ser::Error),
18+
1519
/// The response body contains invalid UTF-8, so the `as_str()`
1620
/// conversion failed.
1721
InvalidUtf8InBody(str::Utf8Error),
@@ -95,6 +99,7 @@ impl fmt::Display for Error {
9599
match self {
96100
#[cfg(feature = "json-using-serde")]
97101
SerdeJsonError(err) => write!(f, "{}", err),
102+
SerdeUrlencodeError(err) => write!(f, "{}", err),
98103
#[cfg(feature = "std")]
99104
IoError(err) => write!(f, "{}", err),
100105
InvalidUtf8InBody(err) => write!(f, "{}", err),

bitreq/src/request.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,16 @@ impl Request {
159159
self.with_header("Content-Length", format!("{}", body_length))
160160
}
161161

162+
/// Add support for form url encode
163+
pub fn with_form<T: serde::ser::Serialize>(mut self, body: &T) -> Result<Request, Error> {
164+
self.headers
165+
.insert("Content-Type".to_string(), "application/x-www-form-urlencoded".to_string());
166+
match serde_urlencoded::to_string(&body) {
167+
Ok(json) => Ok(self.with_body(json)),
168+
Err(err) => Err(Error::SerdeUrlencodeError(err)),
169+
}
170+
}
171+
162172
/// Adds given key and value as query parameter to request url
163173
/// (resource).
164174
///
@@ -711,3 +721,76 @@ mod encoding_tests {
711721
assert_eq!(&req.url.path_and_query, "/?%C3%B3w%C3%B2=what%27s%20this?%20%F0%9F%91%80");
712722
}
713723
}
724+
725+
#[cfg(all(test, feature = "std"))]
726+
mod form_tests {
727+
use alloc::collections::BTreeMap;
728+
729+
use super::post;
730+
731+
#[test]
732+
fn test_with_form_sets_content_type() {
733+
let mut form_data = BTreeMap::new();
734+
form_data.insert("key", "value");
735+
736+
let req =
737+
post("http://www.example.org").with_form(&form_data).expect("form encoding failed");
738+
assert_eq!(
739+
req.headers.get("Content-Type"),
740+
Some(&"application/x-www-form-urlencoded".to_string())
741+
);
742+
}
743+
744+
#[test]
745+
fn test_with_form_sets_content_length() {
746+
let mut form_data = BTreeMap::new();
747+
form_data.insert("key", "value");
748+
749+
let req =
750+
post("http://www.example.org").with_form(&form_data).expect("form encoding failed");
751+
// "key=value" is 9 bytes
752+
assert_eq!(req.headers.get("Content-Length"), Some(&"9".to_string()));
753+
}
754+
755+
#[test]
756+
fn test_with_form_encodes_body() {
757+
let mut form_data = BTreeMap::new();
758+
form_data.insert("name", "test");
759+
form_data.insert("value", "42");
760+
761+
let req =
762+
post("http://www.example.org").with_form(&form_data).expect("form encoding failed");
763+
let body = req.body.expect("body should be set");
764+
let body_str = String::from_utf8(body).expect("body should be valid UTF-8");
765+
// BTreeMap provides ordered iteration
766+
assert_eq!(body_str, "name=test&value=42");
767+
}
768+
769+
#[test]
770+
fn test_with_form_encodes_special_characters() {
771+
let mut form_data = BTreeMap::new();
772+
form_data.insert("message", "hello world");
773+
form_data.insert("special", "a&b=c");
774+
775+
let req =
776+
post("http://www.example.org").with_form(&form_data).expect("form encoding failed");
777+
let body = req.body.expect("body should be set");
778+
let body_str = String::from_utf8(body).expect("body should be valid UTF-8");
779+
// Spaces are encoded as + and special chars are percent-encoded
780+
assert!(
781+
body_str.contains("message=hello+world") || body_str.contains("message=hello%20world")
782+
);
783+
assert!(body_str.contains("special=a%26b%3Dc"));
784+
}
785+
786+
#[test]
787+
fn test_with_form_empty() {
788+
let form_data: BTreeMap<&str, &str> = BTreeMap::new();
789+
790+
let req =
791+
post("http://www.example.org").with_form(&form_data).expect("form encoding failed");
792+
let body = req.body.expect("body should be set");
793+
assert!(body.is_empty());
794+
assert_eq!(req.headers.get("Content-Length"), Some(&"0".to_string()));
795+
}
796+
}

bitreq/tests/main.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,44 @@ async fn test_json_using_serde() {
3232
assert_eq!(actual_json, original_json);
3333
}
3434

35+
#[tokio::test]
36+
async fn test_with_form() {
37+
use std::collections::HashMap;
38+
39+
setup();
40+
let mut form_data = HashMap::new();
41+
form_data.insert("name", "test");
42+
form_data.insert("value", "42");
43+
44+
let response = make_request(
45+
bitreq::post(url("/echo")).with_form(&form_data).expect("form encoding failed"),
46+
)
47+
.await;
48+
let body = response.as_str().expect("response body should be valid UTF-8");
49+
// Form data is URL encoded, order may vary due to HashMap
50+
assert!(body.contains("name=test"));
51+
assert!(body.contains("value=42"));
52+
}
53+
54+
#[tokio::test]
55+
async fn test_with_form_special_chars() {
56+
use std::collections::HashMap;
57+
58+
setup();
59+
let mut form_data = HashMap::new();
60+
form_data.insert("message", "hello world");
61+
form_data.insert("special", "a&b=c");
62+
63+
let response = make_request(
64+
bitreq::post(url("/echo")).with_form(&form_data).expect("form encoding failed"),
65+
)
66+
.await;
67+
let body = response.as_str().expect("response body should be valid UTF-8");
68+
// Special characters should be URL encoded
69+
assert!(body.contains("message=hello+world") || body.contains("message=hello%20world"));
70+
assert!(body.contains("special=a%26b%3Dc"));
71+
}
72+
3573
#[tokio::test]
3674
async fn test_timeout_too_low() {
3775
setup();

0 commit comments

Comments
 (0)