Skip to content

avformat/http: close-delimited (unknown-length) responses end with AVERROR(EIO) "Stream ends prematurely" instead of EOF #45

Description

@ronag

Summary

For HTTP responses that carry neither Content-Length nor Transfer-Encoding: chunked — i.e. bodies delimited by connection close (HTTP/1.0-style responses, CGI gateways, ICY/Shoutcast radio streams) — s->filesize stays at its "unknown" sentinel UINT64_MAX, so target_end in http_buf_read() is UINT64_MAX. The premature-end check then treats every normal connection close as a premature termination: it logs Stream ends prematurely at N, should be 18446744073709551615 and returns AVERROR(EIO) instead of AVERROR_EOF. The original upstream commit 3668701 guarded this check with s->filesize >= 0 (signed, -1 = length unknown); the signed→unsigned conversion in 2a05c8f dropped the equivalent target_end >= 0 term, silently changing the semantics so the check fires whenever the length is unknown.

Location

  • http_buf_read() — premature-end check:

    FFmpeg/libavformat/http.c

    Lines 1824 to 1838 in 9a83bff

    uint64_t file_end = s->end_off ? s->end_off : s->filesize;
    uint64_t target_end = s->range_end ? s->range_end : file_end;
    if ((!s->willclose || s->chunksize == UINT64_MAX) && s->off >= file_end)
    return AVERROR_EOF;
    if (s->off == target_end && target_end < file_end)
    return AVERROR(EAGAIN); /* reached end of content range */
    len = ffurl_read(s->hd, buf, size);
    if ((!len || len == AVERROR_EOF) &&
    (!s->willclose || s->chunksize == UINT64_MAX) && s->off < target_end) {
    av_log(h, AV_LOG_ERROR,
    "Stream ends prematurely at %"PRIu64", should be %"PRIu64"\n",
    s->off, target_end
    );
    return AVERROR(EIO);
    }
  • http_read_stream()is_premature computed against filesize == UINT64_MAX:

    FFmpeg/libavformat/http.c

    Lines 1908 to 1937 in 9a83bff

    retry:
    read_ret = http_buf_read(h, buf, size);
    while (read_ret < 0) {
    uint64_t target = h->is_streamed ? 0 : s->off;
    bool is_premature = s->filesize > 0 && s->off < s->filesize;
    if (read_ret == AVERROR_EXIT)
    break;
    else if (read_ret == AVERROR(EAGAIN)) {
    /* send new request for more data on existing connection */
    AVDictionary *options = NULL;
    if (s->willclose)
    ffurl_closep(&s->hd);
    s->initial_requests = 0; /* continue streaming uninterrupted from now on */
    read_ret = http_open_cnx(h, &options);
    av_dict_free(&options);
    if (read_ret == 0)
    goto retry;
    }
    if (h->is_streamed && !s->reconnect_streamed)
    break;
    if (!(s->reconnect && is_premature) &&
    !(s->reconnect_at_eof && read_ret == AVERROR_EOF)) {
    if (is_premature)
    return AVERROR(EIO);
    else
    break;
    }

Local checkout: /Users/ronagy/GitHub/nxtedition/ffmpeg/libavformat/http.c

Details

    } else {
        uint64_t file_end   = s->end_off   ? s->end_off   : s->filesize;
        uint64_t target_end = s->range_end ? s->range_end : file_end;
        if ((!s->willclose || s->chunksize == UINT64_MAX) && s->off >= file_end)
            return AVERROR_EOF;
        if (s->off == target_end && target_end < file_end)
            return AVERROR(EAGAIN); /* reached end of content range */
        len = ffurl_read(s->hd, buf, size);
        if ((!len || len == AVERROR_EOF) &&
            (!s->willclose || s->chunksize == UINT64_MAX) && s->off < target_end) {
            av_log(h, AV_LOG_ERROR,
                   "Stream ends prematurely at %"PRIu64", should be %"PRIu64"\n",
                   s->off, target_end
                  );
            return AVERROR(EIO);
        }
    }

(libavformat/http.c lines 1824–1838)

Step-by-step trace:

  1. Server responds e.g. HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n<body> — no Content-Length, no Transfer-Encoding. In process_line(), Connection: close sets s->willclose = 1 (lines 1331–1333). s->filesize keeps the UINT64_MAX it was initialized to in http_connect() (line 1725), and s->chunksize keeps the not-chunked sentinel UINT64_MAX (line 1483).
  2. In the http_buf_read() else-branch (lines 1824–1825): file_end = s->end_off ? s->end_off : s->filesize = UINT64_MAX; target_end = s->range_end ? s->range_end : file_end = UINT64_MAX (no Content-Range, so range_end is 0).
  3. When the server finishes sending the body and closes the connection, tcp_read() returns AVERROR_EOF; retry_transfer_wrapper() (libavformat/avio.c lines 541–542) returns AVERROR_EOF for a zero-length read, so len = ffurl_read(...) at line 1830 is AVERROR_EOF.
  4. The condition at lines 1831–1832 evaluates fully true: (!len || len == AVERROR_EOF) → true; (!s->willclose || s->chunksize == UINT64_MAX) → true even though willclose == 1, because chunksize == UINT64_MAX (not chunked); s->off < target_endoff < UINT64_MAX → always true when the length is unknown.
  5. Lines 1833–1837 log Stream ends prematurely at N, should be 18446744073709551615 and return AVERROR(EIO).
  6. In http_read_stream(), is_premature = s->filesize > 0 && s->off < s->filesize (line 1912) is also true (UINT64_MAX > 0), so the clean end of stream either returns AVERROR(EIO) to the caller (line 1934) or — with -reconnect_streamed/-reconnect on a seekable context — enters a pointless reconnect/backoff loop until the retry limits are exhausted (lines 1939–1958).

Git archaeology: the check was introduced by upstream 3668701 ("avformat/http: Return an error in case of prematurely ending data", 2015) as !len && (!s->willclose || s->chunksize < 0) && s->filesize >= 0 && s->off < s->filesize — the s->filesize >= 0 term meant "expected length is known" (signed, -1 = unknown). The signed→unsigned conversion 2a05c8f ("http: make length/offset-related variables unsigned.", 2016) rewrote target_end >= 0 && s->off < target_end as just s->off < target_end, dropping the "length known" precondition. With unsigned target_end == UINT64_MAX, s->off < target_end is effectively always true.

Impact

Severity: medium.

  • Every close-delimited HTTP resource (HTTP/1.0 servers, CGI/streaming gateways without Content-Length, ICY/Shoutcast audio streams) ends with a spurious AVERROR(EIO) error and a misleading Stream ends prematurely ... should be 18446744073709551615 log line instead of a clean EOF. Downstream demuxer/player code sees an I/O error at what is actually a normal end of stream.
  • With reconnect options enabled, the false "premature" classification triggers needless reconnect attempts with exponential backoff against a stream that has legitimately ended, delaying shutdown and hammering the origin.

Suggested fix

Restore the "expected length is known" precondition that 2a05c8f dropped — an unknown length means a connection close is a normal EOF:

--- a/libavformat/http.c
+++ b/libavformat/http.c
@@ static int http_buf_read(URLContext *h, uint8_t *buf, int size)
         len = ffurl_read(s->hd, buf, size);
-        if ((!len || len == AVERROR_EOF) &&
-            (!s->willclose || s->chunksize == UINT64_MAX) && s->off < target_end) {
+        if ((!len || len == AVERROR_EOF) && target_end != UINT64_MAX &&
+            (!s->willclose || s->chunksize == UINT64_MAX) && s->off < target_end) {
             av_log(h, AV_LOG_ERROR,
                    "Stream ends prematurely at %"PRIu64", should be %"PRIu64"\n",
                    s->off, target_end
                   );
             return AVERROR(EIO);
         }

Complementary fix in http_read_stream() so the unknown-length case is not classified as premature there either (otherwise line 1934 still converts the propagated AVERROR_EOF into AVERROR(EIO) on non-streamed contexts, and the reconnect loop still triggers):

@@ static int http_read_stream(URLContext *h, uint8_t *buf, int size)
-        bool is_premature = s->filesize > 0 && s->off < s->filesize;
+        bool is_premature = s->filesize > 0 && s->filesize != UINT64_MAX &&
+                            s->off < s->filesize;

This matches the pre-2a05c8f813 semantics (unknown length ⇒ connection close is a normal EOF).

Upstream status

The identical condition exists in upstream FFmpeg master (git show upstream/master:libavformat/http.c, lines 1766–1774 — same (!len || len == AVERROR_EOF) && (!s->willclose || s->chunksize == UINT64_MAX) && s->off < target_end check, and the same s->filesize > 0 && s->off < s->filesize premature test in its reconnect logic). This is not a fork regression; the http_buf_read() part of the fix is worth reporting/submitting upstream as well.


Consolidates duplicate findings from the review: avformat/http: premature-end check turns clean EOF of read-until-close responses (no Content-Length) into AVERROR(EIO).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinghttplibavformat/http.cupstreamAlso present in upstream FFmpeg

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions