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.MintClientover HTTP. ArangoDB removed VelocyStream in 3.12, soArangox.VelocyClientmust now be selected explicitly (client: Arangox.VelocyClient) and needs a 3.11 server. The Gun client remains available asArangox.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 viassl_opts: [cacertfile: ...]or opt out withssl_opts: [verify: :verify_none]. - A pool configured with
:authrefuses 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 withallow_cleartext_auth: true. - Every client failure is an
%Arangox.Error{}with:reasonpopulated — HTTP status, ArangoDBerrorNum, and an atom reason derived from it. Connect failures are returned, never raised. - Stream transactions are handle-based.
Arangox.begin_transaction/2returns an%Arangox.Transaction{}handle; requests join it with the per-requesttransaction:option. The option accepts only the handle struct, never a bare identifier binary. DBConnection.prepare/2for AQL is refused: ArangoDB has no prepared statements. The AQL plan cache (ArangoDB 3.12.4+) is an explicit opt-in viause_plan_cache: true, withArangox.plan_cache/1andArangox.clear_plan_cache/1to 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_libraryand:vst_maxsizeare per-pool start options. The application-config forms still work and warn once per pool; they will be removed in the next release.:pool_sizedefaults to 10 rather thandb_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
:databaseoption (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:headersstart 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 (firstcontent-typeentry, any casing) or the:content_typepool 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:
:sslverifies peers by default from OTP 26, and the TLS defaults above rely on that.
- The default client is
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 isArangox.API.Documents.get_many/4, which always sendsonlyget=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:velocydependency) 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.MintClientit is opt-in per pool withclient_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.GunClientfollows: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_valuenaming 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/4as the one-shot AQL door, mapping snake_case options to the server's keys, plus streamed cursors throughDBConnection.stream/4.:max_body_sizepool 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_errorrestores 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-typenaming neither JSON nor VelocyPack (text/plainfor/_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.APIoperations declaring a single non-JSON request media type set the header themselves.
- A resource API: 230 operations across 22
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
DELETEfor 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::gunwrites 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/1no longer disconnects a healthy pool when:request_timeoutis 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_timeoutbounds 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
:authtuple carrying a non-string credential is refused atstart_link/1, and again described — never rendered — by the connect pipeline, so a pool started directly throughDBConnection.start_link/2cannot 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_librarydecoding probe responses), failing the connect in order instead of crash-looping the connection process. Arangox.GunClienthandles a1xxinformational response by waiting for the real one instead of failing the request.Exception.message/1on 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
:databaseno 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.
- 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
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 — 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
authstart option now only accepts{:basic, username, password}or{:bearer, token}- No longer authenticates with "root:" by default
- Requires Elixir v1.7+.