go icon indicating copy to clipboard operation
go copied to clipboard

proposal: x/net/http2: add support for RFC 9218 priorities

Open nicholashusin opened this issue 2 months ago • 22 comments

Proposal Details

In x/net/http2, http2.WriteScheduler is used to determine the order in which data is written to HTTP/2 streams.

x/net/http2 currently implements three write schedulers: round-robin (the default), random, and priority. The priority write scheduler is buggy, CPU hungry, and based off a prioritization scheme from RFC 7540 that has been deprecated as of RFC 9113 (see #67817). As part of moving x/net/http2 into std (#67810), we would ideally like to deprecate this write scheduler.

While most users are likely to use the default round-robin schedulers, users of the priority write scheduler does exist, which prevents us from deprecating it. Therefore, to move #67817 along, we want to add support for RFC 9218 priority, such that users can move off the old RFC 7540 priority scheduler.

I propose that we take the following actions:

1. Implement RFC 9218 priority write scheduler

To overly-simplify https://www.rfc-editor.org/rfc/rfc9218.html, we will create a new priority write scheduler that prioritizes streams based on priority provided by clients. Like the round-robin write scheduler, this scheduler and its constructor will not be exported. The priority provided by client is composed of two aspects:

  • urgency: a number between 0 and 7, inclusive, where lower number denotes a higher urgency. Defaults to 3.
  • incremental: a boolean, where false indicates that a stream can benefit from getting partial chunks, rather than needing to wait for the complete response. Defaults to false.

Our new scheduler can then make the following prioritization:

  • It will prioritize streams with lower urgency value (i.e. highest urgency), before streams with higher urgency value.
  • Given a group of incremental streams, it will round-robin writing between them, as they can all benefit immediately from partial responses. Conversely, for non-incremental streams, the scheduler will focus on writing to streams one-by-one till completion.
  • If streams are coming from a proxy / intermediary, we override all received priority to urgency=3 and incremental=true to ensure fairness among end-users of the proxy.

At this stage, there are no API changes, and the write scheduler is not really usable end-to-end yet.

2. Implement RFC 9218 end-to-end

To allow the RFC 9218 prioritization to work end-to-end, we need to propagate the priority that clients provide (via headers and/or PRIORITY_UPDATE frame) to our write scheduler. Doing so involves the following steps and corresponding API changes:

Framer support for PRIORITY_UPDATE frame

We will add support for reading and writing RFC 9218 PRIORITY_UPDATE frames to http2.Framer. Strictly speaking, this does not need to be an exported API, but doing so is consistent with handling for other frames. Exported API changes are as follows:

package http2

const FramePriorityUpdate FrameType = 0x10

type PriorityUpdateFrame struct {
    FrameHeader
    Priority string
    PrioritizedStreamID uint32
}

func (f *Framer) WritePriorityUpdate(streamID uint32, priority string) error {} 

New SETTINGS_NO_RFC7540_PRIORITIES setting

RFC 9218 defines a SETTINGS_NO_RFC7540_PRIORITIES setting to permit clients or servers to indicate that they are not using the deprecated RFC 7540 prioritization scheme.

We will add a const for this setting, matching other settings handled by the http2 package:

package http2

const SettingNoRFC7540Priorities SettingID = 0x9

RFC 9218 does not provide a prioritization scheme negotiation mechanism. There is no way for a server to indicate that it supports RFC 9218 priorities, but can fall back to RFC 7540 when available.

When the HTTP/2 server uses a write scheduler that we know does not use RFC 7540 priorities (round robin or the new RFC 9218 scheduler), we will send a SETTINGS_NO_RFC7540_PRIORITIES value of 1 in its initial SETTINGS. Consequently, any PRIORITY frame that the server receives will be ignored.

3. Use the RFC 9218 priority write scheduler as the default

After RFC 9218 support is fully implemented, we can swap out the default scheduler that user gets from the round-robin scheduler to the RFC 9218 priority scheduler.

Worth noting here, is that there will be a significant behavior change if we follow the RFC 9218 recommendation strictly:

  • The current default round-robin scheduler distributes writes evenly across all streams.
  • With the new priority scheduler, it is likely that the majority of streams will be handled one-by-one until completion. This is because the default priority has incremental as false. Under the assumption that many HTTP/2 requests do not provide priority, most of the streams received will cause our scheduler to avoid round-robin.

To avoid a sudden significant behavior change for those who might be unaware of RFC 9218, we will only use false as the default incremental value for connections that has indicated that it is aware of RFC 9218 priorities. That is to say:

  • When a client has never sent a priority header or PRIORITY_UPDATE frame, we will use urgency=3 and incremental=true as the default value for all of its streams.
  • Once the client has sent a priority header or PRIORITY_UPDATE frame at least once, we will use urgency=3 and incremental=false as the default value.

This ensures that clients who do not utilize RFC 9218 will retain its previous behavior (urgency=3 and incremental=true for all streams are effectively round-robin).

Additionally, we will also add a new field within http.Server to allow users to disable stream prioritization if they so choose:

type Server struct {
  // Existing fields...
  DisableClientPriority bool // When true, round-robin across all streams.
}

After this is done, #67817 can then make further progress on deprecating the http2.WriteScheduler interface.

nicholashusin avatar Sep 17 '25 13:09 nicholashusin