Skip to content

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

@AlexTheWizardL

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions