{"record":{"id":"8f3b4775d1405d6f","repo":"phacility/phabricator","slug":"editor-pattern-s-is-invalid-the-final-characte","errorCode":null,"errorMessage":"Editor pattern \"%s\" is invalid: the final character in a pattern may not be an unencoded percent symbol (\"%%\"). Use \"%%%%\" to encode a literal percent symbol.","messagePattern":"Editor pattern \"(.+?)\" is invalid: the final character in a pattern may not be an unencoded percent symbol \\(\"%%\"\\)\\. Use \"%%%%\" to encode a literal percent symbol\\.","errorType":"validation","errorClass":"PhabricatorEditorURIParserException","httpStatus":null,"severity":"error","filePath":"src/infrastructure/editor/PhabricatorEditorURIEngine.php","lineNumber":322,"sourceCode":"\n      $result[] = $token_value;\n    }\n\n    $result = implode('', $result);\n\n    return $result;\n  }\n\n  public static function newPatternTokens($raw_pattern) {\n    $token_positions = array();\n\n    $len = strlen($raw_pattern);\n\n    for ($ii = 0; $ii < $len; $ii++) {\n      $c = $raw_pattern[$ii];\n      if ($c === '%') {\n        if (!isset($raw_pattern[$ii + 1])) {\n          throw new PhabricatorEditorURIParserException(\n            pht(\n              'Editor pattern \"%s\" is invalid: the final character in a '.\n              'pattern may not be an unencoded percent symbol (\"%%\"). '.\n              'Use \"%%%%\" to encode a literal percent symbol.',\n              $raw_pattern));\n        }\n\n        $token_positions[] = $ii;\n        $ii++;\n      }\n    }\n\n    // Add a final marker past the end of the string, so we'll collect any\n    // trailing literal bytes.\n    $token_positions[] = $len;\n\n    $tokens = array();\n    $cursor = 0;","sourceCodeStart":304,"sourceCodeEnd":340,"githubUrl":"https://github.com/phacility/phabricator/blob/5720a38cfe95b00ca4be5016dd0d2f3195f4fa04/src/infrastructure/editor/PhabricatorEditorURIEngine.php#L304-L340","documentation":"newPatternTokens() walks the pattern character by character; a '%' consumes the following character as part of a variable token. A '%' in the final position has no following character, so it is ambiguous (a variable name is missing) and the parser throws PhabricatorEditorURIParserException telling you to use %% to encode a literal percent. This is the trailing-edge case of the same tokenizer used for all editor pattern validation.","triggerScenarios":"Any editor pattern (config `editor` or user preference) whose last character is a single unescaped '%', e.g. 'vim://open?url=%f&rate=100%' - the loop hits $ii+1 beyond the string end and throws before any URI is built.","commonSituations":"Patterns embedding percent-encoded fragments or literals (a trailing '100%', a URL like '...%25' trimmed wrong); hand-crafted patterns never exercised until a user clicks an editor link; migration scripts concatenating strings that end with '%'.","solutions":["Double the trailing percent: '%%' - e.g. 'vim://open?url=file://%f&rate=100%%'.","Audit the pattern for any other lone '%' that is not part of %f/%l/%n/%d/%p/%r/%% and escape it as %%; check both `./bin config get editor` and user preferences.","Validate patterns programmatically with PhabricatorEditorURIEngine::newPatternTokens($pattern) before saving them to config."],"exampleFix":"// before: trailing bare percent\nvim://open?url=file://%f&pct=90%\n\n// after: encoded literal percent\nvim://open?url=file://%f&pct=90%%","handlingStrategy":"validation","validationCode":"if (substr($pattern, -1) === '%') {\n  throw new Exception(\n    'Pattern may not end with a bare %; use %% for a literal percent.');\n}","typeGuard":null,"tryCatchPattern":"try {\n  PhabricatorEditorURIEngine::newPatternTokens($pattern);\n} catch (PhabricatorEditorURIParserException $ex) {\n  // keep the previously saved pattern and report the parse error inline\n  $errors[] = $ex->getMessage();\n}","preventionTips":["Escape every literal percent as %% when writing editor patterns.","Run a one-line lint (pattern parses cleanly) in your config management before deploying.","Remember %-sequences come in pairs: odd trailing counts are a smell."],"tags":["phabricator","editor-link","pattern","escaping","tokenizer"],"backgroundTag":"uri-template-syntax-invalid","analyzedSha":"5720a38cfe95b00ca4be5016dd0d2f3195f4fa04","analyzedAt":"2026-08-21T05:07:25.672Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}