1
2
3
4
5 package http3
6
7 import (
8 "context"
9 "crypto/tls"
10 "errors"
11 "fmt"
12 "maps"
13 "net"
14 "net/http"
15 "net/textproto"
16 "os"
17 "slices"
18 "strconv"
19 "strings"
20 "sync"
21 "time"
22
23 "golang.org/x/net/http/httpguts"
24 "golang.org/x/net/internal/httpcommon"
25 "golang.org/x/net/quic"
26 )
27
28
29
30 type server struct {
31 srv1 *http.Server
32 opts ServerOpts
33
34 initOnce sync.Once
35
36
37
38
39 connClosed chan any
40 mu sync.Mutex
41 activeConns map[*serverConn]struct{}
42 }
43
44
45
46 type netHTTPServer struct {
47 *server
48 }
49
50
51 func (netHTTPServer) Accept() (net.Conn, error) { return nil, net.ErrClosed }
52 func (netHTTPServer) Close() error { return nil }
53 func (netHTTPServer) Addr() net.Addr { return nil }
54
55
56
57
58
59 func (s netHTTPServer) ServeHTTP3(ctx context.Context, conn net.PacketConn, tlsConfig *tls.Config, h http.Handler) error {
60 s.init()
61 e, err := quic.NewEndpoint(conn, newQUICConfig(s.opts.QUICConfig, tlsConfig))
62 if err != nil {
63 return err
64 }
65 return s.serve(ctx, e, h)
66 }
67
68
69 func (s netHTTPServer) Shutdown(ctx context.Context) error {
70 s.shutdown(ctx)
71 return nil
72 }
73
74 type ServerOpts struct {
75
76
77
78
79
80 QUICConfig *quic.Config
81 }
82
83
84
85
86
87 func RegisterServer(s *http.Server, opts ServerOpts) error {
88 if err := s.Serve(netHTTPServer{&server{
89 opts: opts,
90 srv1: s,
91 }}); err != nil {
92 return errors.New("http3: net/http does not support HTTP/3")
93 }
94 return nil
95 }
96
97 func (s *server) init() {
98 s.initOnce.Do(func() {
99 s.activeConns = make(map[*serverConn]struct{})
100 s.connClosed = make(chan any, 1)
101 })
102 }
103
104
105
106 func (s *server) serve(ctx context.Context, e *quic.Endpoint, h http.Handler) error {
107 s.init()
108 defer e.Close(canceledCtx)
109 for {
110 qconn, err := e.Accept(ctx)
111 if err != nil {
112 return err
113 }
114 go s.newServerConn(ctx, qconn, h)
115 }
116 }
117
118
119 func (s *server) shutdown(ctx context.Context) {
120
121 if ctx == nil {
122 var cancel context.CancelFunc
123 ctx, cancel = context.WithTimeout(context.Background(), time.Second)
124 defer cancel()
125 }
126
127
128
129 s.mu.Lock()
130 for sc := range s.activeConns {
131
132
133 go sc.sendGoaway()
134 }
135 s.mu.Unlock()
136
137
138
139 defer func() {
140 s.mu.Lock()
141 defer s.mu.Unlock()
142 for sc := range s.activeConns {
143 sc.abort(&connectionError{
144 code: errH3NoError,
145 message: "server is shutting down",
146 })
147 }
148 }()
149 noMoreConns := func() bool {
150 s.mu.Lock()
151 defer s.mu.Unlock()
152 return len(s.activeConns) == 0
153 }
154 for {
155 if noMoreConns() {
156 return
157 }
158 select {
159 case <-ctx.Done():
160 return
161 case <-s.connClosed:
162 }
163 }
164 }
165
166 func (s *server) registerConn(sc *serverConn) {
167 s.mu.Lock()
168 defer s.mu.Unlock()
169 s.activeConns[sc] = struct{}{}
170 }
171
172 func (s *server) unregisterConn(sc *serverConn) {
173 s.mu.Lock()
174 delete(s.activeConns, sc)
175 s.mu.Unlock()
176 select {
177 case s.connClosed <- struct{}{}:
178 default:
179
180
181 }
182 }
183
184 func (s *server) readHeaderTimeout() time.Duration {
185 if s.srv1 == nil || s.srv1.ReadHeaderTimeout == 0 {
186 return s.readTimeout()
187 }
188 return s.srv1.ReadHeaderTimeout
189 }
190
191 func (s *server) readTimeout() time.Duration {
192 if s.srv1 == nil {
193 return 0
194 }
195 return s.srv1.ReadTimeout
196 }
197
198 func (s *server) writeTimeout() time.Duration {
199 if s.srv1 == nil {
200 return 0
201 }
202 return s.srv1.WriteTimeout
203 }
204
205
206 func (s *server) idleTimeout() time.Duration {
207 if s.srv1 == nil || s.srv1.IdleTimeout == 0 {
208 return s.readTimeout()
209 }
210 return s.srv1.IdleTimeout
211 }
212
213 type serverConn struct {
214 qconn *quic.Conn
215 srv *server
216 baseCtx context.Context
217 handler http.Handler
218
219 genericConn
220 enc qpackEncoder
221 dec qpackDecoder
222
223
224 controlStream *stream
225 mu sync.Mutex
226 maxRequestStreamID int64
227 goawaySent bool
228 }
229
230
231
232 func (s *server) newServerConn(baseCtx context.Context, qconn *quic.Conn, h http.Handler) {
233 sc := &serverConn{
234 qconn: qconn,
235 srv: s,
236 baseCtx: baseCtx,
237 handler: h,
238 }
239 s.registerConn(sc)
240 defer s.unregisterConn(sc)
241 sc.enc.init()
242
243
244
245 var err error
246 sc.controlStream, err = newConnStream(context.Background(), sc.qconn, streamTypeControl)
247 if err != nil {
248 return
249 }
250 sc.controlStream.writeSettings()
251 sc.controlStream.Flush()
252
253 sc.acceptStreams(sc.qconn, sc)
254 }
255
256 func (sc *serverConn) handleControlStream(st *stream) error {
257
258
259 if err := st.readSettings(func(settingsType, settingsValue int64) error {
260 switch settingsType {
261 case settingsMaxFieldSectionSize:
262 _ = settingsValue
263 case settingsQPACKMaxTableCapacity:
264 _ = settingsValue
265 case settingsQPACKBlockedStreams:
266 _ = settingsValue
267 default:
268
269 }
270 return nil
271 }); err != nil {
272 return err
273 }
274
275 for {
276 ftype, err := st.readFrameHeader()
277 if err != nil {
278 return err
279 }
280 switch ftype {
281 case frameTypeCancelPush:
282
283
284
285
286 return &connectionError{
287 code: errH3IDError,
288 message: "CANCEL_PUSH for unsent push ID",
289 }
290 case frameTypeGoaway:
291 return errH3NoError
292 default:
293
294 if err := st.discardUnknownFrame(ftype); err != nil {
295 return err
296 }
297 }
298 }
299 }
300
301 func (sc *serverConn) handleEncoderStream(*stream) error {
302
303 return nil
304 }
305
306 func (sc *serverConn) handleDecoderStream(*stream) error {
307
308 return nil
309 }
310
311 func (sc *serverConn) handlePushStream(*stream) error {
312
313
314
315 return &connectionError{
316 code: errH3StreamCreationError,
317 message: "client created push stream",
318 }
319 }
320
321
322
323
324
325
326
327
328
329
330
331 func hasDisallowedConnectionHeader(h http.Header) bool {
332 neverAllowed := []string{
333 "Connection",
334 "Keep-Alive",
335 "Proxy-Connection",
336 "Transfer-Encoding",
337 "Upgrade",
338 }
339 for _, k := range neverAllowed {
340 if _, ok := h[k]; ok {
341 return true
342 }
343 }
344 if te, ok := h["Te"]; ok && (len(te) != 1 || te[0] != "trailers") {
345 return true
346 }
347 return false
348 }
349
350 type pseudoHeader struct {
351 method string
352 scheme string
353 path string
354 authority string
355 }
356
357 func (sc *serverConn) parseHeader(st *stream) (http.Header, pseudoHeader, error) {
358 ftype, err := st.readFrameHeader()
359 if err != nil {
360 return nil, pseudoHeader{}, err
361 }
362 if ftype != frameTypeHeaders {
363 return nil, pseudoHeader{}, &streamError{errH3MessageError, "received other frames when expecting HEADERS"}
364 }
365 header := make(http.Header)
366 var pHeader pseudoHeader
367 var dec qpackDecoder
368 var hasMethod, hasScheme, hasPath, hasAuthority bool
369 if err := dec.decode(st, func(_ indexType, name, value string) error {
370 if !httpguts.ValidHeaderFieldValue(value) {
371 return &streamError{errH3MessageError, "invalid field value"}
372 }
373 if name == "" || (name[0] == ':' && value == "") {
374
375
376
377 return &streamError{errH3MessageError, "invalid field"}
378 }
379 switch name {
380 case ":method":
381 if hasMethod {
382 return &streamError{errH3MessageError, "duplicate :method"}
383 }
384 hasMethod = true
385 pHeader.method = value
386 case ":scheme":
387 if hasScheme {
388 return &streamError{errH3MessageError, "duplicate :scheme"}
389 }
390 hasScheme = true
391 pHeader.scheme = value
392 case ":path":
393 if hasPath {
394 return &streamError{errH3MessageError, "duplicate :path"}
395 }
396 hasPath = true
397 pHeader.path = value
398 case ":authority":
399 if hasAuthority {
400 return &streamError{errH3MessageError, "duplicate :authority"}
401 }
402 hasAuthority = true
403 pHeader.authority = value
404 default:
405 if !validWireHeaderFieldName(name) {
406 return &streamError{errH3MessageError, "invalid field name"}
407 }
408 header.Add(name, value)
409 }
410 return nil
411 }); err != nil {
412 return nil, pseudoHeader{}, err
413 }
414 if err := st.endFrame(); err != nil {
415 return nil, pseudoHeader{}, err
416 }
417 if hasDisallowedConnectionHeader(header) {
418 return nil, pseudoHeader{}, &streamError{errH3MessageError, "invalid connection-related header"}
419 }
420
421
422
423
424
425
426
427
428
429 if !hasMethod {
430 return nil, pseudoHeader{}, &streamError{errH3MessageError, "missing :method"}
431 }
432 if pHeader.method != "CONNECT" && (!hasScheme || !hasPath) {
433 return nil, pseudoHeader{}, &streamError{errH3MessageError, "missing :scheme or :path for non-CONNECT requests"}
434 }
435 if pHeader.method == "CONNECT" && (hasScheme || hasPath || !hasAuthority) {
436 return nil, pseudoHeader{}, &streamError{
437 errH3MessageError, "CONNECT request must only have :method and :authority pseudo-headers",
438 }
439 }
440 return header, pHeader, nil
441 }
442
443 func (sc *serverConn) sendGoaway() {
444 sc.mu.Lock()
445 if sc.goawaySent || sc.controlStream == nil {
446 sc.mu.Unlock()
447 return
448 }
449 sc.goawaySent = true
450 sc.mu.Unlock()
451
452
453
454 sc.controlStream.writeVarint(int64(frameTypeGoaway))
455 sc.controlStream.writeVarint(int64(sizeVarint(uint64(sc.maxRequestStreamID))))
456 sc.controlStream.writeVarint(sc.maxRequestStreamID)
457 sc.controlStream.Flush()
458 }
459
460
461
462 func (sc *serverConn) requestShouldGoaway(st *stream) bool {
463 sc.mu.Lock()
464 defer sc.mu.Unlock()
465 if sc.goawaySent {
466 return st.stream.ID() >= sc.maxRequestStreamID
467 } else {
468 sc.maxRequestStreamID = max(sc.maxRequestStreamID, st.stream.ID())
469 return false
470 }
471 }
472
473 func (sc *serverConn) handleRequestStream(st *stream) error {
474 if sc.requestShouldGoaway(st) {
475 return &streamError{
476 code: errH3RequestRejected,
477 message: "GOAWAY request with equal or lower ID than the stream has been sent",
478 }
479 }
480
481 readStartTime := time.Now()
482 if t := sc.srv.readHeaderTimeout(); t > 0 {
483 st.readDeadline.set(readStartTime.Add(t))
484 }
485 header, pHeader, err := sc.parseHeader(st)
486 if err != nil {
487 if errors.Is(err, os.ErrDeadlineExceeded) {
488 return &streamError{
489 code: errH3RequestRejected,
490 message: "exceeded deadline while parsing header",
491 }
492 }
493 return err
494 }
495
496 if t := sc.srv.readTimeout(); t > 0 {
497 st.readDeadline.set(readStartTime.Add(t))
498 } else {
499 st.readDeadline.set(time.Time{})
500 }
501 reqInfo := httpcommon.NewServerRequest(httpcommon.ServerRequestParam{
502 Method: pHeader.method,
503 Scheme: pHeader.scheme,
504 Authority: pHeader.authority,
505 Path: pHeader.path,
506 Header: header,
507 })
508 if reqInfo.InvalidReason != "" {
509 return &streamError{
510 code: errH3MessageError,
511 message: reqInfo.InvalidReason,
512 }
513 }
514
515 contentLength := int64(-1)
516 if n, err := strconv.ParseUint(header.Get("Content-Length"), 10, 63); err == nil {
517 contentLength = int64(n)
518 }
519
520 req := (&http.Request{
521 Proto: "HTTP/3.0",
522 Method: pHeader.method,
523 Host: reqInfo.Host,
524 URL: reqInfo.URL,
525 RequestURI: reqInfo.RequestURI,
526 Trailer: reqInfo.Trailer,
527 ProtoMajor: 3,
528 RemoteAddr: sc.qconn.RemoteAddr().String(),
529 Header: header,
530 ContentLength: contentLength,
531 }).WithContext(sc.baseCtx)
532
533 rw := &responseWriter{
534 st: st,
535 headers: make(http.Header),
536 trailer: make(http.Header),
537 bb: make(bodyBuffer, 0, defaultBodyBufferCap),
538 cannotHaveBody: req.Method == "HEAD",
539 bw: &bodyWriter{
540 st: st,
541 remain: -1,
542 flush: false,
543 name: "response",
544 enc: &sc.enc,
545 },
546 }
547
548 if contentLength != 0 || len(reqInfo.Trailer) != 0 {
549 req.Body = &serverRequestReader{
550 rw: rw,
551 br: bodyReader{
552 st: st,
553 remain: contentLength,
554 trailer: reqInfo.Trailer,
555 filterTrailer: true,
556 },
557 needsContinue: reqInfo.NeedsContinue,
558 }
559 defer req.Body.Close()
560 } else {
561 req.Body = http.NoBody
562 }
563
564
565 if t := sc.srv.writeTimeout(); t > 0 {
566 st.writeDeadline.set(time.Now().Add(t))
567 }
568 sc.handler.ServeHTTP(rw, req)
569 return rw.close()
570 }
571
572
573 func (sc *serverConn) abort(err error) {
574 if e, ok := err.(*connectionError); ok {
575 sc.qconn.Abort(&quic.ApplicationError{
576 Code: uint64(e.code),
577 Reason: e.message,
578 })
579 } else {
580 sc.qconn.Abort(err)
581 }
582 }
583
584
585
586 func responseCanHaveBody(status int) bool {
587 switch {
588 case status >= 100 && status <= 199:
589 return false
590 case status == 204:
591 return false
592 case status == 304:
593 return false
594 }
595 return true
596 }
597
598
599
600
601
602 const trailerPrefix = "Trailer:"
603
604 type responseWriter struct {
605 st *stream
606 bw *bodyWriter
607 mu sync.Mutex
608 headers http.Header
609 snapHeaders http.Header
610 trailer http.Header
611 bb bodyBuffer
612 wroteHeader bool
613 statusCode int
614 sent100 bool
615 cannotHaveBody bool
616 bodyLenLeft int64
617 }
618
619 func (rw *responseWriter) Header() http.Header {
620 return rw.headers
621 }
622
623
624
625
626 func (rw *responseWriter) prepareTrailerForWriteLocked() {
627 for name := range rw.trailer {
628 if val, ok := rw.headers[name]; ok {
629 rw.trailer[name] = val
630 } else {
631 delete(rw.trailer, name)
632 }
633 }
634 for name, vals := range rw.headers {
635 if name, found := strings.CutPrefix(name, trailerPrefix); found {
636 name = textproto.CanonicalMIMEHeaderKey(textproto.TrimString(name))
637 rw.trailer[name] = vals
638 }
639 }
640 if len(rw.trailer) > 0 {
641 rw.bw.trailer = rw.trailer
642 }
643 }
644
645
646
647
648
649 func (rw *responseWriter) writeHeaderLockedOnce() {
650 if rw.wroteHeader {
651 return
652 }
653 if !responseCanHaveBody(rw.statusCode) {
654 rw.cannotHaveBody = true
655 }
656
657
658
659 if _, ok := rw.snapHeaders["Trailer"]; ok {
660 extractTrailerFromHeader(rw.snapHeaders, rw.trailer)
661 rw.snapHeaders.Set("Trailer", strings.Join(slices.Sorted(maps.Keys(rw.trailer)), ", "))
662 }
663
664 rw.bb.inferHeader(rw.snapHeaders, rw.statusCode)
665 encHeaders := rw.bw.enc.encode(func(f func(itype indexType, name, value string)) {
666 f(mayIndex, ":status", strconv.Itoa(rw.statusCode))
667 for name, values := range rw.snapHeaders {
668 if !httpguts.ValidHeaderFieldName(name) {
669 continue
670 }
671 for _, val := range values {
672 if !httpguts.ValidHeaderFieldValue(val) {
673 continue
674 }
675
676 f(mayIndex, name, val)
677 }
678 }
679 })
680
681 rw.st.writeVarint(int64(frameTypeHeaders))
682 rw.st.writeVarint(int64(len(encHeaders)))
683 rw.st.Write(encHeaders)
684 rw.wroteHeader = true
685 }
686
687
688
689
690
691 func (rw *responseWriter) writeHeaderLocked(statusCode int) {
692 if rw.wroteHeader {
693 return
694 }
695 if statusCode == 100 {
696 if rw.sent100 {
697 return
698 }
699 rw.sent100 = true
700 }
701 encHeaders := rw.bw.enc.encode(func(f func(itype indexType, name, value string)) {
702 f(mayIndex, ":status", strconv.Itoa(statusCode))
703 for name, values := range rw.headers {
704 if name == "Content-Length" || name == "Transfer-Encoding" {
705 continue
706 }
707 if !httpguts.ValidHeaderFieldName(name) {
708 continue
709 }
710 for _, val := range values {
711 if !httpguts.ValidHeaderFieldValue(val) {
712 continue
713 }
714
715 f(mayIndex, name, val)
716 }
717 }
718 })
719 rw.st.writeVarint(int64(frameTypeHeaders))
720 rw.st.writeVarint(int64(len(encHeaders)))
721 rw.st.Write(encHeaders)
722 }
723
724 func isInfoStatus(status int) bool {
725 return status >= 100 && status < 200
726 }
727
728
729 func checkWriteHeaderCode(code int) {
730
731
732
733
734
735
736
737
738
739
740 if code < 100 || code > 999 {
741 panic(fmt.Sprintf("invalid WriteHeader code %v", code))
742 }
743 }
744
745 func (rw *responseWriter) WriteHeader(statusCode int) {
746
747 rw.mu.Lock()
748 defer rw.mu.Unlock()
749 if rw.statusCode != 0 {
750 return
751 }
752 checkWriteHeaderCode(statusCode)
753
754
755
756 if isInfoStatus(statusCode) {
757 rw.writeHeaderLocked(statusCode)
758 rw.st.Flush()
759 return
760 }
761
762
763
764 if n, err := strconv.ParseUint(rw.headers.Get("Content-Length"), 10, 63); err == nil {
765 rw.bodyLenLeft = int64(n)
766 } else {
767 rw.headers.Del("Content-Length")
768 rw.bodyLenLeft = -1
769 }
770 rw.statusCode = statusCode
771 rw.snapHeaders = rw.headers.Clone()
772 }
773
774
775
776
777
778 func (rw *responseWriter) trimWriteLocked(b []byte) ([]byte, bool) {
779 if rw.bodyLenLeft < 0 {
780 return b, false
781 }
782 n := min(int64(len(b)), rw.bodyLenLeft)
783 rw.bodyLenLeft -= n
784 return b[:n], n != int64(len(b))
785 }
786
787 func (rw *responseWriter) Write(b []byte) (n int, err error) {
788
789
790 rw.WriteHeader(http.StatusOK)
791 rw.mu.Lock()
792 defer rw.mu.Unlock()
793
794 if rw.statusCode == http.StatusNotModified {
795 return 0, http.ErrBodyNotAllowed
796 }
797
798 b, trimmed := rw.trimWriteLocked(b)
799 if trimmed {
800 defer func() {
801 err = http.ErrContentLength
802 }()
803 }
804
805
806
807
808
809
810 initialBLen := len(b)
811 initialBufLen := len(rw.bb)
812 if !rw.wroteHeader || len(b) <= cap(rw.bb)-len(rw.bb) {
813 b = rw.bb.write(b)
814 if len(b) == 0 {
815 return initialBLen, nil
816 }
817 }
818
819
820
821
822
823
824
825 rw.writeHeaderLockedOnce()
826 if rw.cannotHaveBody {
827 return initialBLen, nil
828 }
829 if n, err := rw.bw.write(rw.bb, b); err != nil {
830 return max(0, n-initialBufLen), err
831 }
832 rw.bb.discard()
833 return initialBLen, nil
834 }
835
836 func (rw *responseWriter) SetReadDeadline(deadline time.Time) error {
837 rw.st.readDeadline.set(deadline)
838 return nil
839 }
840
841 func (rw *responseWriter) SetWriteDeadline(deadline time.Time) error {
842 rw.st.writeDeadline.set(deadline)
843 return nil
844 }
845
846 func (rw *responseWriter) EnableFullDuplex() error {
847 return nil
848 }
849
850 func (rw *responseWriter) Flush() { rw.FlushError() }
851 func (rw *responseWriter) FlushError() error {
852
853
854 rw.WriteHeader(http.StatusOK)
855 rw.mu.Lock()
856 defer rw.mu.Unlock()
857 rw.writeHeaderLockedOnce()
858 if !rw.cannotHaveBody {
859 _, err := rw.bw.Write(rw.bb)
860 rw.bb.discard()
861 if err != nil {
862 return err
863 }
864 }
865 return rw.st.Flush()
866 }
867
868 func (rw *responseWriter) close() error {
869 if errors.Is(rw.st.writeDeadline.err(), os.ErrDeadlineExceeded) {
870 return &streamError{
871 code: errH3RequestCancelled,
872 message: "exceeded deadline while writing response",
873 }
874 }
875
876 retErr := rw.FlushError()
877 rw.mu.Lock()
878 defer rw.mu.Unlock()
879 rw.prepareTrailerForWriteLocked()
880 if err := rw.bw.Close(); retErr == nil {
881 retErr = err
882 }
883 if errors.Is(retErr, os.ErrDeadlineExceeded) {
884 return &streamError{
885 code: errH3RequestCancelled,
886 message: retErr.Error(),
887 }
888 }
889 return retErr
890 }
891
892
893
894
895
896 const defaultBodyBufferCap = 512
897
898
899 type bodyBuffer []byte
900
901
902
903 func (bb *bodyBuffer) write(b []byte) []byte {
904 n := min(len(b), cap(*bb)-len(*bb))
905 *bb = append(*bb, b[:n]...)
906 return b[n:]
907 }
908
909
910 func (bb *bodyBuffer) discard() {
911 *bb = (*bb)[:0]
912 }
913
914
915
916
917
918
919 func (bb *bodyBuffer) inferHeader(h http.Header, status int) {
920 if _, ok := h["Date"]; !ok {
921 h.Set("Date", time.Now().UTC().Format(http.TimeFormat))
922 }
923
924
925 hasCE := len(h.Get("Content-Encoding")) > 0
926 _, hasCT := h["Content-Type"]
927 if !hasCE && !hasCT && responseCanHaveBody(status) && len(*bb) > 0 {
928 h.Set("Content-Type", http.DetectContentType(*bb))
929 }
930
931
932
933
934 }
935
936
937
938
939 type serverRequestReader struct {
940 rw *responseWriter
941 br bodyReader
942 needsContinue bool
943 }
944
945
946
947
948
949 func (srr *serverRequestReader) maybeSendContinue() {
950 if !srr.needsContinue {
951 return
952 }
953 srr.rw.mu.Lock()
954 defer srr.rw.mu.Unlock()
955 if srr.rw.sent100 {
956 return
957 }
958 if srr.rw.statusCode != 0 {
959 srr.br.Close()
960 return
961 }
962 srr.rw.writeHeaderLocked(100)
963 srr.rw.st.Flush()
964 }
965
966 func (srr *serverRequestReader) Read(p []byte) (int, error) {
967 srr.maybeSendContinue()
968 return srr.br.Read(p)
969 }
970
971 func (srr *serverRequestReader) Close() error {
972 return srr.br.Close()
973 }
974
View as plain text