denoland/deno · error

Invalid deep-link scheme

Error message

Invalid deep-link scheme {scheme:?}: {reason}.

What it means

Thrown by validate_url_scheme in cli/tools/desktop.rs when a deep-link scheme passed via `deno desktop --deep-link <scheme>` (or the config file `deep_links` array) violates the RFC 3986 scheme grammar enforced by the tool: it must start with an ASCII letter, may contain only letters/digits/'+'/'-'/'.', and must not be one of the reserved schemes http/https/file/ftp/ws/wss. The {reason} clause names the exact rule that failed. Reserved schemes are rejected because registering them as app handlers would hijack normal browsing.

Solutions

  1. Pass only the bare scheme without `://` or colon, e.g. `--deep-link acme` instead of `--deep-link "acme://app"`.
  2. Make the scheme start with an ASCII letter and contain only letters, digits, '+', '-', '.' — replace `my_app` with `my-app`.
  3. Pick a custom scheme (e.g. `acme`, `com.acme.app`) instead of a reserved one (http, https, file, ftp, ws, wss).
  4. Trim whitespace and remove empty strings from the `deep_links` entries in your deno config file.

Example fix

# before
deno desktop --deep-link "acme://login" --deep-link my_app main.ts

# after
deno desktop --deep-link acme --deep-link my-app main.ts
Defensive patterns

Strategy: validation

Validate before calling

# bash: validate deep-link schemes before building
is_valid_scheme() {
  local s="$1"
  [[ "$s" =~ ^[A-Za-z][A-Za-z0-9+.-]*$ ]] || return 1
  case "$s" in http|https|file|ftp|ws|wss) return 1;; esac
  return 0
}
for scheme in "${DEEP_LINKS[@]}"; do
  is_valid_scheme "$scheme" || { echo "bad scheme: $s" >&2; exit 1; }
done

Type guard

// TypeScript (validating a deno.json config before invoking the CLI)
const RESERVED = new Set(["http", "https", "file", "ftp", "ws", "wss"]);
function isValidDeepLinkScheme(s: string): boolean {
  return /^[A-Za-z][A-Za-z0-9+.-]*$/.test(s) && !RESERVED.has(s);
}

Prevention

When it happens

Trigger: Running `deno desktop --deep-link "acme://app" main.ts` (full URL passed instead of the bare scheme), `--deep-link 1app` (starts with a digit), `--deep-link my_app` (underscore is not in the allowed set), `--deep-link "my app"` (whitespace), `--deep-link https` (reserved), or an empty/blank entry coming from a config file `deep_links` array.

Common situations: Copying a full URL like `myapp://callback` from OAuth docs and pasting it as the scheme; including the trailing colon or `://`; using `https` or `file` because the app also handles web links; underscored names common in package names (my_app) which are legal elsewhere but not in URI schemes; whitespace sneaking in from a JSON config or shell quoting.

Related errors


AI-assisted analysis of denoland/deno@f7822238ca (2026-08-20). Data as JSON: /api/errors/ac86b2322a366739. Report an issue: GitHub.

Appendix: source

Thrown at cli/tools/desktop.rs:690

      }
    }
  };
  match target_os {
    "macos" => make_self_extracting_macos(bundle_path, format, desktop_flags),
    "windows" => make_self_extracting_dir(bundle_path, format, true),
    _ => make_self_extracting_dir(bundle_path, format, false),
  }
}

/// Validate a deep-link URL scheme. Follows the RFC 3986 `scheme` grammar:
/// `ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )`. We additionally reject the
/// common reserved schemes (`http`, `https`, `file`, `ftp`, `ws`, `wss`) since
/// registering those as app handlers is almost never intended and would hijack
/// normal browsing.
fn validate_url_scheme(scheme: &str) -> Result<(), AnyError> {
  let reserved = ["http", "https", "file", "ftp", "ws", "wss"];
  let bail = |reason: &str| {
    Err(deno_core::anyhow::anyhow!(
      "Invalid deep-link scheme {scheme:?}: {reason}."
    ))
  };
  match scheme.chars().next() {
    None => return bail("scheme is empty"),
    Some(c) if !c.is_ascii_alphabetic() => {
      return bail("scheme must start with an ASCII letter");
    }
    _ => {}
  }
  if !scheme
    .chars()
    .all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.'))
  {
    return bail("scheme may only contain letters, digits, '+', '-', and '.'");
  }
  if reserved.contains(&scheme) {
    return bail("scheme is reserved and cannot be used as a deep link");

View on GitHub (pinned to f7822238ca)