{"record":{"id":"756f70a8214e9fc7","repo":"apache/pulsar","slug":"sink-package-doesn-t-contain-the-meta-inf-services","errorCode":null,"errorMessage":"Sink package doesn't contain the META-INF/services/pulsar-io.yaml file.","messagePattern":"Sink package doesn't contain the META-INF/services/pulsar-io\\.yaml file\\.","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"pulsar-functions/utils/src/main/java/org/apache/pulsar/functions/utils/SinkConfigUtils.java","lineNumber":464,"sourceCode":"        if (sinkConfig.getParallelism() != null && sinkConfig.getParallelism() <= 0) {\n            throw new IllegalArgumentException(\"Sink parallelism must be a positive number\");\n        }\n\n        if (sinkConfig.getResources() != null) {\n            ResourceConfigUtils.validate(sinkConfig.getResources());\n        }\n\n        if (sinkConfig.getTimeoutMs() != null && sinkConfig.getTimeoutMs() < 0) {\n            throw new IllegalArgumentException(\"Sink timeout must be a positive number\");\n        }\n\n        String sinkClassName = sinkConfig.getClassName();\n        // if class name in sink config is not set, this should be a built-in sink\n        // thus we should try to find it class name in the NAR service definition\n        if (sinkClassName == null) {\n            ConnectorDefinition connectorDefinition = sinkFunction.getFunctionMetaData(ConnectorDefinition.class);\n            if (connectorDefinition == null) {\n                throw new IllegalArgumentException(\n                        \"Sink package doesn't contain the META-INF/services/pulsar-io.yaml file.\");\n            }\n            sinkClassName = connectorDefinition.getSinkClass();\n            if (sinkClassName == null) {\n                throw new IllegalArgumentException(\"Failed to extract sink class from archive\");\n            }\n        }\n\n        // check if sink implements the correct interfaces\n        TypeDefinition sinkClass;\n        try {\n            sinkClass = sinkFunction.resolveType(sinkClassName);\n        } catch (TypePool.Resolution.NoSuchTypeException e) {\n            throw new IllegalArgumentException(\n                    String.format(\"Sink class %s not found\", sinkClassName), e);\n        }\n\n        String functionClassName = sinkConfig.getTransformFunctionClassName();","sourceCodeStart":446,"sourceCodeEnd":482,"githubUrl":"https://github.com/apache/pulsar/blob/820761864ed8e2a7d2e52dd9763ad2ae117c1395/pulsar-functions/utils/src/main/java/org/apache/pulsar/functions/utils/SinkConfigUtils.java#L446-L482","documentation":"When the sink config has no className, the framework treats the submitted package as a built-in NAR connector and reads META-INF/services/pulsar-io.yaml from it to obtain the ConnectorDefinition. If that descriptor is missing, validateAndExtractDetails throws this IllegalArgumentException — the archive is neither a plain-Java sink (no className) nor a valid NAR connector (no pulsar-io.yaml).","triggerScenarios":"Submitting a sink with className unset and an archive (jar/nar) that lacks META-INF/services/pulsar-io.yaml; uploading a plain fat-jar without the connector descriptor while omitting className.","commonSituations":"Building a custom connector NAR and forgetting the pulsar-io.yaml service file; using the wrong archive path in --archive; packaging a plain sink jar but not passing --className; NAR built with the wrong packaging plugin that strips resources.","solutions":["Add META-INF/services/pulsar-io.yaml to the NAR archive declaring the sinkClass.","Or set sinkConfig.setClassName(\"com.example.MySink\") so the archive is treated as a regular jar.","Verify with 'unzip -l my-sink.nar | grep pulsar-io.yaml' that the descriptor is present at the right path.","Check the NAR build config (pulsar-io NAR plugin / maven-nar) isn't excluding resources."],"exampleFix":"// before\nSinkConfig cfg = new SinkConfig();\ncfg.setTenant(\"public\");\ncfg.setNamespace(\"default\");\ncfg.setName(\"my-sink\");\n// archive: plain jar without pulsar-io.yaml and no className\n// after — either declare className:\ncfg.setClassName(\"org.example.MySink\");\n// or ship a proper NAR containing:\n// META-INF/services/pulsar-io.yaml with:\n// name: my-sink\n// description: ...\n// sinkClass: org.example.MySink","handlingStrategy":"validation","validationCode":"if (sinkConfig.getClassName() == null) {\n    try (JarFile jar = new JarFile(archivePath.toFile())) {\n        if (jar.getJarEntry(\"META-INF/services/pulsar-io.yaml\") == null) {\n            throw new IllegalArgumentException(\"archive lacks META-INF/services/pulsar-io.yaml; set className or use a valid NAR\");\n        }\n    }\n}","typeGuard":"static boolean isBuiltInConnectorArchive(Path archive, String className) throws IOException {\n    if (className != null) return true;\n    try (JarFile jar = new JarFile(archive.toFile())) {\n        return jar.getJarEntry(\"META-INF/services/pulsar-io.yaml\") != null;\n    }\n}","tryCatchPattern":"try {\n    SinkConfigUtils.validateAndExtractDetails(cfg, sinkPkg, transformPkg, true);\n} catch (IllegalArgumentException e) {\n    if (e.getMessage().contains(\"pulsar-io.yaml\")) {\n        log.error(\"Archive is not a valid connector NAR: provide className or fix packaging\", e);\n    }\n    throw e;\n}","preventionTips":["For plain jar sinks, always set className explicitly.","Verify NAR packaging includes META-INF/services/pulsar-io.yaml (unzip -l).","Use the official pulsar-io NAR build plugin so the descriptor is generated/kept.","Check the archive path passed to --archive points at the NAR, not a transitive jar."],"tags":["pulsar","pulsar-io","nar-packaging","connector"],"backgroundTag":"missing-descriptor-file","analyzedSha":"820761864ed8e2a7d2e52dd9763ad2ae117c1395","analyzedAt":"2026-09-06T00:14:20.138Z","contentChangedAt":"2026-09-06T00:14:20.138Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}