#multimedia-streaming #pure-rust #multimedia #io #streaming-io

oxideav-http

HTTP/HTTPS source driver for oxideav (pure-Rust via ureq + rustls + webpki-roots)

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

MIT license

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-Range header MUST be present.
  • Range unit MUST be bytes (case-insensitive).
  • first-byte-pos MUST 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 the Content-Length observed 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 */N unsatisfied-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 Range and shipped the full body per RFC 7233 §3.1), the GET's Content-Length — when present — MUST equal the Content-Length observed 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 by Content-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 ETag is taken as-is when it lacks the W/ weakness prefix (§8.8.3 — weak entity-tags are MUST-NOT for If-Range per §13.1.5).
  • Failing that, Last-Modified is taken only when the companion Date header 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 Range GETs (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