1use json::{array, object, JsonValue};
6use log::{debug, error, info};
7use std::collections::HashMap;
8use std::fmt::Debug;
9use std::io::Write;
10use std::path::Path;
11use std::process::Command;
12use std::str::Lines;
13use std::{env, fs, str};
14use xmltree::Element;
15
16pub struct Client {
17 pub url: String,
18 debug: bool,
19 pub method: Method,
20 pub header: HashMap<String, String>,
21 body_data: JsonValue,
22 pub content_type: ContentType,
23 proxy: ProxyInfo,
24 cert_p12: bool,
25 cert_p12_filename: String,
26 cert_p12_password: String,
27 args: Vec<String>,
28 request_timeout: usize,
30 response_timeout: usize,
32 retry: usize,
34 command: Vec<String>,
35}
36
37impl Default for Client {
38 fn default() -> Self {
39 Self::new()
40 }
41}
42
43impl Client {
44 pub fn new() -> Self {
45 Self {
46 url: "".to_string(),
47 debug: false,
48 method: Method::NONE,
49 header: Default::default(),
50 body_data: object! {},
51 content_type: ContentType::Text,
52 proxy: ProxyInfo::None,
53 cert_p12: false,
54 cert_p12_filename: "".to_string(),
55 cert_p12_password: "".to_string(),
56 args: vec![],
57 request_timeout: 0,
58 response_timeout: 0,
59 retry: 0,
60 command: vec![],
61 }
62 }
63 pub fn debug(&mut self) -> &mut Self {
64 self.debug = true;
65 self
66 }
67 pub fn post(&mut self, url: &str) -> &mut Self {
68 self.url = url.to_string();
69 self.method = Method::POST;
70 self
71 }
72 pub fn get(&mut self, url: &str) -> &mut Self {
73 self.url = url.to_string();
74 self.method = Method::GET;
75 self
76 }
77 pub fn put(&mut self, url: &str) -> &mut Self {
78 self.url = url.to_string();
79 self.method = Method::PUT;
80 self
81 }
82 pub fn patch(&mut self, url: &str) -> &mut Self {
83 self.url = url.to_string();
84 self.method = Method::PATCH;
85 self
86 }
87 pub fn delete(&mut self, url: &str) -> &mut Self {
88 self.url = url.to_string();
89 self.method = Method::DELETE;
90 self
91 }
92 pub fn head(&mut self, url: &str) -> &mut Self {
93 self.url = url.to_string();
94 self.method = Method::HEAD;
95 self
96 }
97 pub fn options(&mut self, url: &str) -> &mut Self {
98 self.url = url.to_string();
99 self.method = Method::OPTIONS;
100 self
101 }
102 pub fn trace(&mut self, url: &str) -> &mut Self {
103 self.url = url.to_string();
104 self.method = Method::TRACE;
105 self
106 }
107 pub fn header(&mut self, key: &str, value: &str) -> &mut Self {
108 self.header.insert(key.to_string(), value.to_string());
109 self
110 }
111
112 pub fn query(&mut self, params: JsonValue) -> &mut Self {
113 let mut txt = vec![];
114 for (key, value) in params.entries() {
115 txt.push(format!("{key}={value}"));
116 }
117 if self.url.contains('?') {
118 if !txt.is_empty() {
119 self.url = format!("{}&{}", self.url, txt.join("&"));
120 }
121 } else if !txt.is_empty() {
122 self.url = format!("{}?{}", self.url, txt.join("&"));
123 }
124 self
125 }
126 pub fn set_request_timeout(&mut self, s: usize) -> &mut Self {
128 self.request_timeout = s;
129 self
130 }
131
132 pub fn set_response_timeout(&mut self, s: usize) -> &mut Self {
133 self.response_timeout = s;
134 self
135 }
136
137 pub fn set_retry(&mut self, count: usize) -> &mut Self {
138 self.retry = count;
139 self
140 }
141
142 pub fn set_command(&mut self, cmd: Vec<String>) -> &mut Self {
143 self.command = cmd;
144 self
145 }
146
147 pub fn raw_json(&mut self, data: JsonValue) -> &mut Self {
149 self.header("Content-Type", ContentType::Json.str().as_str());
150 self.content_type = ContentType::Json;
151 self.body_data = data;
152 self
153 }
154 pub fn raw_xml(&mut self, data: JsonValue) -> &mut Self {
155 self.header("Content-Type", ContentType::Xml.str().as_str());
156 self.content_type = ContentType::Xml;
157 self.body_data = data;
158 self
159 }
160 pub fn raw_stream(&mut self, filename: &str) -> &mut Self {
161 self.content_type = ContentType::Stream;
162 self.body_data = filename.into();
163 self
164 }
165 pub fn raw_binary_str(&mut self, text: &str) -> &mut Self {
166 self.content_type = ContentType::BinaryStr;
167 self.body_data = text.into();
168 self
169 }
170 pub fn raw_stream_urlencode(&mut self, text: &str) -> &mut Self {
171 self.content_type = ContentType::StreamUrlencode;
172 self.body_data = text.into();
173 self
174 }
175 pub fn form_data(&mut self, data: JsonValue) -> &mut Self {
177 self.header("Content-Type", ContentType::FormData.str().as_str());
178 self.content_type = ContentType::FormData;
179 self.body_data = data;
180 self
181 }
182 pub fn form_urlencoded(&mut self, data: JsonValue) -> &mut Self {
184 self.header("Content-Type", ContentType::FormUrlencoded.str().as_str());
185 self.content_type = ContentType::FormUrlencoded;
186 self.body_data = data;
187 self
188 }
189 pub fn set_cert_p12(&mut self, filename: &str, password: &str) -> &mut Self {
191 self.cert_p12_filename = filename.to_string();
192 self.cert_p12_password = password.to_string();
193 self.cert_p12 = true;
194 self
195 }
196 pub fn add_proxy(&mut self, proxy_addr: String) {
199 self.proxy = ProxyInfo::Addr(proxy_addr);
200 }
201
202 pub fn add_proxy_with_account(&mut self, proxy_addr: String, user: String, password: String) {
205 self.proxy = ProxyInfo::AddrWithAccount(proxy_addr, user, password);
206 }
207
208 pub fn send(&mut self) -> Result<Response, String> {
209 let mut output = Command::new("curl");
210
211 if !self.command.is_empty() {
212 self.args.extend(self.command.clone());
213 }
214 if self.cert_p12 {
215 self.args.extend([
216 "--cert-type".to_string(),
217 "P12".to_string(),
218 "--cert".to_string(),
219 self.cert_p12_filename.to_string(),
220 "--pass".to_string(),
221 self.cert_p12_password.to_string(),
222 ]);
223 }
224
225 if self.debug {
226 self.args.extend(["--trace-ascii".to_string(), "br-reqwest.log".to_string()]);
227 }
228
229 if self.request_timeout > 0 {
230 self.args.extend([
231 "--connect-timeout".to_string(),
232 self.request_timeout.to_string(),
233 ]);
234 }
235 if self.response_timeout > 0 {
236 self.args.extend(["--max-time".to_string(), self.response_timeout.to_string()]);
237 }
238
239 if self.retry > 0 {
240 self.args.extend(["--retry".to_string(), self.retry.to_string()]);
241 }
242
243 match &self.proxy {
244 ProxyInfo::None => {
245 self.args.extend(["--noproxy".to_string(), "*".to_string()]);
246 }
247 ProxyInfo::Addr(proxy) => {
248 self.args.extend(["-x".to_string(), proxy.to_string()]);
249 }
250 ProxyInfo::AddrWithAccount(proxy, user, pwd) => {
251 self.args.extend([
252 "-x".to_string(),
253 proxy.to_string(),
254 "-U".to_string(),
255 format!("{user}:{pwd}"),
256 ]);
257 }
258 }
259
260 match self.method {
261 Method::HEAD => {
262 self.args.push("-I".to_string());
263 }
264 _ => {
265 self.args.extend([
266 "-i".to_string(),
267 "-X".to_string(),
268 self.method.to_str().to_uppercase(),
269 ]);
270 }
271 }
272 self.args.push(self.url.to_string());
273
274 if !self.header.is_empty() {
275 for (key, value) in self.header.iter() {
276 self.args.extend(["-H".to_string(), format!("{key}: {value}")]);
277 }
278 }
279
280 match self.content_type {
281 ContentType::FormData => {
282 for (_, value) in self.body_data.entries() {
283 self.args.push("-F".to_string());
284
285 if value[2].is_empty() {
286 self.args.push(format!("{}={}", value[0], value[1]));
287 } else {
288 self.args.push(format!("{}={};{}", value[0], value[1], value[2]));
289 }
290 }
291 }
292 ContentType::FormUrlencoded => {
293 self.args.push("-d".to_string());
294 let mut d = vec![];
295 for (key, value) in self.body_data.entries() {
296 d.push(format!("{key}={value}"))
297 }
298 self.args.push(d.join("&").to_string());
299 }
300 ContentType::Json | ContentType::Xml => {
301 self.args.extend(["-d".to_string(), format!("{}", self.body_data)]);
302 }
303 ContentType::Javascript => {}
304 ContentType::Text => {}
305 ContentType::Html => {}
306 ContentType::Other(_) => {}
307 ContentType::Stream => {
308 self.args.push("--data-binary".to_string());
310 self.args.push(format!("@{}", self.body_data));
311 }
312 ContentType::BinaryStr => {
313 self.args.push("--data-binary".to_string());
314 self.args.push(format!("{}", self.body_data));
315 }
316 ContentType::StreamUrlencode => {
317 self.args.push("--data-urlencode".to_string());
318 self.args.push(format!("{}", self.body_data));
319 }
320 }
321 output.args(self.args.as_slice());
322 if self.debug {
323 info!("curl {}", self.args.join(" "));
324 }
325 let req = match output.output() {
326 Ok(e) => e,
327 Err(e) => {
328 return Err(e.to_string());
329 }
330 };
331
332 if req.status.success() {
333 let body = req.stdout.clone();
334 if self.debug {
335 let text = String::from_utf8_lossy(&req.stdout);
336 info!("响应内容:\n{text}");
337 }
338 Ok(Response::new(self.debug, body)?)
339 } else {
340 let err = String::from_utf8_lossy(&req.stderr).to_string();
341 let txt = match err.find("ms:") {
342 None => err,
343 Some(e) => err[e + 3..].trim().to_string(),
344 };
345 Err(txt)
346 }
347 }
348}
349
350enum ProxyInfo {
351 None,
352 Addr(String),
353 AddrWithAccount(String, String, String),
354}
355
356#[derive(Debug)]
357pub struct Response {
358 debug: bool,
359 version: String,
360 status: i32,
361 status_text: String,
362 headers: JsonValue,
363 cookies: JsonValue,
364 body: Body,
366}
367impl Response {
368 const CRLF2: [u8; 4] = [13, 10, 13, 10];
369
370 fn new(debug: bool, body: Vec<u8>) -> Result<Self, String> {
371 let mut that = Self {
372 debug,
373 version: "".to_string(),
374 status: 0,
375 status_text: "".to_string(),
376 headers: object! {},
377 cookies: object! {},
378 body: Default::default(),
379 };
380
381 let (header, body) = match body.windows(Self::CRLF2.len()).position(|window| window == Self::CRLF2) {
382 None => (vec![], vec![]),
383 Some(index) => {
384 let header = body[..index].to_vec();
385 let body = Vec::from(&body[index + Self::CRLF2.len()..]);
386 (header, body)
387 }
388 };
389
390 let text = String::from_utf8_lossy(header.as_slice());
391 let lines = text.lines();
392
393 that.get_request_line(lines.clone().next().expect("get next line in response err"))?;
394 that.get_header(lines.clone())?;
396 that.body.stream = body.clone();
397 that.body.set_content(body);
398 Ok(that)
399 }
400
401 fn get_request_line(&mut self, line: &str) -> Result<(), String> {
403 let lines = line.split_whitespace().collect::<Vec<&str>>();
404 if lines.len() < 2 {
405 return Err("请求行错误".to_string());
406 }
407 self.version = lines[0].to_string();
408 self.status = lines[1].parse::<i32>().unwrap();
409 if lines.len() > 2 && !lines[2].is_empty() {
410 self.status_text = lines[2].trim().to_string();
411 }
412 Ok(())
413 }
414
415 fn get_header(&mut self, data: Lines) -> Result<(), String> {
417 let mut header = object! {};
418 let mut cookie = object! {};
419 let mut body = Body::default();
420
421 for text in data {
422 let (key, value) = match text.trim().find(":") {
423 None => continue,
424 Some(e) => {
425 let key = text[..e].trim().to_lowercase().clone();
426 let value = text[e + 1..].trim().to_string();
427 (key, value)
428 }
429 };
430 match key.as_str() {
431 "content-type" => match value {
432 _ if value.contains("multipart/form-data") => {
433 let boundarys = value.split("boundary=").collect::<Vec<&str>>();
434 body.boundary = boundarys[1..].join("");
435 body.content_type = ContentType::from("multipart/form-data");
436 let _ = header.insert(key.as_str(), "multipart/form-data");
437 }
438 _ => {
439 let value = match value.find(";") {
440 None => value,
441 Some(e) => value[..e].trim().to_string(),
442 };
443 body.content_type = ContentType::from(value.as_str());
444 let _ = header.insert(key.as_str(), body.content_type.str());
445 }
446 },
447 "content-length" => {
448 body.content_length = value.to_string().parse::<usize>().unwrap_or(0);
449 }
450 "cookie" => {
451 let _ = value.split(";").collect::<Vec<&str>>().iter().map(|&x| {
452 match x.find("=") {
453 None => {}
454 Some(index) => {
455 let key = x[..index].trim().to_string();
456 let val = x[index + 1..].trim().to_string();
457 let _ = cookie.insert(key.as_str(), val);
458 }
459 };
460 ""
461 }).collect::<Vec<&str>>();
462 }
463 _ => {
464 if self.debug {
465 debug!("header: {key} = {value}");
466 }
467 let _ = header.insert(key.as_str(), value);
468 }
469 };
470 }
471 self.headers = header.clone();
472 self.cookies = cookie.clone();
473 self.body = body.clone();
474 Ok(())
475 }
476 pub fn status(&self) -> i32 {
477 self.status
478 }
479 pub fn status_text(&self) -> String {
480 self.status_text.clone()
481 }
482 pub fn version(&self) -> String {
483 self.version.clone()
484 }
485 pub fn headers(&self) -> JsonValue {
486 self.headers.clone()
487 }
488 pub fn cookies(&self) -> JsonValue {
489 self.cookies.clone()
490 }
491 pub fn content_type(&self) -> String {
492 self.body.content_type.str().clone()
493 }
494 pub fn json(&self) -> Result<JsonValue, String> {
495 if self.body.content.is_empty() {
496 Ok(object! {})
497 } else {
498 match json::parse(self.body.content.to_string().as_str()) {
499 Ok(e) => Ok(e),
500 Err(e) => Err(e.to_string()),
501 }
502 }
503 }
504 pub fn xml(&self) -> Result<JsonValue, String> {
505 if self.body.content.is_empty() {
506 Ok(object! {})
507 } else {
508 let json = match Element::parse(self.body.content.to_string().as_bytes()) {
509 Ok(e) => xml_element_to_json(&e),
510 Err(e) => {
511 if self.debug {
512 error!("{:?} {}", e.to_string(), self.body.content.clone());
513 }
514 self.body.content.clone()
515 }
516 };
517 Ok(json)
518 }
519 }
520 pub fn body(&self) -> JsonValue {
521 self.body.content.clone()
522 }
523 pub fn stream(&self) -> Vec<u8> {
524 self.body.stream.clone()
525 }
526}
527fn xml_element_to_json(elem: &Element) -> JsonValue {
528 let mut obj = object! {};
529
530 for child in &elem.children {
531 if let xmltree::XMLNode::Element(e) = child {
532 obj[e.name.clone()] = xml_element_to_json(e);
533 }
534 }
535
536 match elem.get_text() {
537 None => obj,
538 Some(text) => JsonValue::from(text.to_string()),
539 }
540}
541
542#[derive(Clone, Debug)]
543pub enum Method {
544 GET,
545 POST,
546 OPTIONS,
547 PATCH,
548 HEAD,
549 DELETE,
550 TRACE,
551 PUT,
552 NONE,
553}
554
555impl Method {
556 pub fn to_str(&self) -> String {
557 match self {
558 Method::GET => "GET",
559 Method::POST => "POST",
560 Method::OPTIONS => "OPTIONS",
561 Method::PATCH => "PATCH",
562 Method::HEAD => "HEAD",
563 Method::DELETE => "DELETE",
564 Method::TRACE => "TRACE",
565 Method::PUT => "PUT",
566 Method::NONE => "NONE",
567 }.to_string()
568 }
569 pub fn from(name: &str) -> Self {
570 match name.to_lowercase().as_str() {
571 "post" => Self::POST,
572 "get" => Self::GET,
573 "head" => Self::HEAD,
574 "put" => Self::PUT,
575 "delete" => Self::DELETE,
576 "options" => Self::OPTIONS,
577 "patch" => Self::PATCH,
578 "trace" => Self::TRACE,
579 _ => Self::NONE,
580 }
581 }
582}
583#[derive(Clone, Debug)]
584pub enum Version {
585 Http09,
586 Http10,
587 Http11,
588 H2,
589 H3,
590 None,
591}
592
593impl Version {
594 pub fn str(&mut self) -> String {
595 match self {
596 Version::Http09 => "HTTP/0.9",
597 Version::Http10 => "HTTP/1.0",
598 Version::Http11 => "HTTP/1.1",
599 Version::H2 => "HTTP/2.0",
600 Version::H3 => "HTTP/3.0",
601 Version::None => "",
602 }.to_string()
603 }
604 pub fn from(name: &str) -> Version {
605 match name {
606 "HTTP/0.9" => Self::Http09,
607 "HTTP/1.0" => Self::Http10,
608 "HTTP/1.1" => Self::Http11,
609 "HTTP/2.0" => Self::H2,
610 "HTTP/3.0" => Self::H3,
611 _ => Self::None,
612 }
613 }
614 pub fn set_version(name: &str) -> Version {
615 match name {
616 "0.9" => Self::Http09,
617 "1.0" => Self::Http10,
618 "1.1" => Self::Http11,
619 "2.0" => Self::H2,
620 "3.0" => Self::H3,
621 _ => Self::None,
622 }
623 }
624}
625
626#[derive(Clone, Debug)]
627pub enum FormData {
628 Text(String, JsonValue, String),
632 File(String, String, String),
636 None,
637}
638
639#[derive(Debug, Clone)]
640pub struct Body {
641 pub content_type: ContentType,
642 pub boundary: String,
643 pub content_length: usize,
644 pub content: JsonValue,
645 pub stream: Vec<u8>,
646}
647impl Body {
648 pub fn decode(input: &str) -> Result<String, String> {
650 let mut decoded = String::new();
651 let bytes = input.as_bytes();
652 let mut i = 0;
653
654 while i < bytes.len() {
655 if bytes[i] == b'%' {
656 if i + 2 >= bytes.len() {
657 return Err("Incomplete percent-encoding".into());
658 }
659 let hex = &input[i + 1..i + 3];
660 match u8::from_str_radix(hex, 16) {
661 Ok(byte) => decoded.push(byte as char),
662 Err(_) => return Err(format!("Invalid percent-encoding: %{hex}")),
663 }
664 i += 3;
665 } else if bytes[i] == b'+' {
666 decoded.push(' ');
667 i += 1;
668 } else {
669 decoded.push(bytes[i] as char);
670 i += 1;
671 }
672 }
673
674 Ok(decoded)
675 }
676 pub fn set_content(&mut self, data: Vec<u8>) {
677 match self.content_type.clone() {
678 ContentType::FormData | ContentType::Stream => {
679 let mut fields = object! {};
680 let boundary_marker = format!("--{}", self.boundary);
681 let text = unsafe { String::from_utf8_unchecked(data) };
682 let parts = text.split(&boundary_marker).collect::<Vec<&str>>();
683 for part in parts {
684 let part = part.trim();
685 if part.is_empty() || part == "--" {
686 continue; }
688
689 let mut headers_and_body = part.splitn(2, "\r\n\r\n");
690 if let (Some(headers), Some(body)) = (headers_and_body.next(), headers_and_body.next())
691 {
692 let headers = headers.split("\r\n");
694
695 let mut field_name = "";
696 let mut filename = "";
697 let mut content_type = ContentType::Text;
698
699 for header in headers {
700 if header.to_lowercase().starts_with("content-disposition:") {
701 match header.find("filename=\"") {
702 None => {}
703 Some(filename_start) => {
704 let filename_len = filename_start + 10;
705 let filename_end = header[filename_len..].find('"').unwrap() + filename_len;
706 filename = &header[filename_len..filename_end];
707 }
708 }
709
710 match header.find("name=\"") {
711 None => {}
712 Some(name_start) => {
713 let name_start = name_start + 6;
714 let name_end = header[name_start..].find('"').unwrap() + name_start;
715 field_name = &header[name_start..name_end];
716 }
717 }
718 }
719 if header.to_lowercase().starts_with("content-type:") {
720 content_type = ContentType::from(
721 header.to_lowercase().trim_start_matches("content-type:").trim(),
722 );
723 }
724 }
725
726 if filename.is_empty() {
727 fields[field_name.to_string()] = JsonValue::from(body);
728 } else {
729 let mut temp_dir = env::temp_dir();
731 temp_dir.push(filename);
733 let mut temp_file = match fs::File::create(&temp_dir) {
735 Ok(e) => e,
736 Err(_) => continue,
737 };
738 if temp_file.write(body.as_bytes()).is_ok() {
739 if fields[field_name.to_string()].is_empty() {
740 fields[field_name.to_string()] = array![]
741 }
742
743 let extension = Path::new(filename).extension() .and_then(|ext| ext.to_str()); let suffix = extension.unwrap_or("txt");
747
748 fields[field_name.to_string()].push(object! {
749 name:filename,
751 suffix:suffix,
752 size:body.len(),
753 "type":content_type.str(),
754 file:temp_dir.to_str()
755 }).unwrap();
756 };
757 }
758 }
759 }
760 self.content = fields;
761 }
762 ContentType::FormUrlencoded => {
763 let text = unsafe { String::from_utf8_unchecked(data) };
764 let params = text.split("&").collect::<Vec<&str>>();
765 let mut list = object! {};
766 for param in params.iter() {
767 let t = param.split("=").collect::<Vec<&str>>().iter().map(|&x| Body::decode(x).unwrap_or(x.to_string())).collect::<Vec<String>>();
768 list[t[0].to_string()] = t[1].clone().into();
769 }
770 self.content = list;
771 }
772 ContentType::Json => {
773 let text = unsafe { String::from_utf8_unchecked(data) };
774 self.content = json::parse(text.as_str()).unwrap_or(object! {});
775 }
776 ContentType::Xml => {
777 let text = unsafe { String::from_utf8_unchecked(data) };
778 self.content = text.into();
779 }
780 ContentType::Html | ContentType::Text | ContentType::Javascript => {
781 let text = unsafe { String::from_utf8_unchecked(data) };
782 self.content = text.into();
783 }
784 ContentType::Other(_) => self.content = data.into(),
785 ContentType::BinaryStr => todo!(),
786 ContentType::StreamUrlencode => todo!(),
787 }
788 }
789}
790
791impl Default for Body {
792 fn default() -> Self {
793 Self {
794 content_type: ContentType::Other("text/plain".to_string()),
795 boundary: "".to_string(),
796 content_length: 0,
797 content: object! {},
798 stream: vec![],
799 }
800 }
801}
802
803#[derive(Debug, Clone)]
805pub enum ContentType {
806 FormData,
807 FormUrlencoded,
808 Json,
809 Xml,
810 Javascript,
811 Text,
812 Html,
813 Stream,
814 BinaryStr,
815 StreamUrlencode,
816 Other(String),
817}
818impl ContentType {
819 pub fn from(name: &str) -> Self {
820 match name {
821 "multipart/form-data" => Self::FormData,
822 "application/x-www-form-urlencoded" => Self::FormUrlencoded,
823 "application/json" => Self::Json,
824 "application/xml" | "text/xml" => Self::Xml,
825 "application/javascript" => Self::Javascript,
826 "text/html" => Self::Html,
827 "text/plain" => Self::Text,
828 "application/octet-stream" => Self::Stream,
829 _ => Self::Other(name.to_string()),
830 }
831 }
832 pub fn str(&self) -> String {
833 match self {
834 Self::FormData => "multipart/form-data",
835 Self::FormUrlencoded => "application/x-www-form-urlencoded",
836 Self::Json => "application/json",
837 Self::Xml => "application/xml",
838 Self::Javascript => "application/javascript",
839 Self::Text => "text/plain",
840 Self::Html => "text/html",
841 Self::Other(name) => name,
842 Self::Stream => "application/octet-stream",
843 Self::BinaryStr => "application/octet-stream",
844 ContentType::StreamUrlencode => "application/x-www-form-urlencoded",
845 }.to_string()
846 }
847}