risingwavelabs/risingwave · error

unsupported scheme in UDF link

Error message

unsupported scheme in UDF link: {}

What it means

External UDF links must be `http://` or `https://` URLs; `https` enables TLS. If the URL parses but uses a different scheme and contains an explicit `://`, `parse_udf_link` rejects it, since other schemes (grpc, tcp, file) are not supported.

Solutions

  1. Change the link to http:// or https:// with the UDF service's host and port (defaults 80/443)
  2. If the service speaks gRPC, front it with an http-compatible endpoint as expected by the arrow-udf-wasm/remote protocol
  3. Remove the scheme to let RisingWave default to http://host:port

Example fix

-- before
USING LINK 'grpc://127.0.0.1:50051'
-- after
USING LINK 'http://127.0.0.1:50051'
Defensive patterns

Strategy: validation

Validate before calling

const u = new URL(link); if (!['http:','https:'].includes(u.protocol)) throw new Error(`unsupported scheme in UDF link: ${u.protocol}`);

Try / catch

try { await rw.query(`CREATE FUNCTION ... USING LINK '${link}'`) } catch (e) { if (String(e).includes('unsupported scheme in UDF link')) { /* rewrite link to http(s) and retry */ } else throw e; }

Prevention

When it happens

Trigger: `CREATE FUNCTION ... USING LINK 'grpc://host:port'` (or any non-http scheme with `://`) — any caller of `parse_udf_link` such as `connect_tonic`.

Common situations: Copying a link from a gRPC-based UDF example ('grpc://'), using 'unix://' sockets, or file:// links; older configs from before the http/https-only policy.

Understand the failure class

Background: "Invalid URL" / "URL cannot be empty": fix the malformed or missing URL behind request-construction failures — this error's family across 50 libraries.

Related errors


AI-assisted analysis of risingwavelabs/risingwave@6469eb736d (2026-09-11). Data as JSON: /api/errors/dacdb8826d40e284. Report an issue: GitHub.

Appendix: source

Thrown at src/expr/impl/src/udf/external.rs:236

        // ginepro sets the TLS domain name (SNI) to `host` rather than the resolved IPs.
        builder = builder.with_tls(ClientTlsConfig::new().with_native_roots());
    }
    let channel = builder
        .channel()
        .await
        .with_context(|| format!("failed to create LoadBalancedChannel, address: {host}:{port}"))?;
    Ok(channel.into())
}

/// Parse a UDF link into `(tls, host, port)`.
///
/// The link is an `http://` / `https://` URL, with `http://` as the default scheme when omitted.
/// `https` enables TLS; an omitted port defaults to 80 / 443.
fn parse_udf_link(link: &str) -> Result<(bool, String, u16)> {
    let url = match url::Url::parse(link) {
        Ok(url) if matches!(url.scheme(), "http" | "https") => url,
        Ok(url) if link.contains("://") => {
            bail!("unsupported scheme in UDF link: {}", url.scheme())
        }
        // no scheme (a plain `host:port` parses as scheme `host`): default to `http://`
        _ => url::Url::parse(&format!("http://{link}"))
            .with_context(|| format!("failed to parse UDF link: {link}"))?,
    };
    let host = url.host_str().expect("http(s) URL always has a host");
    let port = url
        .port_or_known_default()
        .expect("http(s) scheme always has a default port");
    Ok((url.scheme() == "https", host.to_owned(), port))
}

impl ExternalFunction {
    /// Call a function, retry up to 5 times / 3s if connection is broken.
    async fn call_with_retry(
        &self,
        input: &RecordBatch,
    ) -> Result<RecordBatch, arrow_udf_runtime::remote::Error> {

View on GitHub (pinned to 6469eb736d)