# 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 | `ScalewayApi._send` | | query parameter serialisation, a list becoming repeated pairs | `ScalewayApi._send` | | timeout, per call, from the `api_timeout` option | `ScalewayApi._send` | | TLS, and the `api_allow_insecure` escape for an emulator | `ScalewayApi._send` | | error translation, without ever copying the response body | `_error_from_response` | | reading the body, and carrying `x-total-count` into it | `ScalewayApi.request`, `_avec_total_count` | | pagination, and the three conditions that stop it | `paginate` | **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.