quic-go: http3 post request with body failed

this problem origin publish in caddy issue, but i think it is a quic go problems, more info:https://github.com/caddyserver/caddy/issues/3429

post request has body so can not return response. you can reference this link: https://github.com/caddyserver/caddy/issues/3429

i am a co-worker with @majc08149 , i find problem reason and code. But I do not know how to modify? @mholt

I paste problem code : file->net/http/transfer.go:371

if t.BodyCloser != nil { if err := t.BodyCloser.Close(); err != nil { return err } } the function BodyCloser.Close() call quic-go->http3->body.go Close() like : ` func (r *body) Close() error { // quic.Stream.Close() closes the write side, not the read side if r.isRequest { return r.str.Close() } r.requestDone() r.str.CancelRead(quic.ErrorCode(errorRequestCanceled)) return nil }

`

this call quic-go->stream.go function 👍 func (s *stream) Close() error { if err := s.sendStream.Close(); err != nil { return err } return nil }

the function s.sendStream.Close() will call func (s *sendStream) Close() error , this function call s.ctxCancel() to cancel context. so the request reverseproxy will be canceled.

please help me how to fix this bug?

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Comments: 53 (31 by maintainers)

Most upvoted comments

you can use quiche client to reproduce it. the quiche link : https://github.com/cloudflare/quiche, it is a http3 library and include http3 client tool @mholt @marten-seemann

git clone --recursive https://github.com/cloudflare/quiche
cd quiche
cargo run --manifest-path=tools/apps/Cargo.toml --bin quiche-client -- http://127.0.0.1:8080/bbb.txt  --no-verify --method
 POST --body D:\mysdk\quiche1\tools\apps\target\debug\testbody

[2020-05-22T05:33:11.940792500Z ERROR quiche_apps] connection timed out after 5.0243821s and only completed 0

use caddy version:2.0.0 caddy file:

{

    experimental_http3
    debug
}


:8080 {
    tls test.pem test.key
	log {
		output stdout
		output stderr
		format single_field logfmt
        }

	reverse_proxy {
	  to http://192.168.194.73:8001
	  transport http {
	  	keepalive 30s
	  	keepalive_idle_conns 100
	  }
	}
}

However, I too would like to know exactly which Close() call is removed that makes it work.

Of course, the trick is to remove the right Close() call…

Yeah right, if you remove the call to Close(), then the stream doesn’t get closed. But that’s not a solution. The stream will never get garbage-collected and your connection will stop working once the stream limit is reached…

@marten-seemann Thanks for doing this, I’ll try to take a look after the holiday!

I wrote a client to reproduce the bug in Go, so we don’t have to rely on quiche here. I don’t know how sophisticated their client implementation is - there’s no reason to time out on in the situation, and in fact quic-go wouldn’t do that.

The client is here: https://gist.github.com/marten-seemann/1cb0d87a43935c377c9e59a49c3dbc71. You should be able to just go run this file.

Usage:

go run main.go <url>

Performs a POST request via HTTP/2 / HTTP/1.1.

go run main.go -quic <url>

Performs the same POST request via HTTP/3. It also outputs a qlog. qlogs can be loaded in qvis to inspect the trace. Not sure if that really helps us debug this issue though, all I see is the client sending the request (on stream 0) and then sending a FIN, whereas the server sends a FIN at offset 0.

@mholt Any idea how to further debug this?

@marten-seemann you are right. So I need your help to fix . Maybe close quic-go stream later.

file: go/src/net/http/transfer.go

func (t *transferWriter) writeBody(w io.Writer) error {
	var err error
	var ncopy int64

	// Write body. We "unwrap" the body first if it was wrapped in a
	// nopCloser. This is to ensure that we can take advantage of
	// OS-level optimizations in the event that the body is an
	// *os.File.
	if t.Body != nil {
		var body = t.unwrapBody()
		if chunked(t.TransferEncoding) {
			if bw, ok := w.(*bufio.Writer); ok && !t.IsResponse {
				w = &internal.FlushAfterChunkWriter{Writer: bw}
			}
			cw := internal.NewChunkedWriter(w)
			_, err = t.doBodyCopy(cw, body)
			if err == nil {
				err = cw.Close()
			}
		} else if t.ContentLength == -1 {
			dst := w
			if t.Method == "CONNECT" {
				dst = bufioFlushWriter{dst}
			}
			ncopy, err = t.doBodyCopy(dst, body)
		} else {
			ncopy, err = t.doBodyCopy(w, io.LimitReader(body, t.ContentLength))
			if err != nil {
				return err
			}
			var nextra int64
			nextra, err = t.doBodyCopy(ioutil.Discard, body)
			ncopy += nextra
		}
		if err != nil {
			return err
		}
	}
	if t.BodyCloser != nil {   // here, will call body.close to cancel context
		if err := t.BodyCloser.Close(); err != nil {
			return err
		}
	}

	if !t.ResponseToHEAD && t.ContentLength != -1 && t.ContentLength != ncopy {
		return fmt.Errorf("http: ContentLength=%d with Body length %d",
			t.ContentLength, ncopy)
	}

@mholt if I remove the extra call to Close() in quic-go, it be fine. and I solve my problems.

Please go read https://guides.github.com/features/mastering-markdown/ this is killing me

You’re using > for quotes. Don’t do that. Quotes don’t preserve whitespace. Use ``` for code blocks.