Summary
For responses with an unknown total size (Content-Range: bytes X-Y/*), the fork-specific parsing introduced in 3e345ae (avformat/http: parse Content-Range: <range>/*) sets s->filesize_unknown, which makes s->filesize = UINT64_MAX while s->range_end = Y+1 is known. When such a resource has been fully read and is not actually growing, http_buf_read() returns AVERROR(EAGAIN) at the range end, http_read_stream() issues a follow-up Range: bytes=<end>- request, and the server answers 416. With reconnect_partial=0 (the default, added in 5319653 / db08bc9) the 416 is a hard error, and because is_premature is computed as s->filesize > 0 && s->off < s->filesize with filesize == UINT64_MAX, the function returns AVERROR(EIO) at the true end of a fully and correctly downloaded resource instead of AVERROR_EOF. With -reconnect 1 it instead retries with backoff until reconnect_delay_total_max and then fails.
Location
parse_content_range() — * handling:
|
static void parse_content_range(URLContext *h, const char *p) |
|
{ |
|
HTTPContext *s = h->priv_data; |
|
const char *slash, *end; |
|
|
|
if (!strncmp(p, "bytes ", 6)) { |
|
p += 6; |
|
s->off = strtoull(p, NULL, 10); |
|
if ((end = strchr(p, '-')) && strlen(end) > 0) |
|
s->range_end = strtoull(end + 1, NULL, 10) + 1; |
|
if ((slash = strchr(p, '/')) && strlen(slash) > 0) { |
|
const char *size = slash + 1; |
|
if (!strcmp(size, "*")) |
|
s->filesize_unknown = 1; |
|
else |
|
s->filesize_from_content_range = strtoull(size, NULL, 10); |
|
} |
|
} |
|
if (s->seekable == -1 && (!s->is_akamai || s->filesize != 2147483647)) |
|
h->is_streamed = 0; /* we _can_ in fact seek */ |
|
} |
http_read_header() — filesize_unknown → s->filesize = UINT64_MAX:
|
// filesize from Content-Range can always be used, even if using chunked Transfer-Encoding |
|
if (s->filesize_from_content_range != UINT64_MAX) { |
|
s->filesize = s->filesize_from_content_range; |
|
s->filesize_unknown = 0; /* the case of a 416 error is already handled above */ |
|
} else if (s->filesize_unknown) |
|
s->filesize = UINT64_MAX; |
http_buf_read() — EAGAIN at end of content range:
|
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 */ |
http_read_stream() — EAGAIN handler and is_premature EIO path:
|
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; |
|
} |
- Supporting:
check_http_code() 416 handling
|
static int check_http_code(URLContext *h, int http_code, const char *end) |
|
{ |
|
HTTPContext *s = h->priv_data; |
|
/* error codes are 4xx and 5xx, but regard 401 and 416 as a success, so we |
|
* don't abort until all headers have been parsed. */ |
|
if (http_code >= 400 && http_code < 600 && |
|
(http_code != 401 || s->auth_state.auth_type != HTTP_AUTH_NONE) && |
|
(http_code != 407 || s->proxy_auth_state.auth_type != HTTP_AUTH_NONE) && |
|
(http_code != 416 || !s->reconnect_partial)) { |
|
end += strspn(end, SPACE_CHARS); |
|
av_log(h, AV_LOG_WARNING, "HTTP error %d %s\n", http_code, end); |
|
return ff_http_averror(http_code, AVERROR(EIO)); |
|
} |
|
return 0; |
|
} |
, http_should_reconnect()
|
switch (err) { |
|
case AVERROR_HTTP_RANGE_NOT_SATISFIABLE: |
|
if (s->reconnect_partial) |
|
return 1; |
|
av_fallthrough; |
|
case AVERROR_HTTP_BAD_REQUEST: |
|
case AVERROR_HTTP_UNAUTHORIZED: |
|
case AVERROR_HTTP_FORBIDDEN: |
|
case AVERROR_HTTP_NOT_FOUND: |
|
case AVERROR_HTTP_TOO_MANY_REQUESTS: |
|
case AVERROR_HTTP_OTHER_4XX: |
|
status_group = "4xx"; |
|
break; |
Details
parse_content_range() (http.c:962-968):
if ((slash = strchr(p, '/')) && strlen(slash) > 0) {
const char *size = slash + 1;
if (!strcmp(size, "*"))
s->filesize_unknown = 1;
else
s->filesize_from_content_range = strtoull(size, NULL, 10);
}
http_read_header() (http.c:1525-1526):
} else if (s->filesize_unknown)
s->filesize = UINT64_MAX;
http_buf_read() (http.c:1824-1829):
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 */
http_read_stream() (http.c:1908-1937):
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;
}
Step-by-step trace (e.g. a static 1000-byte resource whose server reports an unknown total):
- The client sends
Range: bytes=0-; the server replies 206 with Content-Range: bytes 0-999/*. parse_content_range() sets s->range_end = 1000 and, for the * total, s->filesize_unknown = 1 (http.c:960-965). http_read_header() then sets s->filesize = UINT64_MAX (http.c:1525-1526).
- All 1000 bytes are read. On the next
http_buf_read() the buffer is empty; file_end = s->filesize = UINT64_MAX, target_end = s->range_end = 1000, so s->off == target_end && target_end < file_end holds and it returns AVERROR(EAGAIN) (http.c:1828-1829).
- In
http_read_stream(), the EAGAIN handler (http.c:1916-1925) calls http_open_cnx(), which re-requests Range: bytes=1000-. The resource is not growing, so the server replies 416. In check_http_code() (http.c:928-934), (http_code != 416 || !s->reconnect_partial) is true because reconnect_partial defaults to 0 (http.c:223), so the open fails with ff_http_averror(416, ...) = AVERROR_HTTP_RANGE_NOT_SATISFIABLE (http.c:628). http_should_reconnect() falls through the reconnect_partial check to the generic 4xx group (http.c:357-368) and returns 0 with reconnect_on_http_error unset, so http_open_cnx() fails and read_ret = AVERROR_HTTP_RANGE_NOT_SATISFIABLE; goto retry is not taken.
- Execution continues in the same loop iteration with
is_premature already computed at http.c:1912 as s->filesize > 0 && s->off < s->filesize → UINT64_MAX > 0 && 1000 < UINT64_MAX → true. With reconnect=0 and read_ret != AVERROR_EOF, the branch at http.c:1931-1934 takes return AVERROR(EIO);.
The caller therefore receives a hard I/O error at the clean end of a fully downloaded resource. With -reconnect 1 the loop instead sleeps/seeks/re-requests, getting 416 every time, until reconnect_delay_total > reconnect_delay_total_max and then returns AVERROR(EIO) (http.c:1939-1941).
This was introduced by the fork-specific commit 3e345ae ("avformat/http: parse Content-Range: /*", together with 5319653 / db08bc9 which added -reconnect_partial and the 416 error mapping). Before that commit, the * was parsed by strtoull() as filesize 0 — a different bug that made such resources fail at open. The fork's parsing makes the data readable but propagates the wrong terminal status. The commit message of 3e345ae itself notes the flow "until the server stops giving us data (416 Range Not Satisfiable)", but the 416 only terminates cleanly when reconnect_partial is enabled; the default path was not handled.
Impact
Severity: medium.
- Any HTTP source that answers range requests with
Content-Range: .../* (unknown total length) but is in fact complete/static causes ffmpeg/libavformat clients to fail with AVERROR(EIO) ("Input/output error") at end of file instead of a clean EOF. Demuxing of the full content succeeds, but the terminal status is an error, which callers typically surface as a failed transfer/transcode.
- With
-reconnect 1 (common in ingest pipelines), end-of-file turns into a retry storm: repeated Range: bytes=<end>- requests answered with 416, with exponential backoff, blocking for up to reconnect_delay_total_max (default 256 s) before finally failing with EIO.
- No memory safety implications; behavior-only regression relative to a correct EOF, fork-specific.
Suggested fix
Treat a 416 on the follow-up range request as end of data when growth is not expected (reconnect_partial off), and record the now-known size. Note that just setting s->filesize is not enough: is_premature is computed at the top of the loop iteration (http.c:1912), before the EAGAIN handler runs, so it must be reset too, otherwise the stale value still triggers the return AVERROR(EIO) at http.c:1933-1934.
--- a/libavformat/http.c
+++ b/libavformat/http.c
@@ -1922,6 +1922,14 @@ retry:
read_ret = http_open_cnx(h, &options);
av_dict_free(&options);
if (read_ret == 0)
goto retry;
+ if (read_ret == AVERROR_HTTP_RANGE_NOT_SATISFIABLE &&
+ !s->reconnect_partial) {
+ /* The resource ends exactly at the previous range end; a 416
+ * here means there is no more data: report EOF. */
+ s->filesize = s->off;
+ is_premature = false;
+ read_ret = AVERROR_EOF;
+ }
}
if (h->is_streamed && !s->reconnect_streamed)
With this change, when reconnect/reconnect_at_eof are off the loop reaches the break at http.c:1936 and http_read_stream() returns AVERROR_EOF; when -reconnect_at_eof is set, the existing retry-at-EOF logic is honored; and s->filesize = s->off makes any subsequent http_buf_read() return AVERROR_EOF directly via the s->off >= file_end check (http.c:1826-1827). The reconnect_partial=1 case is unaffected, since there the 416 is already handled as a soft error by http_should_reconnect().
Summary
For responses with an unknown total size (
Content-Range: bytes X-Y/*), the fork-specific parsing introduced in 3e345ae (avformat/http: parse Content-Range: <range>/*) setss->filesize_unknown, which makess->filesize = UINT64_MAXwhiles->range_end = Y+1is known. When such a resource has been fully read and is not actually growing,http_buf_read()returnsAVERROR(EAGAIN)at the range end,http_read_stream()issues a follow-upRange: bytes=<end>-request, and the server answers 416. Withreconnect_partial=0(the default, added in 5319653 / db08bc9) the 416 is a hard error, and becauseis_prematureis computed ass->filesize > 0 && s->off < s->filesizewithfilesize == UINT64_MAX, the function returnsAVERROR(EIO)at the true end of a fully and correctly downloaded resource instead ofAVERROR_EOF. With-reconnect 1it instead retries with backoff untilreconnect_delay_total_maxand then fails.Location
parse_content_range()—*handling:FFmpeg/libavformat/http.c
Lines 952 to 972 in 9a83bff
http_read_header()—filesize_unknown→s->filesize = UINT64_MAX:FFmpeg/libavformat/http.c
Lines 1521 to 1526 in 9a83bff
http_buf_read()—EAGAINat end of content range:FFmpeg/libavformat/http.c
Lines 1824 to 1829 in 9a83bff
http_read_stream()—EAGAINhandler andis_prematureEIO path:FFmpeg/libavformat/http.c
Lines 1908 to 1937 in 9a83bff
check_http_code()416 handlingFFmpeg/libavformat/http.c
Lines 923 to 937 in 9a83bff
http_should_reconnect()FFmpeg/libavformat/http.c
Lines 356 to 368 in 9a83bff
Details
parse_content_range()(http.c:962-968):http_read_header()(http.c:1525-1526):http_buf_read()(http.c:1824-1829):http_read_stream()(http.c:1908-1937):Step-by-step trace (e.g. a static 1000-byte resource whose server reports an unknown total):
Range: bytes=0-; the server replies206withContent-Range: bytes 0-999/*.parse_content_range()setss->range_end = 1000and, for the*total,s->filesize_unknown = 1(http.c:960-965).http_read_header()then setss->filesize = UINT64_MAX(http.c:1525-1526).http_buf_read()the buffer is empty;file_end = s->filesize = UINT64_MAX,target_end = s->range_end = 1000, sos->off == target_end && target_end < file_endholds and it returnsAVERROR(EAGAIN)(http.c:1828-1829).http_read_stream(), theEAGAINhandler (http.c:1916-1925) callshttp_open_cnx(), which re-requestsRange: bytes=1000-. The resource is not growing, so the server replies416. Incheck_http_code()(http.c:928-934),(http_code != 416 || !s->reconnect_partial)is true becausereconnect_partialdefaults to 0 (http.c:223), so the open fails withff_http_averror(416, ...) = AVERROR_HTTP_RANGE_NOT_SATISFIABLE(http.c:628).http_should_reconnect()falls through thereconnect_partialcheck to the generic4xxgroup (http.c:357-368) and returns 0 withreconnect_on_http_errorunset, sohttp_open_cnx()fails andread_ret = AVERROR_HTTP_RANGE_NOT_SATISFIABLE;goto retryis not taken.is_prematurealready computed at http.c:1912 ass->filesize > 0 && s->off < s->filesize→UINT64_MAX > 0 && 1000 < UINT64_MAX→ true. Withreconnect=0andread_ret != AVERROR_EOF, the branch at http.c:1931-1934 takesreturn AVERROR(EIO);.The caller therefore receives a hard I/O error at the clean end of a fully downloaded resource. With
-reconnect 1the loop instead sleeps/seeks/re-requests, getting 416 every time, untilreconnect_delay_total > reconnect_delay_total_maxand then returnsAVERROR(EIO)(http.c:1939-1941).This was introduced by the fork-specific commit 3e345ae ("avformat/http: parse Content-Range: /*", together with 5319653 / db08bc9 which added
-reconnect_partialand the 416 error mapping). Before that commit, the*was parsed bystrtoull()as filesize 0 — a different bug that made such resources fail at open. The fork's parsing makes the data readable but propagates the wrong terminal status. The commit message of 3e345ae itself notes the flow "until the server stops giving us data (416 Range Not Satisfiable)", but the 416 only terminates cleanly whenreconnect_partialis enabled; the default path was not handled.Impact
Severity: medium.
Content-Range: .../*(unknown total length) but is in fact complete/static causes ffmpeg/libavformat clients to fail withAVERROR(EIO)("Input/output error") at end of file instead of a clean EOF. Demuxing of the full content succeeds, but the terminal status is an error, which callers typically surface as a failed transfer/transcode.-reconnect 1(common in ingest pipelines), end-of-file turns into a retry storm: repeatedRange: bytes=<end>-requests answered with 416, with exponential backoff, blocking for up toreconnect_delay_total_max(default 256 s) before finally failing with EIO.Suggested fix
Treat a 416 on the follow-up range request as end of data when growth is not expected (
reconnect_partialoff), and record the now-known size. Note that just settings->filesizeis not enough:is_prematureis computed at the top of the loop iteration (http.c:1912), before theEAGAINhandler runs, so it must be reset too, otherwise the stale value still triggers thereturn AVERROR(EIO)at http.c:1933-1934.With this change, when
reconnect/reconnect_at_eofare off the loop reaches thebreakat http.c:1936 andhttp_read_stream()returnsAVERROR_EOF; when-reconnect_at_eofis set, the existing retry-at-EOF logic is honored; ands->filesize = s->offmakes any subsequenthttp_buf_read()returnAVERROR_EOFdirectly via thes->off >= file_endcheck (http.c:1826-1827). Thereconnect_partial=1case is unaffected, since there the 416 is already handled as a soft error byhttp_should_reconnect().