From 546a59d218eadc2f1006d4d9ecf0042666b88113 Mon Sep 17 00:00:00 2001 From: Patrick Griffis Date: Wed, 12 Aug 2026 14:31:22 -0500 Subject: [PATCH] server: Report the resource length in 416 responses RFC 9110 section 15.5.17 says a 416 answering a byte-range request should carry a Content-Range header in the unsatisfied-range form, so a client which guessed a range wrong learns how long the resource actually is instead of having to guess again. soup_message_headers_set_content_range() cannot express that form, as it always writes a first-pos and a last-pos, so add a private setter for it and use it on the server's 416 path. --- .../http1/soup-server-message-io-http1.c | 2 + libsoup/soup-message-headers-private.h | 3 ++ libsoup/soup-message-headers.c | 17 +++++++ tests/range-test.c | 49 ++++++++++++++++++- 4 files changed, 69 insertions(+), 2 deletions(-) diff --git a/libsoup/server/http1/soup-server-message-io-http1.c b/libsoup/server/http1/soup-server-message-io-http1.c index 6f12a4eb..ff56fb06 100644 --- a/libsoup/server/http1/soup-server-message-io-http1.c +++ b/libsoup/server/http1/soup-server-message-io-http1.c @@ -257,6 +257,8 @@ handle_partial_get (SoupServerMessage *msg) &ranges, &nranges); if (status == SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE) { soup_server_message_set_status (msg, status, NULL); + soup_message_headers_set_content_range_unsatisfied (response_headers, + response_body->length); soup_message_body_truncate (response_body); return; } else if (status != SOUP_STATUS_PARTIAL_CONTENT) diff --git a/libsoup/soup-message-headers-private.h b/libsoup/soup-message-headers-private.h index 2e96af34..eef34b9a 100644 --- a/libsoup/soup-message-headers-private.h +++ b/libsoup/soup-message-headers-private.h @@ -42,5 +42,8 @@ gboolean soup_message_headers_header_contains_common (SoupMessageHeaders *hdr gboolean soup_message_headers_header_equals_common (SoupMessageHeaders *hdrs, SoupHeaderName name, const char *value); +void soup_message_headers_set_content_range_unsatisfied + (SoupMessageHeaders *hdrs, + goffset total_length); G_END_DECLS diff --git a/libsoup/soup-message-headers.c b/libsoup/soup-message-headers.c index c8f2a598..dc018842 100644 --- a/libsoup/soup-message-headers.c +++ b/libsoup/soup-message-headers.c @@ -1680,6 +1680,23 @@ soup_message_headers_set_content_range (SoupMessageHeaders *hdrs, g_free (header); } +/* Sets @hdrs's Content-Range header to the unsatisfied-range form, + * "bytes * /@total_length", which RFC 9110 §15.5.17 asks a 416 response to + * carry so that the client learns how long the resource actually is. + */ +void +soup_message_headers_set_content_range_unsatisfied (SoupMessageHeaders *hdrs, + goffset total_length) +{ + char *header; + + g_return_if_fail (hdrs); + + header = g_strdup_printf ("bytes */%" G_GINT64_FORMAT, total_length); + soup_message_headers_replace_common (hdrs, SOUP_HEADER_CONTENT_RANGE, header, SOUP_HEADER_VALUE_TRUSTED); + g_free (header); +} + static gboolean parse_content_foo (SoupMessageHeaders *hdrs, SoupHeaderName header_name, diff --git a/tests/range-test.c b/tests/range-test.c index fe8cdfc5..0a1156dd 100644 --- a/tests/range-test.c +++ b/tests/range-test.c @@ -163,6 +163,38 @@ request_single_range_by_string (SoupSession *session, const char *uri, g_object_unref (msg); } +/* Asserts a 416 which also reports how long the resource really is, as + * RFC 9110 §15.5.17 asks for. Kept out of do_range_test() because it makes a + * claim about the response body length that other servers need not match. + */ +static void +request_unsatisfiable_range (SoupSession *session, const char *uri, + const char *range) +{ + SoupMessage *msg; + GBytes *body; + char *expected; + + msg = soup_message_new ("GET", uri); + soup_message_headers_replace (soup_message_get_request_headers (msg), "Range", range); + + debug_printf (1, " Range: %s\n", range); + + body = soup_test_session_async_send (session, msg, NULL, NULL); + + soup_test_assert_message_status (msg, SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE); + + expected = g_strdup_printf ("bytes */%" G_GSIZE_FORMAT, + g_bytes_get_size (full_response)); + g_assert_cmpstr (soup_message_headers_get_one (soup_message_get_response_headers (msg), + "Content-Range"), + ==, expected); + g_free (expected); + + g_clear_pointer (&body, g_bytes_unref); + g_object_unref (msg); +} + /* Like request_single_range_by_string(), but able to check the ranges of a * successful 206 as well. */ static void @@ -764,6 +796,9 @@ static const ContentRangeParsingTest content_range_parsing_tests[] = { { "single byte", "bytes 0-0/1", TRUE, 0, 0, 1 }, { "final byte", "bytes 99-99/100", TRUE, 99, 99, 100 }, { "unknown total length", "bytes 0-9/*", TRUE, 0, 9, -1 }, + /* The unsatisfied-range form a 416 carries has no range to report, so + * there is nothing this function can return for it. */ + { "unsatisfied range", "bytes */10", FALSE, 0, 0, 0 }, { "extra space after the unit", "bytes 0-9/10", TRUE, 0, 9, 10 }, { "large but representable", "bytes 0-9223372036854775805/9223372036854775806", TRUE, 0, 9223372036854775805, 9223372036854775806 }, @@ -871,6 +906,7 @@ do_libsoup_only_range_test (SoupSession *session, const char *uri) { gsize full_response_length = g_bytes_get_size (full_response); GString *range; + char *str; int i; /* A suffix length at least as long as the body selects the whole body. */ @@ -896,6 +932,16 @@ do_libsoup_only_range_test (SoupSession *session, const char *uri) request_single_range_by_string (session, uri, "bytes=9888888888888019900-", SOUP_STATUS_OK); + /* A 416 reports the length of the resource, so that a client which + * guessed a range wrong can work out what to ask for instead. + */ + debug_printf (1, "Requesting (unsatisfiable) past the end of the body\n"); + str = g_strdup_printf ("bytes=%d-%d", + (int) full_response_length + 1, + (int) full_response_length + 100); + request_unsatisfiable_range (session, uri, str); + g_free (str); + /* More ranges than the server is willing to coalesce, which is * rejected rather than answered with the whole body. * https://gitlab.gnome.org/GNOME/libsoup/-/issues/538 @@ -904,8 +950,7 @@ do_libsoup_only_range_test (SoupSession *session, const char *uri) range = g_string_new ("bytes="); for (i = 0; i < MAX_RANGES + 1; i++) g_string_append (range, i > 0 ? ",0-0" : "0-0"); - request_single_range_by_string (session, uri, range->str, - SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE); + request_unsatisfiable_range (session, uri, range->str); g_string_free (range, TRUE); } -- GitLab