{"record":{"id":"35294a371b4efa76","repo":"elastic/elasticsearch","slug":"must-specify-location-for-non-official-plugin-id","errorCode":null,"errorMessage":"Must specify location for non-official plugin [{id}] in [elasticsearch-plugins.yml]","messagePattern":"Must specify location for non-official plugin \\[(.+?)\\] in \\[elasticsearch-plugins\\.yml\\]","errorType":"exception","errorClass":"PluginSyncException","httpStatus":null,"severity":"error","filePath":"distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/PluginsConfig.java","lineNumber":82,"sourceCode":"     * @throws PluginSyncException if validation problems are found\n     */\n    public void validate(Set<String> officialPlugins, Set<String> migratedPlugins) throws PluginSyncException {\n        if (this.plugins.stream().anyMatch(each -> each == null || Strings.isNullOrBlank(each.getId()))) {\n            throw new RuntimeException(\"Cannot have null or empty IDs in [elasticsearch-plugins.yml]\");\n        }\n\n        final Set<String> uniquePluginIds = new HashSet<>();\n        for (final InstallablePlugin plugin : plugins) {\n            if (uniquePluginIds.add(plugin.getId()) == false) {\n                throw new PluginSyncException(\"Duplicate plugin ID [\" + plugin.getId() + \"] found in [elasticsearch-plugins.yml]\");\n            }\n        }\n\n        for (InstallablePlugin plugin : this.plugins) {\n            if (officialPlugins.contains(plugin.getId()) == false\n                && migratedPlugins.contains(plugin.getId()) == false\n                && plugin.getLocation() == null) {\n                throw new PluginSyncException(\n                    \"Must specify location for non-official plugin [\" + plugin.getId() + \"] in [elasticsearch-plugins.yml]\"\n                );\n            }\n        }\n\n        if (this.proxy != null) {\n            final String[] parts = this.proxy.split(\":\");\n            if (parts.length != 2) {\n                throw new PluginSyncException(\"Malformed [proxy], expected [host:port] in [elasticsearch-plugins.yml]\");\n            }\n\n            if (ProxyUtils.validateProxy(parts[0], parts[1]) == false) {\n                throw new PluginSyncException(\"Malformed [proxy], expected [host:port] in [elasticsearch-plugins.yml]\");\n            }\n        }\n\n        for (InstallablePlugin p : plugins) {\n            if (p.getLocation() != null) {","sourceCodeStart":64,"sourceCodeEnd":100,"githubUrl":"https://github.com/elastic/elasticsearch/blob/db6a809a667c081ca1dc7500389d26975573215f/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/PluginsConfig.java#L64-L100","documentation":"Thrown by PluginsConfig.validate for an InstallablePlugin whose id is neither in the official-plugins set nor in the migrated-plugins set, and whose location is null. Elasticsearch can only install-by-name plugins it knows about; everything else needs an explicit location (URL, file path, or Maven coordinate). It is a PluginSyncException so callers can branch on it.","triggerScenarios":"validate iterates the plugins list; for each plugin not recognised as official/migrated and with `plugin.getLocation() == null`, it throws. This fires before the proxy and location-format checks.","commonSituations":"User added a custom or third-party plugin by id only, assuming it is installable by name; typo in the id that no longer matches an official plugin; previously official plugin removed from the official set in a new ES version.","solutions":["Add a `location:` (URL, file:// path, or Maven coordinate) to the plugin entry in elasticsearch-plugins.yml.","Check the plugin id spelling against the official plugins list for your ES version.","If the plugin was migrated to a module, you can leave the id (migrated set) — otherwise supply location."],"exampleFix":"# before:\nplugins:\n  - id: my-custom-plugin   # not official, no location\n# after:\nplugins:\n  - id: my-custom-plugin\n    location: file:///opt/plugins/my-custom-plugin.zip","handlingStrategy":"validation","validationCode":"// Ensure every non-official, non-migrated plugin has a location.\nfor (InstallablePlugin p : parsed.getPlugins()) {\n    if (!official.contains(p.getId()) && !migrated.contains(p.getId()) && p.getLocation() == null) {\n        throw new IllegalArgumentException(\"Plugin \" + p.getId() + \" needs a location\");\n    }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Keep an up-to-date official plugins list for your ES version when templating.","For custom plugins, always template both id and location together.","Run a dry-run sync in CI to catch this before production restart."],"tags":["plugins-config","validation","yaml","plugin-cli"],"backgroundTag":null,"analyzedSha":"db6a809a667c081ca1dc7500389d26975573215f","analyzedAt":"2026-08-12T01:39:14.192Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}