silence
发布于 2026-03-16 / 3 阅读
0
0

304 Not Modified 处理流程

HTTP 304 Not Modified 说明无需再次传输请求的内容,也就是说可以使用缓存的内容。这通常是在一些安全的方法(safe),例如GET 或HEAD 或在请求中附带了头部信息: If-None-Match 或If-Modified-Since。

该响应必须不包含主体,并且必须包含在等价 200 OK 响应中会带有的 Cache-Control、Content-Location、Date、ETag、Expires 和 Vary 标头。

如果一个请求中同时包含了 If-None-Match 和 If-Modified-Since,由于 ETag 比最后修改时间能更精确地标识资源内容,服务器会优先处理 If-None-Match

— 引用自 https://developer.mozilla.org/

对nginx 1.20.0 源码的分析:
当收到http请求后

  1. 首先由 ngx_http_process_request_line 函数读取解析请求行。
  2. ngx_http_process_request_headers 函数解析请求头,并会通过 ngx_http_find_header_handler 函数解析查找预定义的 ngx_http_headers_in 数组,该数组包含了 Nginx 需要特殊处理的头部及其回调函数
// 源码位置:src\http\ngx_http_request.c

// 哈希表定义
static ngx_http_header_t  ngx_http_headers_in[] = {
 // ...
{ ngx_string("If-Modified-Since"),
offsetof(ngx_http_headers_in_t, if_modified_since),
ngx_http_process_unique_header_line },

{ ngx_string("If-Unmodified-Since"),
offsetof(ngx_http_headers_in_t, if_unmodified_since),
ngx_http_process_unique_header_line },

{ ngx_string("If-Match"),
offsetof(ngx_http_headers_in_t, if_match),
ngx_http_process_unique_header_line },

{ ngx_string("If-None-Match"),
offsetof(ngx_http_headers_in_t, if_none_match),
ngx_http_process_unique_header_line },
 // ...
};


ngx_http_process_request_headers(ngx_event_t  *rev)
// ...
hh  =  ngx_hash_find(&cmcf->headers_in_hash, h->hash,
h->lowcase_key, h->key.len);

// 这是在一个for里面,调用该头部的专用处理函数
if (hh  &&  hh->handler(r, h, hh->offset) !=  NGX_OK) {
break;
}
*// ...

static ngx_int_t ngx_http_process_unique_header_line(ngx_http_request_t *r, ngx_table_elt_t *h, ngx_uint_t offset) 函数避免请求头重复出现,将第一个key的值保存到 r->headers_in.if_none_match

  1. 在内容生成阶段响应头被设置后,头部过滤器 ngx_http_not_modified_filter_module 会负责处理条件请求。
    该模块的入口函数是 ngx_http_not_modified_header_filter(位于 src/http/modules/ngx_http_not_modified_filter_module.c)。
    代码可见是否响应304状态码受如下请求头影响:
    if_modified_sinceif_none_match

if_unmodified_since、if_match请求头则进入 src\http\ngx_http_special_response.c 的 ngx_int_t ngx_http_filter_finalize_request(ngx_http_request_t *r, ngx_module_t *m, ngx_int_t error) 函数处理

// 源码:src/http/modules/ngx_http_not_modified_filter_module.c
static ngx_int_t
ngx_http_not_modified_header_filter(ngx_http_request_t *r)
{
    if (r->headers_out.status != NGX_HTTP_OK
        || r != r->main
        || r->disable_not_modified)
    {
        return ngx_http_next_header_filter(r);
    }

    if (r->headers_in.if_unmodified_since
        && !ngx_http_test_if_unmodified(r))
    {
        return ngx_http_filter_finalize_request(r, NULL,
                                                NGX_HTTP_PRECONDITION_FAILED);
    }

    if (r->headers_in.if_match
        && !ngx_http_test_if_match(r, r->headers_in.if_match, 0))
    {
        return ngx_http_filter_finalize_request(r, NULL,
                                                NGX_HTTP_PRECONDITION_FAILED);
    }

    if (r->headers_in.if_modified_since || r->headers_in.if_none_match) {

        if (r->headers_in.if_modified_since
            && ngx_http_test_if_modified(r))
        {
            return ngx_http_next_header_filter(r);
        }

        if (r->headers_in.if_none_match
            && !ngx_http_test_if_match(r, r->headers_in.if_none_match, 1))
        {
            return ngx_http_next_header_filter(r);
        }

        /* not modified */

        r->headers_out.status = NGX_HTTP_NOT_MODIFIED;
        r->headers_out.status_line.len = 0;
        r->headers_out.content_type.len = 0;
        ngx_http_clear_content_length(r);
        ngx_http_clear_accept_ranges(r);

        if (r->headers_out.content_encoding) {
            r->headers_out.content_encoding->hash = 0;
            r->headers_out.content_encoding = NULL;
        }

        return ngx_http_next_header_filter(r);
    }

    return ngx_http_next_header_filter(r);
}

if_none_match

If-None-Match 是一个条件式请求首部。对于 GETGET 和 HEAD 请求方法来说,当且仅当服务器上没有任何资源的 ETag 属性值与这个首部中列出的相匹配的时候,服务器端才会返回所请求的资源,响应码为 200 。对于其他方法来说,当且仅当最终确认没有已存在的资源的 ETag 属性值与这个首部中所列出的相匹配的时候,才会对请求进行相应的处理。
对于 GET 和 HEAD 方法来说,当验证失败的时候,服务器端必须返回响应码 304(Not Modified,未改变)。对于能够引发服务器状态改变的方法,则返回 412(Precondition Failed,前置条件失败)。需要注意的是,服务器端在生成状态码为 304 的响应的时候,必须同时生成以下会存在于对应的 200 响应中的首部:Cache-Control、Content-Location、Date、ETag、Expires 和 Vary。
—摘自:https://developer.mozilla.org/

在函数 static ngx_int_t ngx_http_not_modified_header_filter(ngx_http_request_t *r) 调用 ngx_http_test_if_match 时传参固定了 weak = 1,表示弱比较,会忽略 etag 值开头的 W/

判断过程见代码注释

// 源码: src\http\modules\ngx_http_not_modified_filter_module.c

static ngx_uint_t
ngx_http_test_if_match(ngx_http_request_t *r, ngx_table_elt_t *header,
    ngx_uint_t weak)
{
    u_char     *start, *end, ch;
    ngx_str_t   etag, *list;

    list = &header->value;

    // 如果 if-none-match 的值只有1个字符且时 * 直接返回匹配成功,这里遵循HTTP规范 RFC 7232
    if (list->len == 1 && list->data[0] == '*') {
        return 1;
    }

    // 如果服务器没有生成etag响应头,直接返回匹配失败
    if (r->headers_out.etag == NULL) {
        return 0;
    }
    // 有etag的请求,取出来备用
    etag = r->headers_out.etag->value;

    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                   "http im:\"%V\" etag:%V", list, &etag);
    // 判断是否要弱比较,处理etag值开头的 `W/` 
    if (weak
        && etag.len > 2
        && etag.data[0] == 'W'
        && etag.data[1] == '/')
    {
        etag.len -= 2;
        etag.data += 2;
    }

    start = list->data;
    end = list->data + list->len;
    // 可能有逗号分隔的多个etag值,需要依次匹配
    while (start < end) {

        if (weak
            && end - start > 2
            && start[0] == 'W'
            && start[1] == '/')
        {
            start += 2;
        }
        // 检查长度,如果小于etag长度,那么肯定匹配不上,直接返回失败,减少字符匹配计算量
        if (etag.len > (size_t) (end - start)) {
            return 0;
        }

        if (ngx_strncmp(start, etag.data, etag.len) != 0) {
            goto skip;
        }

        start += etag.len;

        while (start < end) {
            ch = *start;

            if (ch == ' ' || ch == '\t') {
                start++;
                continue;
            }

            break;
        }

        if (start == end || *start == ',') {
            return 1;
        }

    skip:

        while (start < end && *start != ',') { start++; }
        while (start < end) {
            ch = *start;

            if (ch == ' ' || ch == '\t' || ch == ',') {
                start++;
                continue;
            }

            break;
        }
    }

    return 0;
}

if_modified_since

If-Modified-Since 是一个条件式请求首部,服务器只在所请求的资源在给定的日期时间之后对内容进行过修改的情况下才会将资源返回,状态码为 200 。如果请求的资源从那时起未经修改,那么返回一个不带有消息主体的 304 响应,而在 Last-Modified 首部中会带有上次修改时间。不同于 If-Unmodified-Since, If-Modified-Since 只可以用在 GET 或 HEAD 请求中。
当与 If-None-Match 一同出现时,它(If-Modified-Since)会被忽略掉,除非服务器不支持 If-None-Match。
—摘自:https://developer.mozilla.org/

// 源码:src\http\modules\ngx_http_not_modified_filter_module.c

static ngx_uint_t
ngx_http_test_if_modified(ngx_http_request_t *r)
{
    time_t                     ims;
    ngx_http_core_loc_conf_t  *clcf;
    // 检查是否有响应头 last-modified 用于对比
    if (r->headers_out.last_modified_time == (time_t) -1) {
        return 1;
    }

    clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
    // 读nginx配置项 if_modified_since 可能是:off/exact/before
    if (clcf->if_modified_since == NGX_HTTP_IMS_OFF) {
        return 1;
    }

    ims = ngx_parse_http_time(r->headers_in.if_modified_since->value.data,
                              r->headers_in.if_modified_since->value.len);

    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                   "http ims:%T lm:%T", ims, r->headers_out.last_modified_time);

    // 时间相等返回 0(实际是判断了clcf->if_modified_since == NGX_HTTP_IMS_EXACT 的情况)
    if (ims == r->headers_out.last_modified_time) {
        return 0;
    }
    
    // 配置项的值是 exact 或者请求头中的时间小于修改时间返回 1
    if (clcf->if_modified_since == NGX_HTTP_IMS_EXACT
        || ims < r->headers_out.last_modified_time)
    {
        return 1;
    }

    return 0;
}

评论