fix(ovhcloud): use the API endpoint as configured, for both schemes (1.2.5)

1.2.0 rewrote /1.0 to /v1 whenever a service account was in use, reasoning from a
documentation example that showed OAuth2 against a /v1 path. It was wrong, and it
was the cause of the 404s and the 500 that followed: OVH's own API console issues
Bearer requests against /1.0, while /v1 — which exists, and answers 401
unauthenticated like any live route — answers 500 with an empty body for a token
on the telephony routes. A failure with no message at all, produced by a helpful
rewrite nobody asked for.

The endpoint is now whatever was typed, for both authentication schemes. The
config field says where to copy it from.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TN1R4EnNFUV2Xwtd9q98tj
This commit is contained in:
2026-09-19 19:26:02 +00:00
parent 35f287837b
commit 42032e576e
9 changed files with 108 additions and 124 deletions
+13 -15
View File
@@ -34,23 +34,21 @@ def _oauth(cfg):
def _endpoint(cfg):
"""The API base, corrected to match the credentials in use.
"""The API base, exactly as configured.
The same routes answer under /1.0 and under /v1; the first is the signed
scheme's historical spelling and the second is the one OAuth2 documents. An
operator should not have to know that, so a service account gets /v1 whatever
was typed — a Bearer token sent to the legacy spelling is the kind of failure
that reads as "bad credentials" and sends somebody to regenerate a perfectly
good secret.
IT USED TO REWRITE /1.0 TO /v1 FOR A SERVICE ACCOUNT, on the strength of a
documentation example that showed OAuth2 against a /v1 path. That was wrong and
cost two rounds of debugging: OVH's own API console issues Bearer requests
against /1.0, and /v1 — which exists, and answers 401 unauthenticated like any
live route — answers 500 with an empty body for a token on the telephony
routes. A failure with no message, produced by a helpful rewrite nobody asked
for.
So the endpoint is now whatever was typed, for both schemes. Both spellings
work for the signed scheme; only the configured one is used, and OVH's console
is the place to copy it from.
"""
raw = (str(cfg.get("endpoint") or "https://eu.api.ovh.com/1.0")).rstrip("/")
if not _oauth(cfg):
return raw
if raw.endswith("/1.0"):
return raw[:-4] + "/v1"
if raw.endswith("/v1") or raw.endswith("/v2"):
return raw
return raw + "/v1"
return (str(cfg.get("endpoint") or "https://eu.api.ovh.com/1.0")).rstrip("/")
def _token_url(cfg):
@@ -34,23 +34,21 @@ def _oauth(cfg):
def _endpoint(cfg):
"""The API base, corrected to match the credentials in use.
"""The API base, exactly as configured.
The same routes answer under /1.0 and under /v1; the first is the signed
scheme's historical spelling and the second is the one OAuth2 documents. An
operator should not have to know that, so a service account gets /v1 whatever
was typed — a Bearer token sent to the legacy spelling is the kind of failure
that reads as "bad credentials" and sends somebody to regenerate a perfectly
good secret.
IT USED TO REWRITE /1.0 TO /v1 FOR A SERVICE ACCOUNT, on the strength of a
documentation example that showed OAuth2 against a /v1 path. That was wrong and
cost two rounds of debugging: OVH's own API console issues Bearer requests
against /1.0, and /v1 — which exists, and answers 401 unauthenticated like any
live route — answers 500 with an empty body for a token on the telephony
routes. A failure with no message, produced by a helpful rewrite nobody asked
for.
So the endpoint is now whatever was typed, for both schemes. Both spellings
work for the signed scheme; only the configured one is used, and OVH's console
is the place to copy it from.
"""
raw = (str(cfg.get("endpoint") or "https://eu.api.ovh.com/1.0")).rstrip("/")
if not _oauth(cfg):
return raw
if raw.endswith("/1.0"):
return raw[:-4] + "/v1"
if raw.endswith("/v1") or raw.endswith("/v2"):
return raw
return raw + "/v1"
return (str(cfg.get("endpoint") or "https://eu.api.ovh.com/1.0")).rstrip("/")
def _token_url(cfg):
@@ -34,23 +34,21 @@ def _oauth(cfg):
def _endpoint(cfg):
"""The API base, corrected to match the credentials in use.
"""The API base, exactly as configured.
The same routes answer under /1.0 and under /v1; the first is the signed
scheme's historical spelling and the second is the one OAuth2 documents. An
operator should not have to know that, so a service account gets /v1 whatever
was typed — a Bearer token sent to the legacy spelling is the kind of failure
that reads as "bad credentials" and sends somebody to regenerate a perfectly
good secret.
IT USED TO REWRITE /1.0 TO /v1 FOR A SERVICE ACCOUNT, on the strength of a
documentation example that showed OAuth2 against a /v1 path. That was wrong and
cost two rounds of debugging: OVH's own API console issues Bearer requests
against /1.0, and /v1 — which exists, and answers 401 unauthenticated like any
live route — answers 500 with an empty body for a token on the telephony
routes. A failure with no message, produced by a helpful rewrite nobody asked
for.
So the endpoint is now whatever was typed, for both schemes. Both spellings
work for the signed scheme; only the configured one is used, and OVH's console
is the place to copy it from.
"""
raw = (str(cfg.get("endpoint") or "https://eu.api.ovh.com/1.0")).rstrip("/")
if not _oauth(cfg):
return raw
if raw.endswith("/1.0"):
return raw[:-4] + "/v1"
if raw.endswith("/v1") or raw.endswith("/v2"):
return raw
return raw + "/v1"
return (str(cfg.get("endpoint") or "https://eu.api.ovh.com/1.0")).rstrip("/")
def _token_url(cfg):
+13 -15
View File
@@ -34,23 +34,21 @@ def _oauth(cfg):
def _endpoint(cfg):
"""The API base, corrected to match the credentials in use.
"""The API base, exactly as configured.
The same routes answer under /1.0 and under /v1; the first is the signed
scheme's historical spelling and the second is the one OAuth2 documents. An
operator should not have to know that, so a service account gets /v1 whatever
was typed — a Bearer token sent to the legacy spelling is the kind of failure
that reads as "bad credentials" and sends somebody to regenerate a perfectly
good secret.
IT USED TO REWRITE /1.0 TO /v1 FOR A SERVICE ACCOUNT, on the strength of a
documentation example that showed OAuth2 against a /v1 path. That was wrong and
cost two rounds of debugging: OVH's own API console issues Bearer requests
against /1.0, and /v1 — which exists, and answers 401 unauthenticated like any
live route — answers 500 with an empty body for a token on the telephony
routes. A failure with no message, produced by a helpful rewrite nobody asked
for.
So the endpoint is now whatever was typed, for both schemes. Both spellings
work for the signed scheme; only the configured one is used, and OVH's console
is the place to copy it from.
"""
raw = (str(cfg.get("endpoint") or "https://eu.api.ovh.com/1.0")).rstrip("/")
if not _oauth(cfg):
return raw
if raw.endswith("/1.0"):
return raw[:-4] + "/v1"
if raw.endswith("/v1") or raw.endswith("/v2"):
return raw
return raw + "/v1"
return (str(cfg.get("endpoint") or "https://eu.api.ovh.com/1.0")).rstrip("/")
def _token_url(cfg):
@@ -34,23 +34,21 @@ def _oauth(cfg):
def _endpoint(cfg):
"""The API base, corrected to match the credentials in use.
"""The API base, exactly as configured.
The same routes answer under /1.0 and under /v1; the first is the signed
scheme's historical spelling and the second is the one OAuth2 documents. An
operator should not have to know that, so a service account gets /v1 whatever
was typed — a Bearer token sent to the legacy spelling is the kind of failure
that reads as "bad credentials" and sends somebody to regenerate a perfectly
good secret.
IT USED TO REWRITE /1.0 TO /v1 FOR A SERVICE ACCOUNT, on the strength of a
documentation example that showed OAuth2 against a /v1 path. That was wrong and
cost two rounds of debugging: OVH's own API console issues Bearer requests
against /1.0, and /v1 — which exists, and answers 401 unauthenticated like any
live route — answers 500 with an empty body for a token on the telephony
routes. A failure with no message, produced by a helpful rewrite nobody asked
for.
So the endpoint is now whatever was typed, for both schemes. Both spellings
work for the signed scheme; only the configured one is used, and OVH's console
is the place to copy it from.
"""
raw = (str(cfg.get("endpoint") or "https://eu.api.ovh.com/1.0")).rstrip("/")
if not _oauth(cfg):
return raw
if raw.endswith("/1.0"):
return raw[:-4] + "/v1"
if raw.endswith("/v1") or raw.endswith("/v2"):
return raw
return raw + "/v1"
return (str(cfg.get("endpoint") or "https://eu.api.ovh.com/1.0")).rstrip("/")
def _token_url(cfg):
@@ -34,23 +34,21 @@ def _oauth(cfg):
def _endpoint(cfg):
"""The API base, corrected to match the credentials in use.
"""The API base, exactly as configured.
The same routes answer under /1.0 and under /v1; the first is the signed
scheme's historical spelling and the second is the one OAuth2 documents. An
operator should not have to know that, so a service account gets /v1 whatever
was typed — a Bearer token sent to the legacy spelling is the kind of failure
that reads as "bad credentials" and sends somebody to regenerate a perfectly
good secret.
IT USED TO REWRITE /1.0 TO /v1 FOR A SERVICE ACCOUNT, on the strength of a
documentation example that showed OAuth2 against a /v1 path. That was wrong and
cost two rounds of debugging: OVH's own API console issues Bearer requests
against /1.0, and /v1 — which exists, and answers 401 unauthenticated like any
live route — answers 500 with an empty body for a token on the telephony
routes. A failure with no message, produced by a helpful rewrite nobody asked
for.
So the endpoint is now whatever was typed, for both schemes. Both spellings
work for the signed scheme; only the configured one is used, and OVH's console
is the place to copy it from.
"""
raw = (str(cfg.get("endpoint") or "https://eu.api.ovh.com/1.0")).rstrip("/")
if not _oauth(cfg):
return raw
if raw.endswith("/1.0"):
return raw[:-4] + "/v1"
if raw.endswith("/v1") or raw.endswith("/v2"):
return raw
return raw + "/v1"
return (str(cfg.get("endpoint") or "https://eu.api.ovh.com/1.0")).rstrip("/")
def _token_url(cfg):
@@ -34,23 +34,21 @@ def _oauth(cfg):
def _endpoint(cfg):
"""The API base, corrected to match the credentials in use.
"""The API base, exactly as configured.
The same routes answer under /1.0 and under /v1; the first is the signed
scheme's historical spelling and the second is the one OAuth2 documents. An
operator should not have to know that, so a service account gets /v1 whatever
was typed — a Bearer token sent to the legacy spelling is the kind of failure
that reads as "bad credentials" and sends somebody to regenerate a perfectly
good secret.
IT USED TO REWRITE /1.0 TO /v1 FOR A SERVICE ACCOUNT, on the strength of a
documentation example that showed OAuth2 against a /v1 path. That was wrong and
cost two rounds of debugging: OVH's own API console issues Bearer requests
against /1.0, and /v1 — which exists, and answers 401 unauthenticated like any
live route — answers 500 with an empty body for a token on the telephony
routes. A failure with no message, produced by a helpful rewrite nobody asked
for.
So the endpoint is now whatever was typed, for both schemes. Both spellings
work for the signed scheme; only the configured one is used, and OVH's console
is the place to copy it from.
"""
raw = (str(cfg.get("endpoint") or "https://eu.api.ovh.com/1.0")).rstrip("/")
if not _oauth(cfg):
return raw
if raw.endswith("/1.0"):
return raw[:-4] + "/v1"
if raw.endswith("/v1") or raw.endswith("/v2"):
return raw
return raw + "/v1"
return (str(cfg.get("endpoint") or "https://eu.api.ovh.com/1.0")).rstrip("/")
def _token_url(cfg):
@@ -34,23 +34,21 @@ def _oauth(cfg):
def _endpoint(cfg):
"""The API base, corrected to match the credentials in use.
"""The API base, exactly as configured.
The same routes answer under /1.0 and under /v1; the first is the signed
scheme's historical spelling and the second is the one OAuth2 documents. An
operator should not have to know that, so a service account gets /v1 whatever
was typed — a Bearer token sent to the legacy spelling is the kind of failure
that reads as "bad credentials" and sends somebody to regenerate a perfectly
good secret.
IT USED TO REWRITE /1.0 TO /v1 FOR A SERVICE ACCOUNT, on the strength of a
documentation example that showed OAuth2 against a /v1 path. That was wrong and
cost two rounds of debugging: OVH's own API console issues Bearer requests
against /1.0, and /v1 — which exists, and answers 401 unauthenticated like any
live route — answers 500 with an empty body for a token on the telephony
routes. A failure with no message, produced by a helpful rewrite nobody asked
for.
So the endpoint is now whatever was typed, for both schemes. Both spellings
work for the signed scheme; only the configured one is used, and OVH's console
is the place to copy it from.
"""
raw = (str(cfg.get("endpoint") or "https://eu.api.ovh.com/1.0")).rstrip("/")
if not _oauth(cfg):
return raw
if raw.endswith("/1.0"):
return raw[:-4] + "/v1"
if raw.endswith("/v1") or raw.endswith("/v2"):
return raw
return raw + "/v1"
return (str(cfg.get("endpoint") or "https://eu.api.ovh.com/1.0")).rstrip("/")
def _token_url(cfg):
@@ -240,7 +238,7 @@ def _queue_id(cfg):
# It also answers its own VERSION. When a test fails identically twice, the first
# question is whether the new code is the code that ran, and nothing on the screen
# could answer it.
VERSION = "1.2.4"
VERSION = "1.2.5"
def _try(step, fn):