h3: initial version of extensible priority scheme
This builds on top of the QUIC-level priority support.
This change adds a new method `send_response_with_priority()`to
the HTTP/3 API. This takes a `priority` argument, a string that
contains the Extensible Priority header field value formatted
as Structured Headers e.g. "u=2, i". The contents of `priority`
is used for scheduling when sending response body bytes with
`send_body`.
The Extensible Priority scheme defines the defaults u=3 and i=?0.
Providing an empty value for `priority` will cause the defaults
to be used. The existing method `send_response()` has been updated
to send responses using defaults.
At this time, once a response is initiated it's priority cannot
changed.
No new API method is provided for clients to set request priority.
Clients can provide the `Priority` HTTP header in the `Headers`
argument of the `send_request()` method. This will be passed through
unmodified.
The quiche-client and quiche-server applications have been updated
to test this out. quiche-client can be run with e.g.
`-H "Priority:u=4,i"` to send a priority signal. quiche-server
process the Priority header (or assumes the default if none is
provided). It also supports a special query string syntax that
takes precedence over the header. For example, a client can send
`https://example.com?u=4&i=1` to indicate the priority as `u=1,i`.
Co-authored-by: Alessandro Ghedini <alessandro@ghedini.me>
diff --git a/include/quiche.h b/include/quiche.h
index b63fc2e..0ad39b8 100644
--- a/include/quiche.h
+++ b/include/quiche.h
@@ -480,11 +480,17 @@
quiche_h3_header *headers, size_t headers_len,
bool fin);
-// Sends an HTTP/3 response on the specified stream.
+// Sends an HTTP/3 response on the specified stream with default priority.
int quiche_h3_send_response(quiche_h3_conn *conn, quiche_conn *quic_conn,
uint64_t stream_id, quiche_h3_header *headers,
size_t headers_len, bool fin);
+// Sends an HTTP/3 response on the specified stream with specified priority.
+int quiche_h3_send_response_with_priority(quiche_h3_conn *conn,
+ quiche_conn *quic_conn, uint64_t stream_id,
+ quiche_h3_header *headers, size_t headers_len,
+ const char *priority, bool fin);
+
// Sends an HTTP/3 body chunk on the given stream.
ssize_t quiche_h3_send_body(quiche_h3_conn *conn, quiche_conn *quic_conn,
uint64_t stream_id, uint8_t *body, size_t body_len,
diff --git a/src/h3/ffi.rs b/src/h3/ffi.rs
index 50ffb1d..13b8394 100644
--- a/src/h3/ffi.rs
+++ b/src/h3/ffi.rs
@@ -24,10 +24,12 @@
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+use std::ffi;
use std::ptr;
use std::slice;
use std::str;
+use libc::c_char;
use libc::c_int;
use libc::c_void;
use libc::size_t;
@@ -198,6 +200,28 @@
}
#[no_mangle]
+pub extern fn quiche_h3_send_response_with_priority(
+ conn: &mut h3::Connection, quic_conn: &mut Connection, stream_id: u64,
+ headers: *const Header, headers_len: size_t, priority: *const c_char,
+ fin: bool,
+) -> c_int {
+ let resp_headers = headers_from_ptr(headers, headers_len);
+ let priority = unsafe { ffi::CStr::from_ptr(priority).to_str().unwrap() };
+
+ match conn.send_response_with_priority(
+ quic_conn,
+ stream_id,
+ &resp_headers,
+ &priority,
+ fin,
+ ) {
+ Ok(_) => 0,
+
+ Err(e) => e.to_c() as c_int,
+ }
+}
+
+#[no_mangle]
pub extern fn quiche_h3_send_body(
conn: &mut h3::Connection, quic_conn: &mut Connection, stream_id: u64,
body: *const u8, body_len: size_t, fin: bool,
diff --git a/src/h3/mod.rs b/src/h3/mod.rs
index cea128f..0697bd8 100644
--- a/src/h3/mod.rs
+++ b/src/h3/mod.rs
@@ -260,6 +260,9 @@
/// ../struct.Config.html#method.set_application_protos
pub const APPLICATION_PROTOCOL: &[u8] = b"\x05h3-29\x05h3-28\x05h3-27";
+// The offset used when converting HTTP/3 urgency to quiche urgency.
+const PRIORITY_URGENCY_OFFSET: u8 = 124;
+
/// A specialized [`Result`] type for quiche HTTP/3 operations.
///
/// This type is used throughout quiche's HTTP/3 public API for any operation
@@ -628,7 +631,7 @@
Ok(stream_id)
}
- /// Sends an HTTP/3 response on the specified stream.
+ /// Sends an HTTP/3 response on the specified stream with default priority.
///
/// This method sends the provided `headers` without a body. To include a
/// body, set `fin` as `false` and subsequently call [`send_body()`] with
@@ -645,6 +648,66 @@
&mut self, conn: &mut super::Connection, stream_id: u64,
headers: &[Header], fin: bool,
) -> Result<()> {
+ let priority = "u=3";
+
+ self.send_response_with_priority(
+ conn, stream_id, headers, priority, fin,
+ )?;
+
+ Ok(())
+ }
+
+ /// Sends an HTTP/3 response on the specified stream with specified
+ /// priority.
+ ///
+ /// The [`StreamBlocked`] error is returned when the underlying QUIC stream
+ /// doesn't have enough capacity for the operation to complete. When this
+ /// happens the application should retry the operation once the stream is
+ /// reported as writable again.
+ ///
+ /// [`StreamBlocked`]: enum.Error.html#variant.StreamBlocked
+ pub fn send_response_with_priority(
+ &mut self, conn: &mut super::Connection, stream_id: u64,
+ headers: &[Header], priority: &str, fin: bool,
+ ) -> Result<()> {
+ if !self.streams.contains_key(&stream_id) {
+ return Err(Error::FrameUnexpected);
+ }
+
+ let mut urgency = 3;
+ let mut incremental = false;
+
+ for param in priority.split(',') {
+ if param.trim() == "i" {
+ incremental = true;
+ continue;
+ }
+
+ if param.trim().starts_with("u=") {
+ // u is an sh-integer (an i64) but it has a constrained range of
+ // 0-7. So detect anything outside that range and clamp it to
+ // the lowest urgency in order to avoid it interfering with
+ // valid items.
+ //
+ // TODO: this also detects when u is not an sh-integer and
+ // clamps it in the same way. A real structured header parser
+ // would actually fail to parse.
+ let mut u =
+ i64::from_str_radix(param.rsplit('=').next().unwrap(), 10)
+ .unwrap_or(7);
+
+ if u < 0 || u > 7 {
+ u = 7;
+ }
+
+ // The HTTP/3 urgency needs to be shifted into the quiche
+ // urgency range.
+ urgency = (u as u8).saturating_add(PRIORITY_URGENCY_OFFSET);
+ }
+ }
+
+ conn.stream_priority(stream_id, urgency, incremental)?;
+
self.send_headers(conn, stream_id, headers, fin)?;
Ok(())
@@ -751,7 +814,7 @@
None => {
return Err(Error::FrameUnexpected);
},
- }
+ };
let overhead = octets::varint_len(frame::DATA_FRAME_TYPE_ID) +
octets::varint_len(body.len() as u64);
@@ -899,6 +962,23 @@
let mut d = [0; 8];
let mut b = octets::OctetsMut::with_slice(&mut d);
+ match ty {
+ // Control and QPACK streams are the most important to schedule.
+ stream::HTTP3_CONTROL_STREAM_TYPE_ID |
+ stream::QPACK_ENCODER_STREAM_TYPE_ID |
+ stream::QPACK_DECODER_STREAM_TYPE_ID => {
+ conn.stream_priority(stream_id, 0, true)?;
+ },
+
+ // TODO: Server push
+ stream::HTTP3_PUSH_STREAM_TYPE_ID => (),
+
+ // Anything else is a GREASE stream, so make it the least important.
+ _ => {
+ conn.stream_priority(stream_id, 255, true)?;
+ },
+ }
+
conn.stream_send(stream_id, b.put_varint(ty)?, false)?;
// To avoid skipping stream IDs, we only calculate the next available
diff --git a/tools/apps/src/lib.rs b/tools/apps/src/lib.rs
index 7eae3fc..3e637ee 100644
--- a/tools/apps/src/lib.rs
+++ b/tools/apps/src/lib.rs
@@ -677,12 +677,13 @@
/// Builds an HTTP/3 response given a request.
fn build_h3_response(
root: &str, index: &str, request: &[quiche::h3::Header],
- ) -> (Vec<quiche::h3::Header>, Vec<u8>) {
+ ) -> (Vec<quiche::h3::Header>, Vec<u8>, String) {
let mut file_path = path::PathBuf::from(root);
let mut scheme = "";
let mut host = "";
let mut path = "";
let mut method = "";
+ let mut priority = "";
// Parse some of the request headers.
for hdr in request {
@@ -703,6 +704,10 @@
method = hdr.value();
},
+ "priority" => {
+ priority = hdr.value();
+ },
+
_ => (),
}
}
@@ -713,7 +718,7 @@
quiche::h3::Header::new("server", "quiche"),
];
- return (headers, b"Invalid scheme".to_vec());
+ return (headers, b"Invalid scheme".to_vec(), priority.to_string());
}
let url = format!("{}://{}{}", scheme, host, path);
@@ -722,6 +727,23 @@
let pathbuf = path::PathBuf::from(url.path());
let pathbuf = autoindex(pathbuf, index);
+ // Priority query string takes precedence over the header.
+ // So replace the header with one built here.
+ let mut query_priority = "".to_string();
+ for param in url.query_pairs() {
+ if param.0 == "u" {
+ query_priority.push_str(&format!("{}={},", param.0, param.1));
+ }
+
+ if param.0 == "i" && param.1 == "1" {
+ query_priority.push_str("i,");
+ }
+ }
+
+ if !query_priority.is_empty() {
+ priority = &query_priority;
+ }
+
let (status, body) = match method {
"GET" => {
for c in pathbuf.components() {
@@ -744,9 +766,10 @@
quiche::h3::Header::new(":status", &status.to_string()),
quiche::h3::Header::new("server", "quiche"),
quiche::h3::Header::new("content-length", &body.len().to_string()),
+ quiche::h3::Header::new("priority", &priority),
];
- (headers, body)
+ (headers, body, priority.to_string())
}
}
@@ -918,13 +941,12 @@
conn.stream_shutdown(stream_id, quiche::Shutdown::Read, 0)
.unwrap();
- let (headers, body) =
+ let (headers, body, priority) =
Http3Conn::build_h3_response(root, index, &list);
- match self
- .h3_conn
- .send_response(conn, stream_id, &headers, false)
- {
+ match self.h3_conn.send_response_with_priority(
+ conn, stream_id, &headers, &priority, false,
+ ) {
Ok(v) => v,
Err(quiche::h3::Error::StreamBlocked) => {