opendatalab/MinerU · error · RuntimeError
Local API server is already running
Error message
Local API server is already running
What it means
Raised by LocalApiServer.start() when the managed FastAPI subprocess has already been started. The class tracks liveness via self.process, and a second start() call without an intervening stop()/cleanup is a programming error, not an environmental one.
Source
Thrown at mineru/cli/api_client.py:482
queued_ahead: int | None = None
class LocalAPIServer:
def __init__(self, extra_cli_args: Sequence[str] = ()):
self.temp_dir = tempfile.TemporaryDirectory(prefix="mineru-api-client-")
self.temp_root = Path(self.temp_dir.name)
self.output_root = self.temp_root / "output"
self.base_url: str | None = None
self.process: ManagedProcess | None = None
self._atexit_registered = False
self.extra_cli_args = tuple(extra_cli_args)
self._launch_mode = LOCAL_API_LAUNCH_MODE_SUBPROCESS
self._managed_process_group_id: int | None = None
self._use_stdin_shutdown_watcher = False
def start(self) -> str:
if self.process is not None:
raise RuntimeError("Local API server is already running")
resolved_port = find_free_port()
self.base_url = f"http://127.0.0.1:{resolved_port}"
self._launch_mode = get_effective_local_api_launch_mode()
_validate_local_api_launch_mode_platform(self._launch_mode)
# On Windows, the temporary FastAPI child process can stall during
# parsing startup when launched with stdin=PIPE and an EOF-based
# shutdown watcher, so we only enable that path on non-Windows
# subprocess launches.
self._use_stdin_shutdown_watcher = (
self._launch_mode == LOCAL_API_LAUNCH_MODE_SUBPROCESS and os.name != "nt"
)
env, unset_env_names = _build_local_api_server_env(
self.output_root,
use_stdin_shutdown_watcher=self._use_stdin_shutdown_watcher,
)
if self._launch_mode == LOCAL_API_LAUNCH_MODE_SUBPROCESS:
stdin_target = subprocess.PIPEView on GitHub (pinned to 4fe4bde114)
Solutions
- Call stop()/cleanup (and wait for it to finish) before starting again
- Reuse the running server: check client.base_url / whether the process is alive instead of calling start() again
- Create a fresh LocalApiServer instance for each lifecycle instead of reusing one
- Register the atexit/on-failure cleanup so exception paths do not leave a half-alive server object
Example fix
# before
server.start() # ... later, same instance
server.start() # RuntimeError
# after
if server.process is None:
server.start() Defensive patterns
Strategy: type-guard
Validate before calling
if server.process is not None:
print(f"server already running at {server.base_url}")
else:
url = server.start() Type guard
def is_stopped(server: LocalApiServer) -> bool:
"""True when the server can be safely started."""
return server.process is None Try / catch
try:
server.start()
except RuntimeError as e:
if "already running" not in str(e):
raise
# reuse the existing instance Prevention
- Always pair start() with a finally: stop()/cleanup()
- Wrap server lifecycle in a context manager so retries cannot double-start
- In notebooks, keep one server object per kernel and check .process before start
When it happens
Trigger: Calling client.start() twice on the same LocalApiServer instance; calling start() after a previous start succeeded but stop() was never invoked (e.g. an exception path skipped cleanup); reusing a server object across retries without resetting it.
Common situations: Retry/decorator wrappers that transparently re-invoke the function that starts the server; notebooks where a cell starting the server is re-run; long-lived scripts that loop over documents and start a fresh server per iteration on the same object.
Related errors
AI-assisted analysis of opendatalab/MinerU@4fe4bde114 (2026-08-14).
Data as JSON: /api/errors/cf2f1550153a7535.
Report an issue: GitHub.