Skip to main content

HTTP versions

The API port serves HTTP/1.1 and HTTP/2 on the same TCP listener, and each connection uses whichever one the client speaks, with nothing to configure. HTTP/3 runs over QUIC on a UDP listener of its own. It is off by default, and needs a certificate even when a proxy terminates TLS for TCP.

VersionTransportHow a connection gets itTo turn it on
HTTP/1.1TCPAnything that isn't HTTP/2Nothing
HTTP/2TCPPrior knowledge, or ALPN with TLSNothing
HTTP/3QUIC over UDPThe Alt-Svc advertisementserver.http3.mode and a certificate

How a connection picks its version​

Over plaintext, the broker reads the first bytes of each connection. A client that starts with the HTTP/2 connection preface gets HTTP/2 straight away. This is HTTP/2 over cleartext with prior knowledge, as curl --http2-prior-knowledge sends it. Everything else is served as HTTP/1.1.

With server.transport = "tls", the version is agreed in the TLS handshake through ALPN. The broker offers h2 and http/1.1, in that order, and the client chooses, as TLS describes.

Behind a proxy​

A proxy holds two connections, one with the client and one with the broker, and each gets a version of its own. A proxy can speak HTTP/2 to clients and HTTP/1.1 to the broker, or the other way round, and the broker answers in whatever its side of the connection speaks.

For HTTP/2 between the proxy and a plaintext broker, the proxy has to send it with prior knowledge, since the broker never upgrades an HTTP/1.1 connection. Whether your proxy can speak HTTP/2 to an upstream at all is in its own documentation.

The broker closes a connection once it has been idle for server.idle_timeout, 3 minutes by default. Keep that above the time your proxy keeps an idle upstream connection open, so the proxy is always the one that closes it. Most proxies keep one for 60 to 120 seconds, and some for much longer. If the broker closes first, the proxy can send a request down the connection just as it closes, and that request fails.

Connections that carry no request​

Two settings close a TCP connection that isn't carrying a request. Both apply to the API listener and to the management listener, and both take effect after a restart.

SettingDefaultCounts from
server.request_head_timeout30 secondsThe connection opening, and on HTTP/1.1 the first byte of each later request
server.idle_timeout3 minutesThe end of the last response

A new connection has server.request_head_timeout to send a complete request head. On HTTP/1.1, every later request on the connection gets the same time again, from its first byte. If part of an HTTP/1.1 head has arrived when the time runs out, the broker answers 408 Request Timeout with no body and Connection: close, then closes the connection. A connection that sent nothing, or only part of the HTTP/2 preface, is closed without an answer. request_limits.timeout starts only once the head has arrived, so it never covers this wait.

Between requests, a connection may sit idle for server.idle_timeout. A request that runs longer is never cut off, because the time starts only when the last response has gone out. An idle HTTP/1.1 connection is closed. An idle HTTP/2 connection is sent a GOAWAY first and then closed.

What is the same on every version​

The version changes how the bytes travel, not what a request may hold.

  • A request with a body needs Content-Length. Without one, the broker answers 411 with no body, as Strictness explains.
  • request_limits.max_header_section limits the request head, measured the same way on every version, and a head over it gets 431. Over HTTP/3, the limit is also sent to clients as SETTINGS_MAX_FIELD_SECTION_SIZE, so a client that has read it doesn't send larger headers.
  • request_limits.timeout and request_limits.max_body apply to every request.
  • Responses carry the same headers and bodies.
Closing connections

When the broker closes an HTTP/1.1 connection after a response marked Connection: close, it waits server.announced_close_timeout, 1 second by default, so the client can read the response first.

On HTTP/1.1, a refusal that comes before the body is answered without reading the body. That covers 404, 405, 411, 413, 415 and any other answer the broker can give from the request head alone, and each of them carries Connection: close. The broker then leaves the connection open, unread, for server.announced_close_timeout before it closes it. A client still sending its body reads the answer instead of a connection reset, and a proxy streaming the body passes the answer on instead of turning it into a 502.

On shutdown, the broker gives open connections server.graceful_drain_timeout, 30 seconds by default, to finish their requests. Listeners and TLS has these settings and the timeouts above.

HTTP/3​

Andromeda can serve the API over HTTP/3 as well. HTTP/3 runs over QUIC on UDP and always uses TLS 1.3, so the broker needs a certificate and key of its own to serve it, even when a proxy terminates TLS for TCP.

Experimental

HTTP/3 support is experimental. It builds on QUIC and TLS 1.3 libraries that are still maturing, so its behaviour may change between releases, and it may not yet hold up under production load. Try it in a test environment before you rely on it.

Turning it on​

[server.http3]
mode = "enabled"

[tls]
certificate = "/etc/andromeda/tls/tls.crt"
private_key = "/etc/andromeda/tls/tls.key"

HTTP/3 listens on server.host and server.port, over UDP. TCP 1056 and UDP 1056 are separate bindings, so no other port is needed, but both have to be reachable. In Docker, publish both, for example 1056:1056/tcp and 1056:1056/udp, and let inbound UDP through any firewall in front.

The certificate is the broker's one [tls] identity, the same one the TCP listener presents when it terminates TLS. Without tls.certificate and tls.private_key, andromeda configuration validate refuses the configuration and the broker doesn't start.

HTTP/3 always negotiates TLS 1.3 and the h3 ALPN token, so tls.minimum_protocol_version doesn't apply to it. Rotation and tls.client_authentication.mode work exactly as they do over TCP, as TLS describes.

How clients find it​

While HTTP/3 is up, every TCP response carries an advertisement like this one.

Alt-Svc: h3=":1056"; ma=86400

The empty host means the host the client already reached. ma is server.http3.advertisement_lifetime in seconds, a day by default, and says how long a client may remember the advertisement. The header is sent only while the HTTP/3 listener is up, so a client never tries QUIC against a port nobody listens on.

The advertisement names the broker's own port. If a proxy terminates TCP in front of the broker, clients can use HTTP/3 only when the same host also reaches that port over UDP.

Checking it works​

Ask for HTTP/1.1 and for HTTP/2, and look at the status line.

curl -sI --http1.1 http://localhost:1056/ngsi-ld/v1/version
curl -sI --http2-prior-knowledge http://localhost:1056/ngsi-ld/v1/version

The first answers HTTP/1.1 200 OK and the second HTTP/2 200. With TLS, use https://, and --http2 for the second.

For HTTP/3, a certificate that a client trusts directly must not be marked as a CA, or the client refuses it. For a test, make one like this and start the broker with it.

openssl req -x509 -newkey rsa:2048 -nodes -days 365 \
-keyout broker.key -out broker.crt \
-subj "/CN=localhost" \
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1" \
-addext "basicConstraints=critical,CA:FALSE"

ANDROMEDA__SERVER__HTTP3__MODE=enabled \
ANDROMEDA__TLS__CERTIFICATE=$PWD/broker.crt \
ANDROMEDA__TLS__PRIVATE_KEY=$PWD/broker.key \
andromeda

A TCP response should now carry Alt-Svc, and a curl built with HTTP/3 support should get an answer over QUIC alone.

curl -si http://localhost:1056/ngsi-ld/v1/info/sourceIdentity | grep -i alt-svc
curl --http3-only --cacert broker.crt https://localhost:1056/ngsi-ld/v1/info/sourceIdentity

What to watch​

  • quic_tls_config_installed at info

    HTTP/3 presents a rotated certificate to new connections.

  • quic_tls_config_rejected at error

    HTTP/3 couldn't use the rotated certificate and keeps the previous one. The line says why.

If it goes wrong​

  • A client that sends Upgrade: h2c never gets HTTP/2

    The broker doesn't switch an HTTP/1.1 connection to HTTP/2. Configure the client for prior knowledge, or use TLS.

  • An upload gets 411

    The client streams the body without Content-Length, as a chunked HTTP/1.1 upload does. Send the length.

  • Clients never switch to HTTP/3

    UDP on the broker's port is blocked somewhere on the way, or clients reach the API through a host or port other than the one the advertisement names.

  • A client gets 408 with no body, or its new connection is closed without an answer

    It didn't send a complete request head within server.request_head_timeout. Check what the client does between opening the connection and sending its request, or raise the timeout.

  • A proxy logs failed upstream requests on connections the broker had just closed

    The broker closed an idle connection before the proxy did. Raise server.idle_timeout above the proxy's idle timeout for upstream connections.

  • The broker doesn't start with HTTP/3 on

    tls.certificate and tls.private_key must both be set and readable.

  • A client refuses the HTTP/3 certificate

    A self-signed certificate must carry basicConstraints=critical,CA:FALSE, as in the example above.