fix(cli): accept a copied conversation URL as a server, and stop the SPA mislabeling missing API routes - #4374
Conversation
…SPA mislabeling missing API routes A conversation link copied from the browser (`<host>/c/<id>`) is what a user naturally pastes when asked for their omnigent URL, and `omnigent login` stored it verbatim as the default server. `/c/<id>` is a client-side SPA route, so every later API call was addressed under it and matched no router. A bare `omni` then crashed at session-create, on a machine the user never pointed at a remote by hand. Nothing caught the bad URL earlier because the web UI is mounted at `/` and answers any unmatched GET with its HTML shell: `GET <base>/c/<id>/v1/me` returns 200, so the login probe reads it as header-auth mode and persists it, and `/health` passes too. The first request that needs a real route is the session create. That failure then reported `405 Method Not Allowed`, because StaticFiles serves only GET/HEAD and raises 405 for anything else. The body is identical to FastAPI's path-matched-wrong-method response, so the error reads as "this endpoint exists, you used the wrong verb" and points at the server instead of the URL. - Trim the `/c/<id>` route in `_resolve_server_url`, the chokepoint every entry point already normalizes through, so an existing stored link is repaired on the next run rather than needing a hand-edited config. - Answer 404, not 405, for anything reaching the SPA catch-all: nothing that gets there exists, and a non-GET is never an SPA navigation. - Report a failed session create as a ClickException naming the URL, which the function's docstring already promised; the raw client error was reaching the crash handler as a traceback. Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
|
- Return the rstripped URL on the no-match path too, so both branches of strip_conversation_path normalize a trailing slash identically. - Reword the session-create guard's comment: it covers fork and resume rejections as well, not only a wrong base URL. - Pin the OPTIONS case in the catch-all test. No CORS middleware is installed, so a preflight reaching the SPA mount was already a 405 no browser could use; 404 is more accurate rather than a lost capability. Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
|
/review |
|
|
🏷️ Doc impact: Internal bugfixes—stripping the SPA Auto-classified on merge. Set the label manually before merging to override. · run |
Related issue
Closes #4303
Summary
A bare
omnicrashed at session-create withOmnigentError: {'detail': 'Method Not Allowed'}, on a machine that was never pointed at a remote by hand. Three separate defects stack up:https://<host>/c/9bed9ec6...—/c/<id>is the web UI's client-side chat route (web/src/App.tsx), i.e. what's in the address bar with a conversation open, so it's what a user pastes when asked for their omnigent URL.omnigent loginstored it verbatim, and bareomni(which rewrites torun, which falls back to the configserverkey) then addressed every API call under that route, where no router matches./and receives every unmatched request, soGET <base>/c/<id>/v1/mereturns200 text/html. The login probe reads that 200 as "header-auth mode, no login needed" and persists the URL;/healthpasses too. The first request needing a real route is the session create.StaticFilesserves only GET/HEAD and raises 405 otherwise. Its body is byte-identical to FastAPI's path-matched-wrong-method response, soPOST /v1/sessions→405 Method Not Allowedreads as "this endpoint exists, wrong verb" and points at server routing rather than at the URL. (POST /v1/sessionsis registered unconditionally; a healthy server 400s a bogus bundle.)Each fix sits at the single chokepoint for its defect:
strip_conversation_path(the inverse of the existingconversation_url) is applied in_resolve_server_url, which every entry point — including_ensure_backend— already normalizes through. An already-stored bad link is therefore repaired on the next run, with no hand-edited config.ClickExceptionnaming the URL — which the function's docstring already promised. The raw client error was reaching the crash handler as a traceback.ELI5 + request flow
You copied the link to a chat out of your browser and gave it to the CLI as the address of the server. Those look the same but aren't: the chat link points at a page inside the app, not the app's front door. The server's web UI is friendly enough to answer "hello" at any address, so nothing noticed until the CLI tried to actually do something. Then the error message said "wrong knock" instead of "wrong door".
flowchart TD A["bare omni (TTY)"] --> B["rewritten to: run"] B --> C["no --server, so read config server key"] C --> D["https://host/c/9bed9ec6..."] D --> E{"POST base + /v1/sessions"} E -->|"path is /c/id/v1/sessions"| F["no router owns this prefix"] F --> G["falls through to SPA mount at /"] G -->|"before: non-GET"| H["405 Method Not Allowed<br/>(reads as 'endpoint exists')"] H --> I["escapes as traceback"] G -->|"after"| J["404 not_found"] D -.->|"after: trimmed in _resolve_server_url"| K["https://host"] K --> L["POST /v1/sessions matches"]Test Plan
New coverage:
test_strip_conversation_path— 7 cases, including the exact URL from the crash report, and asserting real API bases (/api/2.0/omnigent, loopback) are left untouched.test_strip_conversation_path_inverts_conversation_url— pins the two helpers as inverses, since the CLI prints a link with one and must accept it back through the other.test_unmatched_api_path_404s_for_every_method— 404 across POST/GET/OPTIONS incl. the reported/omnigent/v1/sessionsshape, while/c/<id>still serves the SPA shell.test_prepare_chat_session_via_daemon_reports_create_failure_as_click_error—ClickExceptionnaming the URL, and no runner launched for a session that was never created.Both server-side and client-side tests fail without the fix (405 vs 404; traceback vs
ClickException).Also verified manually: the stored URL from the crash report now resolves to
https://<host>, soPOSTlands on/v1/sessions; and SPA routes,HEAD, real assets, and missing assets are all unchanged — only non-GET-on-unmatched flips 405→404.CI's
Pytest (server-integration)job is green on the final commit. Its first attemptfailed on
test_on_runner_disconnect_spares_idle_sessions_and_labels_interrupted_ones,which is unrelated to this diff and flaky under sharding: it passes locally both with
this change and with these four source files reverted to
main, the same job fails onmainitself (414f1f5, with 20 failures including a different test in that same file),and it passed on re-run here with no code change.
Two notes on this environment, both confirmed pre-existing by stashing the change and re-running:
tests/cli/reports 96 failures from missing optional deps (json5,respx); the failure set is byte-identical to cleanmain(96/873 there vs 96/874 here, the +1 being the new test).pyreflypre-commit hook reports 97 errors on a clean tree in this worktree (it resolves site-packages through another checkout). Run directly against the changed files with the right interpreter it reports 0 errors.Demo
Not a UI change, so no screenshot. The user-visible surface is the CLI's behavior on a
stored bad URL, driven here through the real code paths (
_resolve_server_urland thereal
_SPAStaticFilesmounted at/), with the exact URL from the crash report:The
$ omniblocks are the CLI-level effect of the HTTP results above them; thestatus codes, the resolved URL, and the response bodies are all live output. Reproducing
the traceback verbatim needs the pre-fix build, so that block is quoted from the crash
report in #4303.
Type of change
Test coverage
Coverage notes
Manual verification covered the parts the tests can't assert directly: that the URL from the crash report resolves to a working API base through
_resolve_server_url(the path a realomnitakes), and that the 405→404 change doesn't disturb the SPA's own behaviour (client-side routes,HEAD, real vs missing static assets). Automated tests cover the resolver cases, the round-trip invariant, the server response codes, and the client error type.Not covered: an end-to-end run against a live Databricks Apps deployment. Reproducing the exact deployment shape needs a hosted app, so the SPA catch-all was exercised via its real
_SPAStaticFilesclass mounted on the real app factory instead.Changelog
A conversation link copied from your browser now works wherever a server URL is expected, instead of failing later with an opaque "Method Not Allowed" crash