1use curl::easy::{Easy2, Handler, List, WriteError};
47use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
48use std::borrow::Cow;
49use thiserror::Error;
50use url::Url;
51
52pub struct Response {
54 pub status: StatusCode,
56 pub body: Vec<u8>,
58}
59
60macro_rules! status_codes {
61 ($(
62 $variant:ident => ($code:literal, $reason:literal, $const_name:ident)
63 ),+ $(,)?) => {
64 #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
66 #[repr(u16)]
67 pub enum StatusCode {
68 $(
69 #[doc = $reason]
70 $variant = $code,
71 )+
72 }
73
74 impl StatusCode {
75 pub const fn as_u16(self) -> u16 {
77 self as u16
78 }
79
80 pub const fn canonical_reason(self) -> &'static str {
82 match self {
83 $(StatusCode::$variant => $reason,)+
84 }
85 }
86
87 pub const fn from_u16(code: u16) -> Option<Self> {
89 match code {
90 $($code => Some(StatusCode::$variant),)+
91 _ => None,
92 }
93 }
94
95 $(
96 pub const $const_name: StatusCode = StatusCode::$variant;
98 )+
99 }
100
101 impl std::fmt::Display for StatusCode {
102 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103 write!(f, "{} {}", self.as_u16(), self.canonical_reason())
104 }
105 }
106 };
107}
108
109status_codes! {
110 Continue => (100, "Continue", CONTINUE),
111 SwitchingProtocols => (101, "Switching Protocols", SWITCHING_PROTOCOLS),
112 Processing => (102, "Processing", PROCESSING),
113 EarlyHints => (103, "Early Hints", EARLY_HINTS),
114 Ok => (200, "OK", OK),
115 Created => (201, "Created", CREATED),
116 Accepted => (202, "Accepted", ACCEPTED),
117 NonAuthoritativeInformation => (203, "Non-Authoritative Information", NON_AUTHORITATIVE_INFORMATION),
118 NoContent => (204, "No Content", NO_CONTENT),
119 ResetContent => (205, "Reset Content", RESET_CONTENT),
120 PartialContent => (206, "Partial Content", PARTIAL_CONTENT),
121 MultiStatus => (207, "Multi-Status", MULTI_STATUS),
122 AlreadyReported => (208, "Already Reported", ALREADY_REPORTED),
123 ImUsed => (226, "IM Used", IM_USED),
124 MultipleChoices => (300, "Multiple Choices", MULTIPLE_CHOICES),
125 MovedPermanently => (301, "Moved Permanently", MOVED_PERMANENTLY),
126 Found => (302, "Found", FOUND),
127 SeeOther => (303, "See Other", SEE_OTHER),
128 NotModified => (304, "Not Modified", NOT_MODIFIED),
129 UseProxy => (305, "Use Proxy", USE_PROXY),
130 TemporaryRedirect => (307, "Temporary Redirect", TEMPORARY_REDIRECT),
131 PermanentRedirect => (308, "Permanent Redirect", PERMANENT_REDIRECT),
132 BadRequest => (400, "Bad Request", BAD_REQUEST),
133 Unauthorized => (401, "Unauthorized", UNAUTHORIZED),
134 PaymentRequired => (402, "Payment Required", PAYMENT_REQUIRED),
135 Forbidden => (403, "Forbidden", FORBIDDEN),
136 NotFound => (404, "Not Found", NOT_FOUND),
137 MethodNotAllowed => (405, "Method Not Allowed", METHOD_NOT_ALLOWED),
138 NotAcceptable => (406, "Not Acceptable", NOT_ACCEPTABLE),
139 ProxyAuthenticationRequired => (407, "Proxy Authentication Required", PROXY_AUTHENTICATION_REQUIRED),
140 RequestTimeout => (408, "Request Timeout", REQUEST_TIMEOUT),
141 Conflict => (409, "Conflict", CONFLICT),
142 Gone => (410, "Gone", GONE),
143 LengthRequired => (411, "Length Required", LENGTH_REQUIRED),
144 PreconditionFailed => (412, "Precondition Failed", PRECONDITION_FAILED),
145 PayloadTooLarge => (413, "Content Too Large", PAYLOAD_TOO_LARGE),
146 UriTooLong => (414, "URI Too Long", URI_TOO_LONG),
147 UnsupportedMediaType => (415, "Unsupported Media Type", UNSUPPORTED_MEDIA_TYPE),
148 RangeNotSatisfiable => (416, "Range Not Satisfiable", RANGE_NOT_SATISFIABLE),
149 ExpectationFailed => (417, "Expectation Failed", EXPECTATION_FAILED),
150 ImATeapot => (418, "I'm a teapot", IM_A_TEAPOT),
151 MisdirectedRequest => (421, "Misdirected Request", MISDIRECTED_REQUEST),
152 UnprocessableEntity => (422, "Unprocessable Content", UNPROCESSABLE_ENTITY),
153 Locked => (423, "Locked", LOCKED),
154 FailedDependency => (424, "Failed Dependency", FAILED_DEPENDENCY),
155 TooEarly => (425, "Too Early", TOO_EARLY),
156 UpgradeRequired => (426, "Upgrade Required", UPGRADE_REQUIRED),
157 PreconditionRequired => (428, "Precondition Required", PRECONDITION_REQUIRED),
158 TooManyRequests => (429, "Too Many Requests", TOO_MANY_REQUESTS),
159 RequestHeaderFieldsTooLarge => (431, "Request Header Fields Too Large", REQUEST_HEADER_FIELDS_TOO_LARGE),
160 UnavailableForLegalReasons => (451, "Unavailable For Legal Reasons", UNAVAILABLE_FOR_LEGAL_REASONS),
161 InternalServerError => (500, "Internal Server Error", INTERNAL_SERVER_ERROR),
162 NotImplemented => (501, "Not Implemented", NOT_IMPLEMENTED),
163 BadGateway => (502, "Bad Gateway", BAD_GATEWAY),
164 ServiceUnavailable => (503, "Service Unavailable", SERVICE_UNAVAILABLE),
165 GatewayTimeout => (504, "Gateway Timeout", GATEWAY_TIMEOUT),
166 HttpVersionNotSupported => (505, "HTTP Version Not Supported", HTTP_VERSION_NOT_SUPPORTED),
167 VariantAlsoNegotiates => (506, "Variant Also Negotiates", VARIANT_ALSO_NEGOTIATES),
168 InsufficientStorage => (507, "Insufficient Storage", INSUFFICIENT_STORAGE),
169 LoopDetected => (508, "Loop Detected", LOOP_DETECTED),
170 NotExtended => (510, "Not Extended", NOT_EXTENDED),
171 NetworkAuthenticationRequired => (511, "Network Authentication Required", NETWORK_AUTHENTICATION_REQUIRED),
172}
173
174#[derive(Debug, Error)]
176pub enum Error {
177 #[error("curl error: {0}")]
179 Curl(#[from] curl::Error),
180 #[error("invalid url: {0}")]
182 InvalidUrl(String),
183 #[error("invalid header value for {0}")]
185 InvalidHeaderValue(String),
186 #[error("invalid header name: {0}")]
188 InvalidHeaderName(String),
189 #[error("invalid HTTP status code: {0}")]
191 InvalidStatusCode(u32),
192}
193
194#[derive(Clone)]
196pub enum Header<'a> {
197 Authorization(Cow<'a, str>),
199 Accept(Cow<'a, str>),
201 ContentType(Cow<'a, str>),
203 UserAgent(Cow<'a, str>),
205 AcceptEncoding(Cow<'a, str>),
209 AcceptLanguage(Cow<'a, str>),
211 CacheControl(Cow<'a, str>),
213 Referer(Cow<'a, str>),
215 Origin(Cow<'a, str>),
217 Host(Cow<'a, str>),
219 Custom(Cow<'a, str>, Cow<'a, str>),
223}
224
225#[derive(Clone)]
227pub struct QueryParam<'a> {
228 key: Cow<'a, str>,
229 value: Cow<'a, str>,
230}
231
232pub enum Verb {
234 Get,
236 Post,
238 Put,
240 Delete,
242 Head,
244 Options,
246 Patch,
248 Connect,
250 Trace,
252}
253
254struct Collector(Vec<u8>);
255
256impl Handler for Collector {
257 fn write(&mut self, data: &[u8]) -> Result<usize, WriteError> {
258 self.0.extend_from_slice(data);
259 Ok(data.len())
260 }
261}
262
263pub struct Curl<'a> {
267 verb: Verb,
268 headers: Vec<Header<'a>>,
269 query: Vec<QueryParam<'a>>,
270 body: Option<Body<'a>>,
271 default_user_agent: Option<Cow<'a, str>>,
272}
273
274impl<'a> Default for Curl<'a> {
275 fn default() -> Self {
276 Self {
277 verb: Verb::Get,
278 headers: Vec::new(),
279 query: Vec::new(),
280 body: None,
281 default_user_agent: None,
282 }
283 }
284}
285
286impl<'a> Curl<'a> {
287 pub fn new() -> Self {
289 Self::default()
290 }
291
292 pub fn with_user_agent(agent: impl Into<Cow<'a, str>>) -> Self {
296 Self {
297 default_user_agent: Some(agent.into()),
298 ..Self::default()
299 }
300 }
301
302 pub fn verb(mut self, verb: Verb) -> Self {
304 self.verb = verb;
305 self
306 }
307
308 pub fn get(self) -> Self {
310 self.verb(Verb::Get)
311 }
312
313 pub fn post(self) -> Self {
315 self.verb(Verb::Post)
316 }
317
318 pub fn put(self) -> Self {
320 self.verb(Verb::Put)
321 }
322
323 pub fn delete(self) -> Self {
325 self.verb(Verb::Delete)
326 }
327
328 pub fn head(self) -> Self {
330 self.verb(Verb::Head)
331 }
332
333 pub fn options(self) -> Self {
335 self.verb(Verb::Options)
336 }
337
338 pub fn patch(self) -> Self {
340 self.verb(Verb::Patch)
341 }
342
343 pub fn connect(self) -> Self {
345 self.verb(Verb::Connect)
346 }
347
348 pub fn trace(self) -> Self {
350 self.verb(Verb::Trace)
351 }
352
353 pub fn header(mut self, header: Header<'a>) -> Self {
367 self.headers.push(header);
368 self
369 }
370
371 pub fn headers<I>(mut self, headers: I) -> Self
388 where
389 I: IntoIterator<Item = Header<'a>>,
390 {
391 self.headers.extend(headers);
392 self
393 }
394
395 pub fn query_param(mut self, param: QueryParam<'a>) -> Self {
409 self.query.push(param);
410 self
411 }
412
413 pub fn query_param_kv(
427 self,
428 key: impl Into<Cow<'a, str>>,
429 value: impl Into<Cow<'a, str>>,
430 ) -> Self {
431 self.query_param(QueryParam::new(key, value))
432 }
433
434 pub fn query_params<I>(mut self, params: I) -> Self
451 where
452 I: IntoIterator<Item = QueryParam<'a>>,
453 {
454 self.query.extend(params);
455 self
456 }
457
458 pub fn body(mut self, body: Body<'a>) -> Self {
472 self.body = Some(body);
473 self
474 }
475
476 pub fn body_bytes(self, bytes: impl Into<Cow<'a, [u8]>>) -> Self {
490 self.body(Body::Bytes(bytes.into()))
491 }
492
493 pub fn body_text(self, text: impl Into<Cow<'a, str>>) -> Self {
507 self.body(Body::Text(text.into()))
508 }
509
510 pub fn body_json(self, json: impl Into<Cow<'a, str>>) -> Self {
524 self.body(Body::Json(json.into()))
525 }
526
527 pub fn send(self, url: &str) -> Result<Response, Error> {
533 let mut easy = Easy2::new(Collector(Vec::new()));
534 self.verb.apply(&mut easy)?;
535 let mut list = List::new();
536 let mut has_headers = false;
537 for header in &self.headers {
538 list.append(&header.to_line()?)?;
539 has_headers = true;
540 }
541 if let Some(default_user_agent) = &self.default_user_agent {
542 if !self.has_user_agent_header() {
543 list.append(&format!("User-Agent: {default_user_agent}"))?;
544 has_headers = true;
545 }
546 }
547 if let Some(content_type) = self.body_content_type() {
548 if !self.has_content_type_header() {
549 list.append(&format!("Content-Type: {content_type}"))?;
550 has_headers = true;
551 }
552 }
553 if has_headers {
554 easy.http_headers(list)?;
555 }
556 if let Some(body) = &self.body {
557 easy.post_fields_copy(body.bytes())?;
558 }
559 let url = add_query_params(url, &self.query);
560 validate_url(url.as_ref())?;
561 easy.url(url.as_ref())?;
562 easy.perform()?;
563
564 let status_code = easy.response_code()?;
565 let status_u16 =
566 u16::try_from(status_code).map_err(|_| Error::InvalidStatusCode(status_code))?;
567 let status =
568 StatusCode::from_u16(status_u16).ok_or(Error::InvalidStatusCode(status_code))?;
569 let body = easy.get_ref().0.clone();
570 Ok(Response { status, body })
571 }
572
573 fn has_content_type_header(&self) -> bool {
574 self.headers.iter().any(|header| match header {
575 Header::ContentType(_) => true,
576 Header::Custom(name, _) => name.eq_ignore_ascii_case("Content-Type"),
577 _ => false,
578 })
579 }
580
581 fn has_user_agent_header(&self) -> bool {
582 self.headers.iter().any(|header| match header {
583 Header::UserAgent(_) => true,
584 Header::Custom(name, _) => name.eq_ignore_ascii_case("User-Agent"),
585 _ => false,
586 })
587 }
588
589 fn body_content_type(&self) -> Option<&'static str> {
590 match &self.body {
591 Some(Body::Json(_)) => Some("application/json"),
592 Some(Body::Text(_)) => Some("text/plain; charset=utf-8"),
593 Some(Body::Bytes(_)) => None,
594 None => None,
595 }
596 }
597}
598
599impl Verb {
600 fn apply(&self, easy: &mut Easy2<Collector>) -> Result<(), Error> {
601 match self {
602 Verb::Get => easy.get(true)?,
603 Verb::Post => easy.post(true)?,
604 Verb::Put => easy.custom_request("PUT")?,
605 Verb::Delete => easy.custom_request("DELETE")?,
606 Verb::Head => easy.nobody(true)?,
607 Verb::Options => easy.custom_request("OPTIONS")?,
608 Verb::Patch => easy.custom_request("PATCH")?,
609 Verb::Connect => easy.custom_request("CONNECT")?,
610 Verb::Trace => easy.custom_request("TRACE")?,
611 }
612 Ok(())
613 }
614}
615
616impl Header<'_> {
617 fn to_line(&self) -> Result<String, Error> {
618 let name = self.name();
619 let value = self.value();
620 if value.contains('\n') || value.contains('\r') {
621 return Err(Error::InvalidHeaderValue(name.to_string()));
622 }
623 if matches!(self, Header::Custom(_, _)) {
624 validate_header_name(name)?;
625 }
626 match self {
627 Header::Authorization(value) => Ok(format!("Authorization: {value}")),
628 Header::Accept(value) => Ok(format!("Accept: {value}")),
629 Header::ContentType(value) => Ok(format!("Content-Type: {value}")),
630 Header::UserAgent(value) => Ok(format!("User-Agent: {value}")),
631 Header::AcceptEncoding(value) => Ok(format!("Accept-Encoding: {value}")),
632 Header::AcceptLanguage(value) => Ok(format!("Accept-Language: {value}")),
633 Header::CacheControl(value) => Ok(format!("Cache-Control: {value}")),
634 Header::Referer(value) => Ok(format!("Referer: {value}")),
635 Header::Origin(value) => Ok(format!("Origin: {value}")),
636 Header::Host(value) => Ok(format!("Host: {value}")),
637 Header::Custom(name, value) => Ok(format!("{}: {}", name, value)),
638 }
639 }
640
641 fn name(&self) -> &str {
642 match self {
643 Header::Authorization(_) => "Authorization",
644 Header::Accept(_) => "Accept",
645 Header::ContentType(_) => "Content-Type",
646 Header::UserAgent(_) => "User-Agent",
647 Header::AcceptEncoding(_) => "Accept-Encoding",
648 Header::AcceptLanguage(_) => "Accept-Language",
649 Header::CacheControl(_) => "Cache-Control",
650 Header::Referer(_) => "Referer",
651 Header::Origin(_) => "Origin",
652 Header::Host(_) => "Host",
653 Header::Custom(name, _) => name.as_ref(),
654 }
655 }
656
657 fn value(&self) -> &str {
658 match self {
659 Header::Authorization(value) => value.as_ref(),
660 Header::Accept(value) => value.as_ref(),
661 Header::ContentType(value) => value.as_ref(),
662 Header::UserAgent(value) => value.as_ref(),
663 Header::AcceptEncoding(value) => value.as_ref(),
664 Header::AcceptLanguage(value) => value.as_ref(),
665 Header::CacheControl(value) => value.as_ref(),
666 Header::Referer(value) => value.as_ref(),
667 Header::Origin(value) => value.as_ref(),
668 Header::Host(value) => value.as_ref(),
669 Header::Custom(_, value) => value.as_ref(),
670 }
671 }
672}
673
674pub enum Body<'a> {
675 Json(Cow<'a, str>),
677 Text(Cow<'a, str>),
679 Bytes(Cow<'a, [u8]>),
681}
682
683impl Body<'_> {
684 fn bytes(&self) -> &[u8] {
685 match self {
686 Body::Json(value) => value.as_bytes(),
687 Body::Text(value) => value.as_bytes(),
688 Body::Bytes(value) => value.as_ref(),
689 }
690 }
691}
692
693impl<'a> QueryParam<'a> {
694 pub fn new(key: impl Into<Cow<'a, str>>, value: impl Into<Cow<'a, str>>) -> Self {
705 Self {
706 key: key.into(),
707 value: value.into(),
708 }
709 }
710}
711
712fn add_query_params<'a>(url: &'a str, params: &[QueryParam<'_>]) -> Cow<'a, str> {
713 if params.is_empty() {
714 return Cow::Borrowed(url);
715 }
716
717 let (base, fragment) = match url.split_once('#') {
718 Some((base, fragment)) => (base, Some(fragment)),
719 None => (url, None),
720 };
721
722 let mut out = String::with_capacity(base.len() + 1);
723 out.push_str(base);
724
725 if base.contains('?') {
726 if !base.ends_with('?') && !base.ends_with('&') {
727 out.push('&');
728 }
729 } else {
730 out.push('?');
731 }
732
733 for (idx, param) in params.iter().enumerate() {
734 if idx > 0 {
735 out.push('&');
736 }
737 out.push_str(&encode_query_component(param.key.as_ref()));
738 out.push('=');
739 out.push_str(&encode_query_component(param.value.as_ref()));
740 }
741
742 if let Some(fragment) = fragment {
743 out.push('#');
744 out.push_str(fragment);
745 }
746
747 Cow::Owned(out)
748}
749
750fn encode_query_component(value: &str) -> String {
751 utf8_percent_encode(value, NON_ALPHANUMERIC).to_string()
752}
753
754fn validate_url(url: &str) -> Result<(), Error> {
755 Url::parse(url)
756 .map(|_| ())
757 .map_err(|_| Error::InvalidUrl(url.to_string()))
758}
759
760fn validate_header_name(name: &str) -> Result<(), Error> {
761 if name.is_empty() {
762 return Err(Error::InvalidHeaderName(name.to_string()));
763 }
764 for b in name.bytes() {
765 if !is_tchar(b) {
766 return Err(Error::InvalidHeaderName(name.to_string()));
767 }
768 }
769 Ok(())
770}
771
772fn is_tchar(b: u8) -> bool {
773 matches!(
774 b,
775 b'!' | b'#' | b'$' | b'%' | b'&' | b'\'' | b'*' | b'+' | b'-' | b'.' | b'^' | b'_' | b'`'
776 | b'|' | b'~' | b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z'
777 )
778}
779
780pub fn request(verb: Verb, url: &str) -> Result<Response, Error> {
793 Curl::default().verb(verb).send(url)
794}
795
796pub fn request_with_headers(
813 verb: Verb,
814 url: &str,
815 headers: &[Header<'_>],
816) -> Result<Response, Error> {
817 Curl::default()
818 .verb(verb)
819 .headers(headers.iter().cloned())
820 .send(url)
821}
822
823pub fn get(url: &str) -> Result<Response, Error> {
836 Curl::default().get().send(url)
837}
838
839pub fn post(url: &str) -> Result<Response, Error> {
852 Curl::default().post().send(url)
853}
854
855pub fn get_with_headers(url: &str, headers: &[Header<'_>]) -> Result<Response, Error> {
871 Curl::default()
872 .get()
873 .headers(headers.iter().cloned())
874 .send(url)
875}
876
877pub fn post_with_headers(url: &str, headers: &[Header<'_>]) -> Result<Response, Error> {
893 Curl::default()
894 .post()
895 .headers(headers.iter().cloned())
896 .send(url)
897}
898
899#[cfg(test)]
900mod tests {
901 use super::*;
902
903 #[test]
904 fn query_params_are_encoded_and_appended() {
905 let params = [
906 QueryParam::new("q", "rust curl"),
907 QueryParam::new("page", "1"),
908 ];
909 let url = add_query_params("https://example.com/search", ¶ms);
910 assert_eq!(
911 url.as_ref(),
912 "https://example.com/search?q=rust%20curl&page=1"
913 );
914 }
915
916 #[test]
917 fn query_params_preserve_fragments() {
918 let params = [QueryParam::new("a", "b")];
919 let url = add_query_params("https://example.com/path#frag", ¶ms);
920 assert_eq!(url.as_ref(), "https://example.com/path?a=b#frag");
921 }
922
923 #[test]
924 fn query_params_noop_is_borrowed() {
925 let url = add_query_params("https://example.com", &[]);
926 assert!(matches!(url, Cow::Borrowed(_)));
927 }
928
929 #[test]
930 fn header_rejects_newlines() {
931 let header = Header::UserAgent("bad\r\nvalue".into());
932 let err = header.to_line().expect_err("expected invalid header");
933 assert!(matches!(err, Error::InvalidHeaderValue(name) if name == "User-Agent"));
934 }
935
936 #[test]
937 fn custom_header_rejects_invalid_name() {
938 let header = Header::Custom("X Bad".into(), "ok".into());
939 let err = header.to_line().expect_err("expected invalid header name");
940 assert!(matches!(err, Error::InvalidHeaderName(name) if name == "X Bad"));
941 }
942
943 #[test]
944 fn custom_header_allows_standard_token_chars() {
945 let header = Header::Custom("X-Request-Id".into(), "abc123".into());
946 let line = header.to_line().expect("expected valid header");
947 assert_eq!(line, "X-Request-Id: abc123");
948 }
949
950 #[test]
951 fn body_content_type_defaults() {
952 let curl = Curl::default().body_json(r#"{"ok":true}"#);
953 assert_eq!(curl.body_content_type(), Some("application/json"));
954
955 let curl = Curl::default().body_text("hi");
956 assert_eq!(curl.body_content_type(), Some("text/plain; charset=utf-8"));
957 }
958
959 #[test]
960 fn content_type_header_overrides_body_default() {
961 let curl = Curl::default()
962 .body_json(r#"{"ok":true}"#)
963 .header(Header::ContentType("application/custom+json".into()));
964 assert!(curl.has_content_type_header());
965 assert_eq!(curl.body_content_type(), Some("application/json"));
966 }
967
968 #[test]
969 fn with_user_agent_sets_default() {
970 let curl = Curl::with_user_agent("my-agent/1.0");
971 assert_eq!(curl.default_user_agent.as_deref(), Some("my-agent/1.0"));
972 }
973
974 #[test]
975 fn user_agent_detection_handles_custom_header() {
976 let curl = Curl::default().header(Header::Custom("User-Agent".into(), "custom".into()));
977 assert!(curl.has_user_agent_header());
978 }
979
980 #[test]
981 fn url_validation_rejects_invalid_urls() {
982 let err = validate_url("http://[::1").expect_err("expected invalid url");
983 assert!(matches!(err, Error::InvalidUrl(_)));
984 }
985
986 #[test]
987 fn query_params_append_to_existing_query() {
988 let params = [QueryParam::new("b", "2")];
989 let url = add_query_params("https://example.com/path?a=1", ¶ms);
990 assert_eq!(url.as_ref(), "https://example.com/path?a=1&b=2");
991 }
992
993 #[test]
994 fn query_params_encode_unicode() {
995 let params = [QueryParam::new("q", "café")];
996 let url = add_query_params("https://example.com/search", ¶ms);
997 assert_eq!(url.as_ref(), "https://example.com/search?q=caf%C3%A9");
998 }
999
1000 #[test]
1001 fn header_name_and_value_match() {
1002 let header = Header::Accept("application/json".into());
1003 assert_eq!(header.name(), "Accept");
1004 assert_eq!(header.value(), "application/json");
1005 }
1006}