1#[cfg(feature = "online")]
8use crate::constants::GRAMS_PER_GRAIN;
9use serde::{Deserialize, Serialize};
10use std::time::Duration;
11
12#[derive(Debug, Clone, Serialize)]
14pub struct TrajectoryRequest {
15 pub bc_value: f64,
17 pub bc_type: String,
19 pub bullet_mass: f64,
21 pub muzzle_velocity: f64,
23 pub target_distance: f64,
25 #[serde(skip_serializing_if = "Option::is_none")]
27 pub zero_range: Option<f64>,
28 #[serde(skip_serializing_if = "Option::is_none")]
30 pub wind_speed: Option<f64>,
31 #[serde(skip_serializing_if = "Option::is_none")]
33 pub wind_angle: Option<f64>,
34 #[serde(skip_serializing_if = "Option::is_none")]
36 pub temperature: Option<f64>,
37 #[serde(skip_serializing_if = "Option::is_none")]
39 pub pressure: Option<f64>,
40 #[serde(skip_serializing_if = "Option::is_none")]
42 pub humidity: Option<f64>,
43 #[serde(skip_serializing_if = "Option::is_none")]
45 pub altitude: Option<f64>,
46 #[serde(skip_serializing_if = "Option::is_none")]
48 pub latitude: Option<f64>,
49 #[serde(skip_serializing_if = "Option::is_none")]
51 pub longitude: Option<f64>,
52 #[serde(skip_serializing_if = "Option::is_none")]
54 pub shot_direction: Option<f64>,
55 #[serde(skip_serializing_if = "Option::is_none")]
57 pub shooting_angle: Option<f64>,
58 #[serde(skip_serializing_if = "Option::is_none")]
60 pub twist_rate: Option<f64>,
61 #[serde(skip_serializing_if = "Option::is_none")]
63 pub bullet_diameter: Option<f64>,
64 #[serde(skip_serializing_if = "Option::is_none")]
66 pub bullet_length: Option<f64>,
67 #[serde(skip_serializing_if = "Option::is_none")]
69 pub ground_threshold: Option<f64>,
70 #[serde(skip_serializing_if = "Option::is_none")]
72 pub enable_weather_zones: Option<bool>,
73 #[serde(skip_serializing_if = "Option::is_none")]
75 pub enable_3d_weather: Option<bool>,
76 #[serde(skip_serializing_if = "Option::is_none")]
78 pub wind_shear_model: Option<String>,
79 #[serde(skip_serializing_if = "Option::is_none")]
81 pub weather_zone_interpolation: Option<String>,
82 #[serde(skip_serializing_if = "Option::is_none")]
85 pub sample_interval: Option<f64>,
86}
87
88#[derive(Debug, Clone, Deserialize)]
90pub struct TrajectoryResponse {
91 pub trajectory: Vec<ApiTrajectoryPoint>,
93 pub zero_angle: f64,
95 pub time_of_flight: f64,
97 #[serde(default)]
99 pub bc_confidence: Option<f64>,
100 #[serde(default)]
102 pub ml_corrections_applied: Option<Vec<String>>,
103 #[serde(default)]
105 pub max_ordinate: Option<f64>,
106 #[serde(default)]
108 pub impact_velocity: Option<f64>,
109 #[serde(default)]
111 pub impact_energy: Option<f64>,
112}
113
114#[derive(Debug, Clone, Deserialize)]
116pub struct ApiTrajectoryPoint {
117 pub range: f64,
119 pub drop: f64,
121 pub drift: f64,
123 pub velocity: f64,
125 pub energy: f64,
127 pub time: f64,
129}
130
131#[derive(Debug, Clone, Serialize)]
133pub struct TrueVelocityRequest {
134 pub measured_drop_mil: f64,
136 pub range_yd: f64,
138 pub bc: f64,
140 pub drag_model: String,
142 pub weight_gr: f64,
144 pub caliber: f64,
146 #[serde(skip_serializing_if = "Option::is_none")]
148 pub zero_range_yd: Option<f64>,
149 #[serde(skip_serializing_if = "Option::is_none")]
151 pub chrono_velocity_fps: Option<f64>,
152 #[serde(skip_serializing_if = "Option::is_none")]
154 pub altitude_ft: Option<f64>,
155 #[serde(skip_serializing_if = "Option::is_none")]
157 pub temperature_f: Option<f64>,
158 #[serde(skip_serializing_if = "Option::is_none")]
160 pub pressure_inhg: Option<f64>,
161 #[serde(skip_serializing_if = "Option::is_none")]
163 pub humidity: Option<f64>,
164 #[serde(skip_serializing_if = "Option::is_none")]
166 pub sight_height_in: Option<f64>,
167 #[serde(skip_serializing_if = "Option::is_none")]
169 pub use_bc_enhancement: Option<bool>,
170}
171
172#[derive(Debug, Clone, Deserialize)]
174pub struct TrueVelocityResponse {
175 pub effective_velocity_fps: f64,
177 #[serde(default)]
179 pub velocity_adjustment_fps: Option<f64>,
180 #[serde(default)]
182 pub adjustment_percent: Option<f64>,
183 pub confidence: String,
185 pub iterations: i32,
187 pub final_error_mil: f64,
189 pub calculated_drop_mil: f64,
191}
192
193#[derive(Debug)]
195pub enum ApiError {
196 NetworkError(String),
198 Timeout,
200 InvalidResponse(String),
202 ServerError(u16, String),
204 RequestError(String),
206}
207
208impl std::fmt::Display for ApiError {
209 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
210 match self {
211 ApiError::NetworkError(msg) => write!(f, "Network error: {}", msg),
212 ApiError::Timeout => write!(f, "Request timed out"),
213 ApiError::InvalidResponse(msg) => write!(f, "Invalid response: {}", msg),
214 ApiError::ServerError(code, msg) => write!(f, "Server error {}: {}", code, msg),
215 ApiError::RequestError(msg) => write!(f, "Request error: {}", msg),
216 }
217 }
218}
219
220impl std::error::Error for ApiError {}
221
222pub struct ApiClient {
224 base_url: String,
225 timeout: Duration,
226}
227
228impl ApiClient {
229 pub fn new(base_url: &str, timeout_secs: u64) -> Self {
235 let base_url = base_url.trim_end_matches('/').to_string();
237
238 Self {
239 base_url,
240 timeout: Duration::from_secs(timeout_secs),
241 }
242 }
243
244 #[cfg(feature = "online")]
253 pub fn calculate_trajectory(
254 &self,
255 request: &TrajectoryRequest,
256 ) -> Result<TrajectoryResponse, ApiError> {
257 let url = format!("{}/v1/calculate", self.base_url);
259
260 let velocity_fps = request.muzzle_velocity / 0.3048; let mass_grains = request.bullet_mass / GRAMS_PER_GRAIN; let distance_yards = request.target_distance / 0.9144; let mut req = ureq::get(&url)
266 .config()
267 .timeout_global(Some(self.timeout))
268 .build()
269 .header("Accept", "application/json")
270 .header("User-Agent", &format!("ballistics-cli/{}", env!("CARGO_PKG_VERSION")))
271 .query("bc_value", request.bc_value.to_string())
272 .query("bc_type", &request.bc_type)
273 .query("bullet_mass", format!("{:.1}", mass_grains))
274 .query("muzzle_velocity", format!("{:.1}", velocity_fps))
275 .query("target_distance", format!("{:.1}", distance_yards));
276
277 if let Some(zero_range) = request.zero_range {
279 let zero_yards = zero_range / 0.9144;
280 req = req.query("zero_distance", format!("{:.1}", zero_yards));
281 }
282 if let Some(wind_speed) = request.wind_speed {
283 let wind_mph = wind_speed * 2.23694; req = req.query("wind_speed", format!("{:.1}", wind_mph));
285 }
286 if let Some(wind_angle) = request.wind_angle {
287 req = req.query("wind_angle", format!("{:.1}", wind_angle));
288 }
289 if let Some(temp) = request.temperature {
290 let temp_f = temp * 9.0 / 5.0 + 32.0; req = req.query("temperature", format!("{:.1}", temp_f));
292 }
293 if let Some(pressure) = request.pressure {
294 let pressure_inhg = pressure / 33.8639; req = req.query("pressure", format!("{:.2}", pressure_inhg));
296 }
297 if let Some(humidity) = request.humidity {
298 req = req.query("humidity", format!("{:.1}", humidity));
299 }
300 if let Some(altitude) = request.altitude {
301 let altitude_ft = altitude / 0.3048; req = req.query("altitude", format!("{:.1}", altitude_ft));
303 }
304 if let Some(shooting_angle) = request.shooting_angle {
305 req = req.query("shooting_angle", format!("{:.1}", shooting_angle));
306 }
307 if let Some(latitude) = request.latitude {
308 req = req.query("latitude", format!("{:.2}", latitude));
309 }
310 if let Some(longitude) = request.longitude {
311 req = req.query("longitude", format!("{:.2}", longitude));
312 }
313 if let Some(shot_direction) = request.shot_direction {
314 req = req.query("shot_direction", format!("{:.1}", shot_direction));
315 }
316 if let Some(twist_rate) = request.twist_rate {
317 req = req.query("twist_rate", format!("{:.1}", twist_rate));
318 }
319 if let Some(diameter) = request.bullet_diameter {
320 let diameter_in = diameter / 0.0254; req = req.query("bullet_diameter", format!("{:.3}", diameter_in));
322 }
323 if let Some(threshold) = request.ground_threshold {
324 if threshold.is_finite() {
331 req = req.query("ground_threshold", format!("{:.1}", threshold));
332 } else if threshold == f64::NEG_INFINITY {
333 req = req.query("ground_threshold", "-1000000000.0");
334 }
335 }
336 if let Some(enable) = request.enable_weather_zones {
337 req = req.query("enable_weather_zones", if enable { "true" } else { "false" });
338 }
339 if let Some(enable) = request.enable_3d_weather {
340 req = req.query("enable_3d_weather", if enable { "true" } else { "false" });
341 }
342 if let Some(ref model) = request.wind_shear_model {
343 req = req.query("wind_shear_model", model);
344 }
345 if let Some(ref method) = request.weather_zone_interpolation {
346 req = req.query("weather_zone_interpolation", method);
347 }
348 if let Some(sample_interval) = request.sample_interval {
349 let step_yards = sample_interval / 0.9144;
351 req = req.query("trajectory_step", format!("{:.4}", step_yards));
352 }
353
354 let mut response = req.call().map_err(map_ureq_error)?;
355
356 let status = response.status();
360 if !status.is_success() {
361 let body = response.body_mut().read_to_string().unwrap_or_default();
362 return Err(ApiError::ServerError(status.as_u16(), body));
363 }
364
365 let body = response
366 .body_mut()
367 .read_to_string()
368 .map_err(|e| ApiError::InvalidResponse(e.to_string()))?;
369
370 let api_response: serde_json::Value = serde_json::from_str(&body)
372 .map_err(|e| ApiError::InvalidResponse(format!("JSON parse error: {}", e)))?;
373
374 self.convert_api_response(&api_response)
376 }
377
378 #[cfg(feature = "online")]
380 fn extract_value(val: &serde_json::Value) -> Option<f64> {
381 val.get("value")
383 .and_then(|v| v.as_f64())
384 .or_else(|| val.as_f64())
385 }
386
387 #[cfg(feature = "online")]
388 fn convert_api_response(&self, api_response: &serde_json::Value) -> Result<TrajectoryResponse, ApiError> {
389 let results = api_response.get("results");
391
392 let trajectory_array = api_response.get("trajectory")
395 .and_then(|t| t.as_array())
396 .ok_or_else(|| ApiError::InvalidResponse("Missing trajectory array".to_string()))?;
397
398 let trajectory: Vec<ApiTrajectoryPoint> = trajectory_array
399 .iter()
400 .filter_map(|point| {
401 let range_yards = point.get("distance")
403 .and_then(Self::extract_value)?;
404 let drop_inches = point.get("drop")
405 .and_then(Self::extract_value)
406 .unwrap_or(0.0);
407 let drift_inches = point.get("wind_drift")
408 .and_then(Self::extract_value)
409 .unwrap_or(0.0);
410 let velocity_fps = point.get("velocity")
411 .and_then(Self::extract_value)?;
412 let energy_ftlbs = point.get("energy")
413 .and_then(Self::extract_value)
414 .unwrap_or(0.0);
415 let time = point.get("time")
416 .and_then(Self::extract_value)
417 .unwrap_or(0.0);
418
419 Some(ApiTrajectoryPoint {
420 range: range_yards * 0.9144, drop: drop_inches * 0.0254, drift: drift_inches * 0.0254, velocity: velocity_fps * 0.3048, energy: energy_ftlbs * 1.35582, time,
426 })
427 })
428 .collect();
429
430 let zero_angle = results
432 .and_then(|r| r.get("barrel_angle"))
433 .and_then(Self::extract_value)
434 .unwrap_or(0.0)
435 .to_radians();
436
437 let time_of_flight = results
438 .and_then(|r| r.get("time_of_flight"))
439 .and_then(Self::extract_value)
440 .unwrap_or_else(|| trajectory.last().map(|p| p.time).unwrap_or(0.0));
441
442 let bc_confidence = api_response.get("bc_confidence")
443 .and_then(|v| v.as_f64());
444
445 let ml_corrections = api_response.get("ml_corrections_applied")
446 .or_else(|| api_response.get("corrections_applied"))
447 .and_then(|v| v.as_array())
448 .map(|arr| {
449 arr.iter()
450 .filter_map(|v| v.as_str().map(String::from))
451 .collect()
452 });
453
454 let max_ordinate = results
456 .and_then(|r| r.get("max_height"))
457 .and_then(|v| v.as_f64())
458 .map(|h| h * 0.0254); let impact_velocity = results
461 .and_then(|r| r.get("final_velocity"))
462 .and_then(Self::extract_value)
463 .map(|v| v * 0.3048); let impact_energy = results
466 .and_then(|r| r.get("final_energy"))
467 .and_then(Self::extract_value)
468 .map(|e| e * 1.35582); Ok(TrajectoryResponse {
471 trajectory,
472 zero_angle,
473 time_of_flight,
474 bc_confidence,
475 ml_corrections_applied: ml_corrections,
476 max_ordinate,
477 impact_velocity,
478 impact_energy,
479 })
480 }
481
482 #[cfg(feature = "online")]
484 pub fn health_check(&self) -> Result<bool, ApiError> {
485 let url = format!("{}/health", self.base_url);
486
487 let response = ureq::get(&url)
488 .config()
489 .timeout_global(Some(Duration::from_secs(5)))
490 .build()
491 .call()
492 .map_err(map_ureq_error)?;
493
494 Ok(response.status().as_u16() == 200)
495 }
496
497 #[cfg(feature = "online")]
506 pub fn true_velocity(
507 &self,
508 request: &TrueVelocityRequest,
509 ) -> Result<TrueVelocityResponse, ApiError> {
510 let url = format!("{}/v1/true-velocity", self.base_url);
511
512 let body = serde_json::to_string(request)
513 .map_err(|e| ApiError::RequestError(format!("Failed to serialize request: {}", e)))?;
514
515 let mut response = ureq::post(&url)
516 .config()
517 .timeout_global(Some(self.timeout))
518 .build()
519 .header("Content-Type", "application/json")
520 .header("Accept", "application/json")
521 .header("User-Agent", &format!("ballistics-cli/{}", env!("CARGO_PKG_VERSION")))
522 .send(&body)
523 .map_err(map_ureq_error)?;
524
525 let status = response.status();
527 if !status.is_success() {
528 let body = response.body_mut().read_to_string().unwrap_or_default();
529 return Err(ApiError::ServerError(status.as_u16(), body));
530 }
531
532 let response_body = response
533 .body_mut()
534 .read_to_string()
535 .map_err(|e| ApiError::InvalidResponse(format!("Failed to read response: {}", e)))?;
536
537 serde_json::from_str(&response_body)
538 .map_err(|e| ApiError::InvalidResponse(format!("JSON parse error: {}", e)))
539 }
540}
541
542#[cfg(feature = "online")]
549fn map_ureq_error(e: ureq::Error) -> ApiError {
550 match e {
551 ureq::Error::Timeout(_) => ApiError::Timeout,
553 ureq::Error::Io(io_err) => {
555 if io_err.kind() == std::io::ErrorKind::TimedOut {
556 ApiError::Timeout
557 } else {
558 ApiError::NetworkError(io_err.to_string())
559 }
560 }
561 other => ApiError::NetworkError(other.to_string()),
563 }
564}
565
566#[derive(Default)]
568pub struct TrajectoryRequestBuilder {
569 bc_value: Option<f64>,
570 bc_type: Option<String>,
571 bullet_mass: Option<f64>,
572 muzzle_velocity: Option<f64>,
573 target_distance: Option<f64>,
574 zero_range: Option<f64>,
575 wind_speed: Option<f64>,
576 wind_angle: Option<f64>,
577 temperature: Option<f64>,
578 pressure: Option<f64>,
579 humidity: Option<f64>,
580 altitude: Option<f64>,
581 latitude: Option<f64>,
582 longitude: Option<f64>,
583 shot_direction: Option<f64>,
584 shooting_angle: Option<f64>,
585 twist_rate: Option<f64>,
586 bullet_diameter: Option<f64>,
587 bullet_length: Option<f64>,
588 ground_threshold: Option<f64>,
589 enable_weather_zones: Option<bool>,
590 enable_3d_weather: Option<bool>,
591 wind_shear_model: Option<String>,
592 weather_zone_interpolation: Option<String>,
593 sample_interval: Option<f64>,
594}
595
596impl TrajectoryRequestBuilder {
597 pub fn new() -> Self {
598 Self::default()
599 }
600
601 pub fn bc_value(mut self, value: f64) -> Self {
602 self.bc_value = Some(value);
603 self
604 }
605
606 pub fn bc_type(mut self, value: &str) -> Self {
607 self.bc_type = Some(value.to_string());
608 self
609 }
610
611 pub fn bullet_mass(mut self, value: f64) -> Self {
612 self.bullet_mass = Some(value);
613 self
614 }
615
616 pub fn muzzle_velocity(mut self, value: f64) -> Self {
617 self.muzzle_velocity = Some(value);
618 self
619 }
620
621 pub fn target_distance(mut self, value: f64) -> Self {
622 self.target_distance = Some(value);
623 self
624 }
625
626 pub fn zero_range(mut self, value: f64) -> Self {
627 self.zero_range = Some(value);
628 self
629 }
630
631 pub fn wind_speed(mut self, value: f64) -> Self {
632 self.wind_speed = Some(value);
633 self
634 }
635
636 pub fn wind_angle(mut self, value: f64) -> Self {
637 self.wind_angle = Some(value);
638 self
639 }
640
641 pub fn temperature(mut self, value: f64) -> Self {
642 self.temperature = Some(value);
643 self
644 }
645
646 pub fn pressure(mut self, value: f64) -> Self {
647 self.pressure = Some(value);
648 self
649 }
650
651 pub fn humidity(mut self, value: f64) -> Self {
652 self.humidity = Some(value);
653 self
654 }
655
656 pub fn altitude(mut self, value: f64) -> Self {
657 self.altitude = Some(value);
658 self
659 }
660
661 pub fn latitude(mut self, value: f64) -> Self {
662 self.latitude = Some(value);
663 self
664 }
665
666 pub fn longitude(mut self, value: f64) -> Self {
667 self.longitude = Some(value);
668 self
669 }
670
671 pub fn shot_direction(mut self, value: f64) -> Self {
672 self.shot_direction = Some(value);
673 self
674 }
675
676 pub fn shooting_angle(mut self, value: f64) -> Self {
677 self.shooting_angle = Some(value);
678 self
679 }
680
681 pub fn twist_rate(mut self, value: f64) -> Self {
682 self.twist_rate = Some(value);
683 self
684 }
685
686 pub fn bullet_diameter(mut self, value: f64) -> Self {
687 self.bullet_diameter = Some(value);
688 self
689 }
690
691 pub fn bullet_length(mut self, value: f64) -> Self {
692 self.bullet_length = Some(value);
693 self
694 }
695
696 pub fn ground_threshold(mut self, value: f64) -> Self {
697 self.ground_threshold = Some(value);
698 self
699 }
700
701 pub fn enable_weather_zones(mut self, value: bool) -> Self {
702 self.enable_weather_zones = Some(value);
703 self
704 }
705
706 pub fn enable_3d_weather(mut self, value: bool) -> Self {
707 self.enable_3d_weather = Some(value);
708 self
709 }
710
711 pub fn wind_shear_model(mut self, value: &str) -> Self {
712 self.wind_shear_model = Some(value.to_string());
713 self
714 }
715
716 pub fn weather_zone_interpolation(mut self, value: &str) -> Self {
717 self.weather_zone_interpolation = Some(value.to_string());
718 self
719 }
720
721 pub fn sample_interval(mut self, value: f64) -> Self {
722 self.sample_interval = Some(value);
723 self
724 }
725
726 pub fn build(self) -> Result<TrajectoryRequest, String> {
732 let bc_value = self.bc_value.ok_or("bc_value is required")?;
733 let bc_type = self.bc_type.ok_or("bc_type is required")?;
734 let bullet_mass = self.bullet_mass.ok_or("bullet_mass is required")?;
735 let muzzle_velocity = self.muzzle_velocity.ok_or("muzzle_velocity is required")?;
736 let target_distance = self.target_distance.ok_or("target_distance is required")?;
737
738 Ok(TrajectoryRequest {
739 bc_value,
740 bc_type,
741 bullet_mass,
742 muzzle_velocity,
743 target_distance,
744 zero_range: self.zero_range,
745 wind_speed: self.wind_speed,
746 wind_angle: self.wind_angle,
747 temperature: self.temperature,
748 pressure: self.pressure,
749 humidity: self.humidity,
750 altitude: self.altitude,
751 latitude: self.latitude,
752 longitude: self.longitude,
753 shot_direction: self.shot_direction,
754 shooting_angle: self.shooting_angle,
755 twist_rate: self.twist_rate,
756 bullet_diameter: self.bullet_diameter,
757 bullet_length: self.bullet_length,
758 ground_threshold: self.ground_threshold,
759 enable_weather_zones: self.enable_weather_zones,
760 enable_3d_weather: self.enable_3d_weather,
761 wind_shear_model: self.wind_shear_model,
762 weather_zone_interpolation: self.weather_zone_interpolation,
763 sample_interval: self.sample_interval,
764 })
765 }
766}
767
768#[cfg(test)]
769mod tests {
770 use super::*;
771
772 #[test]
773 fn test_request_builder_required_fields() {
774 let result = TrajectoryRequestBuilder::new()
775 .bc_value(0.238)
776 .bc_type("G7")
777 .bullet_mass(9.07) .muzzle_velocity(860.0)
779 .target_distance(1000.0)
780 .build();
781
782 assert!(result.is_ok());
783 let request = result.unwrap();
784 assert_eq!(request.bc_value, 0.238);
785 assert_eq!(request.bc_type, "G7");
786 }
787
788 #[test]
789 fn test_request_builder_missing_fields() {
790 let result = TrajectoryRequestBuilder::new()
791 .bc_value(0.238)
792 .build();
793
794 assert!(result.is_err());
795 }
796
797 #[test]
798 fn test_request_builder_all_optional_fields() {
799 let result = TrajectoryRequestBuilder::new()
800 .bc_value(0.238)
801 .bc_type("G7")
802 .bullet_mass(9.07)
803 .muzzle_velocity(860.0)
804 .target_distance(1000.0)
805 .zero_range(100.0)
806 .wind_speed(5.0)
807 .wind_angle(90.0)
808 .temperature(15.0)
809 .pressure(1013.25)
810 .humidity(50.0)
811 .altitude(500.0)
812 .latitude(45.0)
813 .shooting_angle(0.0)
814 .twist_rate(10.0)
815 .bullet_diameter(0.00671)
816 .bullet_length(0.035)
817 .build();
818
819 assert!(result.is_ok());
820 let request = result.unwrap();
821 assert_eq!(request.zero_range, Some(100.0));
822 assert_eq!(request.wind_speed, Some(5.0));
823 assert_eq!(request.latitude, Some(45.0));
824 }
825
826 #[test]
827 fn test_api_client_url_normalization() {
828 let client1 = ApiClient::new("https://api.example.com/", 10);
829 assert_eq!(client1.base_url, "https://api.example.com");
830
831 let client2 = ApiClient::new("https://api.example.com", 10);
832 assert_eq!(client2.base_url, "https://api.example.com");
833 }
834
835 #[test]
836 fn test_api_error_display() {
837 assert_eq!(
838 format!("{}", ApiError::NetworkError("connection refused".to_string())),
839 "Network error: connection refused"
840 );
841 assert_eq!(format!("{}", ApiError::Timeout), "Request timed out");
842 assert_eq!(
843 format!("{}", ApiError::ServerError(500, "Internal error".to_string())),
844 "Server error 500: Internal error"
845 );
846 }
847}