HTTP/2: parse Priority request header

Parse the RFC 9218 Priority request header, an RFC 8941 Structured
Fields Dictionary such as "u=3, i".

The header lines are collected during header block processing and, once
the block is complete, combined into a single value and parsed once in
ngx_http_v2_run_request(), as RFC 8941, Section 4.2, requires all field
lines of the same field to be combined before parsing.  This mirrors how
cookies are handled.  The header is still passed through to the request
so it stays visible to variables and proxying.

A malformed field is ignored, leaving the defaults.  A PRIORITY_UPDATE
frame overrides the header (RFC 9218, Section 7): if a frame has already
been applied to the stream the header is not consulted.

Assisted-by: opencode:claude-opus-4.8
This commit is contained in:
Andrew Clayton 2026-09-22 01:52:41 +01:00
parent 2b6c2de41d
commit 4d147459db
2 changed files with 126 additions and 0 deletions

View file

@ -160,6 +160,9 @@ static ngx_int_t ngx_http_v2_construct_request_line(ngx_http_request_t *r);
static ngx_int_t ngx_http_v2_cookie(ngx_http_request_t *r,
ngx_http_v2_header_t *header);
static ngx_int_t ngx_http_v2_construct_cookie_header(ngx_http_request_t *r);
static ngx_int_t ngx_http_v2_priority(ngx_http_request_t *r,
ngx_http_v2_header_t *header);
static ngx_int_t ngx_http_v2_process_priority(ngx_http_request_t *r);
static ngx_int_t ngx_http_v2_construct_host_header(ngx_http_request_t *r);
static void ngx_http_v2_run_request(ngx_http_request_t *r);
static ngx_int_t ngx_http_v2_process_request_body(ngx_http_request_t *r,
@ -1354,6 +1357,7 @@ ngx_http_v2_state_headers(ngx_http_v2_connection_t *h2c, u_char *pos,
if (node->priority_set) {
stream->priority.client = node->priority;
node->priority_set = 0;
stream->priority_update = 1;
ngx_http_priority_state_update(&stream->priority);
@ -1741,6 +1745,7 @@ ngx_http_v2_state_process_header(ngx_http_v2_connection_t *h2c, u_char *pos,
ngx_http_core_main_conf_t *cmcf;
static ngx_str_t cookie = ngx_string("cookie");
static ngx_str_t priority = ngx_string("priority");
header = &h2c->state.header;
@ -1839,6 +1844,15 @@ ngx_http_v2_state_process_header(ngx_http_v2_connection_t *h2c, u_char *pos,
}
}
if (header->name.len == priority.len
&& ngx_memcmp(header->name.data, priority.data, priority.len) == 0)
{
if (ngx_http_v2_priority(r, header) != NGX_OK) {
return ngx_http_v2_connection_error(h2c,
NGX_HTTP_V2_INTERNAL_ERROR);
}
}
if (header->name.len == cookie.len
&& ngx_memcmp(header->name.data, cookie.data, cookie.len) == 0)
{
@ -2201,6 +2215,7 @@ ngx_http_v2_state_priority_update(ngx_http_v2_connection_t *h2c, u_char *pos,
} else if (node && node->stream) {
stream = node->stream;
stream->priority.client = priority;
stream->priority_update = 1;
ngx_http_priority_state_update(&stream->priority);
}
@ -3904,6 +3919,111 @@ ngx_http_v2_construct_cookie_header(ngx_http_request_t *r)
}
static ngx_int_t
ngx_http_v2_priority(ngx_http_request_t *r, ngx_http_v2_header_t *header)
{
ngx_str_t *val;
ngx_array_t *priorities;
priorities = r->stream->priorities;
if (priorities == NULL) {
priorities = ngx_array_create(r->pool, 1, sizeof(ngx_str_t));
if (priorities == NULL) {
return NGX_ERROR;
}
r->stream->priorities = priorities;
}
val = ngx_array_push(priorities);
if (val == NULL) {
return NGX_ERROR;
}
val->len = header->value.len;
val->data = header->value.data;
return NGX_OK;
}
static ngx_int_t
ngx_http_v2_process_priority(ngx_http_request_t *r)
{
u_char *p;
size_t len;
ngx_str_t value, *vals;
ngx_uint_t i;
ngx_array_t *priorities;
ngx_http_priority_t priority;
priorities = r->stream->priorities;
if (priorities == NULL) {
return NGX_OK;
}
/*
* RFC 9218, Section 7: a PRIORITY_UPDATE frame overrides the Priority
* header field, so ignore the header once a frame has been applied.
*/
if (r->stream->priority_update) {
return NGX_OK;
}
vals = priorities->elts;
if (priorities->nelts == 1) {
value = vals[0];
} else {
/* combine the field lines into a single value, "v1, v2, ..." */
len = 0;
for (i = 0; i < priorities->nelts; i++) {
len += vals[i].len + 2;
}
len -= 2;
p = ngx_pnalloc(r->pool, len);
if (p == NULL) {
ngx_http_v2_close_stream(r->stream,
NGX_HTTP_INTERNAL_SERVER_ERROR);
return NGX_ERROR;
}
value.data = p;
value.len = len;
for (i = 0; /* void */ ; i++) {
p = ngx_cpymem(p, vals[i].data, vals[i].len);
if (i == priorities->nelts - 1) {
break;
}
*p++ = ','; *p++ = ' ';
}
}
if (ngx_http_priority_parse(&value, &priority) != NGX_OK) {
ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
"http2 ignoring malformed priority header");
return NGX_OK;
}
r->stream->priority.client = priority;
ngx_http_priority_state_update(&r->stream->priority);
return NGX_OK;
}
static ngx_int_t
ngx_http_v2_construct_host_header(ngx_http_request_t *r)
{
@ -3980,6 +4100,10 @@ ngx_http_v2_run_request(ngx_http_request_t *r)
goto failed;
}
if (ngx_http_v2_process_priority(r) != NGX_OK) {
goto failed;
}
r->http_state = NGX_HTTP_PROCESS_REQUEST_STATE;
if (r->headers_in.connection) {

View file

@ -214,6 +214,7 @@ struct ngx_http_v2_stream_s {
ngx_queue_t queue;
ngx_array_t *cookies;
ngx_array_t *priorities;
ngx_http_priority_state_t priority;
@ -228,6 +229,7 @@ struct ngx_http_v2_stream_s {
unsigned rst_sent:1;
unsigned no_flow_control:1;
unsigned skip_data:1;
unsigned priority_update:1;
};