HTTP/2: handle PRIORITY_UPDATE frames

Implement the RFC 9218 PRIORITY_UPDATE frame (type 0x10).  The frame is
sent on stream 0 and carries a prioritized stream id and a Priority
Field Value; it is validated for framing errors and dispatched from
ngx_http_v2_state_head().

A PRIORITY_UPDATE conveys a complete set of parameters (RFC 9218,
Section 7): omitted parameters reset to their defaults, so the value
replaces any earlier signal.  A frame for a stream that already exists
is applied at once.  A frame for a not-yet-created stream is buffered on
its node until the HEADERS frame arrives; the node is parked in
h2c->closed and, while the RFC 7540 tree is still present, attached to
the root as is done for a PRIORITY frame with a zero dependency.  The
number of buffered signals is bounded by the node reuse budget.

A malformed value is logged and ignored, leaving the current priority
untouched.  A value that does not fit the state buffer while the frame
is fragmented is skipped.

The signals are only collected here; the scheduler still uses the
RFC 7540 dependency tree until the switch to RFC 9218.

Assisted-by: opencode:claude-opus-4.8
This commit is contained in:
Andrew Clayton 2026-09-23 21:17:33 +01:00
parent b5bd4b3487
commit 2b6c2de41d
2 changed files with 159 additions and 10 deletions

View file

@ -85,6 +85,8 @@ static u_char *ngx_http_v2_handle_continuation(ngx_http_v2_connection_t *h2c,
u_char *pos, u_char *end, ngx_http_v2_handler_pt handler);
static u_char *ngx_http_v2_state_priority(ngx_http_v2_connection_t *h2c,
u_char *pos, u_char *end);
static u_char *ngx_http_v2_state_priority_update(ngx_http_v2_connection_t *h2c,
u_char *pos, u_char *end);
static u_char *ngx_http_v2_state_rst_stream(ngx_http_v2_connection_t *h2c,
u_char *pos, u_char *end);
static u_char *ngx_http_v2_state_settings(ngx_http_v2_connection_t *h2c,
@ -907,6 +909,11 @@ ngx_http_v2_state_head(ngx_http_v2_connection_t *h2c, u_char *pos, u_char *end)
type, h2c->state.flags, h2c->state.length, h2c->state.sid);
if (type >= NGX_HTTP_V2_FRAME_STATES) {
if (type == NGX_HTTP_V2_PRIORITY_UPDATE_FRAME) {
return ngx_http_v2_state_priority_update(h2c, pos, end);
}
ngx_log_error(NGX_LOG_INFO, h2c->connection->log, 0,
"client sent frame with unknown type %ui", type);
return ngx_http_v2_state_skip(h2c, pos, end);
@ -1342,6 +1349,20 @@ ngx_http_v2_state_headers(ngx_http_v2_connection_t *h2c, u_char *pos,
ngx_http_v2_set_dependency(h2c, node, depend, excl);
}
/* a PRIORITY_UPDATE may have arrived before HEADERS */
if (node->priority_set) {
stream->priority.client = node->priority;
node->priority_set = 0;
ngx_http_priority_state_update(&stream->priority);
ngx_log_debug2(NGX_LOG_DEBUG_HTTP, h2c->connection->log, 0,
"http2 buffered priority: u=%ud i=%ud",
stream->priority.effective.urgency,
stream->priority.effective.incremental);
}
clcf = ngx_http_get_module_loc_conf(h2c->http_connection->conf_ctx,
ngx_http_core_module);
@ -2066,6 +2087,127 @@ ngx_http_v2_state_priority(ngx_http_v2_connection_t *h2c, u_char *pos,
}
static u_char *
ngx_http_v2_state_priority_update(ngx_http_v2_connection_t *h2c, u_char *pos,
u_char *end)
{
size_t len;
ngx_str_t value;
ngx_uint_t sid;
ngx_http_v2_node_t *node;
ngx_http_priority_t priority;
ngx_http_v2_stream_t *stream;
if (h2c->state.sid != 0) {
ngx_log_error(NGX_LOG_INFO, h2c->connection->log, 0,
"client sent PRIORITY_UPDATE frame with incorrect "
"identifier");
return ngx_http_v2_connection_error(h2c, NGX_HTTP_V2_PROTOCOL_ERROR);
}
if (h2c->state.length < 4) {
ngx_log_error(NGX_LOG_INFO, h2c->connection->log, 0,
"client sent PRIORITY_UPDATE frame with incorrect "
"length %uz", h2c->state.length);
return ngx_http_v2_connection_error(h2c, NGX_HTTP_V2_SIZE_ERROR);
}
len = h2c->state.length;
if ((size_t) (end - pos) < len) {
if (len > NGX_HTTP_V2_STATE_BUFFER_SIZE) {
return ngx_http_v2_state_skip(h2c, pos, end);
}
return ngx_http_v2_state_save(h2c, pos, end,
ngx_http_v2_state_priority_update);
}
sid = ngx_http_v2_parse_sid(pos);
if (sid % 2 == 0) {
ngx_log_error(NGX_LOG_INFO, h2c->connection->log, 0,
"client sent PRIORITY_UPDATE frame with incorrect "
"prioritized stream %ui", sid);
return ngx_http_v2_connection_error(h2c, NGX_HTTP_V2_PROTOCOL_ERROR);
}
value.data = pos + 4;
value.len = len - 4;
pos += len;
if (ngx_http_priority_parse(&value, &priority) != NGX_OK) {
ngx_log_error(NGX_LOG_INFO, h2c->connection->log, 0,
"client sent PRIORITY_UPDATE frame with malformed value "
"for stream %ui", sid);
return ngx_http_v2_state_complete(h2c, pos, end);
}
ngx_log_debug3(NGX_LOG_DEBUG_HTTP, h2c->connection->log, 0,
"http2 PRIORITY_UPDATE frame sid:%ui u=%ud i=%ud",
sid, priority.urgency, priority.incremental);
/*
* A PRIORITY_UPDATE conveys a complete set of parameters (RFC 9218,
* Section 7): omitted parameters reset to their defaults, so the result
* replaces any earlier signal rather than merging with it.
*/
node = ngx_http_v2_get_node_by_id(h2c, sid, sid > h2c->last_sid);
if (sid > h2c->last_sid) {
/*
* The stream is not created yet; park the node in h2c->closed holding
* the buffered priority. While the RFC 7540 tree is still present a
* parked node has to be part of it, so attach it to the root as is
* done for a PRIORITY frame with a zero dependency.
*
* RFC 9218, Section 7.1, bounds idle prioritized streams together with
* active ones and requires a connection error on overflow; here idle
* signals are bounded on their own by the node reuse budget and the
* oldest is dropped instead, which is deliberately more lenient.
*/
if (node == NULL) {
return ngx_http_v2_connection_error(h2c,
NGX_HTTP_V2_INTERNAL_ERROR);
}
if (node->parent == NULL) {
h2c->closed_nodes++;
} else {
ngx_queue_remove(&node->reuse);
}
ngx_queue_insert_tail(&h2c->closed, &node->reuse);
if (node->parent == NULL) {
/* a node in the closed queue is expected to be in the tree */
node->weight = NGX_HTTP_V2_DEFAULT_WEIGHT;
ngx_http_v2_set_dependency(h2c, node, 0, 0);
}
node->priority = priority;
node->priority_set = 1;
} else if (node && node->stream) {
stream = node->stream;
stream->priority.client = priority;
ngx_http_priority_state_update(&stream->priority);
}
return ngx_http_v2_state_complete(h2c, pos, end);
}
static u_char *
ngx_http_v2_state_rst_stream(ngx_http_v2_connection_t *h2c, u_char *pos,
u_char *end)
@ -3106,6 +3248,8 @@ ngx_http_v2_create_stream(ngx_http_v2_connection_t *h2c)
stream->request = r;
stream->connection = h2c;
ngx_http_priority_state_init(&stream->priority);
h2scf = ngx_http_get_module_srv_conf(r, ngx_http_v2_module);
stream->send_window = h2c->init_window;

View file

@ -27,16 +27,17 @@
#define NGX_HTTP_V2_FRAME_HEADER_SIZE 9
/* frame types */
#define NGX_HTTP_V2_DATA_FRAME 0x0
#define NGX_HTTP_V2_HEADERS_FRAME 0x1
#define NGX_HTTP_V2_PRIORITY_FRAME 0x2
#define NGX_HTTP_V2_RST_STREAM_FRAME 0x3
#define NGX_HTTP_V2_SETTINGS_FRAME 0x4
#define NGX_HTTP_V2_PUSH_PROMISE_FRAME 0x5
#define NGX_HTTP_V2_PING_FRAME 0x6
#define NGX_HTTP_V2_GOAWAY_FRAME 0x7
#define NGX_HTTP_V2_WINDOW_UPDATE_FRAME 0x8
#define NGX_HTTP_V2_CONTINUATION_FRAME 0x9
#define NGX_HTTP_V2_DATA_FRAME 0x00
#define NGX_HTTP_V2_HEADERS_FRAME 0x01
#define NGX_HTTP_V2_PRIORITY_FRAME 0x02
#define NGX_HTTP_V2_RST_STREAM_FRAME 0x03
#define NGX_HTTP_V2_SETTINGS_FRAME 0x04
#define NGX_HTTP_V2_PUSH_PROMISE_FRAME 0x05
#define NGX_HTTP_V2_PING_FRAME 0x06
#define NGX_HTTP_V2_GOAWAY_FRAME 0x07
#define NGX_HTTP_V2_WINDOW_UPDATE_FRAME 0x08
#define NGX_HTTP_V2_CONTINUATION_FRAME 0x09
#define NGX_HTTP_V2_PRIORITY_UPDATE_FRAME 0x10
/* frame flags */
#define NGX_HTTP_V2_NO_FLAG 0x00
@ -183,6 +184,8 @@ struct ngx_http_v2_node_s {
ngx_uint_t weight;
double rel_weight;
ngx_http_v2_stream_t *stream;
ngx_http_priority_t priority;
ngx_uint_t priority_set; /* unsigned priority_set:1 */
};
@ -212,6 +215,8 @@ struct ngx_http_v2_stream_s {
ngx_array_t *cookies;
ngx_http_priority_state_t priority;
ngx_pool_t *pool;
unsigned initialized:1;