Hmbown/CodeWhale · error

outbound origin must be a public service host

Error message

outbound origin must be a public service host

What it means

The validator requires the origin host to be a public service: names ending in .local (mDNS) or .internal (private DNS zones) are rejected because they only resolve on internal networks and defeat the public-host guarantee for credential-bearing calls.

Solutions

  1. Use the service's public hostname behind your gateway/proxy instead of the .local/.internal name.
  2. Expose the sandbox service via a public https endpoint (or tunnel) and configure that origin.
  3. For purely local testing, use debug builds with localhost rather than .internal names.

Example fix

// before
export DAYTONA_API_URL=https://daytona.internal
// after
export DAYTONA_API_URL=https://daytona.mycompany.example.com
Defensive patterns

Strategy: validation

Validate before calling

let host = ... // extract host as in 765
if host.ends_with(".local") || host.ends_with(".internal") { return Err("use a public hostname"); }

Try / catch

if let Err(e) = validate_outbound_origin(raw) {
    if e.to_string().contains("public service host") {
        eprintln!(".local/.internal hosts are rejected; expose the service publicly");
    }
}

Prevention

When it happens

Trigger: Setting the remote endpoint or toolbox URL to something like https://daytona.internal or https://toolbox.local.

Common situations: Corporate/internal cluster URLs pasted into client config; self-hosted deployments exposing only internal DNS names.

Understand the failure class

Background: "Invalid value" and "allowed values are" config errors: what your library rejected and how to fix it — this error's family across 41 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/23ba7bd5fac8b64e. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/cloud_dispatch.rs:1288

        .host_str()
        .context("outbound origin has no host")?
        .trim_end_matches('.')
        .to_ascii_lowercase();
    // `Url::host_str` keeps IPv6 brackets; strip them for the checks below.
    let host = host
        .strip_prefix('[')
        .and_then(|inner| inner.strip_suffix(']'))
        .map(str::to_string)
        .unwrap_or(host);
    let loopback_name = host == "localhost" || host == "127.0.0.1" || host == "::1";
    if loopback_name {
        if cfg!(debug_assertions) {
            return Ok(url);
        }
        bail!("loopback origins are not allowed in release builds");
    }
    if host.ends_with(".local") || host.ends_with(".internal") {
        bail!("outbound origin must be a public service host");
    }
    if let Ok(ip) = host.parse::<std::net::IpAddr>() {
        let blocked = match ip {
            std::net::IpAddr::V4(v4) => {
                let octets = v4.octets();
                v4.is_loopback()
                    || v4.is_private()
                    || v4.is_link_local()
                    || v4.is_unspecified()
                    || v4.is_broadcast()
                    || v4.is_multicast()
                    || v4.is_documentation()
                    // 100.64.0.0/10 (carrier-grade NAT, `is_shared` is
                    // not stable yet)
                    || (octets[0] == 100 && (octets[1] & 0b1100_0000) == 0b0100_0000)
            }
            std::net::IpAddr::V6(v6) => {
                if let Some(v4) = v6.to_ipv4_mapped() {

View on GitHub (pinned to 73e0f67d83)