1use crate::ISSUE_STATUS_VALUES_NEXT_STEP;
4use std::borrow::Cow;
5
6#[derive(Debug, thiserror::Error, PartialEq, Eq)]
8pub enum CliError {
9 #[error("unknown or missing command")]
11 UnknownCommand,
12 #[error("unknown command: {command}")]
14 UnknownCommandName {
15 command: String,
17 next: &'static str,
19 },
20 #[error("missing argument: {argument}")]
22 MissingArgument {
23 argument: &'static str,
25 next: &'static str,
27 },
28 #[error("missing value for {flag}")]
30 MissingFlagValue {
31 flag: &'static str,
33 next: &'static str,
35 },
36 #[error("duplicate flag: {flag}")]
38 DuplicateFlag {
39 flag: &'static str,
41 next: &'static str,
43 },
44 #[error("unexpected argument for {command}: {argument}")]
46 UnexpectedArgument {
47 argument: String,
49 command: &'static str,
51 next: &'static str,
53 },
54 #[error("unknown flag: {flag}")]
56 UnknownFlag {
57 flag: String,
59 next: &'static str,
61 },
62 #[error("unsupported flag for {command}: {flag}")]
64 UnsupportedFlag {
65 flag: String,
67 command: &'static str,
69 next: &'static str,
71 },
72 #[error("unknown resource: {resource}")]
74 UnknownResource {
75 resource: String,
77 next: &'static str,
79 },
80 #[error("unknown issue status: {0}")]
82 UnknownStatus(String),
83 #[error("unknown trace status: {0}")]
85 UnknownTraceStatus(String),
86 #[error("unknown log level: {0}")]
88 UnknownLogLevel(String),
89 #[error("invalid limit: {0}")]
91 InvalidLimit(String),
92 #[error("invalid minimum duration: {0}")]
94 InvalidMinDuration(String),
95 #[error("unknown pagination mode")]
97 UnknownPagination,
98 #[error("invalid action cursor: {0}")]
100 InvalidActionCursor(String),
101 #[error("invalid log cursor: {0}")]
103 InvalidLogCursor(String),
104 #[error("invalid issue cursor: {0}")]
106 InvalidIssueCursor(String),
107 #[error("invalid support cursor: {0}")]
109 InvalidSupportCursor(String),
110 #[error("unknown support category")]
112 UnknownSupportCategory,
113 #[error("invalid support ticket id")]
115 InvalidSupportTicketId,
116 #[error("invalid support retry key")]
118 InvalidSupportRetryKey,
119 #[error("invalid support context reply")]
121 InvalidSupportContextReply,
122 #[error("invalid support context command")]
124 InvalidSupportContextCommand,
125 #[error("invalid support context")]
127 InvalidSupportContext,
128 #[error("invalid issue investigation command")]
130 InvalidInvestigationCommand,
131 #[error("invalid project doctor command")]
133 InvalidDoctorCommand,
134 #[error("invalid project create command")]
136 InvalidProjectCreateCommand,
137 #[error("invalid project ingest key create command")]
139 InvalidProjectIngestKeyCreateCommand,
140 #[error("invalid project archive command")]
142 InvalidProjectArchiveCommand,
143 #[error("invalid projects command")]
145 InvalidProjectsCommand,
146 #[error("invalid login provider")]
148 InvalidLoginProvider,
149 #[error("invalid usage command")]
151 InvalidUsageCommand,
152 #[error("invalid native debug-artifact command")]
154 InvalidNativeDebugCommand,
155 #[error("invalid native debug-artifact identity")]
157 InvalidNativeDebugIdentity,
158 #[error("invalid setup source: {0}")]
160 InvalidSetupSource(String),
161}
162
163#[derive(Debug, thiserror::Error)]
165pub enum RuntimeError {
166 #[error(transparent)]
168 Cli(#[from] CliError),
169 #[error(transparent)]
171 Io(#[from] std::io::Error),
172 #[error(transparent)]
174 Http(#[from] reqwest::Error),
175 #[error("not logged in: run logbrew login")]
177 MissingToken,
178 #[error("api returned status {status}: {body}")]
180 Api {
181 status: u16,
183 body: String,
185 auth_source: &'static str,
187 auth_label: &'static str,
189 },
190 #[error("LogBrew API unreachable: {message}")]
192 StatusUnavailable {
193 api_url: String,
195 status_code: Option<u16>,
197 body: Option<String>,
199 authenticated: bool,
201 auth_source: &'static str,
203 auth_label: &'static str,
205 message: String,
207 },
208 #[error("{message}")]
210 Unavailable {
211 message: &'static str,
213 next: &'static str,
215 },
216 #[error("issue investigation returned an invalid response")]
218 InvestigationResponseInvalid,
219 #[error("native debug artifact is invalid")]
221 NativeDebugArtifactInvalid,
222 #[error("native debug-artifact response is invalid")]
224 NativeDebugResponseInvalid,
225 #[error("native debug-artifact verification failed")]
227 NativeDebugVerificationFailed,
228}
229
230pub fn write_cli_error<W: std::io::Write>(
236 error: &CliError,
237 json: bool,
238 output: &mut W,
239) -> Result<(), std::io::Error> {
240 if json {
241 let body = serde_json::json!({
242 "ok": false,
243 "error": cli_error_code(error),
244 "message": error.to_string(),
245 "next": cli_error_next_step(error),
246 });
247 writeln!(output, "{body}")
248 } else {
249 writeln!(output, "{error}")?;
250 writeln!(output, "Next: {}", cli_error_next_step(error))
251 }
252}
253
254pub fn write_runtime_error<W: std::io::Write>(
260 error: &RuntimeError,
261 json: bool,
262 output: &mut W,
263) -> Result<(), std::io::Error> {
264 if json {
265 let body = runtime_error_json(error);
266 writeln!(output, "{body}")
267 } else {
268 write_human_runtime_error(error, output)
269 }
270}
271
272pub fn write_native_debug_runtime_error<W: std::io::Write>(
278 error: &RuntimeError,
279 output: &mut W,
280) -> Result<(), std::io::Error> {
281 let body = match error {
282 RuntimeError::Api { status, body, .. } => {
283 let details = ApiErrorDetails::parse(body);
284 let (fallback_code, fallback_next) = native_debug_api_recovery(*status);
285 let code = details
286 .code
287 .as_deref()
288 .and_then(|value| allowed_native_debug_code(*status, value))
289 .unwrap_or(fallback_code);
290 let next = details
291 .next
292 .as_deref()
293 .and_then(|value| allowed_native_debug_next(*status, value))
294 .unwrap_or(fallback_next);
295 serde_json::json!({
296 "ok": false,
297 "error": code,
298 "status": status,
299 "next": next,
300 })
301 }
302 RuntimeError::Cli(_)
303 | RuntimeError::Io(_)
304 | RuntimeError::Http(_)
305 | RuntimeError::MissingToken
306 | RuntimeError::StatusUnavailable { .. }
307 | RuntimeError::Unavailable { .. }
308 | RuntimeError::InvestigationResponseInvalid
309 | RuntimeError::NativeDebugArtifactInvalid
310 | RuntimeError::NativeDebugResponseInvalid
311 | RuntimeError::NativeDebugVerificationFailed => serde_json::json!({
312 "ok": false,
313 "error": runtime_error_code(error),
314 "next": fallback_runtime_error_next_step(error),
315 }),
316 };
317 writeln!(output, "{body}")
318}
319
320const fn native_debug_api_recovery(status: u16) -> (&'static str, &'static str) {
322 match status {
323 400 => (
324 "validation_failed",
325 "check the artifact identity and request scope, then retry",
326 ),
327 401 | 403 => (
328 "unauthorized",
329 "sign in and retry the native debug-artifact command",
330 ),
331 404 => (
332 "not_found",
333 "check the exact project, release, environment, service, UUID, and architecture",
334 ),
335 405 => (
336 "method_not_allowed",
337 "use the supported native debug-artifact request method",
338 ),
339 408 => (
340 "request_timeout",
341 "retry the same native debug-artifact request",
342 ),
343 413 => (
344 "payload_too_large",
345 "reduce the native debug-artifact upload below the documented size limits and retry",
346 ),
347 422 => (
348 "validation_failed",
349 "send manifest and debug_file_N multipart parts from LogBrew Apple release tooling",
350 ),
351 429 => (
352 "rate_limited",
353 "retry the same native debug-artifact command later",
354 ),
355 500..=599 => (
356 "server_error",
357 "retry the same native debug-artifact command later",
358 ),
359 _ => (
360 "unexpected_response",
361 "retry the native debug-artifact command",
362 ),
363 }
364}
365
366fn allowed_native_debug_code(status: u16, value: &str) -> Option<&'static str> {
368 match (status, value) {
369 (400 | 422, "validation_failed") => Some("validation_failed"),
370 (401 | 403, "unauthorized") => Some("unauthorized"),
371 (404, "not_found") => Some("not_found"),
372 (405, "method_not_allowed") => Some("method_not_allowed"),
373 (408, "request_timeout") => Some("request_timeout"),
374 (413, "payload_too_large") => Some("payload_too_large"),
375 (429, "rate_limited") => Some("rate_limited"),
376 (500..=599, "server_error") => Some("server_error"),
377 _ => None,
378 }
379}
380
381fn allowed_native_debug_next(status: u16, value: &str) -> Option<&'static str> {
383 match (status, value) {
384 (400, "check the artifact identity and request scope, then retry") => {
385 Some("check the artifact identity and request scope, then retry")
386 }
387 (401 | 403, "sign in and retry the native debug-artifact command") => {
388 Some("sign in and retry the native debug-artifact command")
389 }
390 (404, "check the exact project, release, environment, service, UUID, and architecture") => {
391 Some("check the exact project, release, environment, service, UUID, and architecture")
392 }
393 (404, "check the exact project and upload scope") => {
394 Some("check the exact project and upload scope")
395 }
396 (404, "start the native debug artifact upload session again with the same manifest") => {
397 Some("start the native debug artifact upload session again with the same manifest")
398 }
399 (405, "use the supported native debug-artifact request method") => {
400 Some("use the supported native debug-artifact request method")
401 }
402 (408, "retry the same native debug-artifact request") => {
403 Some("retry the same native debug-artifact request")
404 }
405 (
406 413,
407 "reduce the native debug-artifact upload below the documented size limits and retry",
408 ) => Some(
409 "reduce the native debug-artifact upload below the documented size limits and retry",
410 ),
411 (
412 422,
413 "send manifest and debug_file_N multipart parts from LogBrew Apple release tooling",
414 ) => Some(
415 "send manifest and debug_file_N multipart parts from LogBrew Apple release tooling",
416 ),
417 (422, "check the native debug-artifact manifest and retry") => {
418 Some("check the native debug-artifact manifest and retry")
419 }
420 (422, "retry only the missing native debug artifact chunk with its exact digest") => {
421 Some("retry only the missing native debug artifact chunk with its exact digest")
422 }
423 (
424 422,
425 "wait briefly, then retry the same upload completion or verify the exact artifact lookup",
426 ) => Some(
427 "wait briefly, then retry the same upload completion or verify the exact artifact lookup",
428 ),
429 (422, "check the native debug-artifact manifest and upload session, then retry") => {
430 Some("check the native debug-artifact manifest and upload session, then retry")
431 }
432 (429 | 500..=599, "retry the same native debug-artifact request later") => {
433 Some("retry the same native debug-artifact request later")
434 }
435 (429 | 500..=599, "retry the same native debug-artifact command later") => {
436 Some("retry the same native debug-artifact command later")
437 }
438 _ => None,
439 }
440}
441
442fn write_human_runtime_error<W: std::io::Write>(
444 error: &RuntimeError,
445 output: &mut W,
446) -> Result<(), std::io::Error> {
447 match error {
448 RuntimeError::StatusUnavailable {
449 api_url,
450 status_code,
451 body,
452 auth_label,
453 message,
454 ..
455 } => {
456 writeln!(output, "LogBrew API unreachable.")?;
457 writeln!(output, "API: {api_url}")?;
458 writeln!(output, "Auth: {auth_label}")?;
459 if let Some(status_code) = status_code {
460 writeln!(output, "Status: {status_code}")?;
461 }
462 if let Some(body) = body.as_ref().filter(|body| !body.is_empty()) {
463 writeln!(output, "Body: {body}")?;
464 } else {
465 writeln!(output, "Reason: {message}")?;
466 }
467 writeln!(output, "Next: {STATUS_UNAVAILABLE_NEXT_STEP}")?;
468 Ok(())
469 }
470 RuntimeError::Api {
471 status,
472 body,
473 auth_label,
474 ..
475 } => {
476 let api_details = ApiErrorDetails::parse(body);
477 writeln!(output, "{error}")?;
478 if let Some(code) = api_details.code.as_deref() {
479 writeln!(output, "Code: {code}")?;
480 }
481 writeln!(output, "Auth: {auth_label}")?;
482 writeln!(output, "Next: {}", api_next_step(*status, &api_details))
483 }
484 RuntimeError::Cli(_)
485 | RuntimeError::Io(_)
486 | RuntimeError::Http(_)
487 | RuntimeError::MissingToken
488 | RuntimeError::InvestigationResponseInvalid
489 | RuntimeError::NativeDebugArtifactInvalid
490 | RuntimeError::NativeDebugResponseInvalid
491 | RuntimeError::NativeDebugVerificationFailed
492 | RuntimeError::Unavailable { .. } => {
493 writeln!(output, "{error}")?;
494 writeln!(output, "Next: {}", runtime_error_next_step(error))?;
495 Ok(())
496 }
497 }
498}
499
500fn runtime_error_json(error: &RuntimeError) -> serde_json::Value {
502 match error {
503 RuntimeError::MissingToken
504 | RuntimeError::Unavailable { .. }
505 | RuntimeError::InvestigationResponseInvalid
506 | RuntimeError::NativeDebugArtifactInvalid
507 | RuntimeError::NativeDebugResponseInvalid
508 | RuntimeError::NativeDebugVerificationFailed
509 | RuntimeError::Io(_)
510 | RuntimeError::Http(_) => serde_json::json!({
511 "ok": false,
512 "error": runtime_error_code(error),
513 "message": error.to_string(),
514 "next": runtime_error_next_step(error),
515 }),
516 RuntimeError::Api {
517 status,
518 body,
519 auth_source,
520 ..
521 } => {
522 let api_details = ApiErrorDetails::parse(body);
523 let next = api_next_step(*status, &api_details);
524 serde_json::json!({
525 "ok": false,
526 "error": runtime_error_code(error),
527 "message": error.to_string(),
528 "status": status,
529 "body": body,
530 "api_error": api_details.error.as_deref(),
531 "api_code": api_details.code.as_deref(),
532 "api_next": api_details.next.as_deref(),
533 "auth_source": auth_source,
534 "next": next,
535 })
536 }
537 RuntimeError::StatusUnavailable {
538 api_url,
539 status_code,
540 body,
541 authenticated,
542 auth_source,
543 message,
544 ..
545 } => serde_json::json!({
546 "ok": false,
547 "error": runtime_error_code(error),
548 "status": "unreachable",
549 "status_code": status_code,
550 "body": body,
551 "api_url": api_url,
552 "authenticated": authenticated,
553 "auth_source": auth_source,
554 "message": message,
555 "next": runtime_error_next_step(error),
556 }),
557 RuntimeError::Cli(error) => serde_json::json!({
558 "ok": false,
559 "error": cli_error_code(error),
560 "message": error.to_string(),
561 "next": cli_error_next_step(error),
562 }),
563 }
564}
565
566const fn cli_error_code(error: &CliError) -> &'static str {
568 match error {
569 CliError::UnknownCommand | CliError::UnknownCommandName { .. } => "unknown_command",
570 CliError::MissingArgument { .. } => "missing_argument",
571 CliError::MissingFlagValue { .. } => "missing_flag_value",
572 CliError::DuplicateFlag { .. } => "duplicate_flag",
573 CliError::UnexpectedArgument { .. } => "unexpected_argument",
574 CliError::UnknownFlag { .. } => "unknown_flag",
575 CliError::UnsupportedFlag { .. } => "unsupported_flag",
576 CliError::UnknownResource { .. } => "unknown_resource",
577 CliError::UnknownStatus(_) => "unknown_status",
578 CliError::UnknownTraceStatus(_) => "unknown_trace_status",
579 CliError::UnknownLogLevel(_) => "unknown_log_level",
580 CliError::InvalidLimit(_) => "invalid_limit",
581 CliError::InvalidMinDuration(_) => "invalid_min_duration",
582 CliError::UnknownPagination => "unknown_pagination",
583 CliError::InvalidActionCursor(_) => "invalid_action_cursor",
584 CliError::InvalidLogCursor(_) => "invalid_log_cursor",
585 CliError::InvalidIssueCursor(_) => "invalid_issue_cursor",
586 CliError::InvalidSupportCursor(_) => "invalid_support_cursor",
587 CliError::UnknownSupportCategory => "unknown_support_category",
588 CliError::InvalidSupportTicketId => "invalid_support_ticket_id",
589 CliError::InvalidSupportRetryKey => "invalid_support_retry_key",
590 CliError::InvalidSupportContextReply => "invalid_support_context_reply",
591 CliError::InvalidSupportContextCommand => "invalid_support_context_command",
592 CliError::InvalidSupportContext => "invalid_support_context",
593 CliError::InvalidInvestigationCommand => "invalid_investigation_command",
594 CliError::InvalidDoctorCommand => "invalid_doctor_command",
595 CliError::InvalidProjectCreateCommand => "invalid_project_create_command",
596 CliError::InvalidProjectIngestKeyCreateCommand => {
597 "invalid_project_ingest_key_create_command"
598 }
599 CliError::InvalidProjectArchiveCommand => "invalid_project_archive_command",
600 CliError::InvalidProjectsCommand => "invalid_projects_command",
601 CliError::InvalidLoginProvider => "invalid_login_provider",
602 CliError::InvalidUsageCommand => "invalid_usage_command",
603 CliError::InvalidNativeDebugCommand | CliError::InvalidNativeDebugIdentity => {
604 "invalid_native_debug_command"
605 }
606 CliError::InvalidSetupSource(_) => "invalid_setup_source",
607 }
608}
609
610const fn cli_error_next_step(error: &CliError) -> &'static str {
612 match error {
613 CliError::InvalidLimit(_) => "use --limit with a positive whole number",
614 CliError::InvalidMinDuration(_) => "use --min-duration-ms with a non-negative whole number",
615 CliError::UnknownPagination
616 | CliError::InvalidActionCursor(_)
617 | CliError::InvalidLogCursor(_)
618 | CliError::InvalidIssueCursor(_)
619 | CliError::InvalidSupportCursor(_) => {
620 "use --pagination cursor alone for the first page, then use --cursor-time and --cursor-id together from next_cursor"
621 }
622 CliError::UnknownSupportCategory => {
623 "use sdk_install_failure, ingest_failure, auth_failure, project_setup, dashboard_issue, docs_confusion, cli_issue, mobile_issue, billing_question, or other"
624 }
625 CliError::InvalidSupportTicketId => {
626 "use the ticket_id returned by logbrew support create or list"
627 }
628 CliError::InvalidSupportRetryKey => {
629 "use --retry-key with 1 to 128 visible ASCII characters and reuse it only for an exact retry"
630 }
631 CliError::InvalidSupportContextReply => {
632 "use support reply <ticket_id> --context <text> --retry-key <key>"
633 }
634 CliError::InvalidSupportContextCommand => {
635 "use support context <ticket_id> with optional --json"
636 }
637 CliError::InvalidSupportContext => {
638 "use --context with 1 to 4000 characters after trimming whitespace"
639 }
640 CliError::InvalidInvestigationCommand => {
641 "use logbrew investigate issue <issue_id> with optional --json"
642 }
643 CliError::InvalidDoctorCommand => {
644 "use logbrew doctor --project <project_id> with optional --json"
645 }
646 CliError::InvalidProjectCreateCommand => {
647 "use logbrew projects create <name> --ingest-key-file <path> with optional --runtime, --environment, --abandon-retry, and --json"
648 }
649 CliError::InvalidProjectIngestKeyCreateCommand => {
650 "use logbrew projects keys create <project_id> --ingest-key-file <path> with optional --label, --kind sdk|browser|server|cli, --abandon-retry, and --json"
651 }
652 CliError::InvalidProjectArchiveCommand => {
653 "use logbrew projects archive <project_id> --yes with optional --json"
654 }
655 CliError::InvalidProjectsCommand => {
656 "use logbrew projects with optional --json, or logbrew projects --help"
657 }
658 CliError::InvalidLoginProvider => "use --provider github, gitlab, or bitbucket",
659 CliError::InvalidUsageCommand => "use logbrew usage with optional --json",
660 CliError::InvalidNativeDebugCommand => {
661 "use logbrew debug-artifacts upload <path> --project <project_id> --release <release> --environment <environment> --service <service> with optional --expect-image-uuid, --dry-run, and --json"
662 }
663 CliError::InvalidNativeDebugIdentity => {
664 "use a UUID in 8-4-4-4-12 form and architecture arm64, arm64e, or x86_64"
665 }
666 CliError::InvalidSetupSource(_) => "use --source api, cli, or sdk",
667 CliError::MissingArgument { next, .. }
668 | CliError::MissingFlagValue { next, .. }
669 | CliError::DuplicateFlag { next, .. }
670 | CliError::UnexpectedArgument { next, .. }
671 | CliError::UnsupportedFlag { next, .. }
672 | CliError::UnknownFlag { next, .. }
673 | CliError::UnknownResource { next, .. }
674 | CliError::UnknownCommandName { next, .. } => next,
675 CliError::UnknownCommand => "run logbrew --help",
676 CliError::UnknownStatus(_) => ISSUE_STATUS_VALUES_NEXT_STEP,
677 CliError::UnknownTraceStatus(_) => "use --status error or --status ok",
678 CliError::UnknownLogLevel(_) => "use one of info, warning, error, critical",
679 }
680}
681
682const fn runtime_error_code(error: &RuntimeError) -> &'static str {
684 match error {
685 RuntimeError::Cli(error) => cli_error_code(error),
686 RuntimeError::Io(_) => "io_error",
687 RuntimeError::Http(_) => "http_error",
688 RuntimeError::MissingToken => "not_logged_in",
689 RuntimeError::Api { .. } => "api_error",
690 RuntimeError::StatusUnavailable { .. } => "status_unreachable",
691 RuntimeError::Unavailable { .. } => "unavailable",
692 RuntimeError::InvestigationResponseInvalid => "investigation_response_invalid",
693 RuntimeError::NativeDebugArtifactInvalid => "native_debug_artifact_invalid",
694 RuntimeError::NativeDebugResponseInvalid => "native_debug_response_invalid",
695 RuntimeError::NativeDebugVerificationFailed => "native_debug_verification_failed",
696 }
697}
698
699const STATUS_UNAVAILABLE_NEXT_STEP: &str = "check LOGBREW_API_URL or network";
701
702#[derive(Debug, Clone, Default, PartialEq, Eq)]
704struct ApiErrorDetails {
705 error: Option<String>,
707 code: Option<String>,
709 next: Option<String>,
711}
712
713impl ApiErrorDetails {
714 fn parse(body: &str) -> Self {
716 let Ok(value) = serde_json::from_str::<serde_json::Value>(body) else {
717 return Self::default();
718 };
719
720 Self {
721 error: json_string_field(&value, "error"),
722 code: json_string_field(&value, "code"),
723 next: json_string_field(&value, "next"),
724 }
725 }
726}
727
728fn json_string_field(value: &serde_json::Value, key: &str) -> Option<String> {
730 value
731 .get(key)
732 .and_then(serde_json::Value::as_str)
733 .map(str::trim)
734 .filter(|value| !value.is_empty())
735 .map(ToOwned::to_owned)
736}
737
738fn runtime_error_next_step(error: &RuntimeError) -> Cow<'static, str> {
740 match error {
741 RuntimeError::Api { status, body, .. } => {
742 let api_details = ApiErrorDetails::parse(body);
743 api_next_step(*status, &api_details)
744 }
745 RuntimeError::Cli(_)
746 | RuntimeError::Io(_)
747 | RuntimeError::Http(_)
748 | RuntimeError::MissingToken
749 | RuntimeError::InvestigationResponseInvalid
750 | RuntimeError::NativeDebugArtifactInvalid
751 | RuntimeError::NativeDebugResponseInvalid
752 | RuntimeError::NativeDebugVerificationFailed
753 | RuntimeError::StatusUnavailable { .. }
754 | RuntimeError::Unavailable { .. } => {
755 Cow::Borrowed(fallback_runtime_error_next_step(error))
756 }
757 }
758}
759
760fn api_next_step(status: u16, api_details: &ApiErrorDetails) -> Cow<'static, str> {
762 api_details.next.as_ref().map_or_else(
763 || Cow::Borrowed(fallback_api_next_step(status)),
764 |next| Cow::Owned(next.clone()),
765 )
766}
767
768const fn fallback_api_next_step(status: u16) -> &'static str {
770 match status {
771 401 | 403 => "run logbrew login",
772 400 | 422 => "check command arguments or filters",
773 404 => "check the resource id or filters",
774 429 => "retry later",
775 500..=599 => "check LOGBREW_API_URL or retry later",
776 _ => "check command arguments or retry later",
777 }
778}
779
780const fn fallback_runtime_error_next_step(error: &RuntimeError) -> &'static str {
782 match error {
783 RuntimeError::Cli(error) => cli_error_next_step(error),
784 RuntimeError::MissingToken => "run logbrew login",
785 RuntimeError::Api { status, .. } => fallback_api_next_step(*status),
786 RuntimeError::StatusUnavailable { .. } | RuntimeError::Http(_) => {
787 STATUS_UNAVAILABLE_NEXT_STEP
788 }
789 RuntimeError::Unavailable { next, .. } => next,
790 RuntimeError::InvestigationResponseInvalid => {
791 "retry the issue investigation; if it repeats, report the public response contract"
792 }
793 RuntimeError::NativeDebugArtifactInvalid => {
794 "provide one validated Apple dSYM, ZIP, or Mach-O object matching every --expect-image-uuid value"
795 }
796 RuntimeError::NativeDebugResponseInvalid => {
797 "retry the native debug-artifact request; if it repeats, report the public response contract"
798 }
799 RuntimeError::NativeDebugVerificationFailed => {
800 "retry exact native debug-artifact lookup before using native symbolication"
801 }
802 RuntimeError::Io(_) => "check local files and permissions",
803 }
804}