ADR-014. The runtime owns its HTTP transport
Status: accepted, 8 September 2026. Still in force.
Context
The Scaleway Python SDK is the collection’s dependency, and the generator uses
its Client and Profile to hold credentials. What remained open was how a
generated module reaches an operation.
The first answer, recorded in the runtime architecture page as “Decision 1”, was
scaleway_core.api.API._request(method, path, ...). It kept the contract as the
only source: the generator guesses no SDK method name from an operationId, and
the IR already carries http_method and path. The price was named at the
time, and accepted: a dependency on a method whose name starts with an
underscore, held by a guard that checked its signature.
Two measurements closed that question.
The SDK passes no timeout. Read in its source: API._request calls
requests.request() without one. A connection that opens and never answers
holds a module forever, and wait_timeout does not help, because it bounds a
wait loop rather than a single call. No playbook should carry that risk, and no
guard on a signature would have caught it: the signature was correct, the
behaviour was not.
An underscore is not a contract. A minor release of the SDK may move
_request without promising anything to anyone. The guard on its signature
turned that into a red build instead of a broken playbook, which was the right
mitigation for a dependency that should not have existed.
The migration happened before 0.4.0 and the changelog described it. What did not
happen is the record: the architecture page kept describing the reversed
decision, and it is the page a contributor reads to understand why the runtime
has the shape it has (#131). Worse, the replacement carried the same defect one
floor down: to hand the paginator the x-total-count header, the runtime wrote
requests.Response._content, a private attribute of a third-party library, in
code written to stop depending on one (#132).
Decision
The runtime composes its own HTTP request, and owns every responsibility that comes with it. The SDK supplies the profile and the credentials; it does not execute.
Each responsibility has one place in plugins/module_utils/scaleway.py:
responsibility |
where |
|---|---|
authentication headers, user agent, content type |
|
query parameter serialisation, a list becoming repeated pairs |
|
timeout, per call, from the |
|
TLS, and the |
|
error translation, without ever copying the response body |
|
reading the body, and carrying |
|
pagination, and the three conditions that stop it |
|
No private attribute of a third-party library is read or written in what is
shipped. The rule now has a control rather than a paragraph: mise run api:privee reads plugins/ and refuses an underscored attribute on anything
but self. It cannot infer types, so it refuses what it cannot justify; in
shipped code, an underscore on something that is not one’s own asks to be
argued, and an argument gets written down.
Carrying x-total-count therefore merges into the body that was read, never
into the response object. The need was never to change the response: it was to
give its reader what the header knows.
Consequences
The collection now maintains an HTTP client. Retries, connection pooling and
proxy handling are ours to decide rather than inherited. None of them is
implemented today, and that is a deliberate gap rather than an oversight: #91
carries the retry policy, per kind of operation, because retrying a POST that
triggers an action is not the same act as retrying a GET.
The SDK stays a dependency, for a narrower purpose. Its profile resolution,
its configuration file format and its credential validation are used as they
are. A future release that changed Client or Profile would still reach us;
what no longer reaches us is a change to how it sends requests.
One dependency was traded for another. requests is now used directly, and
it is what ansible-core and the SDK already pull in. It has a public API, and
the control above is what keeps us on it.
What this record does not decide
Whether an operation should be retried, and under which conditions: #91.
ADR-003 decides what a managing module sends and why a PUT reads first; this
record decides how anything is sent at all.