projectdiscovery/nuclei · error

output callback cannot be nil

Error message

output callback cannot be nil

What it means

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.

Source

Thrown at pkg/tmplexec/flow/flow_executor.go:259

		for proto := range f.protoFunctions {
			_ = runtime.GlobalObject().Delete(proto)
		}
		runtime.RemoveContextValue("executionId")
	}()

	// TODO(dwisiswant0): remove this once we get the RCA.
	defer func() {
		if ci.IsCI() {
			return
		}

		if r := recover(); r != nil {
			f.ctx.LogError(fmt.Errorf("panic occurred while executing flow: %v", r))
		}
	}()

	if ctx.OnResult == nil {
		return fmt.Errorf("output callback cannot be nil")
	}
	// before running register set of builtins
	if err := runtime.Set("set", func(call goja.FunctionCall) goja.Value {
		varName := call.Argument(0).Export()
		varValue := call.Argument(1).Export()
		f.options.GetTemplateCtx(f.ctx.Input.MetaInput).Set(types.ToString(varName), varValue)
		return goja.Null()
	}); err != nil {
		return err
	}
	// also register functions that allow executing protocols from js
	for proto, fn := range f.protoFunctions {
		if err := runtime.Set(proto, fn); err != nil {
			return err
		}
	}
	// register template object
	tmplObj := f.options.GetTemplateCtx(f.ctx.Input.MetaInput).GetAll()

View on GitHub (pinned to 265b3a3dec)

Solutions

  1. Set ctx.OnResult before executing: scanCtx.OnResult = func(e *output.ResultEvent) { ... } (even a no-op sink satisfies the check)
  2. Prefer the nuclei lib's high-level APIs (nuclei.NewExecutor / ExecuteWithResults wrappers) which wire the callback for you
  3. Guard in shared code: if tmpl has a flow, assert OnResult != nil before dispatch
  4. Check the flow docs example (projectdiscovery/nuclei examples/) for the canonical ScanContext setup

Example fix

// before
scanCtx := scan.NewScanContext(input)
err := executer.Execute(scanCtx) // flow template: output callback cannot be nil

// after
scanCtx := scan.NewScanContext(input)
scanCtx.OnResult = func(r *output.ResultEvent) { results = append(results, r) }
err := executer.Execute(scanCtx)
Defensive patterns

Strategy: validation

Validate before calling

// Always attach an OnResult sink before executing a flow template:
scanCtx := scan.NewScanContext(input)
scanCtx.OnResult = func(e *output.ResultEvent) {
    // collect, forward to channel, or no-op — must be non-nil for flow
}
if err := executer.Execute(scanCtx); err != nil { ... }

Type guard

func readyForFlow(ctx *scan.ScanContext) bool { return ctx != nil && ctx.OnResult != nil }

Try / catch

if err := executer.Execute(scanCtx); err != nil {
    if strings.Contains(err.Error(), "output callback cannot be nil") {
        scanCtx.OnResult = func(*output.ResultEvent) {} // minimal sink, then retry once
        return executer.Execute(scanCtx)
    }
    return err
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of projectdiscovery/nuclei@265b3a3dec (2026-08-15). Data as JSON: /api/errors/5e101349853677b0. Report an issue: GitHub.