{"record":{"id":"dacdb8826d40e284","repo":"risingwavelabs/risingwave","slug":"unsupported-scheme-in-udf-link","errorCode":null,"errorMessage":"unsupported scheme in UDF link: {}","messagePattern":"unsupported scheme in UDF link: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/expr/impl/src/udf/external.rs","lineNumber":236,"sourceCode":"        // ginepro sets the TLS domain name (SNI) to `host` rather than the resolved IPs.\n        builder = builder.with_tls(ClientTlsConfig::new().with_native_roots());\n    }\n    let channel = builder\n        .channel()\n        .await\n        .with_context(|| format!(\"failed to create LoadBalancedChannel, address: {host}:{port}\"))?;\n    Ok(channel.into())\n}\n\n/// Parse a UDF link into `(tls, host, port)`.\n///\n/// The link is an `http://` / `https://` URL, with `http://` as the default scheme when omitted.\n/// `https` enables TLS; an omitted port defaults to 80 / 443.\nfn parse_udf_link(link: &str) -> Result<(bool, String, u16)> {\n    let url = match url::Url::parse(link) {\n        Ok(url) if matches!(url.scheme(), \"http\" | \"https\") => url,\n        Ok(url) if link.contains(\"://\") => {\n            bail!(\"unsupported scheme in UDF link: {}\", url.scheme())\n        }\n        // no scheme (a plain `host:port` parses as scheme `host`): default to `http://`\n        _ => url::Url::parse(&format!(\"http://{link}\"))\n            .with_context(|| format!(\"failed to parse UDF link: {link}\"))?,\n    };\n    let host = url.host_str().expect(\"http(s) URL always has a host\");\n    let port = url\n        .port_or_known_default()\n        .expect(\"http(s) scheme always has a default port\");\n    Ok((url.scheme() == \"https\", host.to_owned(), port))\n}\n\nimpl ExternalFunction {\n    /// Call a function, retry up to 5 times / 3s if connection is broken.\n    async fn call_with_retry(\n        &self,\n        input: &RecordBatch,\n    ) -> Result<RecordBatch, arrow_udf_runtime::remote::Error> {","sourceCodeStart":218,"sourceCodeEnd":254,"githubUrl":"https://github.com/risingwavelabs/risingwave/blob/6469eb736d691e8e9b8a419a57edd6429ca77417/src/expr/impl/src/udf/external.rs#L218-L254","documentation":"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.","triggerScenarios":"`CREATE FUNCTION ... USING LINK 'grpc://host:port'` (or any non-http scheme with `://`) — any caller of `parse_udf_link` such as `connect_tonic`.","commonSituations":"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.","solutions":["Change the link to http:// or https:// with the UDF service's host and port (defaults 80/443)","If the service speaks gRPC, front it with an http-compatible endpoint as expected by the arrow-udf-wasm/remote protocol","Remove the scheme to let RisingWave default to http://host:port"],"exampleFix":"-- before\nUSING LINK 'grpc://127.0.0.1:50051'\n-- after\nUSING LINK 'http://127.0.0.1:50051'","handlingStrategy":"validation","validationCode":"const u = new URL(link); if (!['http:','https:'].includes(u.protocol)) throw new Error(`unsupported scheme in UDF link: ${u.protocol}`);","typeGuard":null,"tryCatchPattern":"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; }","preventionTips":["Always use http:// or https:// in USING LINK","Never copy grpc:// or file:// links","Omit the scheme to default to http"],"tags":["udf","url","config"],"backgroundTag":"invalid-url","analyzedSha":"6469eb736d691e8e9b8a419a57edd6429ca77417","analyzedAt":"2026-09-11T21:06:21.487Z","contentChangedAt":"2026-09-11T21:06:21.487Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}