odysseus-dev/odysseus · warning · HTTPException
endpoint_a/endpoint_b or endpoint_a_id/endpoint_b_id is requ
Error message
endpoint_a/endpoint_b or endpoint_a_id/endpoint_b_id is required
What it means
Raised by the compare route when neither an endpoint id nor a raw endpoint URL was supplied for one of the two sides. Each side must resolve to an endpoint somehow; an entirely blank side is a usage error (422), not a lookup failure.
Source
Thrown at routes/compare/compare_routes.py:144
# Prefer an explicit endpoint id: it pins the EXACT registered
# endpoint (and its api_key), even when two endpoints visible to
# the caller share a base_url with different keys — a URL-only
# match would copy whichever row sorts first, i.e. possibly the
# wrong key. Fall back to URL resolution only for legacy / admin
# raw-URL callers that don't send an id.
eid = endpoint_id.strip() if isinstance(endpoint_id, str) else ""
if eid:
ep = _owned_endpoint_by_id(db, eid, user)
if ep is None:
# An id the caller can't see (wrong owner / deleted) must
# NOT silently fall back to a same-URL row with a different
# key — that's exactly the mix-up ids exist to prevent.
raise HTTPException(404, "Model endpoint not found")
# The id already resolved the endpoint; ignore any raw URL the
# caller also sent and dial the stored config instead.
endpoint = ep.base_url
elif not endpoint:
raise HTTPException(
422, "endpoint_a/endpoint_b or endpoint_a_id/endpoint_b_id is required"
)
else:
# Resolve the supplied URL to a ModelEndpoint the caller owns
# (their own rows + legacy null-owner shared rows), scoped so a
# comparison can't borrow another user's private endpoint key.
base = normalize_base(endpoint)
ep = _owned_endpoint_by_url(db, base, user)
# Reject *unregistered* raw URLs for signed-in non-admins; a
# matched registered endpoint supplies an id so the caller can
# still compare endpoints they own. Blanket-rejecting here (the
# earlier `endpoint_id=None` call) locked non-admins out of
# compare entirely, since compare resolves endpoints by URL with
# no endpoint_id. Mirrors the gallery inpaint/harmonize checks.
# Raised here (phase 1), before any session exists.
_reject_raw_endpoint_url_for_non_admin(
request, user, str(ep.id) if ep is not None else None, endpoint
)View on GitHub (pinned to f9235ebbf1)
Solutions
- Send endpoint_a/endpoint_b (raw URLs you own or that are registered) or endpoint_a_id/endpoint_b_id for both sides.
- Prefer the *_id form; it is unambiguous about which key is used.
- Validate the payload client-side before POSTing.
Example fix
// before
{model_a, model_b} // no endpoints
// after
{model_a, model_b, endpoint_a_id: "17", endpoint_b_id: "23"} Defensive patterns
Strategy: validation
Validate before calling
assert (endpoint_a or endpoint_a_id) and (endpoint_b or endpoint_b_id), \
'each compare side needs a URL or an id' Type guard
type CompareSide = { endpoint?: string; endpoint_id?: string }
const sideOk = (s: CompareSide) => !!(s.endpoint?.trim() || s.endpoint_id?.trim()) Prevention
- Send *_id fields preferentially; never rely on server defaults.
- Validate the compare payload against a shared schema client-side.
When it happens
Trigger: POST compare with endpoint_a/endpoint_b missing or empty and endpoint_a_id/endpoint_b_id also missing for at least one side; client sends model names only and assumes the server picks endpoints.
Common situations: Client schema drift — older callers sent only models; form encoding dropping empty-string fields; new frontend forgetting the endpoint pickers.
Related errors
- LaMa / MI-GAN inpainting requires an image mask. Use the edi
- d && d.detail ? d.detail : ('HTTP ' + res.status)
- result.detail || 'Failed to rename session'
- Invalid backup file: ' + e.message
- Import failed: server returned ${res.status}
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/45fca41eb8c98cbe.
Report an issue: GitHub.