{"record":{"id":"5e101349853677b0","repo":"projectdiscovery/nuclei","slug":"output-callback-cannot-be-nil","errorCode":null,"errorMessage":"output callback cannot be nil","messagePattern":"output callback cannot be nil","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/tmplexec/flow/flow_executor.go","lineNumber":259,"sourceCode":"\t\tfor proto := range f.protoFunctions {\n\t\t\t_ = runtime.GlobalObject().Delete(proto)\n\t\t}\n\t\truntime.RemoveContextValue(\"executionId\")\n\t}()\n\n\t// TODO(dwisiswant0): remove this once we get the RCA.\n\tdefer func() {\n\t\tif ci.IsCI() {\n\t\t\treturn\n\t\t}\n\n\t\tif r := recover(); r != nil {\n\t\t\tf.ctx.LogError(fmt.Errorf(\"panic occurred while executing flow: %v\", r))\n\t\t}\n\t}()\n\n\tif ctx.OnResult == nil {\n\t\treturn fmt.Errorf(\"output callback cannot be nil\")\n\t}\n\t// before running register set of builtins\n\tif err := runtime.Set(\"set\", func(call goja.FunctionCall) goja.Value {\n\t\tvarName := call.Argument(0).Export()\n\t\tvarValue := call.Argument(1).Export()\n\t\tf.options.GetTemplateCtx(f.ctx.Input.MetaInput).Set(types.ToString(varName), varValue)\n\t\treturn goja.Null()\n\t}); err != nil {\n\t\treturn err\n\t}\n\t// also register functions that allow executing protocols from js\n\tfor proto, fn := range f.protoFunctions {\n\t\tif err := runtime.Set(proto, fn); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\t// register template object\n\ttmplObj := f.options.GetTemplateCtx(f.ctx.Input.MetaInput).GetAll()","sourceCodeStart":241,"sourceCodeEnd":277,"githubUrl":"https://github.com/projectdiscovery/nuclei/blob/265b3a3dec374741614e342f813c10f8b38d2bb7/pkg/tmplexec/flow/flow_executor.go#L241-L277","documentation":"FlowExecutor.Execute requires scan.ScanContext.OnResult to be set — every matched/misaligned event the flow produces is delivered through this callback. Before registering JS builtins, Execute checks ctx.OnResult == nil and returns 'output callback cannot be nil'. The CLI runner always wires OnResult, so this error is essentially an SDK/library misuse guard.","triggerScenarios":"SDK code that constructs a scan.ScanContext manually (e.g. scan.NewScanContext(input)) and passes it to a flow template's ExecuteWithResults/Execute without attaching OnResult. Non-flow templates tolerate this; flow templates hard-require it, so the difference appears only when a `flow:` template runs through custom harness code.","commonSituations":"Embedding nuclei as a library and collecting results only via the return value of ExecuteWithResults, forgetting the callback channel; migrating SDK code from plain templates to flow templates; test harnesses that stub out the output layer entirely.","solutions":["Set ctx.OnResult before executing: scanCtx.OnResult = func(e *output.ResultEvent) { ... } (even a no-op sink satisfies the check)","Prefer the nuclei lib's high-level APIs (nuclei.NewExecutor / ExecuteWithResults wrappers) which wire the callback for you","Guard in shared code: if tmpl has a flow, assert OnResult != nil before dispatch","Check the flow docs example (projectdiscovery/nuclei examples/) for the canonical ScanContext setup"],"exampleFix":"// before\nscanCtx := scan.NewScanContext(input)\nerr := executer.Execute(scanCtx) // flow template: output callback cannot be nil\n\n// after\nscanCtx := scan.NewScanContext(input)\nscanCtx.OnResult = func(r *output.ResultEvent) { results = append(results, r) }\nerr := executer.Execute(scanCtx)","handlingStrategy":"validation","validationCode":"// Always attach an OnResult sink before executing a flow template:\nscanCtx := scan.NewScanContext(input)\nscanCtx.OnResult = func(e *output.ResultEvent) {\n    // collect, forward to channel, or no-op — must be non-nil for flow\n}\nif err := executer.Execute(scanCtx); err != nil { ... }","typeGuard":"func readyForFlow(ctx *scan.ScanContext) bool { return ctx != nil && ctx.OnResult != nil }","tryCatchPattern":"if err := executer.Execute(scanCtx); err != nil {\n    if strings.Contains(err.Error(), \"output callback cannot be nil\") {\n        scanCtx.OnResult = func(*output.ResultEvent) {} // minimal sink, then retry once\n        return executer.Execute(scanCtx)\n    }\n    return err\n}","preventionTips":["Standardize ScanContext construction in one helper that always sets OnResult","Prefer nuclei lib high-level APIs that wire callbacks automatically","Add a unit test executing one trivial flow template to catch missing-callback wiring early"],"tags":["flow","sdk","callback","invariant"],"backgroundTag":null,"analyzedSha":"265b3a3dec374741614e342f813c10f8b38d2bb7","analyzedAt":"2026-08-15T20:05:51.855Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}