GopeedLab/gopeed · error
engine loop terminated
Error message
engine loop terminated
What it means
The stream module's loop helper (pkg/download/engine/inject/stream/module.go:530-532) returns 'engine loop terminated' when loop.RunOnLoop reports the JS event loop is no longer running. All stream-module operations that hop onto the loop fail with this after the owning Engine is closed (Engine.Close -> loop.Terminate, engine/engine.go:135-137) or the loop has otherwise exited.
Source
Thrown at pkg/download/engine/inject/stream/module.go:531
}
onFulfilled := runtime.ToValue(func(call goja.FunctionCall) goja.Value {
send(call.Argument(0), nil)
return goja.Undefined()
})
onRejected := runtime.ToValue(func(call goja.FunctionCall) goja.Value {
send(nil, exportJSError(call.Argument(0)))
return goja.Undefined()
})
if _, err := thenFn(value, onFulfilled, onRejected); err != nil {
send(nil, err)
}
return
}
}
send(value, nil)
})
if !ok {
return nil, errors.New("engine loop terminated")
}
res := <-ch
return res.value, res.err
}
func exportJSError(value goja.Value) error {
if value == nil || goja.IsUndefined(value) || goja.IsNull(value) {
return errors.New("promise rejected")
}
if err, ok := value.Export().(error); ok {
return err
}
stack := value.String()
if ro, ok := value.(*goja.Object); ok {
stackVal := ro.Get("stack")
if stackVal != nil && stackVal.String() != "" {
stack = stackVal.String()
}View on GitHub (pinned to 7b7327ffb3)
Solutions
- Keep the Engine alive as long as any stream/blob source derived from it is needed; close it only after those sources are released
- Route all stream-module calls through a guard that checks an engine-closed flag before invoking them
- On this error, recreate the engine and re-resolve to obtain fresh streams — existing stream handles are dead
- Fix shutdown order: cancel tasks -> release blob sources -> Engine.Close()
Example fix
// before _, _ = e.RunString(resolverScript) e.Close() // stream source still referenced by a task v, err := streamModule.SomeOp(...) // "engine loop terminated" // after e.Close() only after registry.Release(blobURL) for all sources from e
Defensive patterns
Strategy: try-catch
Validate before calling
if engineClosed.Load() { return nil, errors.New("engine closed") }
val, err := streamModule.Call(...) // guard before hopping onto the loop Try / catch
v, err := streamOp()
if err != nil && strings.Contains(err.Error(), "engine loop terminated") {
// engine gone: recreate engine, re-resolve, obtain a new stream source
} Prevention
- Keep the Engine alive until every stream/blob source it produced is released
- Shutdown order: cancel tasks -> release sources -> Engine.Close
- Don't cache stream handles across engine restarts
When it happens
Trigger: Stream module operations (opening/reading an injected stream from script) still in flight when the parent Engine is Closed; shutdown code closing the engine while a resolver script's stream read callback is queued; an engine pooled and closed by one request while another request still streams from it.
Common situations: Extension lifecycle bugs: engine closed on script completion but stream kept by the fetcher for later range reads; graceful-shutdown ordering that closes the engine before download tasks are torn down; races between task cancel and stream finalizers.
Related errors
- engine loop terminated
- promise.then is not callable
- blob source not found
- blob source revoked
- blob source closed
AI-assisted analysis of GopeedLab/gopeed@7b7327ffb3 (2026-08-16).
Data as JSON: /api/errors/aea87e34b46899b1.
Report an issue: GitHub.