http3 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/ext/http3/session.c CHANGED
@@ -6,6 +6,7 @@
6
6
  #include <stdlib.h>
7
7
  #include <string.h>
8
8
  #include <sys/random.h>
9
+ #include <time.h>
9
10
  #include <unistd.h>
10
11
 
11
12
  /*
@@ -28,7 +29,7 @@
28
29
  VALUE rb_cHTTP3Session;
29
30
 
30
31
  static ID id_on_begin_headers, id_on_header, id_on_end_headers, id_on_data,
31
- id_on_end_stream, id_on_fields_too_large;
32
+ id_on_end_stream, id_on_fields_too_large, id_on_reset, id_on_stop_sending;
32
33
 
33
34
  /* What h3_session.field_sizes holds for a request whose header section has
34
35
  * gone over max_field_section_size. */
@@ -65,19 +66,28 @@ typedef struct {
65
66
  size_t nchunks, cap;
66
67
  size_t handed; /* how many of chunks read_data has handed to nghttp3 */
67
68
  uint64_t unsent; /* bytes queued but not handed to nghttp3 yet */
69
+ uint64_t queued; /* bytes ever queued, the pieces read from a file too */
68
70
  uint64_t acked; /* bytes the peer has acknowledged */
69
71
  uint64_t shifted; /* bytes of the chunks already dropped */
70
72
  int fd; /* a file body (#attach_file) not read in full yet, or -1 */
71
73
  off_t file_off, file_end;
72
- bool eof; /* nothing follows the queued chunks */
73
- bool blocked; /* read_data returned NGHTTP3_ERR_WOULDBLOCK */
74
+ /* When the body began to wait on the peer, or last moved: the peer
75
+ * acknowledged some of it, or widened the stream's flow control window
76
+ * (CLOCK_MONOTONIC seconds; #response_stalled_since) */
77
+ double since;
78
+ bool fc_blocked; /* the peer's flow control window holds it back */
79
+ bool eof; /* nothing follows the queued chunks */
80
+ bool blocked; /* read_data returned NGHTTP3_ERR_WOULDBLOCK */
81
+ bool aborted; /* #abort_stream stopped it: no more of the body is sent */
74
82
  } h3_stream;
75
83
 
76
84
  typedef struct {
77
85
  VALUE self; /* the object wrapping this struct; dcompact follows it */
78
86
  VALUE quic_conn; /* the QUIC::Connection::Server */
79
87
  quic_ext_conn *qc;
80
- nghttp3_conn *h3; /* made on handshake_completed */
88
+ nghttp3_conn *h3; /* made on handshake_completed, deleted by h3_fail */
89
+ bool failed; /* h3_fail has been called: h3 is gone for good */
90
+ int in_callback; /* how deep the Ruby part is in h3_protect */
81
91
  st_table *streams; /* stream_id => h3_stream * */
82
92
  VALUE pending_exc; /* an exception the Ruby part raised in a callback */
83
93
  VALUE closed_ids; /* ids of the streams nghttp3 closed, for #take_closed */
@@ -87,8 +97,18 @@ typedef struct {
87
97
  /* stream_id => size of the header section received so far, while the
88
98
  * headers of that request come in */
89
99
  st_table *field_sizes;
100
+ /* When the peer last acknowledged some response body, on any stream
101
+ * (CLOCK_MONOTONIC seconds) */
102
+ double acked_at;
90
103
  } h3_session;
91
104
 
105
+ /* CLOCK_MONOTONIC in seconds, as Process.clock_gettime gives it. */
106
+ static double h3_now(void) {
107
+ struct timespec ts;
108
+ clock_gettime(CLOCK_MONOTONIC, &ts);
109
+ return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9;
110
+ }
111
+
92
112
  /* Streams */
93
113
 
94
114
  static void h3_chunk_release(h3_chunk *chunk) {
@@ -128,9 +148,20 @@ static int h3_stream_push(h3_stream *st, const uint8_t *base, size_t len,
128
148
  }
129
149
  st->chunks[st->nchunks++] = (h3_chunk){base, len, str};
130
150
  st->unsent += len;
151
+ st->queued += len;
131
152
  return 0;
132
153
  }
133
154
 
155
+ /* Bytes of the body the peer has not acknowledged: queued, the rest of a
156
+ * file not read yet, or sent; 0 once the stream has been aborted. */
157
+ static uint64_t h3_stream_unacked(const h3_stream *st) {
158
+ if (st->aborted) {
159
+ return 0;
160
+ }
161
+ uint64_t rest = st->fd >= 0 ? (uint64_t)(st->file_end - st->file_off) : 0;
162
+ return st->queued - st->acked + rest;
163
+ }
164
+
134
165
  /* The whole file body has been read: let go of its descriptor now rather
135
166
  * than when the stream closes, which waits for the peer to acknowledge
136
167
  * every byte. */
@@ -140,6 +171,24 @@ static void h3_stream_end_file(h3_stream *st) {
140
171
  st->eof = true;
141
172
  }
142
173
 
174
+ /* The stream has been aborted, and no more of its body will be sent: let go
175
+ * at once of what nghttp3 has not taken, and of the file's descriptor,
176
+ * rather than when the stream closes, which waits for the peer to
177
+ * acknowledge the reset. The chunks nghttp3 has taken stay until then,
178
+ * since it still points into them. */
179
+ static void h3_stream_abort(h3_stream *st) {
180
+ for (size_t i = st->handed; i < st->nchunks; i++) {
181
+ h3_chunk_release(&st->chunks[i]);
182
+ }
183
+ st->nchunks = st->handed;
184
+ st->unsent = 0;
185
+ if (st->fd >= 0) {
186
+ close(st->fd);
187
+ st->fd = -1;
188
+ }
189
+ st->aborted = true;
190
+ }
191
+
143
192
  /* Drop the chunks at the front the peer has acknowledged to their end. A
144
193
  * chunk acknowledged only in part stays. */
145
194
  static void h3_stream_acked(h3_stream *st, uint64_t n) {
@@ -163,7 +212,9 @@ static void h3_stream_acked(h3_stream *st, uint64_t n) {
163
212
 
164
213
  static int h3_protect(h3_session *s, VALUE (*body)(VALUE), VALUE arg) {
165
214
  int state = 0;
215
+ s->in_callback++;
166
216
  rb_protect(body, arg, &state);
217
+ s->in_callback--;
167
218
  if (state == 0) {
168
219
  return 0;
169
220
  }
@@ -272,7 +323,14 @@ static VALUE h3_end_headers_body(VALUE arg) {
272
323
  static int h3_acked_stream_data_cb(nghttp3_conn *conn, int64_t id,
273
324
  uint64_t datalen, void *user_data,
274
325
  void *stream_user_data) {
275
- h3_stream *st = h3_stream_find(user_data, id);
326
+ h3_session *s = user_data;
327
+ h3_stream *st = h3_stream_find(s, id);
328
+ if (datalen > 0) {
329
+ s->acked_at = h3_now();
330
+ if (st != NULL) {
331
+ st->since = s->acked_at;
332
+ }
333
+ }
276
334
  if (st != NULL) {
277
335
  h3_stream_acked(st, datalen);
278
336
  }
@@ -437,10 +495,43 @@ static const nghttp3_data_reader h3_data_reader = {.read_data =
437
495
 
438
496
  /* Stream handler (quic -> glue). ud is the h3_session. */
439
497
 
498
+ /* The HTTP/3 connection has failed: the QUIC connection is to close with
499
+ * app_error_code, and the nghttp3 connection goes, since nghttp3 allows
500
+ * nothing but nghttp3_conn_del once it has returned an error. From then on
501
+ * the handler fails every read and write, so that the QUIC connection
502
+ * closes even when the failure came from a call the Ruby part made. Never
503
+ * called while nghttp3 is running. */
504
+ static int h3_fail_with(h3_session *s, uint64_t app_error_code) {
505
+ http3_quic_api->set_application_error(s->qc, app_error_code);
506
+ if (s->h3 != NULL) {
507
+ nghttp3_conn_del(s->h3);
508
+ s->h3 = NULL;
509
+ }
510
+ s->failed = true;
511
+ return -1;
512
+ }
513
+
514
+ /* A failure inside nghttp3, with the HTTP/3 error code it stands for. */
515
+ static int h3_fail(h3_session *s, int64_t rv) {
516
+ return h3_fail_with(s, nghttp3_err_infer_quic_app_error_code((int)rv));
517
+ }
518
+
519
+ /* Raise for rv, which a call the Ruby part made into nghttp3 returned. A
520
+ * fatal error fails the connection first. */
521
+ static void h3_raise(h3_session *s, int rv, const char *message) {
522
+ if (nghttp3_err_is_fatal(rv) || rv == NGHTTP3_ERR_QPACK_FATAL) {
523
+ h3_fail(s, rv);
524
+ }
525
+ nghttp3_rb_raise(rv, "%s", message);
526
+ }
527
+
440
528
  /* Make the nghttp3 connection and its three unidirectional streams, as
441
529
  * ngtcp2's examples/server.cc does once the handshake is done. */
442
530
  static int h3_handshake_completed(void *ud) {
443
531
  h3_session *s = ud;
532
+ if (s->failed) {
533
+ return -1;
534
+ }
444
535
  if (s->h3 != NULL) {
445
536
  return 0;
446
537
  }
@@ -466,34 +557,39 @@ static int h3_handshake_completed(void *ud) {
466
557
 
467
558
  if (nghttp3_conn_server_new(&s->h3, &callbacks, &settings, NULL, s) != 0) {
468
559
  s->h3 = NULL;
469
- return -1;
560
+ return h3_fail_with(s, NGHTTP3_H3_INTERNAL_ERROR);
470
561
  }
471
- nghttp3_conn_set_max_client_streams_bidi(
472
- s->h3, http3_quic_api->local_max_streams_bidi(s->qc));
562
+ uint64_t max_streams = http3_quic_api->local_max_streams_bidi(s->qc);
563
+ nghttp3_conn_set_max_client_streams_bidi(s->h3, max_streams);
564
+ /* Every request #stop_reading gives up on leaves a Stream Cancellation on
565
+ * the QPACK decoder stream, and nghttp3 fails the connection once that
566
+ * stream holds more than max(this, 100) * 20 bytes. */
567
+ nghttp3_conn_set_max_concurrent_streams(s->h3, (size_t)max_streams);
473
568
 
474
569
  int64_t ctrl, enc, dec;
475
- if (http3_quic_api->open_uni_stream(s->qc, &ctrl) != 0 ||
476
- http3_quic_api->open_uni_stream(s->qc, &enc) != 0 ||
477
- http3_quic_api->open_uni_stream(s->qc, &dec) != 0 ||
570
+ int rv = 0;
571
+ if ((rv = http3_quic_api->open_uni_stream(s->qc, &ctrl)) != 0 ||
572
+ (rv = http3_quic_api->open_uni_stream(s->qc, &enc)) != 0 ||
573
+ (rv = http3_quic_api->open_uni_stream(s->qc, &dec)) != 0 ||
478
574
  nghttp3_conn_bind_control_stream(s->h3, ctrl) != 0 ||
479
575
  nghttp3_conn_bind_qpack_streams(s->h3, enc, dec) != 0) {
480
- return -1;
576
+ /* nghttp3 asserts the streams are bound before most calls, so the half
577
+ * made connection must go. A client has to let the server open three
578
+ * unidirectional streams (RFC 9114 section 6.2). */
579
+ return h3_fail_with(s, rv == QUIC_EXT_ERR_STREAM_ID_BLOCKED
580
+ ? NGHTTP3_H3_GENERAL_PROTOCOL_ERROR
581
+ : NGHTTP3_H3_INTERNAL_ERROR);
481
582
  }
482
583
  return 0;
483
584
  }
484
585
 
485
- /* A failure inside nghttp3 closes the QUIC connection with the HTTP/3 error
486
- * code it stands for. */
487
- static int h3_fail(h3_session *s, int64_t rv) {
488
- http3_quic_api->set_application_error(
489
- s->qc, nghttp3_err_infer_quic_app_error_code((int)rv));
490
- return -1;
491
- }
492
-
493
586
  static int h3_recv_stream_data(void *ud, int64_t id, const uint8_t *data,
494
587
  size_t len, int fin, uint64_t ts,
495
588
  size_t *consumed) {
496
589
  h3_session *s = ud;
590
+ if (s->failed) {
591
+ return -1;
592
+ }
497
593
  if (s->h3 == NULL) {
498
594
  /* Nothing can arrive on a stream before the handshake; drop it. */
499
595
  *consumed = len;
@@ -513,6 +609,9 @@ static int h3_next_stream_data(void *ud, int64_t *id, int *fin,
513
609
  *id = -1;
514
610
  *fin = 0;
515
611
  *nvecs = 0;
612
+ if (s->failed) {
613
+ return -1;
614
+ }
516
615
  if (s->h3 == NULL) {
517
616
  return 0;
518
617
  }
@@ -534,14 +633,22 @@ static int h3_stream_data_written(void *ud, int64_t id, size_t n) {
534
633
  return rv == 0 ? 0 : h3_fail(s, rv);
535
634
  }
536
635
 
636
+ /* The peer's flow control window for the stream is used up. */
537
637
  static int h3_stream_blocked(void *ud, int64_t id) {
538
638
  h3_session *s = ud;
639
+ h3_stream *st = h3_stream_find(s, id);
640
+ if (st != NULL) {
641
+ st->fc_blocked = true;
642
+ }
539
643
  if (s->h3 != NULL) {
540
644
  nghttp3_conn_block_stream(s->h3, id);
541
645
  }
542
646
  return 0;
543
647
  }
544
648
 
649
+ /* quic found our side of the stream shut when next_stream_data offered it:
650
+ * nghttp3 sends no more of it. recv_stop_sending and #abort_stream tell
651
+ * nghttp3 first, so this is what is left for a stream shut otherwise. */
545
652
  static int h3_stream_shut_write(void *ud, int64_t id) {
546
653
  h3_session *s = ud;
547
654
  if (s->h3 != NULL) {
@@ -559,8 +666,15 @@ static int h3_acked_stream_data(void *ud, int64_t id, uint64_t n) {
559
666
  return rv == 0 ? 0 : h3_fail(s, rv);
560
667
  }
561
668
 
669
+ /* The peer widened the stream's flow control window: it has read some of
670
+ * the body. */
562
671
  static int h3_extend_max_stream_data(void *ud, int64_t id) {
563
672
  h3_session *s = ud;
673
+ h3_stream *st = h3_stream_find(s, id);
674
+ if (st != NULL) {
675
+ st->fc_blocked = false;
676
+ st->since = h3_now();
677
+ }
564
678
  if (s->h3 == NULL) {
565
679
  return 0;
566
680
  }
@@ -589,9 +703,61 @@ static int h3_stream_stop_sending(void *ud, int64_t id,
589
703
  return h3_shutdown_stream_read(ud, id);
590
704
  }
591
705
 
706
+ /* The client reset its side of the stream. The Ruby part answers a request
707
+ * cut short by it (#on_reset). */
592
708
  static int h3_stream_reset(void *ud, int64_t id, uint64_t final_size,
593
709
  uint64_t app_error_code) {
594
- return h3_shutdown_stream_read(ud, id);
710
+ h3_session *s = ud;
711
+ int rv = h3_shutdown_stream_read(s, id);
712
+ if (rv != 0 || s->h3 == NULL) {
713
+ return rv;
714
+ }
715
+ return h3_call_ruby(s, id_on_reset, id, 0, NULL);
716
+ }
717
+
718
+ /* The client asked for no more of what goes out on the stream
719
+ * (STOP_SENDING); ngtcp2 resets our side of it once this returns. Of a
720
+ * response, what nghttp3 has not taken is let go at once, and nghttp3
721
+ * sends no more. The rest of the request is not read either: RFC 9114
722
+ * section 4.1.1 has a cancelled request stopped in every direction still
723
+ * open. That is done here, as #stop_reading does it, rather than left to
724
+ * the Ruby part, so that nothing more of the request comes in, from the
725
+ * packets read after this one too, even on a stream the Ruby part has not
726
+ * heard of yet, since STOP_SENDING may come before the request. quic sends
727
+ * nothing if all of the request has arrived. The Ruby part then drops the
728
+ * request (#on_stop_sending).
729
+ *
730
+ * Our unidirectional streams are the control and QPACK streams, which the
731
+ * client must not ask us to close, and whose closing is a connection error
732
+ * of type H3_CLOSED_CRITICAL_STREAM (RFC 9114 section 6.2.1, RFC 9204
733
+ * section 4.2): the connection fails at once, rather than when the stream
734
+ * has closed. The client's own unidirectional streams never get here:
735
+ * ngtcp2 refuses STOP_SENDING on a stream we only receive on. */
736
+ static int h3_recv_stop_sending(void *ud, int64_t id, uint64_t app_error_code) {
737
+ h3_session *s = ud;
738
+ if (s->h3 == NULL) {
739
+ return 0;
740
+ }
741
+ if (id & 0x2) {
742
+ return h3_fail_with(s, NGHTTP3_H3_CLOSED_CRITICAL_STREAM);
743
+ }
744
+ h3_stream *st = h3_stream_find(s, id);
745
+ if (st != NULL) {
746
+ h3_stream_abort(st);
747
+ }
748
+ nghttp3_conn_shutdown_stream_write(s->h3, id);
749
+ int rv = h3_shutdown_stream_read(s, id);
750
+ if (rv != 0) {
751
+ return rv;
752
+ }
753
+ if (http3_quic_api->shutdown_stream_read(s->qc, id,
754
+ NGHTTP3_H3_REQUEST_CANCELLED) != 0) {
755
+ return h3_fail_with(s, NGHTTP3_H3_INTERNAL_ERROR);
756
+ }
757
+ /* No end_headers will come to drop the count of a section cut short. */
758
+ st_data_t key = (st_data_t)id;
759
+ st_delete(s->field_sizes, &key, NULL);
760
+ return h3_call_ruby(s, id_on_stop_sending, id, 0, NULL);
595
761
  }
596
762
 
597
763
  static int h3_stream_close(void *ud, int64_t id, int has_code,
@@ -608,8 +774,16 @@ static int h3_stream_close(void *ud, int64_t id, int has_code,
608
774
  return h3_fail(s, rv);
609
775
  }
610
776
 
777
+ /* The size of a handler filled in up to member m. The size tells quic.so
778
+ * which members the handler has (quic/ext.h): those it fills in, whatever
779
+ * quic/ext.h it is built against. */
780
+ #define H3_HANDLER_SIZE_TO(m) \
781
+ (offsetof(quic_ext_stream_handler, m) + \
782
+ sizeof(((quic_ext_stream_handler *)0)->m))
783
+
784
+ /* An ABI 1.1 handler: recv_stop_sending came with quic 0.0.6. */
611
785
  static const quic_ext_stream_handler h3_handler = {
612
- .size = sizeof(quic_ext_stream_handler),
786
+ .size = H3_HANDLER_SIZE_TO(recv_stop_sending),
613
787
  .recv_stream_data = h3_recv_stream_data,
614
788
  .next_stream_data = h3_next_stream_data,
615
789
  .stream_data_written = h3_stream_data_written,
@@ -622,6 +796,7 @@ static const quic_ext_stream_handler h3_handler = {
622
796
  .stream_reset = h3_stream_reset,
623
797
  .stream_close = h3_stream_close,
624
798
  .handshake_completed = h3_handshake_completed,
799
+ .recv_stop_sending = h3_recv_stop_sending,
625
800
  };
626
801
 
627
802
  /* The Ruby object */
@@ -651,6 +826,11 @@ static int h3_free_stream_i(st_data_t key, st_data_t value, st_data_t arg) {
651
826
  return ST_CONTINUE;
652
827
  }
653
828
 
829
+ static int h3_abort_stream_i(st_data_t key, st_data_t value, st_data_t arg) {
830
+ h3_stream_abort((h3_stream *)value);
831
+ return ST_CONTINUE;
832
+ }
833
+
654
834
  /* Never touches qc: the QUIC connection may be gone already. */
655
835
  static void h3_session_free(void *ptr) {
656
836
  h3_session *s = ptr;
@@ -697,8 +877,27 @@ static h3_session *h3_session_get(VALUE self) {
697
877
  return s;
698
878
  }
699
879
 
700
- static h3_session *h3_session_get_open(VALUE self) {
880
+ /* The session, for a method that may call into nghttp3 or the QUIC
881
+ * connection, or fail the connection. The Ruby part's callbacks run inside
882
+ * nghttp3 or inside the QUIC connection's packet handling, where none of
883
+ * that may happen, so such a method raises InvalidStateError there. */
884
+ static h3_session *h3_session_enter(VALUE self) {
701
885
  h3_session *s = h3_session_get(self);
886
+ if (s->in_callback > 0) {
887
+ rb_raise(rb_eNghttp3InvalidStateError,
888
+ "#%" PRIsVALUE
889
+ " cannot be called from inside a callback of this session",
890
+ rb_id2str(rb_frame_this_func()));
891
+ }
892
+ return s;
893
+ }
894
+
895
+ /* h3_session_enter, for a method that needs the nghttp3 connection. */
896
+ static h3_session *h3_session_get_open(VALUE self) {
897
+ h3_session *s = h3_session_enter(self);
898
+ if (s->failed) {
899
+ rb_raise(rb_eNghttp3InvalidStateError, "the HTTP/3 connection has failed");
900
+ }
702
901
  if (s->h3 == NULL) {
703
902
  rb_raise(rb_eNghttp3InvalidStateError,
704
903
  "the HTTP/3 connection is not set up yet (handshake pending)");
@@ -706,12 +905,26 @@ static h3_session *h3_session_get_open(VALUE self) {
706
905
  return s;
707
906
  }
708
907
 
908
+ NORETURN(static void h3_raise_no_body(VALUE rb_id));
909
+ static void h3_raise_no_body(VALUE rb_id) {
910
+ rb_raise(rb_eNghttp3StreamNotFoundError,
911
+ "no response body is open on stream %" PRIsVALUE, rb_id);
912
+ }
913
+
914
+ NORETURN(static void h3_raise_aborted(VALUE rb_id));
915
+ static void h3_raise_aborted(VALUE rb_id) {
916
+ rb_raise(rb_eNghttp3InvalidStateError,
917
+ "stream %" PRIsVALUE " has been aborted", rb_id);
918
+ }
919
+
920
+ /* The body of stream rb_id. Ruby code that runs once it is found may make
921
+ * the connection go on and close the stream, so convert everything else
922
+ * first. */
709
923
  static h3_stream *h3_session_stream(VALUE self, VALUE rb_id) {
710
924
  h3_session *s = h3_session_get(self);
711
925
  h3_stream *st = h3_stream_find(s, NUM2LL(rb_id));
712
926
  if (st == NULL) {
713
- rb_raise(rb_eNghttp3StreamNotFoundError,
714
- "no response body is open on stream %" PRIsVALUE, rb_id);
927
+ h3_raise_no_body(rb_id);
715
928
  }
716
929
  return st;
717
930
  }
@@ -724,7 +937,7 @@ static void h3_stream_wake(h3_session *s, h3_stream *st) {
724
937
  st->blocked = false;
725
938
  int rv = nghttp3_conn_resume_stream(s->h3, st->id);
726
939
  if (rv != 0) {
727
- nghttp3_rb_raise(rv, "Failed to resume stream");
940
+ h3_raise(s, rv, "Failed to resume stream");
728
941
  }
729
942
  }
730
943
 
@@ -783,15 +996,19 @@ static VALUE rb_http3_session_s_attach(VALUE klass, VALUE quic_conn,
783
996
  */
784
997
  static VALUE rb_http3_session_submit_response(VALUE self, VALUE rb_id,
785
998
  VALUE headers, VALUE has_body) {
786
- h3_session *s = h3_session_get_open(self);
787
999
  int64_t id = NUM2LL(rb_id);
788
1000
  Check_Type(headers, T_ARRAY);
789
1001
 
790
- long nvlen = RARRAY_LEN(headers);
791
- VALUE tmp;
792
- nghttp3_nv *nva = ALLOCV_N(nghttp3_nv, tmp, nvlen);
1002
+ /* Every field is converted before a pointer into any is taken: to_ary and
1003
+ * to_str may run Ruby code, which could change or drop the Strings taken
1004
+ * so far, or start a GC that moves them. It goes over a copy of headers,
1005
+ * which that code cannot shrink under it, and keeps the Strings in
1006
+ * strings, two per field. */
1007
+ VALUE fields = rb_ary_dup(headers);
1008
+ long nvlen = RARRAY_LEN(fields);
1009
+ VALUE strings = rb_ary_new_capa(nvlen * 2);
793
1010
  for (long i = 0; i < nvlen; i++) {
794
- VALUE pair = rb_check_array_type(RARRAY_AREF(headers, i));
1011
+ VALUE pair = rb_check_array_type(RARRAY_AREF(fields, i));
795
1012
  if (NIL_P(pair) || RARRAY_LEN(pair) != 2) {
796
1013
  rb_raise(rb_eArgError, "headers must be [name, value] pairs");
797
1014
  }
@@ -799,13 +1016,16 @@ static VALUE rb_http3_session_submit_response(VALUE self, VALUE rb_id,
799
1016
  VALUE value = RARRAY_AREF(pair, 1);
800
1017
  StringValue(name);
801
1018
  StringValue(value);
802
- nva[i] = (nghttp3_nv){.name = (const uint8_t *)RSTRING_PTR(name),
803
- .value = (const uint8_t *)RSTRING_PTR(value),
804
- .namelen = (size_t)RSTRING_LEN(name),
805
- .valuelen = (size_t)RSTRING_LEN(value),
806
- .flags = NGHTTP3_NV_FLAG_NONE};
1019
+ rb_ary_push(strings, name);
1020
+ rb_ary_push(strings, value);
807
1021
  }
808
1022
 
1023
+ /* Then the connection, which that code may have failed, and every
1024
+ * allocation; then the pointers, and nghttp3 copies the names and values
1025
+ * before anything else can run. */
1026
+ h3_session *s = h3_session_get_open(self);
1027
+ VALUE tmp;
1028
+ nghttp3_nv *nva = ALLOCV_N(nghttp3_nv, tmp, nvlen);
809
1029
  h3_stream *st = NULL;
810
1030
  if (RTEST(has_body) && h3_stream_find(s, id) == NULL) {
811
1031
  st = ZALLOC(h3_stream);
@@ -813,40 +1033,101 @@ static VALUE rb_http3_session_submit_response(VALUE self, VALUE rb_id,
813
1033
  st->fd = -1;
814
1034
  st_insert(s->streams, (st_data_t)id, (st_data_t)st);
815
1035
  }
1036
+ for (long i = 0; i < nvlen; i++) {
1037
+ VALUE name = RARRAY_AREF(strings, 2 * i);
1038
+ VALUE value = RARRAY_AREF(strings, 2 * i + 1);
1039
+ nva[i] = (nghttp3_nv){.name = (const uint8_t *)RSTRING_PTR(name),
1040
+ .value = (const uint8_t *)RSTRING_PTR(value),
1041
+ .namelen = (size_t)RSTRING_LEN(name),
1042
+ .valuelen = (size_t)RSTRING_LEN(value),
1043
+ .flags = NGHTTP3_NV_FLAG_NONE};
1044
+ }
816
1045
 
817
- /* nghttp3 copies the header names and values. */
818
1046
  int rv = nghttp3_conn_submit_response(
819
1047
  s->h3, id, nva, (size_t)nvlen, RTEST(has_body) ? &h3_data_reader : NULL);
820
1048
  ALLOCV_END(tmp);
821
- RB_GC_GUARD(headers);
1049
+ RB_GC_GUARD(strings);
822
1050
  if (rv != 0) {
823
1051
  if (st != NULL) {
824
1052
  st_data_t key = (st_data_t)id;
825
1053
  st_delete(s->streams, &key, NULL);
826
1054
  h3_stream_free(st);
827
1055
  }
828
- nghttp3_rb_raise(rv, "Failed to submit response");
1056
+ h3_raise(s, rv, "Failed to submit response");
829
1057
  }
830
1058
  return self;
831
1059
  }
832
1060
 
1061
+ /* rb_id as the id of a request stream: 0..2**62-1 and client-initiated
1062
+ * bidirectional. nghttp3 asserts the range. */
1063
+ static int64_t h3_request_stream_id(VALUE rb_id) {
1064
+ int64_t id = NUM2LL(rb_id);
1065
+ if (id < 0 || (uint64_t)id > H3_VARINT_MAX) {
1066
+ rb_raise(rb_eArgError, "stream_id must be 0..2**62-1 (got %" PRIsVALUE ")",
1067
+ rb_id);
1068
+ }
1069
+ if ((id & 3) != 0) {
1070
+ rb_raise(rb_eArgError,
1071
+ "stream_id %" PRIsVALUE
1072
+ " is not a client-initiated bidirectional stream",
1073
+ rb_id);
1074
+ }
1075
+ return id;
1076
+ }
1077
+
1078
+ /*
1079
+ * call-seq:
1080
+ * session.stop_reading(stream_id) -> self
1081
+ *
1082
+ * Read no more of the request on stream_id, which is being answered
1083
+ * before all of it has arrived: nghttp3 drops what still comes, and the
1084
+ * client gets STOP_SENDING with H3_NO_ERROR. RFC 9114 section 4.1 lets a
1085
+ * server send a complete response before the request is complete and
1086
+ * then ask the client to stop sending it, with H3_NO_ERROR.
1087
+ */
1088
+ static VALUE rb_http3_session_stop_reading(VALUE self, VALUE rb_id) {
1089
+ int64_t id = h3_request_stream_id(rb_id);
1090
+ h3_session *s = h3_session_get_open(self);
1091
+ int rv = nghttp3_conn_shutdown_stream_read(s->h3, id);
1092
+ if (rv != 0) {
1093
+ h3_raise(s, rv, "Failed to stop reading the stream");
1094
+ }
1095
+ if (http3_quic_api->shutdown_stream_read(s->qc, id, NGHTTP3_H3_NO_ERROR) !=
1096
+ 0) {
1097
+ rb_raise(rb_eNghttp3Error, "cannot send STOP_SENDING on stream %" PRIsVALUE,
1098
+ rb_id);
1099
+ }
1100
+ /* No end_headers will come to drop the count of a section cut short. */
1101
+ st_data_t key = (st_data_t)id;
1102
+ st_delete(s->field_sizes, &key, NULL);
1103
+ return self;
1104
+ }
1105
+
833
1106
  /*
834
1107
  * call-seq:
835
1108
  * session.push_chunk(stream_id, str) -> self
836
1109
  *
837
1110
  * Queue a frozen copy of str as the next piece of the response body.
1111
+ * Raises InvalidStateError once the stream has been aborted.
838
1112
  */
839
1113
  static VALUE rb_http3_session_push_chunk(VALUE self, VALUE rb_id, VALUE str) {
840
- h3_session *s = h3_session_get(self);
841
- h3_stream *st = h3_session_stream(self, rb_id);
1114
+ h3_session *s = h3_session_enter(self);
842
1115
  StringValue(str);
1116
+ VALUE copy = rb_str_new_frozen(str);
1117
+ h3_stream *st = h3_session_stream(self, rb_id);
1118
+ if (st->aborted) {
1119
+ h3_raise_aborted(rb_id);
1120
+ }
843
1121
  if (st->eof) {
844
1122
  rb_raise(rb_eNghttp3InvalidStateError, "the body has ended");
845
1123
  }
846
- if (RSTRING_LEN(str) == 0) {
1124
+ if (RSTRING_LEN(copy) == 0) {
847
1125
  return self;
848
1126
  }
849
- VALUE copy = rb_str_new_frozen(str);
1127
+ if (h3_stream_unacked(st) == 0) {
1128
+ /* The body waits on the peer from now. */
1129
+ st->since = h3_now();
1130
+ }
850
1131
  if (h3_stream_push(st, (const uint8_t *)RSTRING_PTR(copy),
851
1132
  (size_t)RSTRING_LEN(copy), copy) != 0) {
852
1133
  rb_raise(rb_eNoMemError, "failed to queue a body chunk");
@@ -860,11 +1141,15 @@ static VALUE rb_http3_session_push_chunk(VALUE self, VALUE rb_id, VALUE str) {
860
1141
  * call-seq:
861
1142
  * session.push_eof(stream_id) -> self
862
1143
  *
863
- * Mark the end of the response body.
1144
+ * Mark the end of the response body. Raises InvalidStateError once the
1145
+ * stream has been aborted.
864
1146
  */
865
1147
  static VALUE rb_http3_session_push_eof(VALUE self, VALUE rb_id) {
866
- h3_session *s = h3_session_get(self);
1148
+ h3_session *s = h3_session_enter(self);
867
1149
  h3_stream *st = h3_session_stream(self, rb_id);
1150
+ if (st->aborted) {
1151
+ h3_raise_aborted(rb_id);
1152
+ }
868
1153
  st->eof = true;
869
1154
  h3_stream_wake(s, st);
870
1155
  return self;
@@ -878,15 +1163,14 @@ static VALUE rb_http3_session_push_eof(VALUE self, VALUE rb_id) {
878
1163
  * descriptor is duplicated, so io may be closed afterwards. The duplicate
879
1164
  * is close-on-exec, as Ruby's own descriptors are, so that a process the
880
1165
  * application spawns does not inherit it, and is closed as soon as the
881
- * whole length has been read.
1166
+ * whole length has been read. Raises InvalidStateError once the stream has
1167
+ * been aborted.
882
1168
  */
883
1169
  static VALUE rb_http3_session_attach_file(VALUE self, VALUE rb_id, VALUE io,
884
1170
  VALUE offset, VALUE length) {
885
- h3_session *s = h3_session_get(self);
886
- h3_stream *st = h3_session_stream(self, rb_id);
887
- if (st->fd >= 0 || st->eof || st->nchunks > 0) {
888
- rb_raise(rb_eNghttp3InvalidStateError, "the body is already given");
889
- }
1171
+ h3_session *s = h3_session_enter(self);
1172
+ /* The arguments first, as #push_chunk does: io.fileno is Ruby code. */
1173
+ int64_t id = NUM2LL(rb_id);
890
1174
  off_t off = NUM2OFFT(offset);
891
1175
  off_t len = NUM2OFFT(length);
892
1176
  if (off < 0 || len < 0) {
@@ -897,9 +1181,23 @@ static VALUE rb_http3_session_attach_file(VALUE self, VALUE rb_id, VALUE io,
897
1181
  rb_sys_fail("dup");
898
1182
  }
899
1183
  rb_update_max_fd(fd);
1184
+ h3_stream *st = h3_stream_find(s, id);
1185
+ if (st == NULL) {
1186
+ close(fd);
1187
+ h3_raise_no_body(rb_id);
1188
+ }
1189
+ if (st->aborted) {
1190
+ close(fd);
1191
+ h3_raise_aborted(rb_id);
1192
+ }
1193
+ if (st->fd >= 0 || st->eof || st->nchunks > 0) {
1194
+ close(fd);
1195
+ rb_raise(rb_eNghttp3InvalidStateError, "the body is already given");
1196
+ }
900
1197
  st->fd = fd;
901
1198
  st->file_off = off;
902
1199
  st->file_end = off + len;
1200
+ st->since = h3_now();
903
1201
  if (len == 0) {
904
1202
  h3_stream_end_file(st);
905
1203
  }
@@ -919,6 +1217,36 @@ static VALUE rb_http3_session_unsent_bytes(VALUE self, VALUE rb_id) {
919
1217
  return ULL2NUM(st == NULL ? 0 : st->unsent);
920
1218
  }
921
1219
 
1220
+ /*
1221
+ * call-seq:
1222
+ * session.response_stalled_since(stream_id) -> Float or nil
1223
+ *
1224
+ * Since when (CLOCK_MONOTONIC seconds, as Process.clock_gettime gives it)
1225
+ * the response body on stream_id has waited on the client: some of it has
1226
+ * not been acknowledged, and nothing has moved since. nil while none of it
1227
+ * waits, for a stream without a body, and once the stream has been
1228
+ * aborted.
1229
+ *
1230
+ * Moving is the client acknowledging some of the body, or widening the
1231
+ * stream's flow control window. While that window does not hold the body
1232
+ * back, an acknowledgement of any response body on the connection counts
1233
+ * too: nghttp3 sends the responses of one priority one at a time, so a
1234
+ * response waiting its turn behind another the client is reading gets no
1235
+ * acknowledgements of its own.
1236
+ */
1237
+ static VALUE rb_http3_session_response_stalled_since(VALUE self, VALUE rb_id) {
1238
+ h3_session *s = h3_session_get(self);
1239
+ h3_stream *st = h3_stream_find(s, NUM2LL(rb_id));
1240
+ if (st == NULL || h3_stream_unacked(st) == 0) {
1241
+ return Qnil;
1242
+ }
1243
+ double since = st->since;
1244
+ if (!st->fc_blocked && s->acked_at > since) {
1245
+ since = s->acked_at;
1246
+ }
1247
+ return DBL2NUM(since);
1248
+ }
1249
+
922
1250
  /*
923
1251
  * call-seq:
924
1252
  * session.take_closed -> Array
@@ -954,13 +1282,13 @@ static VALUE rb_http3_session_take_error(VALUE self) {
954
1282
  * served.
955
1283
  */
956
1284
  static VALUE rb_http3_session_shutdown_notice(VALUE self) {
957
- h3_session *s = h3_session_get(self);
1285
+ h3_session *s = h3_session_enter(self);
958
1286
  if (s->h3 == NULL) {
959
1287
  return self;
960
1288
  }
961
1289
  int rv = nghttp3_conn_submit_shutdown_notice(s->h3);
962
1290
  if (rv != 0) {
963
- nghttp3_rb_raise(rv, "Failed to submit shutdown notice");
1291
+ h3_raise(s, rv, "Failed to submit shutdown notice");
964
1292
  }
965
1293
  return self;
966
1294
  }
@@ -970,13 +1298,21 @@ static VALUE rb_http3_session_shutdown_notice(VALUE self) {
970
1298
  * session.abort_stream(stream_id, app_error_code) -> self
971
1299
  *
972
1300
  * Stop the stream in both directions with app_error_code (for instance
973
- * H3_INTERNAL_ERROR after the response headers went out).
1301
+ * H3_INTERNAL_ERROR after the response headers went out). What of the
1302
+ * response body nghttp3 has not taken is let go at once, so that
1303
+ * #unsent_bytes no longer counts it, and a file body's descriptor is
1304
+ * closed; #push_chunk, #push_eof and #attach_file raise InvalidStateError
1305
+ * from then on.
974
1306
  */
975
1307
  static VALUE rb_http3_session_abort_stream(VALUE self, VALUE rb_id,
976
1308
  VALUE rb_code) {
977
- h3_session *s = h3_session_get(self);
1309
+ h3_session *s = h3_session_enter(self);
978
1310
  int64_t id = NUM2LL(rb_id);
979
1311
  uint64_t code = NUM2ULL(rb_code);
1312
+ h3_stream *st = h3_stream_find(s, id);
1313
+ if (st != NULL) {
1314
+ h3_stream_abort(st);
1315
+ }
980
1316
  if (s->h3 != NULL) {
981
1317
  nghttp3_conn_shutdown_stream_write(s->h3, id);
982
1318
  }
@@ -985,6 +1321,36 @@ static VALUE rb_http3_session_abort_stream(VALUE self, VALUE rb_id,
985
1321
  return self;
986
1322
  }
987
1323
 
1324
+ /*
1325
+ * call-seq:
1326
+ * session.release_connection -> self (private; #release_all calls it)
1327
+ *
1328
+ * Let go of the HTTP/3 connection now, rather than when the Session is
1329
+ * garbage collected: the nghttp3 connection is deleted, and every response
1330
+ * body aborted as #abort_stream aborts one, though no stream is reset, so
1331
+ * that what nghttp3 had not taken of it is let go of and a file body's
1332
+ * descriptor closed. What nghttp3 had taken stays until the Session is
1333
+ * freed, even once the stream closes, since without nghttp3 nothing tells
1334
+ * of that: ngtcp2 may still point into it, to send it again.
1335
+ *
1336
+ * For a QUIC connection that has gone. One still driven goes on as after a
1337
+ * failure (h3_fail_with), with H3_NO_ERROR; without nghttp3, it sends
1338
+ * nothing more of a body let go of, its FIN included. A connection that
1339
+ * failed keeps the error it failed with. From then on #submit_response and
1340
+ * #stop_reading raise InvalidStateError, as on a failed connection, and
1341
+ * #push_chunk, #push_eof and #attach_file as on an aborted stream. Raises
1342
+ * InvalidStateError inside a callback of the Session, where nghttp3 may be
1343
+ * running. Calling it again does nothing.
1344
+ */
1345
+ static VALUE rb_http3_session_release_connection(VALUE self) {
1346
+ h3_session *s = h3_session_enter(self);
1347
+ if (!s->failed) {
1348
+ h3_fail_with(s, NGHTTP3_H3_NO_ERROR);
1349
+ }
1350
+ st_foreach(s->streams, h3_abort_stream_i, 0);
1351
+ return self;
1352
+ }
1353
+
988
1354
  void Init_http3_session(VALUE rb_mHTTP3) {
989
1355
  id_on_begin_headers = rb_intern("on_begin_headers");
990
1356
  id_on_header = rb_intern("on_header");
@@ -992,6 +1358,8 @@ void Init_http3_session(VALUE rb_mHTTP3) {
992
1358
  id_on_data = rb_intern("on_data");
993
1359
  id_on_end_stream = rb_intern("on_end_stream");
994
1360
  id_on_fields_too_large = rb_intern("on_fields_too_large");
1361
+ id_on_reset = rb_intern("on_reset");
1362
+ id_on_stop_sending = rb_intern("on_stop_sending");
995
1363
 
996
1364
  rb_cHTTP3Session = rb_define_class_under(rb_mHTTP3, "Session", rb_cObject);
997
1365
  rb_undef_alloc_func(rb_cHTTP3Session);
@@ -999,6 +1367,8 @@ void Init_http3_session(VALUE rb_mHTTP3) {
999
1367
  rb_http3_session_s_attach, 2);
1000
1368
  rb_define_method(rb_cHTTP3Session, "submit_response",
1001
1369
  rb_http3_session_submit_response, 3);
1370
+ rb_define_method(rb_cHTTP3Session, "stop_reading",
1371
+ rb_http3_session_stop_reading, 1);
1002
1372
  rb_define_method(rb_cHTTP3Session, "push_chunk", rb_http3_session_push_chunk,
1003
1373
  2);
1004
1374
  rb_define_method(rb_cHTTP3Session, "push_eof", rb_http3_session_push_eof, 1);
@@ -1006,6 +1376,8 @@ void Init_http3_session(VALUE rb_mHTTP3) {
1006
1376
  rb_http3_session_attach_file, 4);
1007
1377
  rb_define_method(rb_cHTTP3Session, "unsent_bytes",
1008
1378
  rb_http3_session_unsent_bytes, 1);
1379
+ rb_define_method(rb_cHTTP3Session, "response_stalled_since",
1380
+ rb_http3_session_response_stalled_since, 1);
1009
1381
  rb_define_method(rb_cHTTP3Session, "take_closed",
1010
1382
  rb_http3_session_take_closed, 0);
1011
1383
  rb_define_method(rb_cHTTP3Session, "take_error", rb_http3_session_take_error,
@@ -1014,4 +1386,6 @@ void Init_http3_session(VALUE rb_mHTTP3) {
1014
1386
  rb_http3_session_shutdown_notice, 0);
1015
1387
  rb_define_method(rb_cHTTP3Session, "abort_stream",
1016
1388
  rb_http3_session_abort_stream, 2);
1389
+ rb_define_private_method(rb_cHTTP3Session, "release_connection",
1390
+ rb_http3_session_release_connection, 0);
1017
1391
  }