Slice module 206 requirement

Maxim Dounin mdounin at mdounin.ru
Sun Jul 10 08:35:48 UTC 2022


Hello!

On Fri, Jul 08, 2022 at 07:13:33PM +0000, Lucas Rolff wrote:

> I’m having an nginx instance where I utilise the nginx slice 
> module to slice upstream mp4 files when using proxy_cache.
> 
> However, I have an interesting origin where if sending a range 
> request (which happens when the slice module is enabled), to a 
> file that’s less than the slice range, the origin returns a 200 
> OK, but with the range related headers such as content-range, 
> but obviously the full file is returned since it’s within the 
> requested range.
> 
> When playing the MP4s through Google Chrome and Firefox it works 
> fine when going through the nginx proxy instance, however, it 
> somehow breaks Safari (both on MacOS, and iOS) - I guess Safari 
> is more strict.
> When playing directly through the origin it works fine in all 
> browsers.
> 
> The md5 of response from the origin remains the same, so it’s 
> not that the response itself is an invalid MP4 file, and even if 
> you compare the cache files on disk with a “working” origin and 
> the “broken” origin (one sends a 206 Partial Content, another 
> sends 200 OK) - the content of the cache files remain the same, 
> except obviously the header section of the cache file.
> 
> The origin returns a 206 status code, only if the file exceeds 
> the slice size, so if I configure a slice size of 5 megabyte, 
> only files above 5 megabytes will give 206s. Anything under 5 
> megabytes will result in a 200 OK with content-range and the 
> correct content-length,
>
> Looking in the slice module itself I see:
> https://github.com/nginx/nginx/blob/master/src/http/modules/ngx_http_slice_filter_module.c#L116-L126
> 
> 
>     if (r->headers_out.status != NGX_HTTP_PARTIAL_CONTENT) {
>         if (r == r->main) {
>             ngx_http_set_ctx(r, NULL, ngx_http_slice_filter_module);
>             return ngx_http_next_header_filter(r);
>         }
> 
>         ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
>                       "unexpected status code %ui in slice response",
>                       r->headers_out.status);
>         return NGX_ERROR;
>     }
> 
> This seems like the slice module expects a 206 status code to be 
> returned,

For the main request, the code accepts two basic valid variants:

- 206, so the slice module will combine multiple responses to 
  range requests as needed;

- anything else, so the slice module will give up and simply 
  return the response to the client.

If the module sees a non-206 response to a subrequest, this is an 
error, as the slice module expects underlying resources to be 
immutable, and does not expect that some ranges can be requested, 
while some other aren't.  This isn't something related to your 
case though.

> however, later in the same function 
> https://github.com/nginx/nginx/blob/master/src/http/modules/ngx_http_slice_filter_module.c#L200-L211
> 
> 
>     if (r->headers_out.status == NGX_HTTP_PARTIAL_CONTENT) {
>         if (ctx->start + (off_t) slcf->size <= r->headers_out.content_offset) {
>             ctx->start = slcf->size
>                          * (r->headers_out.content_offset / slcf->size);
>         }
> 
>         ctx->end = r->headers_out.content_offset
>                    + r->headers_out.content_length_n;
> 
>     } else {
>         ctx->end = cr.complete_length;
>     }
> 
> There it will do an else statement if the status code isn’t 206.
> So would this piece of code ever be reached, since there’s the initial error?

Following the initial check, r->headers_out.status is explicitly 
changed to NGX_HTTP_OK.  Later on the 
ngx_http_next_header_filter() call might again change 
r->headers_out.status as long as the client used a range request, 
and this is what checked here.

> Additionally I don’t see in RFC7233 that 206 responses are an 
> absolute requirement, additionally I don’t see content-range 
> being prohibited/forbidden to be used for 200 OK responses.
> Now, if one have a secondary proxy that modifies the response 
> headers in between the origin returning 200 OK with the 
> Content-Range header, and then strip out the Content-Range 
> header, the nginx slice module seems to handle it fine, so 
> somehow the combination of 200 OK and a Content-Range header 
> being present seems to break the slice module from functioning.
> 
> I’m just curious why this happens within the slice module, and 
> if there’s any possible solution for it (like allowing the 
> combination of 200 OK and Content-Range, since those two would 
> still indicate that the origin/upstream supports range requests) 
> - obviously it would be nice to fix the upstream server but 
> sometimes that’s sadly not possible.

>From the above explanation it is probably already clear that 
"disabling slice when an origin returns 200 OK" is what actually 
happens.

The issue does not appear without the slice module in your testing 
because the Content-Range header seems to be only present in your 
backend 200 responses when there was a Range header in the 
request, and this is what happens only with the slice module.

I've done some limited testing with Safari and manually added 
Content-Range header, and there seem to be at least two issues:

- Range filter in nginx does not expect the Content-Range header 
  to be already present in 200 responses and simply adds another 
  one.  This results in incorrect range responses with multiple 
  Content-Range headers, and this breaks Safari.

- Safari also breaks if its test request with "Range: bytes=0-1" 
  results in 200 with the Content-Range header.

My initial fix was to simply disable handling of 200 responses 
with Content-Range headers in the range filter, so such responses 
wouldn't be touched at all.  This is perfectly correct and 
probably the most secure thing to do, but does not work with 
Safari due to the second issue outlined above.

Another approach would be to clear pre-existing Content-Range 
headers in the range filter.  This seems to work, at least in my 
testing.  See below for the patch.

> I know the parts of the slice module haven’t been touched for 
> years, so obviously it works for most people, just dipping my 
> toes here to see if there’s a possible solution other than 
> disabling slice when an origin returns 200 OK for files smaller 
> than the slice size.

Note that that slice module is generally unsafe to use for 
arbitrary upstream servers: it relies on expectations which are 
beyond the HTTP standard requirements.  In particular:

- It requires resources to be immutable, so different range 
  responses can be combined together.

- It does not try to handle edge cases, such as 416 returned by 
  the upstream on empty files (which is correct per RFC, but 
  requires complicated additional handling to convert 416 to 200, so 
  it is better to just return 200 OK).

In general, the slice module is to be used only in your own 
infrastructure when you control the backend and can be sure that 
the slice module expectations are met.  As such, disabling it for 
backends which do something unexpected might actually be a good 
idea.  On the other hand, in this particular case the nginx 
behaviour can be adjusted to handle things gracefully.

Below is a patch to clear pre-existing Content-Range headers 
in the range filter.  Please test if it works for you.

# HG changeset patch
# User Maxim Dounin <mdounin at mdounin.ru>
# Date 1657439390 -10800
#      Sun Jul 10 10:49:50 2022 +0300
# Node ID 219217ea49a8d648f5cadd046f1b1294ef05693c
# Parent  9d98d524bd02a562d9cd83f4e369c7e992c5753b
Range filter: clearing of pre-existing Content-Range headers.

Some servers might emit Conten-Range header on 200 responses, and this
does not seem to contradict RFC 9110: as per RFC 9110, the Content-Range
header have no meaning for status codes other than 206 and 417.  Previously
this resulted in duplicate Content-Range headers in nginx responses handled
by the range filter.  Fix is to clear pre-existing headers.

diff --git a/src/http/modules/ngx_http_range_filter_module.c b/src/http/modules/ngx_http_range_filter_module.c
--- a/src/http/modules/ngx_http_range_filter_module.c
+++ b/src/http/modules/ngx_http_range_filter_module.c
@@ -425,6 +425,10 @@ ngx_http_range_singlepart_header(ngx_htt
         return NGX_ERROR;
     }
 
+    if (r->headers_out.content_range) {
+        r->headers_out.content_range->hash = 0;
+    }
+
     r->headers_out.content_range = content_range;
 
     content_range->hash = 1;
@@ -582,6 +586,11 @@ ngx_http_range_multipart_header(ngx_http
         r->headers_out.content_length = NULL;
     }
 
+    if (r->headers_out.content_range) {
+        r->headers_out.content_range->hash = 0;
+        r->headers_out.content_range = NULL;
+    }
+
     return ngx_http_next_header_filter(r);
 }
 
@@ -598,6 +607,10 @@ ngx_http_range_not_satisfiable(ngx_http_
         return NGX_ERROR;
     }
 
+    if (r->headers_out.content_range) {
+        r->headers_out.content_range->hash = 0;
+    }
+
     r->headers_out.content_range = content_range;
 
     content_range->hash = 1;

-- 
Maxim Dounin
http://mdounin.ru/



More information about the nginx mailing list