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 Client(#[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 Method {
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 Client<'a> {
267 method: Method,
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
274#[deprecated(note = "Renamed to Client; use Client instead.")]
275pub type Curl<'a> = Client<'a>;
276
277impl<'a> Default for Client<'a> {
278 fn default() -> Self {
279 Self {
280 method: Method::Get,
281 headers: Vec::new(),
282 query: Vec::new(),
283 body: None,
284 default_user_agent: None,
285 }
286 }
287}
288
289impl<'a> Client<'a> {
290 pub fn new() -> Self {
292 Self::default()
293 }
294
295 pub fn with_user_agent(agent: impl Into<Cow<'a, str>>) -> Self {
299 Self {
300 default_user_agent: Some(agent.into()),
301 ..Self::default()
302 }
303 }
304
305 pub fn method(mut self, method: Method) -> Self {
307 self.method = method;
308 self
309 }
310
311 pub fn get(self) -> Self {
313 self.method(Method::Get)
314 }
315
316 pub fn post(self) -> Self {
318 self.method(Method::Post)
319 }
320
321 pub fn put(self) -> Self {
323 self.method(Method::Put)
324 }
325
326 pub fn delete(self) -> Self {
328 self.method(Method::Delete)
329 }
330
331 pub fn head(self) -> Self {
333 self.method(Method::Head)
334 }
335
336 pub fn options(self) -> Self {
338 self.method(Method::Options)
339 }
340
341 pub fn patch(self) -> Self {
343 self.method(Method::Patch)
344 }
345
346 pub fn connect(self) -> Self {
348 self.method(Method::Connect)
349 }
350
351 pub fn trace(self) -> Self {
353 self.method(Method::Trace)
354 }
355
356 pub fn header(mut self, header: Header<'a>) -> Self {
370 self.headers.push(header);
371 self
372 }
373
374 pub fn headers<I>(mut self, headers: I) -> Self
391 where
392 I: IntoIterator<Item = Header<'a>>,
393 {
394 self.headers.extend(headers);
395 self
396 }
397
398 pub fn query_param(mut self, param: QueryParam<'a>) -> Self {
412 self.query.push(param);
413 self
414 }
415
416 pub fn query_param_kv(
430 self,
431 key: impl Into<Cow<'a, str>>,
432 value: impl Into<Cow<'a, str>>,
433 ) -> Self {
434 self.query_param(QueryParam::new(key, value))
435 }
436
437 pub fn query_params<I>(mut self, params: I) -> Self
454 where
455 I: IntoIterator<Item = QueryParam<'a>>,
456 {
457 self.query.extend(params);
458 self
459 }
460
461 pub fn body(mut self, body: Body<'a>) -> Self {
475 self.body = Some(body);
476 self
477 }
478
479 pub fn body_bytes(self, bytes: impl Into<Cow<'a, [u8]>>) -> Self {
493 self.body(Body::Bytes(bytes.into()))
494 }
495
496 pub fn body_text(self, text: impl Into<Cow<'a, str>>) -> Self {
510 self.body(Body::Text(text.into()))
511 }
512
513 pub fn body_json(self, json: impl Into<Cow<'a, str>>) -> Self {
527 self.body(Body::Json(json.into()))
528 }
529
530 pub fn send(self, url: &str) -> Result<Response, Error> {
536 let mut easy = Easy2::new(Collector(Vec::new()));
537 self.method.apply(&mut easy)?;
538 let mut list = List::new();
539 let mut has_headers = false;
540 for header in &self.headers {
541 list.append(&header.to_line()?)?;
542 has_headers = true;
543 }
544 if let Some(default_user_agent) = &self.default_user_agent {
545 if !self.has_user_agent_header() {
546 list.append(&format!("User-Agent: {default_user_agent}"))?;
547 has_headers = true;
548 }
549 }
550 if let Some(content_type) = self.body_content_type() {
551 if !self.has_content_type_header() {
552 list.append(&format!("Content-Type: {content_type}"))?;
553 has_headers = true;
554 }
555 }
556 if has_headers {
557 easy.http_headers(list)?;
558 }
559 if let Some(body) = &self.body {
560 easy.post_fields_copy(body.bytes())?;
561 }
562 let url = add_query_params(url, &self.query);
563 validate_url(url.as_ref())?;
564 easy.url(url.as_ref())?;
565 easy.perform()?;
566
567 let status_code = easy.response_code()?;
568 let status_u16 =
569 u16::try_from(status_code).map_err(|_| Error::InvalidStatusCode(status_code))?;
570 let status =
571 StatusCode::from_u16(status_u16).ok_or(Error::InvalidStatusCode(status_code))?;
572 let body = easy.get_ref().0.clone();
573 Ok(Response { status, body })
574 }
575
576 fn has_content_type_header(&self) -> bool {
577 self.headers.iter().any(|header| match header {
578 Header::ContentType(_) => true,
579 Header::Custom(name, _) => name.eq_ignore_ascii_case("Content-Type"),
580 _ => false,
581 })
582 }
583
584 fn has_user_agent_header(&self) -> bool {
585 self.headers.iter().any(|header| match header {
586 Header::UserAgent(_) => true,
587 Header::Custom(name, _) => name.eq_ignore_ascii_case("User-Agent"),
588 _ => false,
589 })
590 }
591
592 fn body_content_type(&self) -> Option<&'static str> {
593 match &self.body {
594 Some(Body::Json(_)) => Some("application/json"),
595 Some(Body::Text(_)) => Some("text/plain; charset=utf-8"),
596 Some(Body::Bytes(_)) => None,
597 None => None,
598 }
599 }
600}
601
602impl Method {
603 fn apply(&self, easy: &mut Easy2<Collector>) -> Result<(), Error> {
604 match self {
605 Method::Get => easy.get(true)?,
606 Method::Post => easy.post(true)?,
607 Method::Put => easy.custom_request("PUT")?,
608 Method::Delete => easy.custom_request("DELETE")?,
609 Method::Head => easy.nobody(true)?,
610 Method::Options => easy.custom_request("OPTIONS")?,
611 Method::Patch => easy.custom_request("PATCH")?,
612 Method::Connect => easy.custom_request("CONNECT")?,
613 Method::Trace => easy.custom_request("TRACE")?,
614 }
615 Ok(())
616 }
617}
618
619impl Header<'_> {
620 fn to_line(&self) -> Result<String, Error> {
621 let name = self.name();
622 let value = self.value();
623 if value.contains('\n') || value.contains('\r') {
624 return Err(Error::InvalidHeaderValue(name.to_string()));
625 }
626 if matches!(self, Header::Custom(_, _)) {
627 validate_header_name(name)?;
628 }
629 match self {
630 Header::Authorization(value) => Ok(format!("Authorization: {value}")),
631 Header::Accept(value) => Ok(format!("Accept: {value}")),
632 Header::ContentType(value) => Ok(format!("Content-Type: {value}")),
633 Header::UserAgent(value) => Ok(format!("User-Agent: {value}")),
634 Header::AcceptEncoding(value) => Ok(format!("Accept-Encoding: {value}")),
635 Header::AcceptLanguage(value) => Ok(format!("Accept-Language: {value}")),
636 Header::CacheControl(value) => Ok(format!("Cache-Control: {value}")),
637 Header::Referer(value) => Ok(format!("Referer: {value}")),
638 Header::Origin(value) => Ok(format!("Origin: {value}")),
639 Header::Host(value) => Ok(format!("Host: {value}")),
640 Header::Custom(name, value) => Ok(format!("{}: {}", name, value)),
641 }
642 }
643
644 fn name(&self) -> &str {
645 match self {
646 Header::Authorization(_) => "Authorization",
647 Header::Accept(_) => "Accept",
648 Header::ContentType(_) => "Content-Type",
649 Header::UserAgent(_) => "User-Agent",
650 Header::AcceptEncoding(_) => "Accept-Encoding",
651 Header::AcceptLanguage(_) => "Accept-Language",
652 Header::CacheControl(_) => "Cache-Control",
653 Header::Referer(_) => "Referer",
654 Header::Origin(_) => "Origin",
655 Header::Host(_) => "Host",
656 Header::Custom(name, _) => name.as_ref(),
657 }
658 }
659
660 fn value(&self) -> &str {
661 match self {
662 Header::Authorization(value) => value.as_ref(),
663 Header::Accept(value) => value.as_ref(),
664 Header::ContentType(value) => value.as_ref(),
665 Header::UserAgent(value) => value.as_ref(),
666 Header::AcceptEncoding(value) => value.as_ref(),
667 Header::AcceptLanguage(value) => value.as_ref(),
668 Header::CacheControl(value) => value.as_ref(),
669 Header::Referer(value) => value.as_ref(),
670 Header::Origin(value) => value.as_ref(),
671 Header::Host(value) => value.as_ref(),
672 Header::Custom(_, value) => value.as_ref(),
673 }
674 }
675}
676
677pub enum Body<'a> {
678 Json(Cow<'a, str>),
680 Text(Cow<'a, str>),
682 Bytes(Cow<'a, [u8]>),
684}
685
686impl Body<'_> {
687 fn bytes(&self) -> &[u8] {
688 match self {
689 Body::Json(value) => value.as_bytes(),
690 Body::Text(value) => value.as_bytes(),
691 Body::Bytes(value) => value.as_ref(),
692 }
693 }
694}
695
696impl<'a> QueryParam<'a> {
697 pub fn new(key: impl Into<Cow<'a, str>>, value: impl Into<Cow<'a, str>>) -> Self {
708 Self {
709 key: key.into(),
710 value: value.into(),
711 }
712 }
713}
714
715fn add_query_params<'a>(url: &'a str, params: &[QueryParam<'_>]) -> Cow<'a, str> {
716 if params.is_empty() {
717 return Cow::Borrowed(url);
718 }
719
720 let (base, fragment) = match url.split_once('#') {
721 Some((base, fragment)) => (base, Some(fragment)),
722 None => (url, None),
723 };
724
725 let mut out = String::with_capacity(base.len() + 1);
726 out.push_str(base);
727
728 if base.contains('?') {
729 if !base.ends_with('?') && !base.ends_with('&') {
730 out.push('&');
731 }
732 } else {
733 out.push('?');
734 }
735
736 for (idx, param) in params.iter().enumerate() {
737 if idx > 0 {
738 out.push('&');
739 }
740 out.push_str(&encode_query_component(param.key.as_ref()));
741 out.push('=');
742 out.push_str(&encode_query_component(param.value.as_ref()));
743 }
744
745 if let Some(fragment) = fragment {
746 out.push('#');
747 out.push_str(fragment);
748 }
749
750 Cow::Owned(out)
751}
752
753fn encode_query_component(value: &str) -> String {
754 utf8_percent_encode(value, NON_ALPHANUMERIC).to_string()
755}
756
757fn validate_url(url: &str) -> Result<(), Error> {
758 Url::parse(url)
759 .map(|_| ())
760 .map_err(|_| Error::InvalidUrl(url.to_string()))
761}
762
763fn validate_header_name(name: &str) -> Result<(), Error> {
764 if name.is_empty() {
765 return Err(Error::InvalidHeaderName(name.to_string()));
766 }
767 for b in name.bytes() {
768 if !is_tchar(b) {
769 return Err(Error::InvalidHeaderName(name.to_string()));
770 }
771 }
772 Ok(())
773}
774
775fn is_tchar(b: u8) -> bool {
776 matches!(
777 b,
778 b'!' | b'#' | b'$' | b'%' | b'&' | b'\'' | b'*' | b'+' | b'-' | b'.' | b'^' | b'_' | b'`'
779 | b'|' | b'~' | b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z'
780 )
781}
782
783pub fn request(method: Method, url: &str) -> Result<Response, Error> {
796 Client::default().method(method).send(url)
797}
798
799pub fn request_with_headers(
816 method: Method,
817 url: &str,
818 headers: &[Header<'_>],
819) -> Result<Response, Error> {
820 Client::default()
821 .method(method)
822 .headers(headers.iter().cloned())
823 .send(url)
824}
825
826pub fn get(url: &str) -> Result<Response, Error> {
839 Client::default().get().send(url)
840}
841
842pub fn post(url: &str) -> Result<Response, Error> {
855 Client::default().post().send(url)
856}
857
858pub fn get_with_headers(url: &str, headers: &[Header<'_>]) -> Result<Response, Error> {
874 Client::default()
875 .get()
876 .headers(headers.iter().cloned())
877 .send(url)
878}
879
880pub fn post_with_headers(url: &str, headers: &[Header<'_>]) -> Result<Response, Error> {
896 Client::default()
897 .post()
898 .headers(headers.iter().cloned())
899 .send(url)
900}
901
902#[cfg(test)]
903mod tests {
904 use super::*;
905
906 #[test]
907 fn query_params_are_encoded_and_appended() {
908 let params = [
909 QueryParam::new("q", "rust curl"),
910 QueryParam::new("page", "1"),
911 ];
912 let url = add_query_params("https://example.com/search", ¶ms);
913 assert_eq!(
914 url.as_ref(),
915 "https://example.com/search?q=rust%20curl&page=1"
916 );
917 }
918
919 #[test]
920 fn query_params_preserve_fragments() {
921 let params = [QueryParam::new("a", "b")];
922 let url = add_query_params("https://example.com/path#frag", ¶ms);
923 assert_eq!(url.as_ref(), "https://example.com/path?a=b#frag");
924 }
925
926 #[test]
927 fn query_params_noop_is_borrowed() {
928 let url = add_query_params("https://example.com", &[]);
929 assert!(matches!(url, Cow::Borrowed(_)));
930 }
931
932 #[test]
933 fn header_rejects_newlines() {
934 let header = Header::UserAgent("bad\r\nvalue".into());
935 let err = header.to_line().expect_err("expected invalid header");
936 assert!(matches!(err, Error::InvalidHeaderValue(name) if name == "User-Agent"));
937 }
938
939 #[test]
940 fn custom_header_rejects_invalid_name() {
941 let header = Header::Custom("X Bad".into(), "ok".into());
942 let err = header.to_line().expect_err("expected invalid header name");
943 assert!(matches!(err, Error::InvalidHeaderName(name) if name == "X Bad"));
944 }
945
946 #[test]
947 fn custom_header_allows_standard_token_chars() {
948 let header = Header::Custom("X-Request-Id".into(), "abc123".into());
949 let line = header.to_line().expect("expected valid header");
950 assert_eq!(line, "X-Request-Id: abc123");
951 }
952
953 #[test]
954 fn body_content_type_defaults() {
955 let curl = Client::default().body_json(r#"{"ok":true}"#);
956 assert_eq!(curl.body_content_type(), Some("application/json"));
957
958 let curl = Client::default().body_text("hi");
959 assert_eq!(curl.body_content_type(), Some("text/plain; charset=utf-8"));
960 }
961
962 #[test]
963 fn content_type_header_overrides_body_default() {
964 let curl = Client::default()
965 .body_json(r#"{"ok":true}"#)
966 .header(Header::ContentType("application/custom+json".into()));
967 assert!(curl.has_content_type_header());
968 assert_eq!(curl.body_content_type(), Some("application/json"));
969 }
970
971 #[test]
972 fn with_user_agent_sets_default() {
973 let curl = Client::with_user_agent("my-agent/1.0");
974 assert_eq!(curl.default_user_agent.as_deref(), Some("my-agent/1.0"));
975 }
976
977 #[test]
978 fn user_agent_detection_handles_custom_header() {
979 let curl = Client::default().header(Header::Custom("User-Agent".into(), "custom".into()));
980 assert!(curl.has_user_agent_header());
981 }
982
983 #[test]
984 fn url_validation_rejects_invalid_urls() {
985 let err = validate_url("http://[::1").expect_err("expected invalid url");
986 assert!(matches!(err, Error::InvalidUrl(_)));
987 }
988
989 #[test]
990 fn query_params_append_to_existing_query() {
991 let params = [QueryParam::new("b", "2")];
992 let url = add_query_params("https://example.com/path?a=1", ¶ms);
993 assert_eq!(url.as_ref(), "https://example.com/path?a=1&b=2");
994 }
995
996 #[test]
997 fn query_params_encode_unicode() {
998 let params = [QueryParam::new("q", "café")];
999 let url = add_query_params("https://example.com/search", ¶ms);
1000 assert_eq!(url.as_ref(), "https://example.com/search?q=caf%C3%A9");
1001 }
1002
1003 #[test]
1004 fn header_name_and_value_match() {
1005 let header = Header::Accept("application/json".into());
1006 assert_eq!(header.name(), "Accept");
1007 assert_eq!(header.value(), "application/json");
1008 }
1009}