Commit faab9c8e56 for ffmpeg
commit faab9c8e563ea3b3d8f27c17febbeecd3ade2f1e
Author: Andreas Menze <andreas.menze@hr.de>
Date: Thu Oct 1 01:40:30 2026 +0200
avformat/http: keep complete size for Content-Range "bytes A-B/*"
"bytes A-B/*" means the complete length is unknown (RFC 9110 14.4), so
don't read it as a size of 0, and never derive the complete size from a
partial reply. Remember it once learned from a 200 Content-Length or a
"bytes A-B/N" total and reuse it for later "*" replies. If the complete
size was never learned, or the total is not a number, it stays unknown.
A 416 to a read or a seek past the end of a resource of unknown size
ends it with EOF and gives its complete length ("bytes */N"), unless it
states a length past the requested offset. That 416 is retried first
only if reconnect_on_http_error lists 416 by number, reconnect_at_eof
does not retry the end it marks, and an open past the end is retried as
configured. Keep the file size and the complete size when a request
fails. A seek that fails or is refused keeps the offset and the end of
the resource, and when reading goes on over the old connection, also
the range end, chunk state, connection mode and compression of the old
reply. A seek to the current offset makes a new request when there is
no connection.
Fixes #24210
Signed-off-by: Kacper Michajłow <kasper93@gmail.com>
diff --git a/doc/protocols.texi b/doc/protocols.texi
index 1d43fa6327..7812d154db 100644
--- a/doc/protocols.texi
+++ b/doc/protocols.texi
@@ -599,6 +599,10 @@ Reconnect automatically when disconnected before EOF is hit.
@item reconnect_at_eof
If set then eof is treated like an error and causes reconnection, this is useful
for live / endless streams.
+This does not apply to the deliberately established end of a resource whose
+complete size is unknown, i.e. when a ranged request past the end is answered
+with @code{416}. Such an end is reported as a clean EOF and is not retried,
+unless @option{reconnect_on_http_error} lists 416 by number.
@item reconnect_on_network_error
Reconnect automatically in case of TCP/TLS errors during connect.
@@ -606,6 +610,10 @@ Reconnect automatically in case of TCP/TLS errors during connect.
@item reconnect_on_http_error
A comma separated list of HTTP status codes to reconnect on. The list can
include specific status codes (e.g. '503') or the strings '4xx' / '5xx'.
+A @code{416} that ends a read or a seek past the end of a resource whose
+complete size is unknown is reported as EOF. It is retried first only if 416
+is listed by number, for example to read a resource that is still being
+written. '4xx' does not cover it.
@item reconnect_streamed
If set then even streamed/non seekable streams will be reconnected on errors.
diff --git a/libavformat/http.c b/libavformat/http.c
index 3245f47d01..675bd81ea6 100644
--- a/libavformat/http.c
+++ b/libavformat/http.c
@@ -122,6 +122,8 @@ typedef struct HTTPContext {
HTTPAuthState auth_state; /* auth_state.auth_type is also a config option */
HTTPAuthState proxy_auth_state;
uint64_t filesize;
+ uint64_t oc_filesize; /* Complete size (200 Content-Length or Content-Range total) */
+ int end_of_resource; /* The end of an unknown-size resource was reached */
int is_akamai;
int is_mediagateway;
/* A dictionary containing cookies keyed by cookie name */
@@ -171,6 +173,8 @@ typedef struct HTTPContext {
/* Temporary during header parsing */
uint64_t filesize_from_content_range;
+ /* Reply is a partial response with unknown total ("bytes A-B/" + '*'). */
+ int content_range_unknown_size;
int line_count;
/******************
@@ -426,23 +430,41 @@ static int redirect_cache_set(HTTPContext *s, const char *source, const char *de
return 0;
}
-/* return non zero if error */
-static int http_open_cnx(URLContext *h, AVDictionary **options)
+/* A 416 reply for a resource of unknown size marks its end, unless it states
+ * a length past the refused offset. */
+static int is_end_of_resource(HTTPContext *s, int err, uint64_t off)
+{
+ return err == AVERROR_HTTP_OTHER_4XX && s->http_code == 416 &&
+ s->oc_filesize == UINT64_MAX &&
+ (s->filesize_from_content_range == UINT64_MAX ||
+ s->filesize_from_content_range <= off);
+}
+
+/* Return non zero if error. With end_is_eof, the caller reports the end of a
+ * resource of unknown size as EOF, and the 416 that marks it is retried only
+ * if reconnect_on_http_error lists 416 by number. */
+static int http_open_cnx(URLContext *h, AVDictionary **options, int end_is_eof)
{
HTTPAuthType cur_auth_type, cur_proxy_auth_type;
HTTPContext *s = h->priv_data;
int ret, conn_attempts = 1, auth_attempts = 0, redirects = 0;
int reconnect_delay = 0;
int reconnect_delay_total = 0;
- uint64_t off;
+ uint64_t off = s->off;
+ uint64_t filesize = s->filesize;
+ uint64_t oc_filesize = s->oc_filesize;
char *cached;
redo:
+ /* every attempt starts with the complete size known before the call */
+ s->oc_filesize = oc_filesize;
cached = redirect_cache_get(s);
if (cached) {
- if (redirects++ >= s->max_redirects)
- return AVERROR(EIO);
+ if (redirects++ >= s->max_redirects) {
+ ret = AVERROR(EIO);
+ goto fail;
+ }
av_free(s->location);
s->location = av_strdup(cached);
@@ -463,6 +485,8 @@ redo:
ret = http_open_cnx_internal(h, options);
if (ret < 0) {
if (!http_should_reconnect(s, ret) ||
+ (end_is_eof && is_end_of_resource(s, ret, off) &&
+ !av_match_list("416", s->reconnect_on_http_error, ',')) ||
reconnect_delay > s->reconnect_delay_max ||
(s->reconnect_max_retries >= 0 && conn_attempts > s->reconnect_max_retries) ||
reconnect_delay_total > s->reconnect_delay_total_max)
@@ -516,8 +540,10 @@ redo:
s->new_location) {
/* url moved, get next */
ffurl_closep(&s->hd);
- if (redirects++ >= s->max_redirects)
- return AVERROR(EIO);
+ if (redirects++ >= s->max_redirects) {
+ ret = AVERROR(EIO);
+ goto fail;
+ }
if (!s->expires) {
s->expires = (s->http_code == 301 || s->http_code == 308) ? INT64_MAX : -1;
@@ -542,6 +568,8 @@ redo:
fail:
s->off = off;
+ s->filesize = filesize;
+ s->oc_filesize = oc_filesize;
if (s->hd)
ffurl_closep(&s->hd);
if (ret < 0)
@@ -598,6 +626,8 @@ int ff_http_do_new_request2(URLContext *h, const char *uri, AVDictionary **opts)
s->chunkend = 0;
s->range_end = 0;
s->off = 0;
+ s->oc_filesize = UINT64_MAX;
+ s->end_of_resource = 0;
s->icy_data_read = 0;
av_free(s->location);
@@ -614,7 +644,7 @@ int ff_http_do_new_request2(URLContext *h, const char *uri, AVDictionary **opts)
return ret;
av_log(s, AV_LOG_INFO, "Opening \'%s\' for %s\n", uri, h->flags & AVIO_FLAG_WRITE ? "writing" : "reading");
- ret = http_open_cnx(h, &options);
+ ret = http_open_cnx(h, &options, 0);
av_dict_free(&options);
return ret;
}
@@ -791,6 +821,8 @@ static int http_open(URLContext *h, const char *uri, int flags,
s->initial_requests = s->seekable != 0 && s->initial_request_size > 0;
s->filesize = UINT64_MAX;
+ s->oc_filesize = UINT64_MAX;
+ s->end_of_resource = 0;
s->location = av_strdup(uri);
if (!s->location)
@@ -820,7 +852,7 @@ static int http_open(URLContext *h, const char *uri, int flags,
if (s->listen) {
return http_listen(h, uri, flags, options);
}
- ret = http_open_cnx(h, options);
+ ret = http_open_cnx(h, options, 0);
bail_out:
if (ret < 0) {
av_dict_free(&s->chained_options);
@@ -985,8 +1017,12 @@ static int parse_content_range(URLContext *h, const char *p)
return AVERROR_INVALIDDATA;
}
}
- if ((slash = strchr(p, '/')) && strlen(slash) > 0)
- s->filesize_from_content_range = strtoull(slash + 1, NULL, 10);
+ if ((slash = strchr(p, '/')) && strlen(slash) > 0) {
+ if (av_isdigit(slash[1]))
+ s->filesize_from_content_range = strtoull(slash + 1, NULL, 10);
+ else
+ s->content_range_unknown_size = 1; /* unknown total size */
+ }
}
if (s->seekable == -1 && (!s->is_akamai || s->filesize != 2147483647))
h->is_streamed = 0; /* we _can_ in fact seek */
@@ -1559,6 +1595,7 @@ static int http_read_header(URLContext *h)
s->expires = 0;
s->chunksize = UINT64_MAX;
s->filesize_from_content_range = UINT64_MAX;
+ s->content_range_unknown_size = 0;
for (;;) {
int parsed_http_code = 0;
@@ -1593,6 +1630,14 @@ static int http_read_header(URLContext *h)
// 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;
+ else if (s->content_range_unknown_size)
+ s->filesize = s->oc_filesize;
+
+ /* Remember a concrete complete size for later unknown-total ("*") replies. */
+ if (s->filesize_from_content_range != UINT64_MAX)
+ s->oc_filesize = s->filesize_from_content_range;
+ else if (s->http_code == 200 && s->filesize != UINT64_MAX)
+ s->oc_filesize = s->filesize;
if (s->seekable == -1 && s->is_mediagateway && s->filesize == 2000000000)
h->is_streamed = 1; /* we can in fact _not_ seek */
@@ -1962,6 +2007,17 @@ static int http_buf_read_compressed(URLContext *h, uint8_t *buf, int size)
}
#endif /* CONFIG_ZLIB */
+/* Record the end of the resource if err is the 416 that marks it. */
+static int http_end_of_resource(HTTPContext *s, int err)
+{
+ if (!is_end_of_resource(s, err, s->off))
+ return 0;
+ s->oc_filesize = s->filesize_from_content_range;
+ s->filesize = s->oc_filesize;
+ s->end_of_resource = 1;
+ return 1;
+}
+
static int64_t http_seek_internal(URLContext *h, int64_t off, int whence, int force_reconnect);
static int http_read_stream(URLContext *h, uint8_t *buf, int size)
@@ -1973,6 +2029,9 @@ static int http_read_stream(URLContext *h, uint8_t *buf, int size)
int reconnect_delay_total = 0;
int conn_attempts = 1;
+ if (s->end_of_resource)
+ return AVERROR_EOF;
+
if (!s->hd)
return s->off < requested_end(s) ? AVERROR(EIO) : AVERROR_EOF;
@@ -2002,10 +2061,14 @@ retry:
if (s->willclose)
ffurl_closep(&s->hd);
s->initial_requests = 0; /* continue streaming uninterrupted from now on */
- read_ret = http_open_cnx(h, &options);
+ read_ret = http_open_cnx(h, &options, 1);
av_dict_free(&options);
if (read_ret == 0)
goto retry;
+ if (http_end_of_resource(s, read_ret)) {
+ read_ret = AVERROR_EOF;
+ break;
+ }
}
if (h->is_streamed && !s->reconnect_streamed)
@@ -2037,6 +2100,10 @@ retry:
av_log(h, AV_LOG_ERROR, "Failed to reconnect at %"PRIu64".\n", target);
return read_ret;
}
+ if (s->end_of_resource) {
+ read_ret = AVERROR_EOF;
+ break;
+ }
read_ret = http_buf_read(h, buf, size);
}
@@ -2211,6 +2278,13 @@ static int64_t http_seek_internal(URLContext *h, int64_t off, int whence, int fo
HTTPContext *s = h->priv_data;
URLContext *old_hd = NULL;
uint64_t old_off = s->off;
+ uint64_t old_range_end = s->range_end;
+ uint64_t old_chunksize = s->chunksize;
+ int old_willclose = s->willclose;
+#if CONFIG_ZLIB
+ int old_compressed = s->compressed;
+#endif
+ int old_end_of_resource = s->end_of_resource;
uint8_t old_buf[BUFFER_SIZE];
int old_buf_size, ret;
AVDictionary *options = NULL;
@@ -2229,12 +2303,12 @@ static int64_t http_seek_internal(URLContext *h, int64_t off, int whence, int fo
return AVERROR(EINVAL);
if (off < 0)
return AVERROR(EINVAL);
- if (!force_reconnect && off == s->off)
+ if (!force_reconnect && off == s->off && s->hd)
return s->off;
- s->off = off;
-
- if (s->off && h->is_streamed)
+ if (off && h->is_streamed)
return AVERROR(ENOSYS);
+ s->off = off;
+ s->end_of_resource = 0;
/* do not try to make a new connection if seeking past the end of the file */
if (s->end_off || s->filesize != UINT64_MAX) {
@@ -2283,8 +2357,8 @@ static int64_t http_seek_internal(URLContext *h, int64_t off, int whence, int fo
remaining -= ret;
}
- ret = http_open_cnx(h, &options);
- if (ret >= 0) {
+ ret = http_open_cnx(h, &options, 1);
+ if (ret >= 0 || http_end_of_resource(s, ret)) {
goto done;
} else {
/* fall back to normal reconnection */
@@ -2297,15 +2371,23 @@ static int64_t http_seek_internal(URLContext *h, int64_t off, int whence, int fo
s->hd = NULL;
}
- if ((ret = http_open_cnx(h, &options)) < 0) {
+ if ((ret = http_open_cnx(h, &options, 1)) < 0 &&
+ !http_end_of_resource(s, ret)) {
/* if it fails, continue on old connection if possible */
if (old_hd) {
memcpy(s->buffer, old_buf, old_buf_size);
s->buf_ptr = s->buffer;
s->buf_end = s->buffer + old_buf_size;
s->hd = old_hd;
- s->off = old_off;
+ s->range_end = old_range_end;
+ s->chunksize = old_chunksize;
+ s->willclose = old_willclose;
+#if CONFIG_ZLIB
+ s->compressed = old_compressed;
+#endif
}
+ s->off = old_off;
+ s->end_of_resource = old_end_of_resource;
av_dict_free(&options);
return ret;
}