{"record":{"id":"0d3ca39ce21d90f5","repo":"zeroclaw-labs/zeroclaw","slug":"gateway-path-prefix-contains-invalid-character-b","errorCode":null,"errorMessage":"gateway.path_prefix contains invalid character '{bad}'; only unreserved and sub-delim URI characters are allowed","messagePattern":"gateway\\.path_prefix contains invalid character '(.+?)'; only unreserved and sub-delim URI characters are allowed","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-config/src/schema.rs","lineNumber":21240,"sourceCode":"                        \"gateway.path_prefix must start with '/'\"\n                    );\n                }\n                if prefix.ends_with('/') {\n                    validation_bail!(\n                        InvalidFormat,\n                        \"gateway.path_prefix\",\n                        \"gateway.path_prefix must not end with '/' (including bare '/')\"\n                    );\n                }\n                // Reject characters unsafe for URL paths or HTML/JS injection.\n                // Whitespace is intentionally excluded from the allowed set.\n                if let Some(bad) = prefix.chars().find(|c| {\n                    !matches!(c, '/' | '-' | '_' | '.' | '~'\n                        | 'a'..='z' | 'A'..='Z' | '0'..='9'\n                        | '!' | '$' | '&' | '\\'' | '(' | ')' | '*' | '+' | ',' | ';' | '='\n                        | ':' | '@')\n                }) {\n                    anyhow::bail!(\n                        \"gateway.path_prefix contains invalid character '{bad}'; \\\n                         only unreserved and sub-delim URI characters are allowed\"\n                    );\n                }\n            }\n        }\n\n        // Skill bundles — directories must stay inside `<install>/shared/`\n        // and no two bundles may resolve to the same directory. Default\n        // directory and the rules themselves live in\n        // [`crate::skill_bundles`] so the runtime SkillsService and this\n        // validator share one implementation.\n        if !self.skill_bundles.is_empty() {\n            let install_root = self.install_root_dir();\n            for alias in self.skill_bundles.keys() {\n                let dir = crate::skill_bundles::resolve_directory(self, &install_root, alias)\n                    .map_err(|e| {\n                        ::zeroclaw_log::record!(","sourceCodeStart":21222,"sourceCodeEnd":21258,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-config/src/schema.rs#L21222-L21258","documentation":"The gateway mount prefix must consist solely of RFC 3986 unreserved characters (alphanumerics and -._~), the sub-delims !$&'()*+,;=, the path-safe delims : and @, plus /. Validation scans the raw prefix and bails naming the first offending character. Percent-encoding is deliberately unsupported — '%' is itself rejected — so the prefix must be written literally.","triggerScenarios":"Set gateway.path_prefix to any value containing a disallowed character: a space, '%', '?', '#', '[', ']', '<', '>', '|', '^', '\"', backslash, braces, or any non-ASCII character. The bail fires during gateway config validation with the bad character interpolated into the message.","commonSituations":"Pasting a percent-encoded URL segment (`/bot%20gw`); including a query string or template placeholder (`/gw?v=2`, `/gw/{team}`); glob/route-template syntax leaking into the mount prefix; non-ASCII separators inserted by editors or IMEs.","solutions":["Replace the character named in the message with an allowed one — in practice stick to letters, digits, '-', '_', '.', '~', and '/'","Strip percent-encoding entirely: write `/zero-claw-gw`, not `/zero%2Dclaw` — '%' is invalid","Move query strings and route placeholders out of path_prefix; they belong on individual routes","Re-run validation after the edit to catch any second bad character (only the first is reported)"],"exampleFix":"# before\n[gateway]\npath_prefix = \"/ze ro claw%20gw/{team}\"\n\n# after\n[gateway]\npath_prefix = \"/zero-claw-gw\"","handlingStrategy":"validation","validationCode":"fn valid_path_prefix(prefix: &str) -> bool {\n    prefix.chars().all(|c| matches!(c,\n        '/' | '-' | '_' | '.' | '~'\n        | 'a'..='z' | 'A'..='Z' | '0'..='9'\n        | '!' | '$' | '&' | '\\'' | '(' | ')' | '*' | '+' | ',' | ';' | '='\n        | ':' | '@'))\n}\n\n// call before Config::validate():\n// assert!(valid_path_prefix(&cfg.gateway.path_prefix));","typeGuard":"fn is_safe_gateway_prefix(p: &str) -> bool {\n    !p.chars().any(|c| matches!(c, '%' | '?' | '#' | '[' | ']' | '<' | '>' | '\"' | '\\\\' | '|' | '^' | '{' | '}' ) || !c.is_ascii())\n}","tryCatchPattern":"if let Err(err) = config.validate() {\n    if err.to_string().contains(\"gateway.path_prefix contains invalid character\") {\n        // re-read the char after 'invalid character' from the message and strip/replace it\n    }\n}","preventionTips":["Restrict prefixes to [A-Za-z0-9/-._~] — the extra sub-delims invite mistakes","Never percent-encode path_prefix; '%' is rejected","Pin the deployed prefix in a unit test that runs the same allowlist"],"tags":["configuration","validation","gateway","uri","zeroclaw","rust"],"backgroundTag":"invalid-url-characters","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}