# Changelog

## v0.8.0 (2026-08-12)

HTTP via Mint is now the default transport for ArangoDB 3.12 and later; 
VelocyStream and active failover remain supported as explicit opt-ins 
for ArangoDB 3.11, which is the last server release that speaks them.

* Breaking changes
  * The default client is `Arangox.MintClient` over HTTP. ArangoDB removed
    VelocyStream in 3.12, so `Arangox.VelocyClient` must now be selected
    explicitly (`client: Arangox.VelocyClient`) and needs a 3.11 server. The
    Gun client remains available as `Arangox.GunClient`.
  * TLS certificates are verified by default (`verify_peer`, hostname checks,
    system trust store), and TLS below 1.2 is refused. Connecting to a server
    with an untrusted certificate now fails until you supply trust material
    via `ssl_opts: [cacertfile: ...]` or opt out with
    `ssl_opts: [verify: :verify_none]`.
  * A pool configured with `:auth` refuses to start when its endpoints are
    cleartext (`http://`) and not on this machine, because the credentials
    would be readable in transit. Loopback endpoints are exempt; opt in
    explicitly with `allow_cleartext_auth: true`.
  * Every client failure is an `%Arangox.Error{}` with `:reason` populated —
    HTTP status, ArangoDB `errorNum`, and an atom reason derived from it.
    Connect failures are returned, never raised.
  * Stream transactions are handle-based. `Arangox.begin_transaction/2`
    returns an `%Arangox.Transaction{}` handle; requests join it with the
    per-request `transaction:` option. The option accepts only the handle
    struct, never a bare identifier binary.
  * `DBConnection.prepare/2` for AQL is refused: ArangoDB has no prepared
    statements. The AQL plan cache (ArangoDB 3.12.4+) is an explicit opt-in
    via `use_plan_cache: true`, with `Arangox.plan_cache/1` and
    `Arangox.clear_plan_cache/1` to manage it.
  * Every request runs under an absolute deadline established when the caller
    enters the pool, so time spent queueing counts against `:timeout`. A
    timed-out request disconnects its connection instead of returning it to
    the pool, and a fully written request is never retried automatically.
  * `:json_library` and `:vst_maxsize` are per-pool start options. The
    application-config forms still work and warn once per pool; they will
    be removed in the next release.
  * `:pool_size` defaults to 10 rather than `db_connection`'s 1. HTTP/1.1
    carries one request per connection at a time, so the pool size is the
    driver's concurrency and a default of 1 serializes an application.
  * The `:database` option (per pool and per request) is validated: names
    that would alter the request path (`/`, `?`, `#`, `%`, control
    characters) are refused, and extended names (spaces, unicode) are
    percent-encoded on HTTP paths.
  * Leader redirects (active failover, 3.11) are only followed when the
    advertised endpoint passes an admission policy; anything else requires an
    explicit `:endpoint_mapper`. Unknown start options log a warning.
  * Headers are lists of `{name, value}` string tuples; maps are no longer
    accepted, at the `:headers` start option or on a request. The wire order
    is fixed — pool headers, then the transaction header when one applies,
    then the request's own — and nothing is merged, deduplicated, renamed or
    re-cased: a request header no longer *replaces* a same-named pool header,
    both are sent. The body codec is selected from the request's own headers
    (first `content-type` entry, any casing) or the `:content_type` pool
    option — never from pool `:headers`. Exactly one transaction header is
    ever added, and a request already naming one, in any casing, runs under
    its own. Response headers are the same shape — the parsed `{name, value}`
    list, order and repeats as received over HTTP, built from the wire map
    over VelocyStream. See the README's "Headers" section for the per-client
    wire behavior.
  * Requires Elixir 1.15+ on OTP 26+. The OTP floor is enforced at compile
    time: `:ssl` verifies peers by default from OTP 26, and the TLS defaults
    above rely on that.

* Enhancements
  * A resource API: 230 operations across 22 `Arangox.API.*` modules —
    `Arangox.API.Collections.create/3`, `Arangox.API.Documents.get/4`, and so
    on — derived from ArangoDB's API description at tag 3.12.10 and owned as
    hand-maintained source. The connection is the first argument and path
    parameters follow it; the database, headers, and every per-request driver
    option are options. A call returns the decoded body, any error status
    returns `{:error, %Arangox.Error{}}`, and every operation has a bang twin
    that raises. Query parameters are written in snake_case and translated to
    ArangoDB's own spelling on the wire. The integration suite checks the
    surface against the API description the tested server itself serves,
    address by address. Reading several documents by key is
    `Arangox.API.Documents.get_many/4`, which always sends `onlyget=true` —
    the flag that separates that read from a bulk replace on the same URL, and
    which arangox therefore does not let a caller set.
  * Opt-in VelocyPack bodies over HTTP: `content_type: :velocypack` (requires
    the optional `:velocy` dependency) encodes request bodies as VelocyPack
    and asks for the same in return. Responses are decoded by their own
    content type, so a server that responds in JSON is still read correctly.
  * HTTP/2 support on both HTTP clients. On `Arangox.MintClient` it is opt-in
    per pool with `client_opts: [protocols: [:http2]]` — settled by ALPN on
    TLS, prior knowledge on cleartext — and HTTP/1.1 remains its default on
    both schemes; see the known limitation below for why it is offered rather
    than assumed. `Arangox.GunClient` follows `:gun`'s own defaults instead
    (HTTP/1.1 on cleartext, ALPN with HTTP/2 preferred on TLS), since the
    limitation is Mint-specific; `client_opts: %{protocols: [:http2]}` asserts
    it on cleartext.
  * Header names and values carrying a carriage return, line feed, or null are
    refused before they reach a transport, with `reason: :invalid_header_value`
    naming the header but never echoing its value. The rule lives at the one
    seam every request and connect probe crosses, so it holds under both
    protocols — HTTP/1.1 refuses such a value locally, while under HTTP/2 the
    header is HPACK-encoded and only the server would object.
  * `Arangox.query/4` as the one-shot AQL door, mapping snake_case options to
    the server's keys, plus streamed cursors through `DBConnection.stream/4`.
  * `:max_body_size` pool option (default 128 MiB): a larger response body is
    rejected with a structured error before any decoding runs. Malformed
    bodies in either codec return structured errors (`:encode_error`,
    `:decode_error`, `:body_too_large`, `:invalid_length`) instead of
    raising, with size and length-prefix bounds enforced before the decoder
    sees a byte.
  * Credential redaction throughout: inspecting connection state, requests,
    or transaction handles never reveals credentials or transaction
    identifiers; the request struct echoed back from a call carries
    `[redacted]` in place of sensitive header values. An endpoint containing
    an `@` anywhere is stored and reported with everything after its scheme
    replaced by `[redacted]` — the redaction never tries to locate the
    userinfo, so a typo'd endpoint cannot smuggle a password past it — and an
    endpoint without an `@` has no userinfo to lose and stays readable in
    errors and logs. `show_sensitive_data_on_connection_error` restores
    credentials in connect-time errors only, for debugging.
  * Unix domain socket support in `Arangox.MintClient`
    (`"http://unix:/path/to.sock"`).
  * Raw request bodies: a `content-type` naming neither JSON nor VelocyPack
    (`text/plain` for `/_api/import`, `application/octet-stream`) sends a
    binary body byte-for-byte instead of running it through the JSON codec —
    which had quietly quoted such bodies since 0.7. `Arangox.API` operations
    declaring a single non-JSON request media type set the header themselves.

* Fixes
  * A cursor the server issued no id for — a single-batch result — is keyed
    under a reference and served entirely from driver memory: abandoning it
    before its batch was delivered no longer builds a `DELETE` for an id the
    server never issued (previously a crash that retired the connection), and
    its cleanup is the local no-op it always should have been. A cursor-create
    reply promising more batches *without* an id is refused at declare: those
    batches could never be fetched.
  * `Arangox.GunClient`'s socket carries the same teardown hygiene as the
    other clients — a bounded driver queue and an abort on close. Per-request
    send bounds deliberately do not apply to it: `:gun` writes from its own
    process, so a caller can never block inside a socket send, which is now
    documented and pinned by a test.
  * A 2xx response whose declared content type is neither JSON nor VelocyPack
    is returned with its body raw instead of failing as `:decode_error` —
    Prometheus metrics, multipart batch responses, and Foxx bundle downloads
    are non-JSON successes. An absent content type still decodes as JSON.
  * `ping/1` no longer disconnects a healthy pool when `:request_timeout` is
    set below the driver's socket-timeout floor: requests that arrive with no
    caller deadline get at least the floor as their budget, so an aggressive
    `:request_timeout` bounds callers without turning every idle health check
    into a reconnect.
  * A budget spent before anything was written reports "before the request
    was sent" rather than borrowing the receive-phase wording that claimed a
    response was in flight.
  * A malformed entry in a request's header list — anything but a
    `{name, value}` tuple — is refused as a described error on a healthy
    connection instead of raising out of the callback and retiring it. The
    entry is never echoed; it may carry a credential.
  * A supported `:auth` tuple carrying a non-string credential is refused at
    `start_link/1`, and again described — never rendered — by the connect
    pipeline, so a pool started directly through `DBConnection.start_link/2`
    cannot leak the value into the connect-failure log.
  * Socket writes are bounded by the request's remaining budget: each request
    (and each VelocyStream chunk) re-derives the socket's send timeout from
    its deadline, the inet driver's send queue is bounded by a watermark so
    the send timer actually runs for large bodies, and sockets close with a
    zero linger so retiring a connection with unsent bytes queued to a dead
    peer aborts instead of stalling in the driver's flush wait.
  * An HTTP/2 connection the server has told to go away (GOAWAY) is retired
    from the pool as soon as a request fails on it. Both HTTP clients now
    consult the socket, not just the error's reason atom, before a connection
    goes back to the pool, and the VelocyStream client reports a retiring
    reason for any failure past the point where bytes reached the wire.
  * The connect pipeline catches throws and exits — not only raises — from
    user-pluggable code (a `:json_library` decoding probe responses), failing
    the connect in order instead of crash-looping the connection process.
  * `Arangox.GunClient` handles a `1xx` informational response by waiting for
    the real one instead of failing the request.
  * `Exception.message/1` on a long error truncates on a UTF-8 character
    boundary.
  * An explicit default port (`:80`/`:443`) is recognized only in the
    endpoint's authority; the same digits in a path or userinfo no longer
    count as writing the port out.
  * Connection processes no longer leak sockets when authentication or the
    availability probe fails during connect.
  * A transport error while probing one endpoint no longer stops the walk —
    the remaining endpoints are still tried.
  * A per-request `:database` no longer double-prefixes a path that already
    names one.
  * The VelocyStream client returns an error instead of raising when
    given an unsupported HTTP method or a body it cannot encode, and its
    connect timeout budget is per endpoint rather than shared across probe
    stages.
  * A cursor drain that outlives its deadline retires the connection instead
    of leaving it poisoned in the pool.

### Known limitations

**`Arangox.MintClient` bounds HTTP/2 request bodies by the peer's
flow-control window.** It hands a request body to `Mint.request/5` whole rather
than streaming it, and HTTP/2 applies flow control to request bodies: one
larger than the window the peer advertises is refused with
`reason: :exceeds_window_size` rather than waiting for a `WINDOW_UPDATE`.
ArangoDB advertises about 64 KiB, so bulk inserts and large documents exceed
it. `Arangox.GunClient` does **not** have this limitation — `:gun` queues the
body and honours `WINDOW_UPDATE` itself — and neither client is affected on
HTTP/1.1, which has no flow control.

This is why HTTP/2 is opt-in on `Arangox.MintClient` rather than its default,
even though ALPN would negotiate it on TLS: a driver whose bulk inserts fail
above 64 KiB is worse than one speaking HTTP/1.1. `Arangox.GunClient` has no
such bound and follows `:gun`'s own defaults, which negotiate HTTP/2 on TLS;
a Mint pool that opts in should keep its bodies below the window its server
advertises.

Streaming request bodies in window-sized chunks is what removes the bound. It
needs the client to interleave writing the body with reading `WINDOW_UPDATE`
frames under the request deadline, so it is deliberately not a 0.8 change; it
lands with the transport rework below, which is also what makes HTTP/2 worth
defaulting to.

**HTTP/2 multiplexing is not exploited.** `DBConnection` runs one in-flight
request per pooled connection, so concurrency comes from `:pool_size` rather
than from streams sharing a connection.

### What comes next

1.0 replaces the `DBConnection` foundation with `http_connection`. That swap is
what makes multiplexing and streamed request bodies reachable, and it is
breaking on its own — `t:Arangox.conn/0`, `DBConnection.ConnectionError`,
`DBConnection.Stream`, and the pool options are all part of the public surface
today. 0.8 exists so the stability promise lands on the foundation that stays;
the next release removes the deprecated application-config fallbacks in between.

### Migrating from v0.7

Work through these in order; most upgrades need only the first two.

**Pick your client.** If you never set `:client`, v0.7 gave you VelocyStream
and v0.8 gives you HTTP via Mint — add `{:mint, "~> 1.9"}` and `{:jason, "~> 1.4"}`
to your deps. If you pass request headers or inspect response headers, migrate
them to the `{name, value}` list shape described above: request maps are no
longer accepted, and responses expose headers as lists rather than maps. If
you must stay on VelocyStream, set `client: Arangox.VelocyClient` explicitly
and stay on an ArangoDB 3.11 server; 3.12 closes VST connections on sight.

**TLS now verifies.** A `ssl://` endpoint that connected fine under v0.7
(against any certificate, silently) fails under v0.8 until the trust question
is answered. Two different situations need two different answers:

  * a certificate from a **private CA**: point the pool at the CA —
    `ssl_opts: [cacertfile: "/path/to/ca.pem"]`;
  * a **self-signed** certificate you cannot get a CA for:
    `ssl_opts: [verify: :verify_none]`, accepting that the connection is
    then unauthenticated.

**Cleartext with credentials refuses to start** unless the endpoint is
loopback. If you authenticate over plain `http://` to another machine, either
move to TLS or acknowledge the exposure with `allow_cleartext_auth: true`.

**Transactions.** `DBConnection.transaction/3` blocks keep working. Code that
held a raw transaction id binary must hold the `%Arangox.Transaction{}` handle
from `Arangox.begin_transaction/2` instead and pass it per request as
`transaction: trx` — the id is accepted nowhere by itself.

**Application config.** `config :arangox, :json_library` and `:vst_maxsize`
still work and warn; move them to `start_link/1` options before the next release.
Two pools can now disagree, which is the point.

**Timeouts.** `:timeout` is a deadline from pool entry, so queue time counts
against it; a timed-out request disconnects its connection and is never
retried by the driver. If you relied on slow requests surviving past their
timeout, raise the timeout — the old behavior was the bug.

**Redirect targets are vetted.** In containerized 3.11 failover setups where
the leader advertises an address only reachable from inside the network, map
it: `endpoint_mapper: %{"http://advertised:8529" => "http://reachable:8529"}`.
Without a mapping (or same-origin match) the redirect is refused rather than
followed with your credentials.

**Custom clients** must adopt the three-argument callback
`request(request, opts, state)` and return `%Arangox.Error{}` for every
failure; bare reason atoms and library exceptions are no longer accepted. The
raise you get on start names exactly what to change.

## v0.7.0 (2024-02-20)

* Enhancements
  * Added support for ArangoDB JWT authentication via bearer tokens

* Breaking changes
  * `auth` start option now only accepts `{:basic, username, password}` or `{:bearer, token}`
  * No longer authenticates with "root:" by default
  * Requires Elixir v1.7+.
