HTTP/2 framing, HPACK and connection state, I/O-free
0

Configure Feed

Select the types of activity you want to include in your feed.

h2: spec test that a GOAWAY names the last processed peer stream

RFC 9113 6.8: the GOAWAY last-stream-id is "the highest-numbered stream identifier for which the sender of the GOAWAY frame might have taken some action on or might yet take action on". After the server processes a peer request on stream 3, both a connection-error GOAWAY (triggered here by an oversized frame, unrelated to any stream id) and a graceful Server.goaway must advertise last-stream-id 3, not 0. Advertising 0 tells the client none of its requests were acted upon, so it may safely retry a non-idempotent request the server already handled.

Both tests fail today: the emitted GOAWAY carries 0.

+81
+81
test/test_server.ml
··· 177 177 | _ -> None) 178 178 (frames_of bytes) 179 179 180 + (* last_stream_id of the first GOAWAY frame in [bytes], if any. *) 181 + let goaway_last_stream_id bytes = 182 + List.find_map 183 + (fun (f : Frame.t) -> 184 + match f.payload with 185 + | Frame.Goaway_payload { last_stream_id; _ } -> Some last_stream_id 186 + | _ -> None) 187 + (frames_of bytes) 188 + 180 189 let no_request events = 181 190 List.for_all (function Server.Request _ -> false | _ -> true) events 182 191 ··· 393 402 "connection terminal after PROTOCOL_ERROR" true 394 403 (Connection.is_closing (Server.connection server)) 395 404 405 + (* RFC 9113 sec 6.8: the GOAWAY last-stream-id is "the highest-numbered stream 406 + identifier for which the sender of the GOAWAY frame might have taken some 407 + action on or might yet take action on". Once the server has processed a peer 408 + stream, a GOAWAY MUST advertise that stream's id, not 0: advertising 0 tells 409 + the client none of its requests were acted upon, so a client may safely retry 410 + a non-idempotent request the server already handled. A valid request on 411 + stream 3 advances the highest processed peer id to 3; a subsequent connection 412 + error (here an oversized frame, unrelated to any stream id) must GOAWAY with 413 + last-stream-id 3, not 0. *) 414 + let test_goaway_last_stream_id_reflects_processed_stream () = 415 + let server, hpack, _ = Server.v () in 416 + let server = setup server hpack in 417 + let client = Connection.hpack Connection.Client in 418 + let hblock = 419 + Connection.encode_headers client ~max_frame_size:16384 420 + [ mk ":method" "GET"; mk ":scheme" "http"; mk ":path" "/" ] 421 + in 422 + let server, events, _ = 423 + Server.incoming server hpack 424 + (fb 425 + (Frame.headers ~stream_id:3l ~end_stream:true ~end_headers:true hblock)) 426 + in 427 + Alcotest.(check bool) 428 + "valid request on stream 3 surfaced" true (request_for 3l events); 429 + (* An oversized frame is a connection error unrelated to any stream id. *) 430 + let oversized = 431 + Bytes.unsafe_to_string 432 + (Frame.serialize_header 433 + { 434 + Frame.length = 0x100000; 435 + kind = Frame.Data; 436 + flags = Frame.Flags.none; 437 + stream_id = 1l; 438 + }) 439 + in 440 + let _server, _events, out = Server.incoming server hpack oversized in 441 + match goaway_last_stream_id out with 442 + | Some id -> 443 + Alcotest.(check int32) 444 + "GOAWAY advertises the highest processed peer stream" 3l id 445 + | None -> 446 + Alcotest.fail "expected a GOAWAY on the oversized frame, none emitted" 447 + 448 + (* RFC 9113 sec 6.8: a graceful GOAWAY (NO_ERROR) likewise names the last stream 449 + the sender will process -- the highest peer id already seen. After a request 450 + on stream 3, Server.goaway must advertise last-stream-id 3, not 0. *) 451 + let test_graceful_goaway_last_stream_id () = 452 + let server, hpack, _ = Server.v () in 453 + let server = setup server hpack in 454 + let client = Connection.hpack Connection.Client in 455 + let hblock = 456 + Connection.encode_headers client ~max_frame_size:16384 457 + [ mk ":method" "GET"; mk ":scheme" "http"; mk ":path" "/" ] 458 + in 459 + let server, _events, _ = 460 + Server.incoming server hpack 461 + (fb 462 + (Frame.headers ~stream_id:3l ~end_stream:true ~end_headers:true hblock)) 463 + in 464 + let _server, out = Server.goaway server Frame.No_error "shutdown" in 465 + match goaway_last_stream_id out with 466 + | Some id -> 467 + Alcotest.(check int32) 468 + "graceful GOAWAY advertises the highest processed peer stream" 3l id 469 + | None -> Alcotest.fail "expected a GOAWAY, none emitted" 470 + 396 471 (* RFC 9113 sec 3.4: "each endpoint is required to send a connection 397 472 preface as a final confirmation of the protocol in use". The client 398 473 connection preface "starts with a sequence of 24 octets" (the magic) ··· 479 554 ( "HEADERS on a reused/lower stream id is a connection error", 480 555 `Quick, 481 556 test_headers_reused_stream_id ); 557 + ( "GOAWAY last-stream-id reflects the processed peer stream", 558 + `Quick, 559 + test_goaway_last_stream_id_reflects_processed_stream ); 560 + ( "graceful GOAWAY last-stream-id reflects the processed peer stream", 561 + `Quick, 562 + test_graceful_goaway_last_stream_id ); 482 563 ( "server Open after the client's preface", 483 564 `Quick, 484 565 test_server_open_after_client_preface );