flyr-cli 1.6.1

Search Google Flights from the terminal
Documentation
use std::fmt;

#[derive(Debug)]
pub enum FlightError {
    Timeout,
    ConnectionFailed(String),
    DnsResolution(String),
    ProxyError(String),
    RateLimited,
    Blocked(u16),
    HttpStatus(u16),
    TlsError(String),
    ScriptTagNotFound,
    JsParse(String),
    NoResults,
    InvalidAirport(String),
    InvalidDate(String),
    Validation(String),
}

impl fmt::Display for FlightError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Timeout => write!(
                f,
                "request timed out — Google may be slow or unreachable. \
                 Try increasing --timeout or check your connection"
            ),
            Self::ConnectionFailed(detail) => write!(
                f,
                "connection failed — check your internet connection ({detail})"
            ),
            Self::DnsResolution(host) => write!(
                f,
                "DNS resolution failed for {host} — check your internet connection"
            ),
            Self::ProxyError(detail) => write!(
                f,
                "proxy error — check your --proxy URL is correct ({detail})"
            ),
            Self::RateLimited => write!(
                f,
                "rate limited by Google (HTTP 429) — wait a few minutes before retrying, \
                 or use --proxy to route through a different IP"
            ),
            Self::Blocked(status) => write!(
                f,
                "request blocked by Google (HTTP {status}) — this usually means \
                 rate limiting or bot detection. Try again later or use --proxy"
            ),
            Self::HttpStatus(status) => write!(
                f,
                "unexpected HTTP status {status} from Google Flights"
            ),
            Self::TlsError(detail) => write!(
                f,
                "TLS/SSL error — connection to Google failed ({detail})"
            ),
            Self::ScriptTagNotFound => write!(
                f,
                "failed to parse Google Flights response — the page structure may have changed, \
                 or Google returned a CAPTCHA/consent page. \
                 Try again, use --proxy, or file an issue if this persists"
            ),
            Self::JsParse(detail) => write!(
                f,
                "failed to parse flight data from response — {detail}. \
                 This may indicate a Google Flights format change"
            ),
            Self::NoResults => write!(f, "no flights found for this search"),
            Self::InvalidAirport(code) => write!(
                f,
                "invalid airport code \"{code}\" — must be exactly 3 letters (e.g. JFK, HEL, NRT)"
            ),
            Self::InvalidDate(date) => write!(
                f,
                "invalid date \"{date}\" — must be YYYY-MM-DD format (e.g. 2026-03-01)"
            ),
            Self::Validation(msg) => write!(f, "{msg}"),
        }
    }
}

impl std::error::Error for FlightError {}

pub fn from_http_error(err: wreq::Error) -> FlightError {
    let msg = err.to_string();
    let lower = msg.to_lowercase();

    if err.is_timeout() {
        return FlightError::Timeout;
    }

    if err.is_connect() {
        if lower.contains("dns") || lower.contains("resolve") || lower.contains("getaddrinfo") {
            return FlightError::DnsResolution(msg);
        }
        return FlightError::ConnectionFailed(msg);
    }

    if lower.contains("proxy") || lower.contains("socks") {
        return FlightError::ProxyError(msg);
    }

    if lower.contains("tls") || lower.contains("ssl") || lower.contains("certificate") {
        return FlightError::TlsError(msg);
    }

    if lower.contains("builder error") && lower.contains("uri") {
        return FlightError::ProxyError(msg);
    }

    FlightError::ConnectionFailed(msg)
}