1use crate::braze::error::BrazeApiError;
8use crate::braze::BrazeClient;
9use crate::resource::{Catalog, CatalogField, CatalogFieldType};
10use reqwest::StatusCode;
11use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Deserialize)]
22struct CatalogsResponse {
23 #[serde(default)]
24 catalogs: Vec<Catalog>,
25 #[serde(default)]
29 next_cursor: Option<String>,
30}
31
32impl BrazeClient {
33 pub async fn list_catalogs(&self) -> Result<Vec<Catalog>, BrazeApiError> {
39 let req = self.get(&["catalogs"]);
40 let resp: CatalogsResponse = self.send_json(req).await?;
41 if let Some(cursor) = resp.next_cursor.as_deref() {
42 if !cursor.is_empty() {
43 return Err(BrazeApiError::PaginationNotImplemented {
44 endpoint: "/catalogs",
45 detail: format!(
46 "got {} catalog(s) plus a non-empty next_cursor; \
47 aborting to prevent silent truncation",
48 resp.catalogs.len()
49 ),
50 });
51 }
52 }
53 Ok(resp.catalogs)
54 }
55
56 pub async fn get_catalog(&self, name: &str) -> Result<Catalog, BrazeApiError> {
63 let req = self.get(&["catalogs", name]);
64 match self.send_json::<CatalogsResponse>(req).await {
65 Ok(resp) => resp
66 .catalogs
67 .into_iter()
68 .next()
69 .ok_or_else(|| BrazeApiError::NotFound {
70 resource: format!("catalog '{name}'"),
71 }),
72 Err(BrazeApiError::Http { status, .. }) if status == StatusCode::NOT_FOUND => {
73 Err(BrazeApiError::NotFound {
74 resource: format!("catalog '{name}'"),
75 })
76 }
77 Err(e) => Err(e),
78 }
79 }
80
81 pub async fn create_catalog(&self, catalog: &Catalog) -> Result<(), BrazeApiError> {
94 let normalized = catalog.normalized();
95 let body = CreateCatalogRequest {
96 catalogs: vec![CreateCatalogEntry {
97 name: &normalized.name,
98 description: normalized.description.as_deref(),
99 fields: normalized
100 .fields
101 .iter()
102 .map(|f| WireField {
103 name: &f.name,
104 field_type: f.field_type,
105 })
106 .collect(),
107 }],
108 };
109 let req = self.post(&["catalogs"]).json(&body);
110 self.send_ok(req).await
111 }
112
113 pub async fn add_catalog_field(
119 &self,
120 catalog_name: &str,
121 field: &CatalogField,
122 ) -> Result<(), BrazeApiError> {
123 let body = AddFieldsRequest {
124 fields: vec![WireField {
125 name: &field.name,
126 field_type: field.field_type,
127 }],
128 };
129 let req = self.post(&["catalogs", catalog_name, "fields"]).json(&body);
130 self.send_ok(req).await
131 }
132
133 pub async fn delete_catalog_field(
142 &self,
143 catalog_name: &str,
144 field_name: &str,
145 ) -> Result<(), BrazeApiError> {
146 let req = self.delete(&["catalogs", catalog_name, "fields", field_name]);
147 self.send_ok(req).await
148 }
149
150 pub async fn delete_catalog(&self, catalog_name: &str) -> Result<(), BrazeApiError> {
156 let req = self.delete(&["catalogs", catalog_name]);
157 self.send_ok(req).await
158 }
159}
160
161#[derive(Serialize)]
162struct AddFieldsRequest<'a> {
163 fields: Vec<WireField<'a>>,
164}
165
166#[derive(Serialize)]
167struct CreateCatalogRequest<'a> {
168 catalogs: Vec<CreateCatalogEntry<'a>>,
169}
170
171#[derive(Serialize)]
172struct CreateCatalogEntry<'a> {
173 name: &'a str,
174 #[serde(skip_serializing_if = "Option::is_none")]
175 description: Option<&'a str>,
176 fields: Vec<WireField<'a>>,
177}
178
179#[derive(Serialize)]
180struct WireField<'a> {
181 name: &'a str,
182 #[serde(rename = "type")]
185 field_type: CatalogFieldType,
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191 use crate::braze::test_client as make_client;
192 use serde_json::json;
193 use wiremock::matchers::{body_json, header, method, path};
194 use wiremock::{Mock, MockServer, ResponseTemplate};
195
196 #[tokio::test]
197 async fn list_catalogs_happy_path() {
198 let server = MockServer::start().await;
199 Mock::given(method("GET"))
200 .and(path("/catalogs"))
201 .and(header("authorization", "Bearer test-key"))
202 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
203 "catalogs": [
204 {
205 "name": "cardiology",
206 "description": "Cardiology catalog",
207 "fields": [
208 {"name": "id", "type": "string"},
209 {"name": "score", "type": "number"}
210 ]
211 },
212 {
213 "name": "dermatology",
214 "fields": [
215 {"name": "id", "type": "string"}
216 ]
217 }
218 ],
219 "message": "success"
220 })))
221 .mount(&server)
222 .await;
223
224 let client = make_client(&server);
225 let cats = client.list_catalogs().await.unwrap();
226 assert_eq!(cats.len(), 2);
227 assert_eq!(cats[0].name, "cardiology");
228 assert_eq!(cats[0].description.as_deref(), Some("Cardiology catalog"));
229 assert_eq!(cats[0].fields.len(), 2);
230 assert_eq!(cats[0].fields[1].field_type, CatalogFieldType::Number);
231 assert_eq!(cats[1].name, "dermatology");
232 assert_eq!(cats[1].description, None);
233 }
234
235 #[tokio::test]
236 async fn list_catalogs_empty() {
237 let server = MockServer::start().await;
238 Mock::given(method("GET"))
239 .and(path("/catalogs"))
240 .respond_with(ResponseTemplate::new(200).set_body_json(json!({"catalogs": []})))
241 .mount(&server)
242 .await;
243 let client = make_client(&server);
244 let cats = client.list_catalogs().await.unwrap();
245 assert!(cats.is_empty());
246 }
247
248 #[tokio::test]
249 async fn list_catalogs_sets_user_agent() {
250 let server = MockServer::start().await;
251 Mock::given(method("GET"))
252 .and(path("/catalogs"))
253 .and(header(
254 "user-agent",
255 concat!("braze-sync/", env!("CARGO_PKG_VERSION")),
256 ))
257 .respond_with(ResponseTemplate::new(200).set_body_json(json!({"catalogs": []})))
258 .mount(&server)
259 .await;
260 let client = make_client(&server);
261 client.list_catalogs().await.unwrap();
262 }
263
264 #[tokio::test]
265 async fn list_catalogs_ignores_unknown_fields_in_response() {
266 let server = MockServer::start().await;
270 Mock::given(method("GET"))
271 .and(path("/catalogs"))
272 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
273 "catalogs": [
274 {
275 "name": "future",
276 "description": "tomorrow",
277 "future_metadata": {"foo": "bar"},
278 "num_items": 1234,
279 "fields": [
280 {"name": "id", "type": "string", "extra": "ignored"}
281 ]
282 }
283 ],
284 "future_top_level": {"whatever": true},
285 "message": "success"
286 })))
287 .mount(&server)
288 .await;
289 let client = make_client(&server);
290 let cats = client.list_catalogs().await.unwrap();
291 assert_eq!(cats.len(), 1);
292 assert_eq!(cats[0].name, "future");
293 }
294
295 #[tokio::test]
296 async fn list_catalogs_errors_when_next_cursor_present() {
297 let server = MockServer::start().await;
299 Mock::given(method("GET"))
300 .and(path("/catalogs"))
301 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
302 "catalogs": [
303 {"name": "cardiology", "fields": [{"name": "id", "type": "string"}]}
304 ],
305 "next_cursor": "abc123"
306 })))
307 .mount(&server)
308 .await;
309 let client = make_client(&server);
310 let err = client.list_catalogs().await.unwrap_err();
311 match err {
312 BrazeApiError::PaginationNotImplemented { endpoint, detail } => {
313 assert_eq!(endpoint, "/catalogs");
314 assert!(detail.contains("next_cursor"), "detail: {detail}");
315 assert!(detail.contains("1 catalog"), "detail: {detail}");
316 }
317 other => panic!("expected PaginationNotImplemented, got {other:?}"),
318 }
319 }
320
321 #[tokio::test]
322 async fn list_catalogs_empty_string_cursor_is_treated_as_no_more_pages() {
323 let server = MockServer::start().await;
329 Mock::given(method("GET"))
330 .and(path("/catalogs"))
331 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
332 "catalogs": [{"name": "only", "fields": []}],
333 "next_cursor": ""
334 })))
335 .mount(&server)
336 .await;
337 let client = make_client(&server);
338 let cats = client.list_catalogs().await.unwrap();
339 assert_eq!(cats.len(), 1);
340 assert_eq!(cats[0].name, "only");
341 }
342
343 #[tokio::test]
344 async fn unauthorized_returns_typed_error() {
345 let server = MockServer::start().await;
346 Mock::given(method("GET"))
347 .and(path("/catalogs"))
348 .respond_with(ResponseTemplate::new(401).set_body_string("invalid api key"))
349 .mount(&server)
350 .await;
351 let client = make_client(&server);
352 let err = client.list_catalogs().await.unwrap_err();
353 assert!(matches!(err, BrazeApiError::Unauthorized), "got {err:?}");
354 }
355
356 #[tokio::test]
357 async fn server_error_carries_status_and_body() {
358 let server = MockServer::start().await;
359 Mock::given(method("GET"))
360 .and(path("/catalogs"))
361 .respond_with(ResponseTemplate::new(500).set_body_string("internal explosion"))
362 .mount(&server)
363 .await;
364 let client = make_client(&server);
365 let err = client.list_catalogs().await.unwrap_err();
366 match err {
367 BrazeApiError::Http { status, body } => {
368 assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
369 assert!(body.contains("internal explosion"));
370 }
371 other => panic!("expected Http, got {other:?}"),
372 }
373 }
374
375 #[tokio::test]
376 async fn retries_on_429_and_succeeds() {
377 let server = MockServer::start().await;
378 Mock::given(method("GET"))
382 .and(path("/catalogs"))
383 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
384 "catalogs": [{"name": "after_retry", "fields": []}]
385 })))
386 .mount(&server)
387 .await;
388 Mock::given(method("GET"))
389 .and(path("/catalogs"))
390 .respond_with(ResponseTemplate::new(429).insert_header("retry-after", "0"))
391 .up_to_n_times(1)
392 .mount(&server)
393 .await;
394
395 let client = make_client(&server);
396 let cats = client.list_catalogs().await.unwrap();
397 assert_eq!(cats.len(), 1);
398 assert_eq!(cats[0].name, "after_retry");
399 }
400
401 #[tokio::test]
402 async fn retries_exhausted_returns_rate_limit_exhausted() {
403 let server = MockServer::start().await;
404 Mock::given(method("GET"))
405 .and(path("/catalogs"))
406 .respond_with(ResponseTemplate::new(429).insert_header("retry-after", "0"))
407 .mount(&server)
408 .await;
409 let client = make_client(&server);
410 let err = client.list_catalogs().await.unwrap_err();
411 assert!(
412 matches!(err, BrazeApiError::RateLimitExhausted),
413 "got {err:?}"
414 );
415 }
416
417 #[tokio::test]
418 async fn get_catalog_happy_path() {
419 let server = MockServer::start().await;
420 Mock::given(method("GET"))
421 .and(path("/catalogs/cardiology"))
422 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
423 "catalogs": [
424 {"name": "cardiology", "fields": [{"name": "id", "type": "string"}]}
425 ]
426 })))
427 .mount(&server)
428 .await;
429 let client = make_client(&server);
430 let cat = client.get_catalog("cardiology").await.unwrap();
431 assert_eq!(cat.name, "cardiology");
432 assert_eq!(cat.fields.len(), 1);
433 }
434
435 #[tokio::test]
436 async fn get_catalog_404_is_mapped_to_not_found() {
437 let server = MockServer::start().await;
438 Mock::given(method("GET"))
439 .and(path("/catalogs/missing"))
440 .respond_with(ResponseTemplate::new(404).set_body_string("not found"))
441 .mount(&server)
442 .await;
443 let client = make_client(&server);
444 let err = client.get_catalog("missing").await.unwrap_err();
445 match err {
446 BrazeApiError::NotFound { resource } => assert!(resource.contains("missing")),
447 other => panic!("expected NotFound, got {other:?}"),
448 }
449 }
450
451 #[tokio::test]
452 async fn get_catalog_empty_response_array_is_not_found() {
453 let server = MockServer::start().await;
454 Mock::given(method("GET"))
455 .and(path("/catalogs/ghost"))
456 .respond_with(ResponseTemplate::new(200).set_body_json(json!({"catalogs": []})))
457 .mount(&server)
458 .await;
459 let client = make_client(&server);
460 let err = client.get_catalog("ghost").await.unwrap_err();
461 assert!(matches!(err, BrazeApiError::NotFound { .. }), "got {err:?}");
462 }
463
464 #[tokio::test]
465 async fn debug_does_not_leak_api_key() {
466 let server = MockServer::start().await;
467 let client = make_client(&server);
468 let dbg = format!("{client:?}");
469 assert!(!dbg.contains("test-key"), "leaked api key in: {dbg}");
470 assert!(dbg.contains("<redacted>"));
471 }
472
473 #[tokio::test]
474 async fn add_catalog_field_happy_path_sends_correct_body() {
475 let server = MockServer::start().await;
476 Mock::given(method("POST"))
477 .and(path("/catalogs/cardiology/fields"))
478 .and(header("authorization", "Bearer test-key"))
479 .and(body_json(json!({
480 "fields": [{"name": "severity_level", "type": "number"}]
481 })))
482 .respond_with(ResponseTemplate::new(200).set_body_json(json!({"message": "success"})))
483 .mount(&server)
484 .await;
485
486 let client = make_client(&server);
487 let field = CatalogField {
488 name: "severity_level".into(),
489 field_type: CatalogFieldType::Number,
490 };
491 client
492 .add_catalog_field("cardiology", &field)
493 .await
494 .unwrap();
495 }
496
497 #[tokio::test]
498 async fn add_catalog_field_unauthorized_propagates() {
499 let server = MockServer::start().await;
500 Mock::given(method("POST"))
501 .and(path("/catalogs/cardiology/fields"))
502 .respond_with(ResponseTemplate::new(401).set_body_string("invalid key"))
503 .mount(&server)
504 .await;
505
506 let client = make_client(&server);
507 let field = CatalogField {
508 name: "x".into(),
509 field_type: CatalogFieldType::String,
510 };
511 let err = client
512 .add_catalog_field("cardiology", &field)
513 .await
514 .unwrap_err();
515 assert!(matches!(err, BrazeApiError::Unauthorized), "got {err:?}");
516 }
517
518 #[tokio::test]
519 async fn add_catalog_field_retries_on_429_then_succeeds() {
520 let server = MockServer::start().await;
521 Mock::given(method("POST"))
525 .and(path("/catalogs/cardiology/fields"))
526 .respond_with(ResponseTemplate::new(200).set_body_json(json!({"message": "ok"})))
527 .mount(&server)
528 .await;
529 Mock::given(method("POST"))
530 .and(path("/catalogs/cardiology/fields"))
531 .respond_with(ResponseTemplate::new(429).insert_header("retry-after", "0"))
532 .up_to_n_times(1)
533 .mount(&server)
534 .await;
535
536 let client = make_client(&server);
537 let field = CatalogField {
538 name: "x".into(),
539 field_type: CatalogFieldType::String,
540 };
541 client
542 .add_catalog_field("cardiology", &field)
543 .await
544 .unwrap();
545 }
546
547 #[tokio::test]
548 async fn create_catalog_happy_path_sends_correct_body() {
549 let server = MockServer::start().await;
550 Mock::given(method("POST"))
551 .and(path("/catalogs"))
552 .and(header("authorization", "Bearer test-key"))
553 .and(body_json(json!({
554 "catalogs": [{
555 "name": "cardiology",
556 "description": "Cardiology catalog",
557 "fields": [
558 {"name": "id", "type": "string"},
559 {"name": "severity_level", "type": "number"}
560 ]
561 }]
562 })))
563 .respond_with(ResponseTemplate::new(201).set_body_json(json!({"message": "success"})))
564 .mount(&server)
565 .await;
566
567 let client = make_client(&server);
568 let cat = Catalog {
569 name: "cardiology".into(),
570 description: Some("Cardiology catalog".into()),
571 fields: vec![
572 CatalogField {
573 name: "id".into(),
574 field_type: CatalogFieldType::String,
575 },
576 CatalogField {
577 name: "severity_level".into(),
578 field_type: CatalogFieldType::Number,
579 },
580 ],
581 };
582 client.create_catalog(&cat).await.unwrap();
583 }
584
585 #[tokio::test]
586 async fn create_catalog_hoists_id_field_to_first_position() {
587 let server = MockServer::start().await;
592 Mock::given(method("POST"))
593 .and(path("/catalogs"))
594 .and(body_json(json!({
595 "catalogs": [{
596 "name": "alpha",
597 "fields": [
598 {"name": "id", "type": "string"},
599 {"name": "URL", "type": "string"},
600 {"name": "author", "type": "string"},
601 {"name": "title", "type": "string"}
602 ]
603 }]
604 })))
605 .respond_with(ResponseTemplate::new(201).set_body_json(json!({"message": "success"})))
606 .mount(&server)
607 .await;
608
609 let client = make_client(&server);
610 let cat = Catalog {
611 name: "alpha".into(),
612 description: None,
613 fields: vec![
614 CatalogField {
615 name: "URL".into(),
616 field_type: CatalogFieldType::String,
617 },
618 CatalogField {
619 name: "author".into(),
620 field_type: CatalogFieldType::String,
621 },
622 CatalogField {
623 name: "id".into(),
624 field_type: CatalogFieldType::String,
625 },
626 CatalogField {
627 name: "title".into(),
628 field_type: CatalogFieldType::String,
629 },
630 ],
631 };
632 client.create_catalog(&cat).await.unwrap();
633 }
634
635 #[tokio::test]
636 async fn create_catalog_omits_description_when_none() {
637 let server = MockServer::start().await;
638 Mock::given(method("POST"))
639 .and(path("/catalogs"))
640 .and(body_json(json!({
641 "catalogs": [{
642 "name": "minimal",
643 "fields": [{"name": "id", "type": "string"}]
644 }]
645 })))
646 .respond_with(ResponseTemplate::new(201).set_body_json(json!({"message": "success"})))
647 .mount(&server)
648 .await;
649
650 let client = make_client(&server);
651 let cat = Catalog {
652 name: "minimal".into(),
653 description: None,
654 fields: vec![CatalogField {
655 name: "id".into(),
656 field_type: CatalogFieldType::String,
657 }],
658 };
659 client.create_catalog(&cat).await.unwrap();
660 }
661
662 #[tokio::test]
663 async fn create_catalog_duplicate_name_propagates_400() {
664 let server = MockServer::start().await;
665 Mock::given(method("POST"))
666 .and(path("/catalogs"))
667 .respond_with(ResponseTemplate::new(400).set_body_json(json!({
668 "errors": [{
669 "id": "catalog-name-already-exists",
670 "message": "A catalog with that name already exists"
671 }]
672 })))
673 .mount(&server)
674 .await;
675
676 let client = make_client(&server);
677 let cat = Catalog {
678 name: "existing".into(),
679 description: None,
680 fields: vec![],
681 };
682 let err = client.create_catalog(&cat).await.unwrap_err();
683 assert!(
684 matches!(
685 &err,
686 BrazeApiError::Http { status, body }
687 if *status == StatusCode::BAD_REQUEST
688 && body.contains("catalog-name-already-exists")
689 ),
690 "got {err:?}"
691 );
692 }
693
694 #[tokio::test]
695 async fn create_catalog_unauthorized_propagates() {
696 let server = MockServer::start().await;
697 Mock::given(method("POST"))
698 .and(path("/catalogs"))
699 .respond_with(ResponseTemplate::new(401).set_body_string("invalid key"))
700 .mount(&server)
701 .await;
702
703 let client = make_client(&server);
704 let cat = Catalog {
705 name: "x".into(),
706 description: None,
707 fields: vec![],
708 };
709 let err = client.create_catalog(&cat).await.unwrap_err();
710 assert!(matches!(err, BrazeApiError::Unauthorized), "got {err:?}");
711 }
712
713 #[tokio::test]
714 async fn create_catalog_retries_on_429_then_succeeds() {
715 let server = MockServer::start().await;
716 Mock::given(method("POST"))
717 .and(path("/catalogs"))
718 .respond_with(ResponseTemplate::new(201).set_body_json(json!({"message": "ok"})))
719 .mount(&server)
720 .await;
721 Mock::given(method("POST"))
722 .and(path("/catalogs"))
723 .respond_with(ResponseTemplate::new(429).insert_header("retry-after", "0"))
724 .up_to_n_times(1)
725 .mount(&server)
726 .await;
727
728 let client = make_client(&server);
729 let cat = Catalog {
730 name: "x".into(),
731 description: None,
732 fields: vec![CatalogField {
733 name: "id".into(),
734 field_type: CatalogFieldType::String,
735 }],
736 };
737 client.create_catalog(&cat).await.unwrap();
738 }
739
740 #[tokio::test]
741 async fn delete_catalog_field_happy_path_uses_segment_encoded_path() {
742 let server = MockServer::start().await;
743 Mock::given(method("DELETE"))
744 .and(path("/catalogs/cardiology/fields/legacy_code"))
745 .and(header("authorization", "Bearer test-key"))
746 .respond_with(ResponseTemplate::new(204))
747 .mount(&server)
748 .await;
749
750 let client = make_client(&server);
751 client
752 .delete_catalog_field("cardiology", "legacy_code")
753 .await
754 .unwrap();
755 }
756
757 #[tokio::test]
758 async fn delete_catalog_field_server_error_returns_http() {
759 let server = MockServer::start().await;
760 Mock::given(method("DELETE"))
761 .and(path("/catalogs/cardiology/fields/x"))
762 .respond_with(ResponseTemplate::new(500).set_body_string("oops"))
763 .mount(&server)
764 .await;
765
766 let client = make_client(&server);
767 let err = client
768 .delete_catalog_field("cardiology", "x")
769 .await
770 .unwrap_err();
771 match err {
772 BrazeApiError::Http { status, body } => {
773 assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
774 assert!(body.contains("oops"));
775 }
776 other => panic!("expected Http, got {other:?}"),
777 }
778 }
779
780 #[tokio::test]
781 async fn delete_catalog_happy_path() {
782 let server = MockServer::start().await;
783 Mock::given(method("DELETE"))
784 .and(path("/catalogs/cardiology"))
785 .and(header("authorization", "Bearer test-key"))
786 .respond_with(ResponseTemplate::new(204))
787 .mount(&server)
788 .await;
789
790 let client = make_client(&server);
791 client.delete_catalog("cardiology").await.unwrap();
792 }
793
794 #[tokio::test]
795 async fn delete_catalog_404_stays_as_http_not_mapped_to_not_found() {
796 let server = MockServer::start().await;
797 Mock::given(method("DELETE"))
798 .and(path("/catalogs/missing"))
799 .respond_with(ResponseTemplate::new(404).set_body_string("not found"))
800 .mount(&server)
801 .await;
802
803 let client = make_client(&server);
804 let err = client.delete_catalog("missing").await.unwrap_err();
805 match err {
806 BrazeApiError::Http { status, .. } => {
807 assert_eq!(status, StatusCode::NOT_FOUND);
808 }
809 other => panic!("expected Http(404), got {other:?}"),
810 }
811 }
812
813 #[tokio::test]
814 async fn delete_catalog_server_error_returns_http() {
815 let server = MockServer::start().await;
816 Mock::given(method("DELETE"))
817 .and(path("/catalogs/cardiology"))
818 .respond_with(ResponseTemplate::new(500).set_body_string("oops"))
819 .mount(&server)
820 .await;
821
822 let client = make_client(&server);
823 let err = client.delete_catalog("cardiology").await.unwrap_err();
824 match err {
825 BrazeApiError::Http { status, body } => {
826 assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
827 assert!(body.contains("oops"));
828 }
829 other => panic!("expected Http, got {other:?}"),
830 }
831 }
832}