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