Skip to content

iOS Safari cannot play video/audio from arweave.net/{tx_id} — missing HTTP 206/Accept-Ranges #878

Description

@AlexTheWizardL

Symptom

Inline <video> and <audio> elements pointing at https://arweave.net/{tx_id} fail to play on iPhone and iPad (all iOS/iPadOS versions, Safari and every other iOS browser, which all use WebKit). Desktop Chrome/Firefox/Safari and Android Chrome play the same URLs without issue.

Production impact

Any site embedding Arweave-hosted media via the canonical arweave.net/{id} URL is broken for iOS traffic network-wide. Concrete example: ArchiTangle Book+ has printed physical books carrying QR codes that resolve to arweave.net/{tx_id} video clips. Those books are already in readers' hands and cannot be reprinted.

Evidence

$ curl -sIL -H 'Range: bytes=0-1' \
    'https://arweave.net/17MIDCT7HcYSX2C_ybksF8AbI05IBqu4C94oFz_hwEc'
HTTP/2 200                    # should be 206
# no Accept-Ranges, no Content-Range, full 99,844,286-byte body streamed

$ curl -sIL -H 'Range: bytes=0-1' \
    'https://arweave.net/raw/17MIDCT7HcYSX2C_ybksF8AbI05IBqu4C94oFz_hwEc'
HTTP/2 206
accept-ranges: bytes
content-range: bytes 0-1/99844286

iOS network trace shows the request carries X-Playback-Session-Id (WebKit's media-player probe). The response is 200 without Accept-Ranges, and the console logs Failed to load resource: Plug-in handled load — WebKit's signal that it refused to hand the resource to the media pipeline.

Root cause

dev_arweave:get_tx/3 serves GET /{tx_id} and returns the data body with status 200 and no range headers. dev_arweave:get_raw/3 serves GET /raw/{tx_id} and correctly emits Accept-Ranges: bytes plus 206 Partial Content on range requests.

Codec signature proof

The two paths are signed by different codecs. The tx@1.0 response codec used by get_tx/3 signs a field list that does not include accept-ranges, content-length, or content-range — so even if the headers were added downstream, they would invalidate the signature and be stripped. The /raw codec signs over exactly those fields, which is why only /raw can legitimately return 206 today. A fix must route through the /raw codec, not patch headers onto the tx codec.

Why iOS requires 206

Apple mandates byte-range support for media elements on iOS: "your server must support byte-range requests". WebKit enforces this in WebKitWebSourceGStreamer.cpp L1089-L1098: a 200 response to a range request is treated as "server does not support seeking," and the media pipeline refuses the resource. Desktop browsers tolerate the same response; iOS does not. RFC 7233 §4.1 specifies 206 as the correct response.

Proposed fix

Route the data-embedded branch of get_tx/3 through the already-working get_raw/3 pipeline:

get_tx(Base, Request, Opts) ->
    case find_key(<<"tx">>, Base, Request, Opts) of
        not_found -> {error, not_found};
        TXID ->
            ExcludeData = hb_util:bool(
                find_key(<<"exclude-data">>, Base, Request, Opts)),
            case ExcludeData of
                true ->
                    request(<<"GET">>, <<"/tx/", TXID/binary>>,
                            Opts#{ exclude_data => true });
                false ->
                    %% Delegate to the range-aware /raw pipeline so
                    %% Accept-Ranges/206/Content-Range are emitted.
                    get_raw(#{ <<"raw">> => TXID }, Request, Opts)
            end
    end.

Happy to open a PR.

Regression window

arweave.net/{id} served 206 correctly in early April 2026 and began returning 200 mid-April, coinciding with Forward Research's migration of arweave.net to HyperBEAM (context: ar-io/ar-io-node#619). /raw/{id} is unaffected.

Environment

HyperBEAM edge @ 3c6a617869967d5356a59ce342f12c933a58691b (2026-04-21). arweave.net production (Server: CDN77-Turbo fronting HyperBEAM). iOS 18.7 Safari + iOS 18.7 Chrome (both WebKit) — broken. macOS Safari + Chrome and Android 14 Chrome — work.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions