5 releases
| 0.0.7 | May 31, 2026 |
|---|---|
| 0.0.6 | May 6, 2026 |
| 0.0.5 | May 3, 2026 |
| 0.0.4 | Apr 25, 2026 |
| 0.0.3 | Apr 19, 2026 |
#1090 in Network programming
36 downloads per month
86KB
1.5K
SLoC
oxideav-http
HTTP/HTTPS source driver for oxideav (pure-Rust via ureq + rustls + webpki-roots).
Registers as a BytesSource on the new typed SourceRegistry, so
reg.open(uri) yields SourceOutput::Bytes(_) ready for any container
demuxer.
Part of the oxideav framework — a pure-Rust media transcoding and streaming stack. Codec, container, and filter crates are implemented from the spec (no C codec libraries linked or wrapped, no *-sys crates). Optional hardware-engine crates (oxideav-videotoolbox / -audiotoolbox / -vaapi / -vdpau / -nvidia / -vulkan-video) bridge to OS APIs via runtime libloading; pass --no-hwaccel (or omit the hwaccel feature) to opt out.
Usage
[dependencies]
oxideav-http = "0.0"
let mut ctx = oxideav_core::RuntimeContext::new();
ctx.sources = oxideav_source::with_defaults();
oxideav_http::register(&mut ctx); // installs http:// + https://
let _r = ctx.sources.open("https://example.com/clip.mp4")?;
Configuring the agent
The default agent uses ureq defaults. To tighten policy (cap redirects,
strip Authorization on cross-host redirects, require https, set a
custom User-Agent, bound connect/global timeouts) build an
HttpConfig and either install it process-wide or scope it to one
source:
use std::time::Duration;
use oxideav_http::{HttpConfig, RedirectAuthPolicy, HttpSource, install_default_config};
let cfg = HttpConfig::builder()
.max_redirects(5)
.redirect_auth_policy(RedirectAuthPolicy::SameHost)
.user_agent("my-app/1.0")
.https_only(true)
.timeout_connect(Some(Duration::from_secs(5)))
.timeout_global(Some(Duration::from_secs(60)))
.build();
// (A) install once at startup so every registry-dispatched open()
// uses these settings:
install_default_config(cfg.clone()).ok();
// (B) or scope per-call without touching the global agent:
let _src = HttpSource::open_with_config("https://example.com/clip.mp4", &cfg)?;
install_default_config is one-shot — it returns ConfigAlreadyInstalled
once the process-wide agent has materialised. Call it before the first
ctx.sources.open(...) if you need it to take effect on
registry-dispatched opens.
Range-response validation
Every 206 (Partial Content) response is validated against RFC 7233 §4.2 before any byte is exposed to the reader:
Content-Rangeheader MUST be present.- Range unit MUST be
bytes(case-insensitive). first-byte-posMUST equal the byte position we asked for — a cache / CDN that slides the start would otherwise silently misalign every subsequent demuxer read.last-byte-pos >= first-byte-pos.complete-length, when concrete, MUST equal theContent-Lengthobserved at HEAD construction — a mid-stream resource resize is a fatal origin/cache disagreement.last-byte-pos < complete-length.*complete-length is accepted (§4.2 explicitly permits it when the server doesn't know the total).bytes */Nunsatisfied-range payloads are rejected on a 206 (they are a 416 payload, never a 206 payload).
If a server ignores the Range header and responds with 200 OK
plus the full body (§3.1 permits this), the prefix [0, self.pos)
is drained in 8 KiB chunks before bytes reach the reader, so the
demuxer's file-offset view stays consistent.
416 Range Not Satisfiable
A 416 response is treated as a distinct error path per RFC 9110
§15.5.17. When the server includes the Content-Range: bytes */<complete-length> body that §14.4 SHOULDs for 416 responses,
the parser extracts the server's authoritative resource length and
the resulting io::Error surfaces BOTH the server's reported length
AND the length observed at HEAD construction. That lets a caller
tell "I asked past EOF" apart from "the resource shrank between the
HEAD and the GET" — the latter is a cache/origin disagreement worth
reporting upstream.
If the 416 omits the SHOULD'd Content-Range, the error still names the status cleanly. If the 416 carries a malformed Content-Range, the parse error surfaces rather than a fabricated length.
RFC 9110 §8.6 Content-Length sanity
Beyond the §13.1.5 strong-validator path (below), the driver
cross-checks the GET-time Content-Length against §8.6's invariants:
- On a 200-fallback (server ignored
Rangeand shipped the full body per RFC 7233 §3.1), the GET'sContent-Length— when present — MUST equal theContent-Lengthobserved at HEAD. §8.6 says "a server MUST NOT send Content-Length in [a HEAD] response unless its field value equals the decimal number of octets that would have been sent in the content of a response if the same request had used the GET method." A different value is a mid-stream resource resize disguised as a soft-fallback; surfacing it stops the demuxer from draining a now-wrong-sized prefix and reading short. - On a 206, the GET's
Content-Length(when present) MUST equal the byte span implied byContent-Range: bytes <first>-<last>/N(i.e.last - first + 1). A mismatch is either a server bug or a multipart/byteranges body (which we never request); either way it would let the reader drift past the satisfied range silently. - Both checks are skipped silently when the GET reply omits
Content-Length(§8.6 makes it a SHOULD, not a MUST, outside specific cases).
Mid-stream mutation detection
The driver implements RFC 9110 §13.1.5 If-Range to catch the case
where a CDN, cache, or origin replaces the resource between the
opening HEAD and a later Range GET. At HEAD we capture a
strong validator:
- An
ETagis taken as-is when it lacks theW/weakness prefix (§8.8.3 — weak entity-tags are MUST-NOT forIf-Rangeper §13.1.5). - Failing that,
Last-Modifiedis taken only when the companionDateheader is at least one second after it (§8.8.2.2's promotion rule from "implicitly weak" to "strong"). - Otherwise no validator is captured and the read path issues
plain
RangeGETs (matching pre-r186 behaviour).
Every range GET that has a captured validator carries
If-Range: <wire-form>. Per §13.1.5 the server then either
satisfies the range normally (206 Partial Content) or responds
with a full 200 OK for the new representation. The latter is
treated as a fatal io::Error naming "If-Range validator did not
match — representation changed since HEAD" so a downstream demuxer
never silently re-anchors against a different resource. When no
If-Range was sent (no strong validator at HEAD), the §3.1
prefix-drain fallback still applies unchanged.
Fuzzing
fuzz/ carries a cargo-fuzz harness (parse_headers) that drives
every internal response-header parser used by the source driver —
parse_byte_content_range (RFC 7233 §4.2 / RFC 9110 §14.4),
parse_byte_unsatisfied_range (§14.4), parse_entity_tag (§8.8.3),
parse_imf_fixdate (§5.6.7), and the composite
derive_strong_validator (§13.1.5 + §8.8.2.2 + §8.8.3). The harness
reaches the parsers through a #[doc(hidden)] pub mod __fuzz
re-export gated on the fuzz cargo feature, so the stable public
surface is unchanged when the crate is consumed normally.
cargo +nightly fuzz run --fuzz-dir fuzz parse_headers
A small seed corpus lives under fuzz/corpus/parse_headers/.
License
MIT — see LICENSE.
Dependencies
~9–21MB
~338K SLoC