Skip to main content

xbp_cli/provider_support/
cloudflare.rs

1use super::http::{AuthStrategy, HttpError, RequestFactory, ResponseBytes};
2use super::models::{
3    CloudflareDnsRecord, CloudflareDnsRecordBatch, CloudflareDnsRecordWrite, CloudflareDnsSettings,
4    CloudflareDnssec, CloudflareDnssecEdit, CloudflareEnvelope, CloudflareWorkerBuild,
5    CloudflareWorkerScript, CloudflareWorkerSecret, CloudflareWorkerSettings, CloudflareZone,
6    CloudflareZoneFilters, DomainAvailability, PaginationInfo, RegisteredDomain, SecretRecord,
7    SecretsQuota, SecretsStore,
8};
9use reqwest::header::{HeaderName, HeaderValue};
10use reqwest::StatusCode;
11use serde::{Deserialize, Serialize};
12use serde_json::{json, Value};
13
14const CLOUDFLARE_API_BASE: &str = "https://api.cloudflare.com/client/v4";
15
16#[derive(Debug, Clone)]
17pub struct CloudflareClient {
18    account_id: String,
19    http: RequestFactory,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct CloudflareStoreCreateRequest {
24    pub name: String,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct CloudflareSecretCreateRequest {
29    pub name: String,
30    pub value: String,
31    #[serde(default)]
32    pub scopes: Vec<String>,
33    #[serde(default)]
34    pub comment: Option<String>,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize, Default)]
38pub struct CloudflareSecretEditRequest {
39    #[serde(default)]
40    pub name: Option<String>,
41    #[serde(default)]
42    pub value: Option<String>,
43    #[serde(default)]
44    pub scopes: Option<Vec<String>>,
45    #[serde(default)]
46    pub comment: Option<String>,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct CloudflareSecretDuplicateRequest {
51    pub name: String,
52    #[serde(default)]
53    pub scopes: Vec<String>,
54    #[serde(default)]
55    pub comment: Option<String>,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct CloudflareBulkDeleteRequest {
60    pub ids: Vec<String>,
61}
62
63#[derive(Debug, Clone, Serialize, Default)]
64pub struct CloudflareZoneCreateRequest {
65    pub name: String,
66    #[serde(default)]
67    pub account: Option<CloudflareZoneAccountWrite>,
68    #[serde(default)]
69    pub jump_start: Option<bool>,
70    #[serde(rename = "type", default)]
71    pub zone_type: Option<String>,
72}
73
74#[derive(Debug, Clone, Serialize, Default)]
75pub struct CloudflareZoneEditRequest {
76    #[serde(default)]
77    pub paused: Option<bool>,
78    #[serde(rename = "type", default)]
79    pub zone_type: Option<String>,
80    #[serde(default)]
81    pub vanity_name_servers: Option<Vec<String>>,
82}
83
84#[derive(Debug, Clone, Serialize, Default)]
85pub struct CloudflareZoneAccountWrite {
86    #[serde(default)]
87    pub id: Option<String>,
88}
89
90#[derive(Debug, Clone, Serialize, Default)]
91pub struct CloudflareDnsRecordListFilters {
92    #[serde(default)]
93    pub name: Option<String>,
94    #[serde(rename = "type", default)]
95    pub record_type: Option<String>,
96    #[serde(default)]
97    pub page: Option<u64>,
98    #[serde(default)]
99    pub per_page: Option<u64>,
100}
101
102impl CloudflareClient {
103    pub fn new(
104        api_token: impl Into<String>,
105        account_id: impl Into<String>,
106    ) -> Result<Self, String> {
107        let token = api_token.into();
108        let http = RequestFactory::new(CLOUDFLARE_API_BASE)
109            .map_err(|error| error.to_string())?
110            .with_auth(AuthStrategy::Bearer(token))
111            .with_default_header(
112                HeaderName::from_static("content-type"),
113                HeaderValue::from_static("application/json"),
114            );
115        Ok(Self {
116            account_id: account_id.into(),
117            http,
118        })
119    }
120
121    pub async fn search_domains(
122        &self,
123        query: &str,
124        extension: &[String],
125    ) -> Result<Vec<DomainAvailability>, String> {
126        #[derive(Serialize)]
127        struct SearchQuery<'a> {
128            query: &'a str,
129            #[serde(skip_serializing_if = "<[String]>::is_empty")]
130            tlds: &'a [String],
131        }
132
133        let envelope: CloudflareEnvelope<Vec<DomainAvailability>> = self
134            .http
135            .get_json(
136                &format!("/accounts/{}/registrar/domain-search", self.account_id),
137                Some(&SearchQuery {
138                    query,
139                    tlds: extension,
140                }),
141            )
142            .await
143            .map_err(render_http_error)?;
144        Ok(envelope.result)
145    }
146
147    pub async fn check_domains(
148        &self,
149        domains: &[String],
150    ) -> Result<Vec<DomainAvailability>, String> {
151        let envelope: CloudflareEnvelope<Vec<DomainAvailability>> = self
152            .http
153            .post_json(
154                &format!("/accounts/{}/registrar/domain-check", self.account_id),
155                &json!({ "domains": domains }),
156            )
157            .await
158            .map_err(render_http_error)?;
159        Ok(envelope.result)
160    }
161
162    pub async fn list_registered_domains(
163        &self,
164    ) -> Result<(Vec<RegisteredDomain>, Option<PaginationInfo>), String> {
165        let envelope: CloudflareEnvelope<Vec<RegisteredDomain>> = self
166            .http
167            .get_json(
168                &format!("/accounts/{}/registrar/domains", self.account_id),
169                Option::<&Value>::None,
170            )
171            .await
172            .map_err(render_http_error)?;
173        Ok((envelope.result, envelope.result_info))
174    }
175
176    pub async fn list_stores(&self) -> Result<(Vec<SecretsStore>, Option<PaginationInfo>), String> {
177        let envelope: CloudflareEnvelope<Vec<SecretsStore>> = self
178            .http
179            .get_json(
180                &format!("/accounts/{}/secrets_store/stores", self.account_id),
181                Option::<&Value>::None,
182            )
183            .await
184            .map_err(render_http_error)?;
185        Ok((envelope.result, envelope.result_info))
186    }
187
188    pub async fn get_store(&self, store_id: &str) -> Result<SecretsStore, String> {
189        let envelope: CloudflareEnvelope<SecretsStore> = self
190            .http
191            .get_json(
192                &format!(
193                    "/accounts/{}/secrets_store/stores/{}",
194                    self.account_id, store_id
195                ),
196                Option::<&Value>::None,
197            )
198            .await
199            .map_err(render_http_error)?;
200        Ok(envelope.result)
201    }
202
203    pub async fn create_store(
204        &self,
205        request: &CloudflareStoreCreateRequest,
206    ) -> Result<SecretsStore, String> {
207        let envelope: CloudflareEnvelope<SecretsStore> = self
208            .http
209            .post_json(
210                &format!("/accounts/{}/secrets_store/stores", self.account_id),
211                request,
212            )
213            .await
214            .map_err(render_http_error)?;
215        Ok(envelope.result)
216    }
217
218    pub async fn delete_store(&self, store_id: &str) -> Result<(), String> {
219        let _: CloudflareEnvelope<Option<Value>> = self
220            .http
221            .delete_json(
222                &format!(
223                    "/accounts/{}/secrets_store/stores/{}",
224                    self.account_id, store_id
225                ),
226                Option::<&Value>::None,
227            )
228            .await
229            .map_err(render_http_error)?;
230        Ok(())
231    }
232
233    pub async fn list_secrets(
234        &self,
235        store_id: &str,
236    ) -> Result<(Vec<SecretRecord>, Option<PaginationInfo>), String> {
237        let envelope: CloudflareEnvelope<Vec<SecretRecord>> = self
238            .http
239            .get_json(
240                &format!(
241                    "/accounts/{}/secrets_store/stores/{}/secrets",
242                    self.account_id, store_id
243                ),
244                Option::<&Value>::None,
245            )
246            .await
247            .map_err(render_http_error)?;
248        Ok((envelope.result, envelope.result_info))
249    }
250
251    pub async fn get_secret(
252        &self,
253        store_id: &str,
254        secret_id: &str,
255    ) -> Result<SecretRecord, String> {
256        let envelope: CloudflareEnvelope<SecretRecord> = self
257            .http
258            .get_json(
259                &format!(
260                    "/accounts/{}/secrets_store/stores/{}/secrets/{}",
261                    self.account_id, store_id, secret_id
262                ),
263                Option::<&Value>::None,
264            )
265            .await
266            .map_err(render_http_error)?;
267        Ok(envelope.result)
268    }
269
270    pub async fn create_secret(
271        &self,
272        store_id: &str,
273        request: &CloudflareSecretCreateRequest,
274    ) -> Result<SecretRecord, String> {
275        let envelope: CloudflareEnvelope<Vec<SecretRecord>> = self
276            .http
277            .post_json(
278                &format!(
279                    "/accounts/{}/secrets_store/stores/{}/secrets",
280                    self.account_id, store_id
281                ),
282                &vec![request],
283            )
284            .await
285            .map_err(render_http_error)?;
286        envelope
287            .result
288            .into_iter()
289            .next()
290            .ok_or_else(|| "Cloudflare did not return a created secret.".to_string())
291    }
292
293    pub async fn edit_secret(
294        &self,
295        store_id: &str,
296        secret_id: &str,
297        request: &CloudflareSecretEditRequest,
298    ) -> Result<SecretRecord, String> {
299        let envelope: CloudflareEnvelope<SecretRecord> = self
300            .http
301            .patch_json(
302                &format!(
303                    "/accounts/{}/secrets_store/stores/{}/secrets/{}",
304                    self.account_id, store_id, secret_id
305                ),
306                request,
307            )
308            .await
309            .map_err(render_http_error)?;
310        Ok(envelope.result)
311    }
312
313    pub async fn delete_secret(&self, store_id: &str, secret_id: &str) -> Result<(), String> {
314        let _: CloudflareEnvelope<Option<Value>> = self
315            .http
316            .delete_json(
317                &format!(
318                    "/accounts/{}/secrets_store/stores/{}/secrets/{}",
319                    self.account_id, store_id, secret_id
320                ),
321                Option::<&Value>::None,
322            )
323            .await
324            .map_err(render_http_error)?;
325        Ok(())
326    }
327
328    pub async fn bulk_delete_secrets(
329        &self,
330        store_id: &str,
331        request: &CloudflareBulkDeleteRequest,
332    ) -> Result<(), String> {
333        let _: CloudflareEnvelope<Option<Value>> = self
334            .http
335            .delete_json_with_body(
336                &format!(
337                    "/accounts/{}/secrets_store/stores/{}/secrets",
338                    self.account_id, store_id
339                ),
340                Option::<&Value>::None,
341                request,
342            )
343            .await
344            .map_err(render_http_error)?;
345        Ok(())
346    }
347
348    pub async fn duplicate_secret(
349        &self,
350        store_id: &str,
351        secret_id: &str,
352        request: &CloudflareSecretDuplicateRequest,
353    ) -> Result<SecretRecord, String> {
354        let envelope: CloudflareEnvelope<SecretRecord> = self
355            .http
356            .post_json(
357                &format!(
358                    "/accounts/{}/secrets_store/stores/{}/secrets/{}/duplicate",
359                    self.account_id, store_id, secret_id
360                ),
361                request,
362            )
363            .await
364            .map_err(render_http_error)?;
365        Ok(envelope.result)
366    }
367
368    pub async fn get_quota(&self) -> Result<SecretsQuota, String> {
369        let envelope: CloudflareEnvelope<SecretsQuota> = self
370            .http
371            .get_json(
372                &format!("/accounts/{}/secrets_store/quota", self.account_id),
373                Option::<&Value>::None,
374            )
375            .await
376            .map_err(render_http_error)?;
377        Ok(envelope.result)
378    }
379
380    pub async fn list_worker_secrets(
381        &self,
382        script_name: &str,
383    ) -> Result<Vec<CloudflareWorkerSecret>, String> {
384        let envelope: CloudflareEnvelope<Vec<CloudflareWorkerSecret>> = self
385            .http
386            .get_json(
387                &format!(
388                    "/accounts/{}/workers/scripts/{}/secrets",
389                    self.account_id, script_name
390                ),
391                Option::<&Value>::None,
392            )
393            .await
394            .map_err(render_http_error)?;
395        Ok(envelope.result)
396    }
397
398    pub async fn get_worker_secret(
399        &self,
400        script_name: &str,
401        secret_name: &str,
402    ) -> Result<CloudflareWorkerSecret, String> {
403        let envelope: CloudflareEnvelope<CloudflareWorkerSecret> = self
404            .http
405            .get_json(
406                &format!(
407                    "/accounts/{}/workers/scripts/{}/secrets/{}",
408                    self.account_id, script_name, secret_name
409                ),
410                Option::<&Value>::None,
411            )
412            .await
413            .map_err(render_http_error)?;
414        Ok(envelope.result)
415    }
416
417    pub async fn put_worker_secret(
418        &self,
419        script_name: &str,
420        secret_name: &str,
421        secret_value: &str,
422    ) -> Result<CloudflareWorkerSecret, String> {
423        let envelope: CloudflareEnvelope<CloudflareWorkerSecret> = self
424            .http
425            .put_json(
426                &format!(
427                    "/accounts/{}/workers/scripts/{}/secrets",
428                    self.account_id, script_name
429                ),
430                &json!({
431                    "name": secret_name,
432                    "text": secret_value,
433                    "type": "secret_text"
434                }),
435            )
436            .await
437            .map_err(render_http_error)?;
438        Ok(envelope.result)
439    }
440
441    pub async fn delete_worker_secret(
442        &self,
443        script_name: &str,
444        secret_name: &str,
445    ) -> Result<(), String> {
446        let _: CloudflareEnvelope<Option<Value>> = self
447            .http
448            .delete_json(
449                &format!(
450                    "/accounts/{}/workers/scripts/{}/secrets/{}",
451                    self.account_id, script_name, secret_name
452                ),
453                Option::<&Value>::None,
454            )
455            .await
456            .map_err(render_http_error)?;
457        Ok(())
458    }
459
460    pub async fn patch_worker_secrets_bulk(
461        &self,
462        script_name: &str,
463        patch: &Value,
464    ) -> Result<Value, String> {
465        let envelope: CloudflareEnvelope<Value> = self
466            .http
467            .patch_json(
468                &format!(
469                    "/accounts/{}/workers/scripts/{}/secrets-bulk",
470                    self.account_id, script_name
471                ),
472                patch,
473            )
474            .await
475            .map_err(render_http_error)?;
476        Ok(envelope.result)
477    }
478
479    pub async fn list_worker_scripts(&self) -> Result<Vec<CloudflareWorkerScript>, String> {
480        let envelope: CloudflareEnvelope<Vec<CloudflareWorkerScript>> = self
481            .http
482            .get_json(
483                &format!("/accounts/{}/workers/scripts", self.account_id),
484                Option::<&Value>::None,
485            )
486            .await
487            .map_err(render_http_error)?;
488        Ok(envelope.result)
489    }
490
491    pub async fn list_worker_builds(
492        &self,
493        worker_tag: &str,
494    ) -> Result<Vec<CloudflareWorkerBuild>, String> {
495        let envelope: CloudflareEnvelope<Vec<CloudflareWorkerBuild>> = self
496            .http
497            .get_json(
498                &format!(
499                    "/accounts/{}/builds/workers/{}/builds",
500                    self.account_id, worker_tag
501                ),
502                Option::<&Value>::None,
503            )
504            .await
505            .map_err(render_http_error)?;
506        Ok(envelope.result)
507    }
508
509    pub async fn get_worker_build_logs(&self, build_uuid: &str) -> Result<Value, String> {
510        let envelope: CloudflareEnvelope<Value> = self
511            .http
512            .get_json(
513                &format!(
514                    "/accounts/{}/builds/builds/{}/logs",
515                    self.account_id, build_uuid
516                ),
517                Option::<&Value>::None,
518            )
519            .await
520            .map_err(render_http_error)?;
521        Ok(envelope.result)
522    }
523
524    pub async fn get_worker_settings(
525        &self,
526        script_name: &str,
527    ) -> Result<Option<CloudflareWorkerSettings>, String> {
528        let result: Result<CloudflareEnvelope<CloudflareWorkerSettings>, HttpError> = self
529            .http
530            .get_json(
531                &format!(
532                    "/accounts/{}/workers/scripts/{}/settings",
533                    self.account_id, script_name
534                ),
535                Option::<&Value>::None,
536            )
537            .await;
538
539        match result {
540            Ok(envelope) => Ok(Some(envelope.result)),
541            Err(HttpError::Request {
542                status: Some(StatusCode::NOT_FOUND),
543                ..
544            }) => Ok(None),
545            Err(error) => Err(render_http_error(error)),
546        }
547    }
548
549    pub async fn list_zones(
550        &self,
551        filters: &CloudflareZoneFilters,
552    ) -> Result<(Vec<CloudflareZone>, Option<PaginationInfo>), String> {
553        let envelope: CloudflareEnvelope<Vec<CloudflareZone>> = self
554            .http
555            .get_json("/zones", Some(filters))
556            .await
557            .map_err(render_http_error)?;
558        Ok((envelope.result, envelope.result_info))
559    }
560
561    pub async fn get_zone(&self, zone_id: &str) -> Result<CloudflareZone, String> {
562        let envelope: CloudflareEnvelope<CloudflareZone> = self
563            .http
564            .get_json(&format!("/zones/{}", zone_id), Option::<&Value>::None)
565            .await
566            .map_err(render_http_error)?;
567        Ok(envelope.result)
568    }
569
570    pub async fn create_zone(
571        &self,
572        request: &CloudflareZoneCreateRequest,
573    ) -> Result<CloudflareZone, String> {
574        let envelope: CloudflareEnvelope<CloudflareZone> = self
575            .http
576            .post_json("/zones", request)
577            .await
578            .map_err(render_http_error)?;
579        Ok(envelope.result)
580    }
581
582    pub async fn edit_zone(
583        &self,
584        zone_id: &str,
585        request: &CloudflareZoneEditRequest,
586    ) -> Result<CloudflareZone, String> {
587        let envelope: CloudflareEnvelope<CloudflareZone> = self
588            .http
589            .patch_json(&format!("/zones/{}", zone_id), request)
590            .await
591            .map_err(render_http_error)?;
592        Ok(envelope.result)
593    }
594
595    pub async fn delete_zone(&self, zone_id: &str) -> Result<(), String> {
596        let _: CloudflareEnvelope<Option<Value>> = self
597            .http
598            .delete_json(&format!("/zones/{}", zone_id), Option::<&Value>::None)
599            .await
600            .map_err(render_http_error)?;
601        Ok(())
602    }
603
604    pub async fn list_records(
605        &self,
606        zone_id: &str,
607        filters: &CloudflareDnsRecordListFilters,
608    ) -> Result<(Vec<CloudflareDnsRecord>, Option<PaginationInfo>), String> {
609        let envelope: CloudflareEnvelope<Vec<CloudflareDnsRecord>> = self
610            .http
611            .get_json(&format!("/zones/{}/dns_records", zone_id), Some(filters))
612            .await
613            .map_err(render_http_error)?;
614        Ok((envelope.result, envelope.result_info))
615    }
616
617    pub async fn get_record(
618        &self,
619        zone_id: &str,
620        record_id: &str,
621    ) -> Result<CloudflareDnsRecord, String> {
622        let envelope: CloudflareEnvelope<CloudflareDnsRecord> = self
623            .http
624            .get_json(
625                &format!("/zones/{}/dns_records/{}", zone_id, record_id),
626                Option::<&Value>::None,
627            )
628            .await
629            .map_err(render_http_error)?;
630        Ok(envelope.result)
631    }
632
633    pub async fn create_record(
634        &self,
635        zone_id: &str,
636        request: &CloudflareDnsRecordWrite,
637    ) -> Result<CloudflareDnsRecord, String> {
638        let envelope: CloudflareEnvelope<CloudflareDnsRecord> = self
639            .http
640            .post_json(&format!("/zones/{}/dns_records", zone_id), request)
641            .await
642            .map_err(render_http_error)?;
643        Ok(envelope.result)
644    }
645
646    pub async fn replace_record(
647        &self,
648        zone_id: &str,
649        record_id: &str,
650        request: &CloudflareDnsRecordWrite,
651    ) -> Result<CloudflareDnsRecord, String> {
652        let envelope: CloudflareEnvelope<CloudflareDnsRecord> = self
653            .http
654            .put_json(
655                &format!("/zones/{}/dns_records/{}", zone_id, record_id),
656                request,
657            )
658            .await
659            .map_err(render_http_error)?;
660        Ok(envelope.result)
661    }
662
663    pub async fn edit_record(
664        &self,
665        zone_id: &str,
666        record_id: &str,
667        request: &CloudflareDnsRecordWrite,
668    ) -> Result<CloudflareDnsRecord, String> {
669        let envelope: CloudflareEnvelope<CloudflareDnsRecord> = self
670            .http
671            .patch_json(
672                &format!("/zones/{}/dns_records/{}", zone_id, record_id),
673                request,
674            )
675            .await
676            .map_err(render_http_error)?;
677        Ok(envelope.result)
678    }
679
680    pub async fn delete_record(&self, zone_id: &str, record_id: &str) -> Result<(), String> {
681        let _: CloudflareEnvelope<Option<Value>> = self
682            .http
683            .delete_json(
684                &format!("/zones/{}/dns_records/{}", zone_id, record_id),
685                Option::<&Value>::None,
686            )
687            .await
688            .map_err(render_http_error)?;
689        Ok(())
690    }
691
692    pub async fn batch_records(
693        &self,
694        zone_id: &str,
695        request: &CloudflareDnsRecordBatch,
696    ) -> Result<Value, String> {
697        let envelope: CloudflareEnvelope<Value> = self
698            .http
699            .post_json(&format!("/zones/{}/dns_records/batch", zone_id), request)
700            .await
701            .map_err(render_http_error)?;
702        Ok(envelope.result)
703    }
704
705    pub async fn export_records(&self, zone_id: &str) -> Result<ResponseBytes, String> {
706        self.http
707            .get_bytes(
708                &format!("/zones/{}/dns_records/export", zone_id),
709                Option::<&Value>::None,
710            )
711            .await
712            .map_err(render_http_error)
713    }
714
715    pub async fn import_records(&self, zone_id: &str, bytes: Vec<u8>) -> Result<Value, String> {
716        let response = self
717            .http
718            .post_bytes(
719                &format!("/zones/{}/dns_records/import", zone_id),
720                bytes,
721                "text/plain",
722            )
723            .await
724            .map_err(render_http_error)?;
725        let envelope: CloudflareEnvelope<Value> =
726            serde_json::from_slice(&response.body).map_err(|error| error.to_string())?;
727        Ok(envelope.result)
728    }
729
730    pub async fn get_dnssec(&self, zone_id: &str) -> Result<CloudflareDnssec, String> {
731        let envelope: CloudflareEnvelope<CloudflareDnssec> = self
732            .http
733            .get_json(
734                &format!("/zones/{}/dnssec", zone_id),
735                Option::<&Value>::None,
736            )
737            .await
738            .map_err(render_http_error)?;
739        Ok(envelope.result)
740    }
741
742    pub async fn edit_dnssec(
743        &self,
744        zone_id: &str,
745        request: &CloudflareDnssecEdit,
746    ) -> Result<CloudflareDnssec, String> {
747        let envelope: CloudflareEnvelope<CloudflareDnssec> = self
748            .http
749            .patch_json(&format!("/zones/{}/dnssec", zone_id), request)
750            .await
751            .map_err(render_http_error)?;
752        Ok(envelope.result)
753    }
754
755    pub async fn get_dns_settings(&self, zone_id: &str) -> Result<CloudflareDnsSettings, String> {
756        let envelope: CloudflareEnvelope<CloudflareDnsSettings> = self
757            .http
758            .get_json(
759                &format!("/zones/{}/dns_settings", zone_id),
760                Option::<&Value>::None,
761            )
762            .await
763            .map_err(render_http_error)?;
764        Ok(envelope.result)
765    }
766
767    pub async fn edit_dns_settings(
768        &self,
769        zone_id: &str,
770        request: &CloudflareDnsSettings,
771    ) -> Result<CloudflareDnsSettings, String> {
772        let envelope: CloudflareEnvelope<CloudflareDnsSettings> = self
773            .http
774            .patch_json(&format!("/zones/{}/dns_settings", zone_id), request)
775            .await
776            .map_err(render_http_error)?;
777        Ok(envelope.result)
778    }
779}
780
781fn render_http_error(error: HttpError) -> String {
782    error.to_string()
783}