Skip to content

fix: comprehensive audit — 66 security/correctness/performance findings resolved - #25

Merged
aarambh-darshan merged 15 commits into
mainfrom
fix/audit-findings
Aug 23, 2026
Merged

aarambh-darshan merged 15 commits into
mainfrom
fix/audit-findings

Conversation

@aarambh-darshan

Copy link
Copy Markdown
Member

Comprehensive Security, Correctness & Performance Audit — 66 Findings Resolved

Overview

This branch resolves all 66 findings from a full line-by-line audit of every
.rs file across the 14 workspace crates (~23.5k lines). The audit covered
correctness bugs, security vulnerabilities, protocol-compliance issues,
reliability gaps, and performance losses, cross-verified against vendored
dependency sources (hyper 1.9, h2, multer 3.1, cookie 0.18, matchit 0.8.6,
tokio 1.51). Two findings were reproduced at runtime before fixing.

Scope: 14 commits · 64 files changed · +4,081 / −721 lines · ~90 new tests

Full finding-by-finding tracker (IDs H1–H8, M1–M24, L1–L34) lives in
AUDIT_FINDINGS.md (kept out of the repo per project preference).


Security fixes (14 findings)

Critical behavior corrections

  • Unknown HTTP methods executed handlers (H2): an empty method bitmask
    subset-matched every registered filter, so a PURGE request on a
    DELETE-only route ran the DELETE handler with 200 — runtime-confirmed.
    Extension methods now get a dedicated MethodFilter::EXTENSION bit and fall
    through to 405; ANY keeps catch-all semantics. (runtime-confirmed)
  • No default body size limit (H1): Json/Form/Bytes/String
    buffered arbitrary attacker-chosen payloads into one heap allocation. A new
    arvik_core::body_limit module enforces a 2 MiB default with 413
    rejections; DefaultBodyLimitLayer raises/disables it per route.
  • nest() silently dropped sub-router middleware (H3): auth/CORS/rate
    limiting attached inside a nested router never ran. Layers now fold onto
    flattened routes; a sub-router .fallback() panics loudly instead of
    vanishing.
  • Rate limiter provided zero protection (H6 + M8 + M9): keys came
    from spoofable X-Forwarded-For (fresh bucket per header value), the window
    parameter was discarded (100/min actually allowed 6000/min), and one
    global mutex serialized all requests with unbounded key growth. Rewritten:
    socket-address keys by default, trust_proxies() rightmost-untrusted-hop
    resolution, real capacity/window refill, 32 lock shards, idle-bucket
    eviction.
  • TLS handshake had no timeout (H5): trickled ClientHello bytes pinned
    tasks/sockets forever. Now bounded by ServerConfig::handshake_timeout()
    (default 10 s) on both TLS backends.
  • CORS very_permissive() mirrored any origin with credentials (M11),
    making every site a trusted credentialed origin. Credentials are dropped
    from that preset and the combination now panics at construction with
    guidance.

Path safety & static files

  • Router percent-decoding handed handlers traversal-ready strings — %2F,
    %5C, %00, CR/LF stay encoded post-match (M2).
  • Static serving opened the file only after statting it for validators —
    deploys mid-request could truncate responses or poison caches. File is
    opened first; metadata is read off the open descriptor (M15).
  • Symlinks resolving outside the serve root are rejected by default;
    .follow_symlinks(true) opts in (L22).

Hardening

  • Constant-time comparisons for Bearer/Basic tokens and CSRF tokens
    (M10, L14).
  • From<String> for Error no longer reflects interpolated strings into
    client responses; .with_message() is the explicit opt-in (L6).
  • Query strings excluded from logs/traces by default (M23); client-supplied
    request ids validated or regenerated (L28); health-check failure details
    logged instead of returned in the public probe body (L29); keystore
    password redacted from config Debug output (H8).

Reliability & correctness (25 findings)

  • Accept-loop resilience (H4): one transient error (EMFILE,
    ECONNABORTED) used to kill the whole server permanently. Errors are now
    classified — transient skipped, resource exhaustion backs off 100 ms, only
    broken listeners remain fatal. TCP-option failures drop one connection, not
    the server.
  • Real graceful shutdown (M5): connections were never signalled — hyper
    served new requests during the entire drain window, then hard-aborted.
    Each connection task now holds a shutdown receiver and calls hyper's
    graceful_shutdown(): idle keep-alive closes instantly (integration test
    measures ~0 s vs the old 30 s), in-flight responses finish, the timeout is a
    last resort. Over-capacity rejects answer 503 Connection: close instead of
    a bare RST (M4 partial; default cap stays opt-in by design).
  • Lifecycle: hot-reload watchers (TLS certs and app config) watched file
    inodes, so any temp-file+rename or certbot symlink rotation silenced them
    forever — both watch parent directories now (M6, M20). OTEL provider no
    longer leaks its exporter task when subscriber install conflicts; new
    ArvikLogger::init_with_otel() composes logging+tracing into one subscriber
    (M21). Lifecycle hooks spawn fire-and-forget so blocking hooks can't stall
    the accept loop (L8).
  • Protocol correctness: stale Content-Length after body transforms and
    decompression caused truncated/corrupt framing (H7, M13); SSE event ids
    sanitize CR/LF, closing payload-drop injection (M17); If-Range is now
    enforced with strong comparison so resumable downloads can't stitch two file
    versions (M16, fs ETags switched weak→strong); unknown Range units and
    multi-range requests serve full 200 per RFC 9110 (L20); WS version
    rejection returns RFC 6455 426 + version hint (L23); HEAD falls back to
    GET handlers (L1, runtime-confirmed).
  • Panics removed: ResponseBuilder::json and deferred builder errors
    (e.g. user-controlled redirect targets) return 500s instead of crashing
    connection tasks (M1, L4); catch_panic no longer re-panics on task
    cancellation (L12) and drops its per-request task spawn (L13); h2 window
    overflow is caught at validation instead of panicking on first HTTP/2
    traffic (M19); non-UTF-8 env vars no longer abort config loading (L27);
    invalid headers log loudly instead of vanishing silently (L5).
  • Config & tooling: typo'd ARVIK_* variables inside known sections fail
    the build naming the variable (M18); proc macros resolve FoundCrate::Itself
    as crate (L32); test cookie jar honors expiry deletions and merges
    explicit Cookie headers so logout assertions behave like real browsers
    (L33); form content-type validated via media-type parse (L24).

Performance (12 findings)

Ranked by measured impact:

  1. Router hot path (M3 + L3): handler lists live behind an Arc
    (router clone = refcount bump instead of deep-cloning every boxed handler)
    and route layers pre-fold into ready-made services at into_service() —
    dispatch no longer rebuilds N wrappers and clones every handler per request.
  2. Streaming compression (M12): bodies stream through the encoders via
    StreamReader/ReaderStream instead of full-buffering — live TTFB for
    large responses, peak memory drops from two whole-body copies to one chunk.
    Streaming limit enforcement in RequestBodyLimitLayer likewise stops
    buffering (M14).
  3. Rate limiter sharding (M9): 32 independent locks replace the single
    global mutex; the hot path allocates nothing for existing keys.
  4. TCP_NODELAY on by default (M7): removes Nagle × delayed-ACK stalls on
    small responses (opt out with .tcp_nodelay(false)).
  5. Observability tax: /metrics encodes in spawn_blocking (M24);
    OTEL stdout uses batch export instead of sync per-span writes (M22);
    span-field strings skip building when spans are filtered out (L31).
  6. Churn: security headers precompute HeaderValues once (L19), Path
    extraction borrows &str end-to-end with zero String clones (L26),
    percent-decode returns Cow (L3), readiness probes cache results behind
    a configurable TTL (L30).

New shared arvik_core::accept negotiator implements real RFC 9110 q-value
semantics (explicit ;q=0 beats *, multi-line values combine) for both
compression selection and precompressed assets (L16, L21) — brotli is no
longer served to clients that refused it.


Intentional behavior changes (flagging for reviewers)

Change Before After
Body size Unlimited buffering 2 MiB default → 413; overridable
Unknown methods Ran first handler 405 Method Not Allowed
nest() middleware Silently dropped Preserved; sub-fallback panics
Rate limit window Ignored (per-second) Honored (capacity/window)
Rate-limit keys Spoofable XFF first Socket addr; proxies opt-in
CORS creds+mirror Allowed (dangerous) Panics at construction
TCP_NODELAY OS default (off) On by default
fs ETags Weak (W/"…") Strong (nginx-style mtime+len)
Compression Full buffer, CL kept Streamed, CL removed
Logs/traces Full URI incl. query Path only

Verification

Every fix landed with regression tests where feasible (~90 added). Final state,
run locally with CI's exact flags:

cargo check --workspace --all-targets                    ✅
cargo check --workspace --all-features                   ✅
cargo clippy --workspace --all-features -- -D warnings   ✅ 0 warnings
cargo test  --workspace --all-features                   ✅ 171 passed / 0 failed
cargo fmt --all -- --check                               ✅
cargo doc  --workspace --all-features --no-deps          ✅ 0 warnings

The CI sweep also fixed three issues invisible under default-feature builds:
the native-tls accept loop was missing its graceful-shutdown channel entirely,
one identity conversion clippy flagged under --all-features, and ~84 broken/
ambiguous rustdoc links across facade docs.


Community files

Also adds .github/FUNDING.yml, ISSUE_TEMPLATE/{bug_report,feature_request}.md
(adapted reproduction skeletons + crate lists), PULL_REQUEST_TEMPLATE.md
aligned to these exact CI gates, and release-notes/0.9.5.md.

aarambh-darshan and others added 15 commits August 23, 2026 08:08
Audit findings H2 and L1:

- MethodFilter::from_method mapped unknown methods to an empty bitmask,
  which subset-matches every registered filter, so extension methods
  (PURGE, PROPFIND, ...) executed the first registered handler with 200.
  Unknown methods now get a dedicated EXTENSION bit and fall through to
  405 Method Not Allowed. ANY keeps catch-all semantics.
- HEAD requests on GET-only routes returned 405. MethodRouter::call now
  serves HEAD with the GET handler when no dedicated HEAD handler is
  registered (RFC 9110 section 9.3.2); explicit .head() takes precedence.

Adds regression tests in both crates.

Co-Authored-By: Claude <noreply@anthropic.com>
…M14)

H1 — no request body size limit anywhere:
Bytes, String, Json and Form buffered the entire request body with no
cap, letting one unauthenticated request force an attacker-chosen heap
allocation. New arvik_core::body_limit module provides DefaultBodyLimit
extension, DEFAULT_BODY_LIMIT (2 MiB), BodyLimitError/BodyTooLarge, and
a resolve_limit helper. Body::to_bytes_limited rejects oversized
Content-Length early and caps mid-stream otherwise. All four buffering
extractors now reject over-limit bodies with 413 Payload Too Large
(new JsonRejection/FormRejection::PayloadTooLarge variants).
DefaultBodyLimitLayer raises/disables the limit per route; re-exported
from the facade alongside DEFAULT_BODY_LIMIT.

M14 — RequestBodyLimitLayer buffered every body fully into memory:
replaced the collect-then-check implementation with a LimitedBody
frame filter that passes chunks through untouched and errors with
BodyTooLarge once the limit is crossed. Readers map that marker to 413,
so streaming uploads survive and enforcement happens during the read.

Co-Authored-By: Claude <noreply@anthropic.com>
Router::nest consumed only other.routes, silently discarding the
sub-router's .layer()/.route_layer() middleware — auth, CORS or
rate-limiting attached inside a nested router never ran, a silent
security misconfiguration. merge() had the same hole for layers.

- nest/merge now fold sub-router layers onto each flattened route's
  method-router stack (handler <- method layers <- route_layer <-
  layer), preserving the documented ordering.
- Documented flattening caveat: sub-router layers run on matched routes
  only; unmatched prefix paths fall through to the outer fallback.
- nest now panics with guidance when the sub-router has its own
  .fallback() instead of silently dropping it (flattening cannot scope
  a fallback under a prefix).
- New MethodRouter::extend_layers pub(crate) accessor; regression tests
  cover nest (layer + route_layer ordering), no leakage to parent
  routes/unmatched paths, fallback rejection, and merge preservation.

Co-Authored-By: Claude <noreply@anthropic.com>
Audit findings H4, H5, L7.

H4 — one transient accept error killed the whole server: all three
accept loops propagated errors with `?`, so EMFILE/ENFILE (fd
exhaustion under load) or ECONNABORTED permanently stopped listening,
and an unsupported tcp_keepalive_retries setting killed the server on
the first connection. Errors are now classified in handle_accept_result:
transient failures are logged and skipped, resource exhaustion backs
off 100ms before continuing, only genuinely broken listeners (EBADF/
EINVAL/EACCES) remain fatal. apply_stream_options failures drop the
single connection instead of the server; accept workers pace persistent
failures instead of spinning.

H5 — TLS handshakes had no timeout, so slow-loris clients pinned a task
and socket forever. rustls and native-tls accepts are wrapped in
tokio::time::timeout driven by ServerConfig::handshake_timeout (default
10s; None disables).

L7 — no user-configurable HTTP/1 hardening surface: added
http1_header_read_timeout and http1_max_buf_size knobs wired into the
auto builder.

Co-Authored-By: Claude <noreply@anthropic.com>
H6 — spoofable keys: extract_ip trusted client-controlled
X-Forwarded-For/X-Real-IP before the socket address with no trust
config, so any client minted fresh buckets per request (and each new
key grew the shared map). Clients are now keyed by socket address by
default; proxy headers are honored only via
RateLimitLayer::trust_proxies, applying the rightmost-untrusted-hop
rule, requiring well-formed IPs (garbage falls back to the peer), and
bounding custom-header key length at 128 bytes.

M8 — window silently ignored: Bucket::new hardcoded refill_rate =
capacity/second, so new(100, 60s) admitted 6000/min. refill_rate is
now capacity/window; the constructor panics on zero windows.

M9 — one global Mutex<HashMap>: every request serialized behind a
single lock and buckets were never evicted. State is now sharded into
32 independently locked maps (hot path allocates nothing for existing
keys) with an amortized sweep evicting buckets idle >5 minutes.

Tests: spoofed-header rejection, untrusted-peer rejection,
rightmost-untrusted-hop resolution, malformed-XFF fallback, key
bounding, refill-rate math, window-driven Retry-After, idle eviction,
concurrent multi-shard throughput.

Co-Authored-By: Claude <noreply@anthropic.com>
…H7, M13)

H7 — MapRequestBodyLayer/MapResponseBodyLayer replaced the body but
reused the original parts untouched, so any size-changing transform
(including the documented append-footer example) left a stale
Content-Length: hyper framed responses by the old length producing
truncated bodies/connection errors, and request-side length checks saw
a value that no longer described the body. Both layers now refresh
Content-Length to the transformed size whenever the header is present.

M13 — DecompressionLayer removed Content-Encoding but kept the
compressed-size Content-Length (~N times smaller than the real body),
defeating downstream size checks. The decompressed length is now set.

Regression tests cover both transform directions (docs' own
append-footer example) and gzip request decompression.

Co-Authored-By: Claude <noreply@anthropic.com>
Audit findings M2, M15, M16, L22.

M2 — router percent-decoding handed handlers traversal-ready strings:
%2F decoded to / and %2E%2E to .. inside captured params after
matching, so encoded dot-segment paths reached filesystem consumers.
percent_decode now keeps structure-changing escapes encoded (%2F, %5C)
and control characters escaped (%00, %0A, %0D); literal URL separators
and ordinary decodes are unaffected.

M15 — ServeDir/ServeFile built Content-Length/ETag/Last-Modified from a
metadata() stat taken before a separate File::open: a deploy swapping
the file in between produced mismatched headers, truncated bodies, or
poisoned caches. The file is now opened first and metadata is read off
the open descriptor, so validators always describe the streamed bytes.

M16 — If-Range was ignored entirely; resumable clients could stitch
bytes from two file versions into corrupt files. Ranges are now gated
by strong-comparison If-Range validation (ETag exact match or exact
Last-Modified date); stale validators downgrade to a full 200.
fs_etag switched from weak (W/) to strong (nginx-style mtime+len) so
If-Range can function at all.

L22 — symlinks were followed without containment, allowing reads
outside the root when a link could be planted inside it. ServeDir now
canonicalizes both sides and rejects requests resolving outside the
root by default; opt out with .follow_symlinks(true) for trusted trees.

Tests: decode preservation, weak-etag If-Range refusal, matching vs
stale If-Range (206 vs 200), symlink escape blocked by default and
allowed on opt-in.

Co-Authored-By: Claude <noreply@anthropic.com>
Audit findings M5, M6, M20, M21, L12, L23.

M5 — graceful shutdown never signalled connections: hyper kept serving
NEW requests on established keep-alive connections for the entire drain
window and then hard-aborted in-flight work. Each connection task now
holds a watch::Receiver; on shutdown the accept loops signal it and the
connection calls hyper's graceful_shutdown() — idle connections close
immediately, in-flight responses finish. The drain timeout remains as a
last-resort abort. Integration test proves an idle keep-alive
connection now closes in ~0s instead of holding a 30s drain.

M6/M20 — TLS and config hot-reload watched file inodes: temp-file+
rename deploys and certbot symlink rotation silently orphaned the
watches forever. Both now watch parent directories (deduplicated),
filter to create/modify/remove events, resolve by path at reload time,
and forward watcher errors arriving during the debounce window instead
of swallowing them.

M21 — OtelConfig::install() published the global tracer provider before
trying to install its subscriber; on conflict with ArvikLogger::init it
returned Err leaving the provider's exporter task running forever.
Subscriber installation now happens first; on failure the provider is
shut down and the global reference reset before returning. Added
ArvikLogger::init_with_otel() composing log + OTel layers into ONE
subscriber so both features can be used together.

L12 — catch_panic called JoinError::into_panic() without is_panic(),
re-panicking on task cancellation during shutdown; cancellations are
now handled as normal termination.

L23 — unsupported Sec-WebSocket-Version returned 400 JSON; RFC 6455
requires 426 Upgrade Required with Sec-WebSocket-Version: 13.

Co-Authored-By: Claude <noreply@anthropic.com>
M10/L14 — Bearer tokens, Basic passwords, and CSRF double-submit tokens
were compared with short-circuiting string equality, leaking matched
prefix length byte-by-byte through response timing. All three now use a
constant-time XOR-accumulate comparison (secret_eq).

M11 — very_permissive() mirrored any origin together with
Access-Control-Allow-Credentials: true, making every site a trusted
credentialed origin — CORS fully disabled while looking merely
convenient. very_permissive() no longer enables credentials, and the
credentials + wildcard/mirrored-origins combination now panics at
construction with actionable guidance instead of silently shipping.
allow_credentials(true) also no longer auto-switches wildcard origins
to mirroring (that switch was the vulnerability).

L15 — cacheable preflight responses carried only Vary: Origin even when
methods/headers were reflected per request; a CDN could serve one
page's preflight to another and have browsers block legitimate calls.
Preflights now emit Vary: Origin, Access-Control-Request-Method,
Access-Control-Request-Headers.

Tests: secret_eq semantics, bearer strategy behavior, construction-time
panic for both dangerous combos, explicit origins + credentials
accepted, preflight Vary completeness.

Co-Authored-By: Claude <noreply@anthropic.com>
Audit findings M3, M7, M12, M22, M24, L13, L19, L26, L31.

M3 — router dispatch rebuilt middleware stacks per request: every
routed request deep-cloned the whole MethodRouter (clone_box per
handler), allocated a base service, and re-folded every route layer.
MethodRouter handlers now live behind an Arc (clone = refcount bump;
builders use copy-on-write), and into_service() pre-folds route_layers
into ready-made services so dispatch clones one stack. BoxCloneService
now requires Send+Sync, matching axum's service bounds.

M7 — TCP_NODELAY defaults to enabled; Nagle x delayed-ACK stalled small
responses by tens of ms (.tcp_nodelay(false) to opt out).

M12 — response compression streamed through async_compression encoders
via StreamReader/ReaderStream instead of buffering the entire body:
TTFB stays live for large responses and peak memory drops from two
whole-body copies to one chunk. HEAD skips compression; Content-Length
is removed for streamed output.

M22 — OTEL stdout exporter uses batch processing instead of the simple
processor that serialized + wrote JSON inline on request workers.

M24 — /metrics gathers and encodes inside spawn_blocking instead of
stalling tokio workers during scrapes.

L13 — catch_panic wraps the handler future in catch_unwind directly,
removing a per-request task spawn.

L19 — security headers precompute HeaderValues at layer construction;
no config clone or HSTS format! per request.

L26 — Path extraction borrows params (&str) through the deserializer;
no per-key/per-value String clones or per-field copies.

L31 — logging/tracing middlewares skip span-field string building and
propagator extraction when INFO spans are filtered out anyway.

Co-Authored-By: Claude <noreply@anthropic.com>
Audit findings M1, M17, M18, M19, L4, L5, L6, L10; M4 partial; L9 verified N/A.

M1/L4 — ResponseBuilder::json no longer .expect()s on serialization
failure (returns 500 like Json<T>), and body() surfaces deferred
builder errors — e.g. a user-controlled Redirect target containing
control characters — as a plain 500 instead of panicking the
connection task.

L5 — invalid header names/values in tuple responses and AppendHeaders
are now logged loudly instead of silently dropped (a vanished CSP or
Set-Cookie previously produced no signal at all).

L6 — From<String>/From<&'static str> for Error keep messages internal;
with_message() remains the explicit opt-in for client-visible text,
matching the crate's leak guarantee.

M17 — SSE Event::id truncates at the first CR/LF in addition to
stripping NUL, closing the framing-injection vector that silently
dropped event payloads.

M18 — typos in ARVIK_* env vars inside known config sections now fail
the build naming the variable instead of silently using file/default
values; foreign variables sharing the prefix are still ignored.

M19 — HTTP/2 window sizes above the h2 protocol maximum ((1<<31)-1)
are rejected at validation time instead of panicking inside h2 on the
first HTTP/2 connection.

L10 — RustlsConfig::with_alpn_http2_only() restricts the ALPN offer to
h2 for http2_only deployments that previously invited http/1.1
negotiation they could not honor.
L9 — verified not applicable on native-tls 0.2.18: accept_alpn is
infallible there.

M4 (partial) — connections rejected at max_connections now receive a
best-effort 503 Service Unavailable + Connection: close before the
socket is dropped, instead of a bare TCP reset load balancers read as
a dead node.

Co-Authored-By: Claude <noreply@anthropic.com>
Audit findings H8, M23, L2-L5, L8, L11(doc), L16-L18, L20, L21,
L24, L25, L27-L30, L32-L34.

M23 — logs and traces record uri.path() only; query strings routinely
carry reset tokens and API keys.

H8 — TlsSection has a manual Debug that renders pkcs12_password as
***REDACTED***; any {:?} of the config is now safe.

L2 — path params take their names from matchit's matched keys instead
of positional pattern parsing that misaligned mid-segment params
(/images/img{id}.png/{tag} exposed wrong names). Regression test added;
the param_names plumbing is gone entirely.

L3 — percent_decode returns Cow (no alloc on the no-% fast path);
nest_service prefix comparison no longer formats a String per request.

L8 — on_connected/on_disconnected hooks spawn fire-and-forget so a
blocking hook cannot stall the accept loop.

L11 — CatchPanic's doc scope now states plainly that body-streaming
panics occur outside its wrapper (handler panics remain covered).

L16/L21 — new arvik_core::accept module parses real RFC 9110 q-values:
explicit ;q=0 refusals beat the * wildcard, multi-line header values
combine, priority order preserved. Compression middleware and static
precompressed selection both use it — brotli is no longer served to
clients that refused it with br;q=0.

L17 — DefaultMakeSpan::include_headers(true) actually records request
headers into the span field http.request_headers.
L18 — trace middleware instruments the handler future instead of
holding span.enter() across await (thread-local stack corruption).

L20 — unknown Range units and multi-range requests serve a full 200 as
RFC 9110 requires, not 416.
L24 — Form validates Content-Type via mime parse (…urlencodedfoo no
longer passes).
L25 — MissingPathParams reports the server-side wiring mistake as 500,
matching sibling rejections.
L27 — env parsing uses vars_os, skipping non-UTF-8 variables instead of
panicking on unrelated process state.
L28 — client-supplied x-request-id must be printable ASCII <= 128 bytes
or it is replaced with a fresh UUID.

L29 — failed health checks log the underlying error and return a
generic "check failed" in the public /health/ready body.
L30 — readiness probes cache results for a configurable TTL (default
1s) so kubelet/LB bursts cost one dependency round-trip.

L32 — proc macros expand FoundCrate::Itself to `crate` per
proc-macro-crate docs.
L33 — the test cookie jar honors Max-Age<=0/past Expires deletions and
merges with explicit Cookie headers instead of clobbering them, so
logout assertions behave like real browsers.
L34 — workspace is clippy-clean: items-after-test-module fixed and the
test module moved to end of file.

Co-Authored-By: Claude <noreply@anthropic.com>
Local run of all six ci.yml commands surfaced issues invisible under
default-feature builds:

- accept_native_tls never created or sent its connection-shutdown watch
  channel: graceful shutdown (M5 fix) silently did not apply to the
  native-tls path, and the loop failed to compile once referenced.
- clippy --all-features flagged an identity conversion in ArvikLogger
  init (try_init already returns the LoggerError box type).
- cargo doc emitted 75 broken intra-doc links and 9 function/module
  ambiguity warnings across facade docs, middleware tables, router
  service docs, ws split/message, static, and body docs. All resolved:
  unresolvable references demoted to plain code spans, ambiguous names
  disambiguated with () URLs.

Verified end to end with RUSTFLAGS=-D warnings:
check (default + all features), clippy --all-features -D warnings,
test --all-features (171 passed / 0 failed), fmt --check,
doc --all-features --no-deps (0 warnings).

Co-Authored-By: Claude <noreply@anthropic.com>
Mirrors the .github structure from our other projects:

- ISSUE_TEMPLATE/bug_report.md — minimal arvik server reproduction
  skeleton, environment table with feature flags
- ISSUE_TEMPLATE/feature_request.md — use-case-first format with
  affected-crate list matching the workspace
- PULL_REQUEST_TEMPLATE.md — change-type checklist aligned with the
  actual CI gates (fmt, clippy --all-features -D warnings,
  test --all-features, doc)
- FUNDING.yml — sponsor button configuration
- release-notes/0.9.5.md — per-version release notes for the
  performance sprint, following the established format

Co-Authored-By: Claude <noreply@anthropic.com>
CI's stable toolchain (rustc/clippy 1.98) enables clippy::result_large_err
by default and flags the nine arvik-ws socket functions that return
tungstenite::Error by value (~200-byte enum).

Boxing the error would break the public API contract and allocate on every
send/recv call; the large-variant cases are rare runtime failures. Adds a
documented crate-level allow instead.

Verified on rustc 1.98.0 (same as CI): cargo clippy --workspace
--all-features -- -D warnings passes with zero errors.

Co-Authored-By: Claude <noreply@anthropic.com>
@aarambh-darshan
aarambh-darshan merged commit b9dc5ef into main Aug 23, 2026
10 checks passed
@aarambh-darshan
aarambh-darshan deleted the fix/audit-findings branch August 23, 2026 11:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant