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