Invalid route timeframe fails silently: API returns 202, session stays in draft forever
Describe the bug
An invalid route timeframe is never validated before the backtest process is spawned, so the API
answers 202 Started backtesting... and the caller gets no indication of failure. The worker
then dies deep inside candle-store initialisation, and the session stays in draft with progress 0
forever. The only trace is the container log.
There are two separate problems, and the second one is what makes users hit the first.
1. The failure is silent for any programmatic caller
jesse/controllers/backtest_controller.py:30-56 — backtest() calls jh.validate_cwd() and then
goes straight to process_manager.add_task(...) + return JSONResponse(..., status_code=202).
No route validation happens before the spawn.
validate_routes() (jesse/services/validators.py, 14 lines total) does not check timeframes
at all — it only verifies that routes exist and warns about route count in live mode. So even the
existing validation call at jesse/modes/backtest_mode.py:116 would not catch this.
The actual crash happens two lines later, at jesse/modes/backtest_mode.py:118 →
jesse/store/state_candles.py:38:
total_bigger_timeframe = int((bucket_size / jh.timeframe_to_one_minutes(timeframe)) + 1)Unhandled exception in the process:
Traceback (most recent call last):
File "jesse/utils.py", line 362, in timeframe_to_one_minutes
return dic[timeframe]
KeyError: '1d'
During handling of the above exception, another exception occurred:
File "jesse/modes/backtest_mode.py", line 118, in _execute_backtest
store.candles.init_storage(5000)
File "jesse/store/state_candles.py", line 40, in init_storage
total_bigger_timeframe = int((bucket_size / jh.timeframe_to_one_minutes(timeframe)) + 1)
jesse.exceptions.InvalidTimeframe: Timeframe "1d" is invalid. Supported timeframes are 1m, 3m, 5m,
15m, 30m, 45m, 1h, 2h, 3h, 4h, 6h, 8h, 12h, 1D, 3D, 1W, 1M.
==> Removed finished worker <session-id> from active workersFrom the client's side the sequence is: 202 Accepted → poll the session → status: "draft",
progressbar.current: 0, metrics: null, exception: null — indefinitely. Nothing distinguishes
"invalid input" from "still starting up".
This matters most for non-dashboard clients (REST API, the MCP server, agents), which cannot see the container log and have no way to learn the request was rejected.
2. The docstring documents timeframes that are invalid
jesse/utils.py:328-337 — the docstring of timeframe_to_one_minutes lists the daily/weekly
timeframes in lowercase:
:param timeframe: str - The timeframe to convert. Supported timeframes include:
- '1m', '3m', '5m', '15m', '30m', '45m', '1h', '2h', '3h', '4h', '6h', '8h', '12h',
'1d', '3d', '1w', '1M'.But the lookup table only has the uppercase forms:
>>> from jesse.utils import TIMEFRAME_TO_ONE_MINUTES as T
>>> list(T.keys())
['1m', '3m', '5m', '15m', '30m', '45m', '1h', '2h', '3h', '4h', '6h', '8h', '12h', '1D', '3D', '1W', '1M']
>>> '1d' in T
FalseSo the documented values '1d', '3d', '1w' all raise InvalidTimeframe. Following the
function's own documentation produces the silent failure described above. (Note '1M' is correct
and is the one uppercase entry in the docstring, which makes the lowercase ones read as intentional
rather than a typo.)
To Reproduce
- Create a backtest with a route whose timeframe is
1dinstead of1D(any exchange/symbol):
{"exchange": "Binance Perpetual Futures", "strategy": "ExampleStrategy",
"symbol": "BTC-USDT", "timeframe": "1d"}- Start it via the API / MCP
run_backtest. Response:202 Started backtesting... - Poll the session — it stays
status: "draft", progress 0,exception: null, forever. - Only
docker compose logs jesseshowsInvalidTimeframe.
Expected behavior
Invalid route input should be rejected before the process is spawned, with a 4xx and the same
helpful message that InvalidTimeframe already produces ("Supported timeframes are …").
Alternatively, if validation must stay in the worker, the failure should be persisted on the session
(status error + the exception text) so pollers can see it.
Fixing the docstring alone would prevent most occurrences, but the silent-failure path would remain for any other invalid input.
Environment
- OS: Docker (
salehmir/jesse:latest), Linux container on Windows host - Version: jesse 2.5.0
- Client: REST API / MCP server (not the dashboard)
Additional context
Related to #601 in shape, not in cause: there too the worker died while the caller had already been told the operation started. Both cases are hard to diagnose for the same reason — a failure inside the spawned process never reaches whoever asked for the work.
I'm happy to send a PR. My inclination would be to validate route timeframes in the controller
before add_task and return 422 with the existing message, plus correct the docstring — but I'd
rather hear which shape you prefer before writing it.
Source: jesse-ai/jesse