Primary :logger filters for crash reports that are noise, not defects.
A primary filter (rather than a per-handler one) because these reports are worthless everywhere: console, file log and the admin Logs buffer alike. Filtering once at the source also keeps them from consuming the admin buffer, which is a fixed-size ring — a steady stream of noise evicts the real entries long before anyone reads them.
The TLS client alerts
When a client closes a TLS connection by sending a user_canceled alert
(Safari and iOS do this routinely on navigation away), Erlang's :ssl sends
the owning process {:ssl_error, socket, {:tls_alert, {:user_canceled, _}}}.
Thousand Island turns that straight into {:stop, reason, state}:
def handle_info({msg, raw_socket, reason}, {socket, state})
when msg in [:tcp_error, :ssl_error] do
{:stop, reason, {socket, state}}
endThe reason is neither :normal nor :shutdown, so OTP logs a full
gen_server terminate crash report with the entire socket struct inlined —
one per closing client. Note this path ignores Thousand Island's
silent_terminate_on_error option, which only covers the handler-return
error path, so there is no configuration knob that suppresses it.
Why an allowlist rather than "drop all TLS alerts"
Most TLS alerts are worth seeing. handshake_failure, unknown_ca and
certificate_expired on a server that terminates TLS itself (this one binds
443 directly — no proxy) are how a broken or expired certificate announces
itself. Dropping every :tls_alert would silence the one class of TLS error
that actually needs a human. Only alerts that mean "the peer went away or
never spoke TLS" are listed.
The corruption alerts
Binding 443 straight to the internet puts every scanner, broken middlebox and half-open mobile connection in front of the TLS stack, and each one costs a full crash report:
bad_record_mac— a record failed its authentication check, in either direction (decryption_failed,record_type_mismatch). Bytes were altered, truncated or replayed in transit; nothing on this side can repair a network path that mangles packets.unexpected_messagecarryingunsupported_record_type— the byte where a TLS content type belongs is not one of the four legal values (20, 21, 22, 23). Whatever connected is not speaking TLS at all.
Both are peer faults by construction. A genuine misconfiguration on this side fails during the handshake, as one of the alerts deliberately kept above.
The non-alert noise
Bandit.TransportError "Unable to obtain conn_data" is a socket that died
between accept and first read: :inet.peername/1 answers :einval because
there is no longer a connection to name. Scanners that connect and instantly
reset produce a steady trickle of these.
Bandit's "Connection that looks like TLS received on a clear channel" is a client speaking TLS to port 80. This app serves 80 only to redirect and to answer ACME challenges, so that is the client's mistake, not a fault here.
Bandit's own protocol-error lines — malformed request lines, forbidden HTTP/2
headers, bodies cut short — are deliberately not handled here. They are
switched off at the source, log_protocol_errors: false on both listeners in
GamendWeb.HostRuntime, because every one of them is the peer's fault by
construction and matching their message strings here would be a patch on top
of a patch. The dead-socket rule below still applies to a host that builds
its own endpoint config and leaves Bandit's default in place.
Summary
Functions
Drops crash reports and warnings caused by the peer rather than by this server.
The host-supplied GamendWeb.LogFilter modules, in configuration order.
Installs the filters. Idempotent: re-installing is a no-op, so a supervisor restart cannot stack duplicates.
Functions
Drops crash reports and warnings caused by the peer rather than by this server.
Returns :stop to drop the event, or :ignore to leave it for the remaining
filters and handlers — never the event itself, since this filter only ever
rejects and must not short-circuit other filters by accepting.
extra is the filter config :logger was given at install. A :host_filters
list there wins over the application env; install/0 passes none, so
production reads the host's config live, and a caller (or a test) can pin
its own list without touching global state.
The host-supplied GamendWeb.LogFilter modules, in configuration order.
Noise from a host's own code is the host's to describe; this is how it says so without patching core.
Installs the filters. Idempotent: re-installing is a no-op, so a supervisor restart cannot stack duplicates.