From a91e67f6250bfb467dea47d8d7e5f1fe9d448681 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Thu, 25 Jan 2024 15:17:10 -0500 Subject: [PATCH 01/40] Tracer --- .../app/connectconformance/server_runner.go | 3 + .../connectconformance/testsuites/basic.yaml | 326 +++++++++--------- .../testsuites/http_to_connect_code.yaml | 165 +++++++++ internal/app/referenceclient/client.go | 21 +- internal/app/referenceclient/impl.go | 3 + internal/tracer/middleware.go | 4 + internal/tracer/tracer.go | 5 + 7 files changed, 361 insertions(+), 166 deletions(-) create mode 100644 internal/app/connectconformance/testsuites/http_to_connect_code.yaml diff --git a/internal/app/connectconformance/server_runner.go b/internal/app/connectconformance/server_runner.go index 3f0265b9..4a4465a6 100644 --- a/internal/app/connectconformance/server_runner.go +++ b/internal/app/connectconformance/server_runner.go @@ -20,6 +20,7 @@ import ( "errors" "fmt" "net/http" + "os" "strconv" "strings" "sync" @@ -183,7 +184,9 @@ func runTestCasesForServer( } } + fmt.Fprintln(os.Stderr, "initter") tracer.Init(req.TestName) + fmt.Fprintln(os.Stderr, "gonz") wg.Add(1) err := client.sendRequest(req, func(name string, resp *conformancev1.ClientCompatResponse, err error) { defer wg.Done() diff --git a/internal/app/connectconformance/testsuites/basic.yaml b/internal/app/connectconformance/testsuites/basic.yaml index a0542806..c3640044 100644 --- a/internal/app/connectconformance/testsuites/basic.yaml +++ b/internal/app/connectconformance/testsuites/basic.yaml @@ -19,166 +19,166 @@ testCases: responseTrailers: - name: x-custom-trailer value: ["bing"] -- request: - testName: unary empty definition - service: connectrpc.conformance.v1.ConformanceService - method: Unary - streamType: STREAM_TYPE_UNARY - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest - responseDefinition: -- request: - testName: unary no definition - service: connectrpc.conformance.v1.ConformanceService - method: Unary - streamType: STREAM_TYPE_UNARY - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest -# Client Stream Tests --------------------------------------------------------- -- request: - testName: client stream success - service: connectrpc.conformance.v1.ConformanceService - method: ClientStream - streamType: STREAM_TYPE_CLIENT_STREAM - requestHeaders: - - name: X-Conformance-Test - value: ["Value1","Value2"] - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest - responseDefinition: - responseHeaders: - - name: x-custom-header - value: ["foo"] - responseData: "dGVzdCByZXNwb25zZQ==" - responseTrailers: - - name: x-custom-trailer - value: ["bing"] - - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest - requestData: "dGVzdCByZXNwb25zZQ==" -- request: - testName: client stream empty definition - service: connectrpc.conformance.v1.ConformanceService - method: ClientStream - streamType: STREAM_TYPE_CLIENT_STREAM - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest - responseDefinition: -- request: - testName: client stream no definition - service: connectrpc.conformance.v1.ConformanceService - method: ClientStream - streamType: STREAM_TYPE_CLIENT_STREAM - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest -# Server Stream Tests --------------------------------------------------------- -- request: - testName: server stream success - service: connectrpc.conformance.v1.ConformanceService - method: ServerStream - streamType: STREAM_TYPE_SERVER_STREAM - requestHeaders: - - name: X-Conformance-Test - value: ["Value1","Value2"] - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest - responseDefinition: - responseHeaders: - - name: x-custom-header - value: ["foo"] - responseData: - - "dGVzdCByZXNwb25zZQ==" - - "dGVzdCByZXNwb25zZQ==" - responseTrailers: - - name: x-custom-trailer - value: ["bing"] -- request: - testName: server stream no response - service: connectrpc.conformance.v1.ConformanceService - method: ServerStream - streamType: STREAM_TYPE_SERVER_STREAM - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest - responseDefinition: - responseHeaders: - - name: x-custom-header - value: ["foo"] - responseTrailers: - - name: x-custom-trailer - value: ["bing"] -- request: - testName: server stream no definition - service: connectrpc.conformance.v1.ConformanceService - method: ServerStream - streamType: STREAM_TYPE_SERVER_STREAM - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest -# Bidi Stream Tests ----------------------------------------------------------- -- request: - testName: bidi full duplex stream success - service: connectrpc.conformance.v1.ConformanceService - method: BidiStream - streamType: STREAM_TYPE_FULL_DUPLEX_BIDI_STREAM - requestHeaders: - - name: X-Conformance-Test - value: ["Value1","Value2"] - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest - responseDefinition: - responseHeaders: - - name: x-custom-header - value: ["foo"] - responseData: - - "dGVzdCByZXNwb25zZQ==" - - "dGVzdCByZXNwb25zZQ==" - responseTrailers: - - name: x-custom-trailer - value: ["bing"] - fullDuplex: true - - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest - requestData: "dGVzdCByZXNwb25zZQ==" -- request: - testName: bidi half duplex stream success - service: connectrpc.conformance.v1.ConformanceService - method: BidiStream - streamType: STREAM_TYPE_HALF_DUPLEX_BIDI_STREAM - requestHeaders: - - name: X-Conformance-Test - value: ["Value1","Value2"] - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest - responseDefinition: - responseHeaders: - - name: x-custom-header - value: ["foo"] - responseData: - - "dGVzdCByZXNwb25zZQ==" - - "dGVzdCByZXNwb25zZQ==" - responseTrailers: - - name: x-custom-trailer - value: ["bing"] - - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest - requestData: "dGVzdCByZXNwb25zZQ==" -- request: - # Note there is no 'full duplex no definition set' test since that is - # logically equivalent to this test and is therefore covered here. - testName: bidi half duplex stream no definition set - service: connectrpc.conformance.v1.ConformanceService - method: BidiStream - streamType: STREAM_TYPE_HALF_DUPLEX_BIDI_STREAM - requestHeaders: - - name: X-Conformance-Test - value: ["Value1","Value2"] - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest -# Misc Tests ------------------------------------------------------------------ -- request: - testName: unimplemented - service: connectrpc.conformance.v1.ConformanceService - method: Unimplemented - streamType: STREAM_TYPE_UNARY - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.UnimplementedRequest - # Override - expectedResponse: - error: - code: 12 +# - request: +# testName: unary empty definition +# service: connectrpc.conformance.v1.ConformanceService +# method: Unary +# streamType: STREAM_TYPE_UNARY +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest +# responseDefinition: +# - request: +# testName: unary no definition +# service: connectrpc.conformance.v1.ConformanceService +# method: Unary +# streamType: STREAM_TYPE_UNARY +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest +# # Client Stream Tests --------------------------------------------------------- +# - request: +# testName: client stream success +# service: connectrpc.conformance.v1.ConformanceService +# method: ClientStream +# streamType: STREAM_TYPE_CLIENT_STREAM +# requestHeaders: +# - name: X-Conformance-Test +# value: ["Value1","Value2"] +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest +# responseDefinition: +# responseHeaders: +# - name: x-custom-header +# value: ["foo"] +# responseData: "dGVzdCByZXNwb25zZQ==" +# responseTrailers: +# - name: x-custom-trailer +# value: ["bing"] +# - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest +# requestData: "dGVzdCByZXNwb25zZQ==" +# - request: +# testName: client stream empty definition +# service: connectrpc.conformance.v1.ConformanceService +# method: ClientStream +# streamType: STREAM_TYPE_CLIENT_STREAM +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest +# responseDefinition: +# - request: +# testName: client stream no definition +# service: connectrpc.conformance.v1.ConformanceService +# method: ClientStream +# streamType: STREAM_TYPE_CLIENT_STREAM +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest +# # Server Stream Tests --------------------------------------------------------- +# - request: +# testName: server stream success +# service: connectrpc.conformance.v1.ConformanceService +# method: ServerStream +# streamType: STREAM_TYPE_SERVER_STREAM +# requestHeaders: +# - name: X-Conformance-Test +# value: ["Value1","Value2"] +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest +# responseDefinition: +# responseHeaders: +# - name: x-custom-header +# value: ["foo"] +# responseData: +# - "dGVzdCByZXNwb25zZQ==" +# - "dGVzdCByZXNwb25zZQ==" +# responseTrailers: +# - name: x-custom-trailer +# value: ["bing"] +# - request: +# testName: server stream no response +# service: connectrpc.conformance.v1.ConformanceService +# method: ServerStream +# streamType: STREAM_TYPE_SERVER_STREAM +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest +# responseDefinition: +# responseHeaders: +# - name: x-custom-header +# value: ["foo"] +# responseTrailers: +# - name: x-custom-trailer +# value: ["bing"] +# - request: +# testName: server stream no definition +# service: connectrpc.conformance.v1.ConformanceService +# method: ServerStream +# streamType: STREAM_TYPE_SERVER_STREAM +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest +# # Bidi Stream Tests ----------------------------------------------------------- +# - request: +# testName: bidi full duplex stream success +# service: connectrpc.conformance.v1.ConformanceService +# method: BidiStream +# streamType: STREAM_TYPE_FULL_DUPLEX_BIDI_STREAM +# requestHeaders: +# - name: X-Conformance-Test +# value: ["Value1","Value2"] +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest +# responseDefinition: +# responseHeaders: +# - name: x-custom-header +# value: ["foo"] +# responseData: +# - "dGVzdCByZXNwb25zZQ==" +# - "dGVzdCByZXNwb25zZQ==" +# responseTrailers: +# - name: x-custom-trailer +# value: ["bing"] +# fullDuplex: true +# - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest +# requestData: "dGVzdCByZXNwb25zZQ==" +# - request: +# testName: bidi half duplex stream success +# service: connectrpc.conformance.v1.ConformanceService +# method: BidiStream +# streamType: STREAM_TYPE_HALF_DUPLEX_BIDI_STREAM +# requestHeaders: +# - name: X-Conformance-Test +# value: ["Value1","Value2"] +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest +# responseDefinition: +# responseHeaders: +# - name: x-custom-header +# value: ["foo"] +# responseData: +# - "dGVzdCByZXNwb25zZQ==" +# - "dGVzdCByZXNwb25zZQ==" +# responseTrailers: +# - name: x-custom-trailer +# value: ["bing"] +# - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest +# requestData: "dGVzdCByZXNwb25zZQ==" +# - request: +# # Note there is no 'full duplex no definition set' test since that is +# # logically equivalent to this test and is therefore covered here. +# testName: bidi half duplex stream no definition set +# service: connectrpc.conformance.v1.ConformanceService +# method: BidiStream +# streamType: STREAM_TYPE_HALF_DUPLEX_BIDI_STREAM +# requestHeaders: +# - name: X-Conformance-Test +# value: ["Value1","Value2"] +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest +# # Misc Tests ------------------------------------------------------------------ +# - request: +# testName: unimplemented +# service: connectrpc.conformance.v1.ConformanceService +# method: Unimplemented +# streamType: STREAM_TYPE_UNARY +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnimplementedRequest +# # Override +# expectedResponse: +# error: +# code: 12 diff --git a/internal/app/connectconformance/testsuites/http_to_connect_code.yaml b/internal/app/connectconformance/testsuites/http_to_connect_code.yaml new file mode 100644 index 00000000..22e3232c --- /dev/null +++ b/internal/app/connectconformance/testsuites/http_to_connect_code.yaml @@ -0,0 +1,165 @@ +name: HTTP to Connect Code Mapping +mode: TEST_MODE_CLIENT +relevantProtocols: + - PROTOCOL_CONNECT +relevantCodecs: + - CODEC_JSON +relevantCompressions: + - COMPRESSION_IDENTITY +relevantHttpVersions: + - HTTP_VERSION_1 +reliesOnTls: false +testCases: +- request: + testName: bad request + service: connectrpc.conformance.v1.ConformanceService + method: Unary + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + responseData: "dGVzdCByZXNwb25zZQ==" + rawResponse: + statusCode: 400 +# - request: +# testName: unauthorized +# service: connectrpc.conformance.v1.ConformanceService +# method: Unary +# streamType: STREAM_TYPE_UNARY +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest +# responseDefinition: +# rawResponse: +# statusCode: 401 +# - request: +# testName: forbidden +# service: connectrpc.conformance.v1.ConformanceService +# method: Unary +# streamType: STREAM_TYPE_UNARY +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest +# responseDefinition: +# rawResponse: +# statusCode: 403 +# - request: +# testName: not found +# service: connectrpc.conformance.v1.ConformanceService +# method: Unary +# streamType: STREAM_TYPE_UNARY +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest +# responseDefinition: +# rawResponse: +# statusCode: 404 +# - request: +# testName: request timeout +# service: connectrpc.conformance.v1.ConformanceService +# method: Unary +# streamType: STREAM_TYPE_UNARY +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest +# responseDefinition: +# rawResponse: +# statusCode: 408 +# # TODO - this is specified in the protocol, but not handled by Connect-Go +# # - request: +# # testName: conflict +# # service: connectrpc.conformance.v1.ConformanceService +# # method: Unary +# # streamType: STREAM_TYPE_UNARY +# # requestMessages: +# # - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest +# # responseDefinition: +# # rawResponse: +# # statusCode: 409 +# - request: +# testName: precondition failed +# service: connectrpc.conformance.v1.ConformanceService +# method: Unary +# streamType: STREAM_TYPE_UNARY +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest +# responseDefinition: +# rawResponse: +# statusCode: 412 +# - request: +# testName: payload too large +# service: connectrpc.conformance.v1.ConformanceService +# method: Unary +# streamType: STREAM_TYPE_UNARY +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest +# responseDefinition: +# rawResponse: +# statusCode: 413 +# # TODO - this is specified in the protocol, but not handled by Connect-Go +# # - request: +# # testName: unsupported media type +# # service: connectrpc.conformance.v1.ConformanceService +# # method: Unary +# # streamType: STREAM_TYPE_UNARY +# # requestMessages: +# # - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest +# # responseDefinition: +# # rawResponse: +# # statusCode: 415 +# - request: +# testName: too many requests +# service: connectrpc.conformance.v1.ConformanceService +# method: Unary +# streamType: STREAM_TYPE_UNARY +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest +# responseDefinition: +# rawResponse: +# statusCode: 429 +# - request: +# testName: request header fields too large +# service: connectrpc.conformance.v1.ConformanceService +# method: Unary +# streamType: STREAM_TYPE_UNARY +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest +# responseDefinition: +# rawResponse: +# statusCode: 431 +# - request: +# testName: bad gateway +# service: connectrpc.conformance.v1.ConformanceService +# method: Unary +# streamType: STREAM_TYPE_UNARY +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest +# responseDefinition: +# rawResponse: +# statusCode: 502 +# - request: +# testName: service unavailable +# service: connectrpc.conformance.v1.ConformanceService +# method: Unary +# streamType: STREAM_TYPE_UNARY +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest +# responseDefinition: +# rawResponse: +# statusCode: 503 +# - request: +# testName: gateway timeout +# service: connectrpc.conformance.v1.ConformanceService +# method: Unary +# streamType: STREAM_TYPE_UNARY +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest +# responseDefinition: +# rawResponse: +# statusCode: 504 +# - request: +# testName: im a teapot +# service: connectrpc.conformance.v1.ConformanceService +# method: Unary +# streamType: STREAM_TYPE_UNARY +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest +# responseDefinition: +# rawResponse: +# statusCode: 418 diff --git a/internal/app/referenceclient/client.go b/internal/app/referenceclient/client.go index 0fc7fa45..79e01930 100644 --- a/internal/app/referenceclient/client.go +++ b/internal/app/referenceclient/client.go @@ -24,6 +24,7 @@ import ( "net" "net/http" "net/url" + "os" "path/filepath" "runtime" "strconv" @@ -44,6 +45,17 @@ import ( "golang.org/x/sync/semaphore" ) +type ContextTracer struct { + tracer *tracer.Tracer +} + +func (t *ContextTracer) Complete(trace tracer.Trace) { + + fmt.Fprintln(os.Stderr, "Wrapped Trace %+v:", trace) + + t.tracer.Complete(trace) +} + // Run runs the client according to a client config read from the 'in' reader. The result of the run // is written to the 'out' writer, including any errors encountered during the actual run. Any error // returned from this function is indicative of an issue with the reader or writer and should not be related @@ -216,9 +228,12 @@ func invoke(ctx context.Context, req *v1.ClientCompatRequest, trace *tracer.Trac case v1.HTTPVersion_HTTP_VERSION_UNSPECIFIED: return nil, errors.New("an HTTP version must be specified") } - if trace != nil { - transport = tracer.TracingRoundTripper(transport, trace) - } + + // TODO - Should we always create the tracing roundtripper? + // ct := &ContextTracer{ + // tracer: trace, + // } + // transport = tracer.TracingRoundTripper(transport, ct) if req.RawRequest != nil { transport = &rawRequestSender{transport: transport, rawRequest: req.RawRequest} diff --git a/internal/app/referenceclient/impl.go b/internal/app/referenceclient/impl.go index acafe4c6..c0e94d4d 100644 --- a/internal/app/referenceclient/impl.go +++ b/internal/app/referenceclient/impl.go @@ -26,6 +26,7 @@ import ( "connectrpc.com/conformance/internal" v1 "connectrpc.com/conformance/internal/gen/proto/go/connectrpc/conformance/v1" "connectrpc.com/conformance/internal/gen/proto/go/connectrpc/conformance/v1/conformancev1connect" + "connectrpc.com/conformance/internal/tracer" "connectrpc.com/connect" ) @@ -33,6 +34,7 @@ const clientName = "connectconformance-referenceclient" type invoker struct { client conformancev1connect.ConformanceServiceClient + tracer *tracer.Tracer } func (i *invoker) Invoke( @@ -119,6 +121,7 @@ func (i *invoker) unary( // Invoke the Unary call resp, err := i.client.Unary(ctx, request) + if err != nil { // If an error was returned, first convert it to a Connect error // so that we can get the headers from the Meta property. Then, diff --git a/internal/tracer/middleware.go b/internal/tracer/middleware.go index f56063d4..70939403 100644 --- a/internal/tracer/middleware.go +++ b/internal/tracer/middleware.go @@ -19,6 +19,7 @@ import ( "fmt" "io" "net/http" + "os" "strconv" "strings" ) @@ -27,6 +28,7 @@ import ( // round tripper will record traces of all operations to the given tracer. func TracingRoundTripper(transport http.RoundTripper, collector Collector) http.RoundTripper { return roundTripperFunc(func(req *http.Request) (*http.Response, error) { + fmt.Fprintln(os.Stderr, "roundtripper") builder := newBuilder(req, collector) req = req.Clone(req.Context()) req.Body = newReader(req.Header, req.Body, true, builder) @@ -56,6 +58,7 @@ func TracingHandler(handler http.Handler, collector Collector) http.Handler { builder: builder, } + fmt.Fprintln(os.Stderr, "soiving http") handler.ServeHTTP( traceWriter, req, @@ -93,6 +96,7 @@ func (t *tracingResponseWriter) Write(data []byte) (int, error) { if err != nil { t.tryFinish(err) } + fmt.Fprintln(os.Stderr, "writing") return n, err } diff --git a/internal/tracer/tracer.go b/internal/tracer/tracer.go index 3cee78e0..fde14246 100644 --- a/internal/tracer/tracer.go +++ b/internal/tracer/tracer.go @@ -18,6 +18,7 @@ import ( "context" "fmt" "net/http" + "os" "sort" "strings" "sync" @@ -46,6 +47,7 @@ type Tracer struct { // Init initializes the tracer to accept data for a trace for the given test name. // This must be called before Clear, Complete, or Await for the same name. func (t *Tracer) Init(testName string) { + fmt.Fprintln(os.Stderr, "tracer.Init") if t == nil { return } @@ -62,6 +64,7 @@ func (t *Tracer) Init(testName string) { // Clear clears the data for the given test name. This frees up resources so // that the tracer doesn't use more memory than necessary. func (t *Tracer) Clear(testName string) { + fmt.Fprintln(os.Stderr, "tracer.Clear") if t == nil { return } @@ -73,6 +76,7 @@ func (t *Tracer) Clear(testName string) { // Complete marks a test as complete with the given trace data. If Clear // has already been called or Init was never called, this does nothing. func (t *Tracer) Complete(trace Trace) { + fmt.Fprintln(os.Stderr, "tracer.Complete") if t == nil { return } @@ -94,6 +98,7 @@ func (t *Tracer) Complete(trace Trace) { // an error if Clear has alreadu been called for the test or if Init was // never called. func (t *Tracer) Await(ctx context.Context, testName string) (*Trace, error) { + fmt.Fprintln(os.Stderr, "tracer.Await") if t == nil { return nil, fmt.Errorf("%s: tracing not enabled", testName) } From 7f26eeb4c91fc1f1aa6f61b551ca511d4b563af7 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Fri, 26 Jan 2024 11:26:56 -0500 Subject: [PATCH 02/40] Tracer --- .../app/connectconformance/testsuites/basic.yaml | 10 ++++++++++ internal/app/referenceclient/client.go | 15 ++++++++++----- internal/app/referenceclient/impl.go | 3 +++ internal/tracer/builder.go | 3 +++ internal/tracer/middleware.go | 4 ---- internal/tracer/tracer.go | 5 ----- 6 files changed, 26 insertions(+), 14 deletions(-) diff --git a/internal/app/connectconformance/testsuites/basic.yaml b/internal/app/connectconformance/testsuites/basic.yaml index c3640044..ce70ed43 100644 --- a/internal/app/connectconformance/testsuites/basic.yaml +++ b/internal/app/connectconformance/testsuites/basic.yaml @@ -1,4 +1,14 @@ name: Basic +relevantProtocols: + - PROTOCOL_CONNECT +relevantCodecs: + - CODEC_JSON +relevantCompressions: + - COMPRESSION_IDENTITY +relevantHttpVersions: + - HTTP_VERSION_1 +reliesOnTls: false +reliesOnTlsClientCerts: false testCases: # Unary Tests ----------------------------------------------------------------- - request: diff --git a/internal/app/referenceclient/client.go b/internal/app/referenceclient/client.go index 79e01930..3fecefac 100644 --- a/internal/app/referenceclient/client.go +++ b/internal/app/referenceclient/client.go @@ -47,11 +47,14 @@ import ( type ContextTracer struct { tracer *tracer.Tracer + ctx context.Context } func (t *ContextTracer) Complete(trace tracer.Trace) { - fmt.Fprintln(os.Stderr, "Wrapped Trace %+v:", trace) + // fmt.Println(t.ctx.Value("response")) + t.ctx = context.WithValue(t.ctx, "response", "HTTP 69") + fmt.Fprintln(os.Stderr, "Wrapped Trace %+v:", trace.Response) t.tracer.Complete(trace) } @@ -229,11 +232,13 @@ func invoke(ctx context.Context, req *v1.ClientCompatRequest, trace *tracer.Trac return nil, errors.New("an HTTP version must be specified") } + ctx = context.WithValue(ctx, "response", nil) // TODO - Should we always create the tracing roundtripper? - // ct := &ContextTracer{ - // tracer: trace, - // } - // transport = tracer.TracingRoundTripper(transport, ct) + ct := &ContextTracer{ + tracer: trace, + ctx: ctx, + } + transport = tracer.TracingRoundTripper(transport, ct) if req.RawRequest != nil { transport = &rawRequestSender{transport: transport, rawRequest: req.RawRequest} diff --git a/internal/app/referenceclient/impl.go b/internal/app/referenceclient/impl.go index c0e94d4d..ed074543 100644 --- a/internal/app/referenceclient/impl.go +++ b/internal/app/referenceclient/impl.go @@ -21,6 +21,7 @@ import ( "io" "net/http" "net/url" + "os" "time" "connectrpc.com/conformance/internal" @@ -119,8 +120,10 @@ func (i *invoker) unary( var trailers []*v1.Header payloads := make([]*v1.ConformancePayload, 0, 1) + fmt.Fprintln(os.Stderr, "Before unary") // Invoke the Unary call resp, err := i.client.Unary(ctx, request) + fmt.Fprintln(os.Stderr, "After unary") if err != nil { // If an error was returned, first convert it to a Connect error diff --git a/internal/tracer/builder.go b/internal/tracer/builder.go index 1a675f92..b6ca657c 100644 --- a/internal/tracer/builder.go +++ b/internal/tracer/builder.go @@ -15,7 +15,9 @@ package tracer import ( + "fmt" "net/http" + "os" "sync" "time" ) @@ -87,5 +89,6 @@ func (b *builder) build() { if trace.TestName == "" { return } + fmt.Fprintln(os.Stderr, "Completing trace") b.collector.Complete(trace) } diff --git a/internal/tracer/middleware.go b/internal/tracer/middleware.go index 70939403..f56063d4 100644 --- a/internal/tracer/middleware.go +++ b/internal/tracer/middleware.go @@ -19,7 +19,6 @@ import ( "fmt" "io" "net/http" - "os" "strconv" "strings" ) @@ -28,7 +27,6 @@ import ( // round tripper will record traces of all operations to the given tracer. func TracingRoundTripper(transport http.RoundTripper, collector Collector) http.RoundTripper { return roundTripperFunc(func(req *http.Request) (*http.Response, error) { - fmt.Fprintln(os.Stderr, "roundtripper") builder := newBuilder(req, collector) req = req.Clone(req.Context()) req.Body = newReader(req.Header, req.Body, true, builder) @@ -58,7 +56,6 @@ func TracingHandler(handler http.Handler, collector Collector) http.Handler { builder: builder, } - fmt.Fprintln(os.Stderr, "soiving http") handler.ServeHTTP( traceWriter, req, @@ -96,7 +93,6 @@ func (t *tracingResponseWriter) Write(data []byte) (int, error) { if err != nil { t.tryFinish(err) } - fmt.Fprintln(os.Stderr, "writing") return n, err } diff --git a/internal/tracer/tracer.go b/internal/tracer/tracer.go index fde14246..3cee78e0 100644 --- a/internal/tracer/tracer.go +++ b/internal/tracer/tracer.go @@ -18,7 +18,6 @@ import ( "context" "fmt" "net/http" - "os" "sort" "strings" "sync" @@ -47,7 +46,6 @@ type Tracer struct { // Init initializes the tracer to accept data for a trace for the given test name. // This must be called before Clear, Complete, or Await for the same name. func (t *Tracer) Init(testName string) { - fmt.Fprintln(os.Stderr, "tracer.Init") if t == nil { return } @@ -64,7 +62,6 @@ func (t *Tracer) Init(testName string) { // Clear clears the data for the given test name. This frees up resources so // that the tracer doesn't use more memory than necessary. func (t *Tracer) Clear(testName string) { - fmt.Fprintln(os.Stderr, "tracer.Clear") if t == nil { return } @@ -76,7 +73,6 @@ func (t *Tracer) Clear(testName string) { // Complete marks a test as complete with the given trace data. If Clear // has already been called or Init was never called, this does nothing. func (t *Tracer) Complete(trace Trace) { - fmt.Fprintln(os.Stderr, "tracer.Complete") if t == nil { return } @@ -98,7 +94,6 @@ func (t *Tracer) Complete(trace Trace) { // an error if Clear has alreadu been called for the test or if Init was // never called. func (t *Tracer) Await(ctx context.Context, testName string) (*Trace, error) { - fmt.Fprintln(os.Stderr, "tracer.Await") if t == nil { return nil, fmt.Errorf("%s: tracing not enabled", testName) } From fd0b009fb80860bc415f0f274fa4772ec18283c2 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Fri, 26 Jan 2024 12:27:31 -0500 Subject: [PATCH 03/40] Tracer --- internal/app/referenceclient/client.go | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/internal/app/referenceclient/client.go b/internal/app/referenceclient/client.go index 3fecefac..88243eac 100644 --- a/internal/app/referenceclient/client.go +++ b/internal/app/referenceclient/client.go @@ -24,7 +24,6 @@ import ( "net" "net/http" "net/url" - "os" "path/filepath" "runtime" "strconv" @@ -51,11 +50,7 @@ type ContextTracer struct { } func (t *ContextTracer) Complete(trace tracer.Trace) { - - // fmt.Println(t.ctx.Value("response")) - t.ctx = context.WithValue(t.ctx, "response", "HTTP 69") - fmt.Fprintln(os.Stderr, "Wrapped Trace %+v:", trace.Response) - + t.ctx = context.WithValue(t.ctx, "response", trace.Response.StatusCode) t.tracer.Complete(trace) } @@ -232,7 +227,6 @@ func invoke(ctx context.Context, req *v1.ClientCompatRequest, trace *tracer.Trac return nil, errors.New("an HTTP version must be specified") } - ctx = context.WithValue(ctx, "response", nil) // TODO - Should we always create the tracing roundtripper? ct := &ContextTracer{ tracer: trace, From 76d823e7b95aefc6711e5969bc96e9ec36b54c48 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Fri, 26 Jan 2024 12:28:41 -0500 Subject: [PATCH 04/40] Revert --- .../connectconformance/testsuites/basic.yaml | 336 +++++++++--------- .../testsuites/http_to_connect_code.yaml | 165 --------- internal/app/referenceclient/impl.go | 6 - internal/tracer/builder.go | 3 - 4 files changed, 163 insertions(+), 347 deletions(-) delete mode 100644 internal/app/connectconformance/testsuites/http_to_connect_code.yaml diff --git a/internal/app/connectconformance/testsuites/basic.yaml b/internal/app/connectconformance/testsuites/basic.yaml index ce70ed43..a0542806 100644 --- a/internal/app/connectconformance/testsuites/basic.yaml +++ b/internal/app/connectconformance/testsuites/basic.yaml @@ -1,14 +1,4 @@ name: Basic -relevantProtocols: - - PROTOCOL_CONNECT -relevantCodecs: - - CODEC_JSON -relevantCompressions: - - COMPRESSION_IDENTITY -relevantHttpVersions: - - HTTP_VERSION_1 -reliesOnTls: false -reliesOnTlsClientCerts: false testCases: # Unary Tests ----------------------------------------------------------------- - request: @@ -29,166 +19,166 @@ testCases: responseTrailers: - name: x-custom-trailer value: ["bing"] -# - request: -# testName: unary empty definition -# service: connectrpc.conformance.v1.ConformanceService -# method: Unary -# streamType: STREAM_TYPE_UNARY -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest -# responseDefinition: -# - request: -# testName: unary no definition -# service: connectrpc.conformance.v1.ConformanceService -# method: Unary -# streamType: STREAM_TYPE_UNARY -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest -# # Client Stream Tests --------------------------------------------------------- -# - request: -# testName: client stream success -# service: connectrpc.conformance.v1.ConformanceService -# method: ClientStream -# streamType: STREAM_TYPE_CLIENT_STREAM -# requestHeaders: -# - name: X-Conformance-Test -# value: ["Value1","Value2"] -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest -# responseDefinition: -# responseHeaders: -# - name: x-custom-header -# value: ["foo"] -# responseData: "dGVzdCByZXNwb25zZQ==" -# responseTrailers: -# - name: x-custom-trailer -# value: ["bing"] -# - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest -# requestData: "dGVzdCByZXNwb25zZQ==" -# - request: -# testName: client stream empty definition -# service: connectrpc.conformance.v1.ConformanceService -# method: ClientStream -# streamType: STREAM_TYPE_CLIENT_STREAM -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest -# responseDefinition: -# - request: -# testName: client stream no definition -# service: connectrpc.conformance.v1.ConformanceService -# method: ClientStream -# streamType: STREAM_TYPE_CLIENT_STREAM -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest -# # Server Stream Tests --------------------------------------------------------- -# - request: -# testName: server stream success -# service: connectrpc.conformance.v1.ConformanceService -# method: ServerStream -# streamType: STREAM_TYPE_SERVER_STREAM -# requestHeaders: -# - name: X-Conformance-Test -# value: ["Value1","Value2"] -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest -# responseDefinition: -# responseHeaders: -# - name: x-custom-header -# value: ["foo"] -# responseData: -# - "dGVzdCByZXNwb25zZQ==" -# - "dGVzdCByZXNwb25zZQ==" -# responseTrailers: -# - name: x-custom-trailer -# value: ["bing"] -# - request: -# testName: server stream no response -# service: connectrpc.conformance.v1.ConformanceService -# method: ServerStream -# streamType: STREAM_TYPE_SERVER_STREAM -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest -# responseDefinition: -# responseHeaders: -# - name: x-custom-header -# value: ["foo"] -# responseTrailers: -# - name: x-custom-trailer -# value: ["bing"] -# - request: -# testName: server stream no definition -# service: connectrpc.conformance.v1.ConformanceService -# method: ServerStream -# streamType: STREAM_TYPE_SERVER_STREAM -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest -# # Bidi Stream Tests ----------------------------------------------------------- -# - request: -# testName: bidi full duplex stream success -# service: connectrpc.conformance.v1.ConformanceService -# method: BidiStream -# streamType: STREAM_TYPE_FULL_DUPLEX_BIDI_STREAM -# requestHeaders: -# - name: X-Conformance-Test -# value: ["Value1","Value2"] -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest -# responseDefinition: -# responseHeaders: -# - name: x-custom-header -# value: ["foo"] -# responseData: -# - "dGVzdCByZXNwb25zZQ==" -# - "dGVzdCByZXNwb25zZQ==" -# responseTrailers: -# - name: x-custom-trailer -# value: ["bing"] -# fullDuplex: true -# - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest -# requestData: "dGVzdCByZXNwb25zZQ==" -# - request: -# testName: bidi half duplex stream success -# service: connectrpc.conformance.v1.ConformanceService -# method: BidiStream -# streamType: STREAM_TYPE_HALF_DUPLEX_BIDI_STREAM -# requestHeaders: -# - name: X-Conformance-Test -# value: ["Value1","Value2"] -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest -# responseDefinition: -# responseHeaders: -# - name: x-custom-header -# value: ["foo"] -# responseData: -# - "dGVzdCByZXNwb25zZQ==" -# - "dGVzdCByZXNwb25zZQ==" -# responseTrailers: -# - name: x-custom-trailer -# value: ["bing"] -# - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest -# requestData: "dGVzdCByZXNwb25zZQ==" -# - request: -# # Note there is no 'full duplex no definition set' test since that is -# # logically equivalent to this test and is therefore covered here. -# testName: bidi half duplex stream no definition set -# service: connectrpc.conformance.v1.ConformanceService -# method: BidiStream -# streamType: STREAM_TYPE_HALF_DUPLEX_BIDI_STREAM -# requestHeaders: -# - name: X-Conformance-Test -# value: ["Value1","Value2"] -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest -# # Misc Tests ------------------------------------------------------------------ -# - request: -# testName: unimplemented -# service: connectrpc.conformance.v1.ConformanceService -# method: Unimplemented -# streamType: STREAM_TYPE_UNARY -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnimplementedRequest -# # Override -# expectedResponse: -# error: -# code: 12 +- request: + testName: unary empty definition + service: connectrpc.conformance.v1.ConformanceService + method: Unary + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: +- request: + testName: unary no definition + service: connectrpc.conformance.v1.ConformanceService + method: Unary + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest +# Client Stream Tests --------------------------------------------------------- +- request: + testName: client stream success + service: connectrpc.conformance.v1.ConformanceService + method: ClientStream + streamType: STREAM_TYPE_CLIENT_STREAM + requestHeaders: + - name: X-Conformance-Test + value: ["Value1","Value2"] + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest + responseDefinition: + responseHeaders: + - name: x-custom-header + value: ["foo"] + responseData: "dGVzdCByZXNwb25zZQ==" + responseTrailers: + - name: x-custom-trailer + value: ["bing"] + - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest + requestData: "dGVzdCByZXNwb25zZQ==" +- request: + testName: client stream empty definition + service: connectrpc.conformance.v1.ConformanceService + method: ClientStream + streamType: STREAM_TYPE_CLIENT_STREAM + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest + responseDefinition: +- request: + testName: client stream no definition + service: connectrpc.conformance.v1.ConformanceService + method: ClientStream + streamType: STREAM_TYPE_CLIENT_STREAM + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest +# Server Stream Tests --------------------------------------------------------- +- request: + testName: server stream success + service: connectrpc.conformance.v1.ConformanceService + method: ServerStream + streamType: STREAM_TYPE_SERVER_STREAM + requestHeaders: + - name: X-Conformance-Test + value: ["Value1","Value2"] + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest + responseDefinition: + responseHeaders: + - name: x-custom-header + value: ["foo"] + responseData: + - "dGVzdCByZXNwb25zZQ==" + - "dGVzdCByZXNwb25zZQ==" + responseTrailers: + - name: x-custom-trailer + value: ["bing"] +- request: + testName: server stream no response + service: connectrpc.conformance.v1.ConformanceService + method: ServerStream + streamType: STREAM_TYPE_SERVER_STREAM + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest + responseDefinition: + responseHeaders: + - name: x-custom-header + value: ["foo"] + responseTrailers: + - name: x-custom-trailer + value: ["bing"] +- request: + testName: server stream no definition + service: connectrpc.conformance.v1.ConformanceService + method: ServerStream + streamType: STREAM_TYPE_SERVER_STREAM + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest +# Bidi Stream Tests ----------------------------------------------------------- +- request: + testName: bidi full duplex stream success + service: connectrpc.conformance.v1.ConformanceService + method: BidiStream + streamType: STREAM_TYPE_FULL_DUPLEX_BIDI_STREAM + requestHeaders: + - name: X-Conformance-Test + value: ["Value1","Value2"] + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest + responseDefinition: + responseHeaders: + - name: x-custom-header + value: ["foo"] + responseData: + - "dGVzdCByZXNwb25zZQ==" + - "dGVzdCByZXNwb25zZQ==" + responseTrailers: + - name: x-custom-trailer + value: ["bing"] + fullDuplex: true + - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest + requestData: "dGVzdCByZXNwb25zZQ==" +- request: + testName: bidi half duplex stream success + service: connectrpc.conformance.v1.ConformanceService + method: BidiStream + streamType: STREAM_TYPE_HALF_DUPLEX_BIDI_STREAM + requestHeaders: + - name: X-Conformance-Test + value: ["Value1","Value2"] + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest + responseDefinition: + responseHeaders: + - name: x-custom-header + value: ["foo"] + responseData: + - "dGVzdCByZXNwb25zZQ==" + - "dGVzdCByZXNwb25zZQ==" + responseTrailers: + - name: x-custom-trailer + value: ["bing"] + - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest + requestData: "dGVzdCByZXNwb25zZQ==" +- request: + # Note there is no 'full duplex no definition set' test since that is + # logically equivalent to this test and is therefore covered here. + testName: bidi half duplex stream no definition set + service: connectrpc.conformance.v1.ConformanceService + method: BidiStream + streamType: STREAM_TYPE_HALF_DUPLEX_BIDI_STREAM + requestHeaders: + - name: X-Conformance-Test + value: ["Value1","Value2"] + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest +# Misc Tests ------------------------------------------------------------------ +- request: + testName: unimplemented + service: connectrpc.conformance.v1.ConformanceService + method: Unimplemented + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnimplementedRequest + # Override + expectedResponse: + error: + code: 12 diff --git a/internal/app/connectconformance/testsuites/http_to_connect_code.yaml b/internal/app/connectconformance/testsuites/http_to_connect_code.yaml deleted file mode 100644 index 22e3232c..00000000 --- a/internal/app/connectconformance/testsuites/http_to_connect_code.yaml +++ /dev/null @@ -1,165 +0,0 @@ -name: HTTP to Connect Code Mapping -mode: TEST_MODE_CLIENT -relevantProtocols: - - PROTOCOL_CONNECT -relevantCodecs: - - CODEC_JSON -relevantCompressions: - - COMPRESSION_IDENTITY -relevantHttpVersions: - - HTTP_VERSION_1 -reliesOnTls: false -testCases: -- request: - testName: bad request - service: connectrpc.conformance.v1.ConformanceService - method: Unary - streamType: STREAM_TYPE_UNARY - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest - responseDefinition: - responseData: "dGVzdCByZXNwb25zZQ==" - rawResponse: - statusCode: 400 -# - request: -# testName: unauthorized -# service: connectrpc.conformance.v1.ConformanceService -# method: Unary -# streamType: STREAM_TYPE_UNARY -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest -# responseDefinition: -# rawResponse: -# statusCode: 401 -# - request: -# testName: forbidden -# service: connectrpc.conformance.v1.ConformanceService -# method: Unary -# streamType: STREAM_TYPE_UNARY -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest -# responseDefinition: -# rawResponse: -# statusCode: 403 -# - request: -# testName: not found -# service: connectrpc.conformance.v1.ConformanceService -# method: Unary -# streamType: STREAM_TYPE_UNARY -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest -# responseDefinition: -# rawResponse: -# statusCode: 404 -# - request: -# testName: request timeout -# service: connectrpc.conformance.v1.ConformanceService -# method: Unary -# streamType: STREAM_TYPE_UNARY -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest -# responseDefinition: -# rawResponse: -# statusCode: 408 -# # TODO - this is specified in the protocol, but not handled by Connect-Go -# # - request: -# # testName: conflict -# # service: connectrpc.conformance.v1.ConformanceService -# # method: Unary -# # streamType: STREAM_TYPE_UNARY -# # requestMessages: -# # - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest -# # responseDefinition: -# # rawResponse: -# # statusCode: 409 -# - request: -# testName: precondition failed -# service: connectrpc.conformance.v1.ConformanceService -# method: Unary -# streamType: STREAM_TYPE_UNARY -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest -# responseDefinition: -# rawResponse: -# statusCode: 412 -# - request: -# testName: payload too large -# service: connectrpc.conformance.v1.ConformanceService -# method: Unary -# streamType: STREAM_TYPE_UNARY -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest -# responseDefinition: -# rawResponse: -# statusCode: 413 -# # TODO - this is specified in the protocol, but not handled by Connect-Go -# # - request: -# # testName: unsupported media type -# # service: connectrpc.conformance.v1.ConformanceService -# # method: Unary -# # streamType: STREAM_TYPE_UNARY -# # requestMessages: -# # - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest -# # responseDefinition: -# # rawResponse: -# # statusCode: 415 -# - request: -# testName: too many requests -# service: connectrpc.conformance.v1.ConformanceService -# method: Unary -# streamType: STREAM_TYPE_UNARY -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest -# responseDefinition: -# rawResponse: -# statusCode: 429 -# - request: -# testName: request header fields too large -# service: connectrpc.conformance.v1.ConformanceService -# method: Unary -# streamType: STREAM_TYPE_UNARY -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest -# responseDefinition: -# rawResponse: -# statusCode: 431 -# - request: -# testName: bad gateway -# service: connectrpc.conformance.v1.ConformanceService -# method: Unary -# streamType: STREAM_TYPE_UNARY -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest -# responseDefinition: -# rawResponse: -# statusCode: 502 -# - request: -# testName: service unavailable -# service: connectrpc.conformance.v1.ConformanceService -# method: Unary -# streamType: STREAM_TYPE_UNARY -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest -# responseDefinition: -# rawResponse: -# statusCode: 503 -# - request: -# testName: gateway timeout -# service: connectrpc.conformance.v1.ConformanceService -# method: Unary -# streamType: STREAM_TYPE_UNARY -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest -# responseDefinition: -# rawResponse: -# statusCode: 504 -# - request: -# testName: im a teapot -# service: connectrpc.conformance.v1.ConformanceService -# method: Unary -# streamType: STREAM_TYPE_UNARY -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest -# responseDefinition: -# rawResponse: -# statusCode: 418 diff --git a/internal/app/referenceclient/impl.go b/internal/app/referenceclient/impl.go index ed074543..acafe4c6 100644 --- a/internal/app/referenceclient/impl.go +++ b/internal/app/referenceclient/impl.go @@ -21,13 +21,11 @@ import ( "io" "net/http" "net/url" - "os" "time" "connectrpc.com/conformance/internal" v1 "connectrpc.com/conformance/internal/gen/proto/go/connectrpc/conformance/v1" "connectrpc.com/conformance/internal/gen/proto/go/connectrpc/conformance/v1/conformancev1connect" - "connectrpc.com/conformance/internal/tracer" "connectrpc.com/connect" ) @@ -35,7 +33,6 @@ const clientName = "connectconformance-referenceclient" type invoker struct { client conformancev1connect.ConformanceServiceClient - tracer *tracer.Tracer } func (i *invoker) Invoke( @@ -120,11 +117,8 @@ func (i *invoker) unary( var trailers []*v1.Header payloads := make([]*v1.ConformancePayload, 0, 1) - fmt.Fprintln(os.Stderr, "Before unary") // Invoke the Unary call resp, err := i.client.Unary(ctx, request) - fmt.Fprintln(os.Stderr, "After unary") - if err != nil { // If an error was returned, first convert it to a Connect error // so that we can get the headers from the Meta property. Then, diff --git a/internal/tracer/builder.go b/internal/tracer/builder.go index b6ca657c..1a675f92 100644 --- a/internal/tracer/builder.go +++ b/internal/tracer/builder.go @@ -15,9 +15,7 @@ package tracer import ( - "fmt" "net/http" - "os" "sync" "time" ) @@ -89,6 +87,5 @@ func (b *builder) build() { if trace.TestName == "" { return } - fmt.Fprintln(os.Stderr, "Completing trace") b.collector.Complete(trace) } From 3198a13f670a744bd9cb052f803a0f3dac70cc7c Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Fri, 26 Jan 2024 12:29:05 -0500 Subject: [PATCH 05/40] Revert --- internal/app/connectconformance/server_runner.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/internal/app/connectconformance/server_runner.go b/internal/app/connectconformance/server_runner.go index 4a4465a6..3f0265b9 100644 --- a/internal/app/connectconformance/server_runner.go +++ b/internal/app/connectconformance/server_runner.go @@ -20,7 +20,6 @@ import ( "errors" "fmt" "net/http" - "os" "strconv" "strings" "sync" @@ -184,9 +183,7 @@ func runTestCasesForServer( } } - fmt.Fprintln(os.Stderr, "initter") tracer.Init(req.TestName) - fmt.Fprintln(os.Stderr, "gonz") wg.Add(1) err := client.sendRequest(req, func(name string, resp *conformancev1.ClientCompatResponse, err error) { defer wg.Done() From 2f87a7fa775e860b6be7f859e71a20923f735424 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Fri, 26 Jan 2024 12:36:10 -0500 Subject: [PATCH 06/40] Revert --- internal/app/referenceclient/client.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/app/referenceclient/client.go b/internal/app/referenceclient/client.go index 88243eac..d8a65521 100644 --- a/internal/app/referenceclient/client.go +++ b/internal/app/referenceclient/client.go @@ -24,6 +24,7 @@ import ( "net" "net/http" "net/url" + "os" "path/filepath" "runtime" "strconv" @@ -51,6 +52,8 @@ type ContextTracer struct { func (t *ContextTracer) Complete(trace tracer.Trace) { t.ctx = context.WithValue(t.ctx, "response", trace.Response.StatusCode) + fmt.Fprintln(os.Stderr, "Wrapped Trace %+v:", trace.Response) + t.tracer.Complete(trace) } @@ -227,6 +230,7 @@ func invoke(ctx context.Context, req *v1.ClientCompatRequest, trace *tracer.Trac return nil, errors.New("an HTTP version must be specified") } + ctx = context.WithValue(ctx, "response", nil) // TODO - Should we always create the tracing roundtripper? ct := &ContextTracer{ tracer: trace, From fbb79ff9a00a5eacef5dc8a628dd6bac189c8ff6 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Fri, 26 Jan 2024 12:37:12 -0500 Subject: [PATCH 07/40] LOgging --- internal/app/referenceclient/impl.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/internal/app/referenceclient/impl.go b/internal/app/referenceclient/impl.go index acafe4c6..4bfe1e8e 100644 --- a/internal/app/referenceclient/impl.go +++ b/internal/app/referenceclient/impl.go @@ -21,11 +21,13 @@ import ( "io" "net/http" "net/url" + "os" "time" "connectrpc.com/conformance/internal" v1 "connectrpc.com/conformance/internal/gen/proto/go/connectrpc/conformance/v1" "connectrpc.com/conformance/internal/gen/proto/go/connectrpc/conformance/v1/conformancev1connect" + "connectrpc.com/conformance/internal/tracer" "connectrpc.com/connect" ) @@ -33,6 +35,7 @@ const clientName = "connectconformance-referenceclient" type invoker struct { client conformancev1connect.ConformanceServiceClient + tracer *tracer.Tracer } func (i *invoker) Invoke( @@ -117,8 +120,13 @@ func (i *invoker) unary( var trailers []*v1.Header payloads := make([]*v1.ConformancePayload, 0, 1) + fmt.Fprintln(os.Stderr, "Before unary") + fmt.Fprintln(os.Stderr, ctx) // Invoke the Unary call resp, err := i.client.Unary(ctx, request) + fmt.Fprintln(os.Stderr, "After unary") + fmt.Fprintln(os.Stderr, ctx) + if err != nil { // If an error was returned, first convert it to a Connect error // so that we can get the headers from the Meta property. Then, From b2ba9b3835ef8fb6f117782f5bcd0065e8f77a3c Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Fri, 26 Jan 2024 12:46:26 -0500 Subject: [PATCH 08/40] Impl --- internal/app/referenceclient/impl.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/app/referenceclient/impl.go b/internal/app/referenceclient/impl.go index 4bfe1e8e..06db2c1f 100644 --- a/internal/app/referenceclient/impl.go +++ b/internal/app/referenceclient/impl.go @@ -121,11 +121,11 @@ func (i *invoker) unary( payloads := make([]*v1.ConformancePayload, 0, 1) fmt.Fprintln(os.Stderr, "Before unary") - fmt.Fprintln(os.Stderr, ctx) + fmt.Fprintln(os.Stderr, ctx.Value("response")) // Invoke the Unary call resp, err := i.client.Unary(ctx, request) fmt.Fprintln(os.Stderr, "After unary") - fmt.Fprintln(os.Stderr, ctx) + fmt.Fprintln(os.Stderr, ctx.Value("response")) if err != nil { // If an error was returned, first convert it to a Connect error From 8049ab98b2adaf9080d069ea8dfb16b077920371 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Sat, 27 Jan 2024 13:24:49 -0500 Subject: [PATCH 09/40] Update --- go.mod | 2 + go.sum | 2 + .../connectconformance/testsuites/basic.yaml | 335 +++++++++--------- internal/app/referenceclient/client.go | 20 +- internal/app/referenceclient/impl.go | 10 +- internal/tracer/middleware.go | 12 + internal/tracer/reader.go | 5 + internal/tracer/resp_tracer.go | 69 ++++ 8 files changed, 268 insertions(+), 187 deletions(-) create mode 100644 internal/tracer/resp_tracer.go diff --git a/go.mod b/go.mod index e43447f4..ad8642bf 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 + go.uber.org/atomic v1.5.0 golang.org/x/net v0.20.0 golang.org/x/sync v0.6.0 google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 @@ -42,6 +43,7 @@ require ( go.uber.org/mock v0.3.0 // indirect golang.org/x/crypto v0.18.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/lint v0.0.0-20190930215403-16217165b5de // indirect golang.org/x/mod v0.12.0 // indirect golang.org/x/sys v0.16.0 // indirect golang.org/x/text v0.14.0 // indirect diff --git a/go.sum b/go.sum index ea3fb614..eaeb16c5 100644 --- a/go.sum +++ b/go.sum @@ -375,6 +375,7 @@ go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo= go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= @@ -403,6 +404,7 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= diff --git a/internal/app/connectconformance/testsuites/basic.yaml b/internal/app/connectconformance/testsuites/basic.yaml index a0542806..372ba624 100644 --- a/internal/app/connectconformance/testsuites/basic.yaml +++ b/internal/app/connectconformance/testsuites/basic.yaml @@ -1,4 +1,13 @@ name: Basic +relevantProtocols: + - PROTOCOL_CONNECT +relevantCodecs: + - CODEC_JSON +relevantCompressions: + - COMPRESSION_IDENTITY +relevantHttpVersions: + - HTTP_VERSION_1 +reliesOnTls: false testCases: # Unary Tests ----------------------------------------------------------------- - request: @@ -19,166 +28,166 @@ testCases: responseTrailers: - name: x-custom-trailer value: ["bing"] -- request: - testName: unary empty definition - service: connectrpc.conformance.v1.ConformanceService - method: Unary - streamType: STREAM_TYPE_UNARY - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest - responseDefinition: -- request: - testName: unary no definition - service: connectrpc.conformance.v1.ConformanceService - method: Unary - streamType: STREAM_TYPE_UNARY - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest -# Client Stream Tests --------------------------------------------------------- -- request: - testName: client stream success - service: connectrpc.conformance.v1.ConformanceService - method: ClientStream - streamType: STREAM_TYPE_CLIENT_STREAM - requestHeaders: - - name: X-Conformance-Test - value: ["Value1","Value2"] - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest - responseDefinition: - responseHeaders: - - name: x-custom-header - value: ["foo"] - responseData: "dGVzdCByZXNwb25zZQ==" - responseTrailers: - - name: x-custom-trailer - value: ["bing"] - - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest - requestData: "dGVzdCByZXNwb25zZQ==" -- request: - testName: client stream empty definition - service: connectrpc.conformance.v1.ConformanceService - method: ClientStream - streamType: STREAM_TYPE_CLIENT_STREAM - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest - responseDefinition: -- request: - testName: client stream no definition - service: connectrpc.conformance.v1.ConformanceService - method: ClientStream - streamType: STREAM_TYPE_CLIENT_STREAM - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest -# Server Stream Tests --------------------------------------------------------- -- request: - testName: server stream success - service: connectrpc.conformance.v1.ConformanceService - method: ServerStream - streamType: STREAM_TYPE_SERVER_STREAM - requestHeaders: - - name: X-Conformance-Test - value: ["Value1","Value2"] - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest - responseDefinition: - responseHeaders: - - name: x-custom-header - value: ["foo"] - responseData: - - "dGVzdCByZXNwb25zZQ==" - - "dGVzdCByZXNwb25zZQ==" - responseTrailers: - - name: x-custom-trailer - value: ["bing"] -- request: - testName: server stream no response - service: connectrpc.conformance.v1.ConformanceService - method: ServerStream - streamType: STREAM_TYPE_SERVER_STREAM - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest - responseDefinition: - responseHeaders: - - name: x-custom-header - value: ["foo"] - responseTrailers: - - name: x-custom-trailer - value: ["bing"] -- request: - testName: server stream no definition - service: connectrpc.conformance.v1.ConformanceService - method: ServerStream - streamType: STREAM_TYPE_SERVER_STREAM - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest -# Bidi Stream Tests ----------------------------------------------------------- -- request: - testName: bidi full duplex stream success - service: connectrpc.conformance.v1.ConformanceService - method: BidiStream - streamType: STREAM_TYPE_FULL_DUPLEX_BIDI_STREAM - requestHeaders: - - name: X-Conformance-Test - value: ["Value1","Value2"] - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest - responseDefinition: - responseHeaders: - - name: x-custom-header - value: ["foo"] - responseData: - - "dGVzdCByZXNwb25zZQ==" - - "dGVzdCByZXNwb25zZQ==" - responseTrailers: - - name: x-custom-trailer - value: ["bing"] - fullDuplex: true - - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest - requestData: "dGVzdCByZXNwb25zZQ==" -- request: - testName: bidi half duplex stream success - service: connectrpc.conformance.v1.ConformanceService - method: BidiStream - streamType: STREAM_TYPE_HALF_DUPLEX_BIDI_STREAM - requestHeaders: - - name: X-Conformance-Test - value: ["Value1","Value2"] - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest - responseDefinition: - responseHeaders: - - name: x-custom-header - value: ["foo"] - responseData: - - "dGVzdCByZXNwb25zZQ==" - - "dGVzdCByZXNwb25zZQ==" - responseTrailers: - - name: x-custom-trailer - value: ["bing"] - - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest - requestData: "dGVzdCByZXNwb25zZQ==" -- request: - # Note there is no 'full duplex no definition set' test since that is - # logically equivalent to this test and is therefore covered here. - testName: bidi half duplex stream no definition set - service: connectrpc.conformance.v1.ConformanceService - method: BidiStream - streamType: STREAM_TYPE_HALF_DUPLEX_BIDI_STREAM - requestHeaders: - - name: X-Conformance-Test - value: ["Value1","Value2"] - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest -# Misc Tests ------------------------------------------------------------------ -- request: - testName: unimplemented - service: connectrpc.conformance.v1.ConformanceService - method: Unimplemented - streamType: STREAM_TYPE_UNARY - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.UnimplementedRequest - # Override - expectedResponse: - error: - code: 12 +# - request: +# testName: unary empty definition +# service: connectrpc.conformance.v1.ConformanceService +# method: Unary +# streamType: STREAM_TYPE_UNARY +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest +# responseDefinition: +# - request: +# testName: unary no definition +# service: connectrpc.conformance.v1.ConformanceService +# method: Unary +# streamType: STREAM_TYPE_UNARY +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest +# # Client Stream Tests --------------------------------------------------------- +# - request: +# testName: client stream success +# service: connectrpc.conformance.v1.ConformanceService +# method: ClientStream +# streamType: STREAM_TYPE_CLIENT_STREAM +# requestHeaders: +# - name: X-Conformance-Test +# value: ["Value1","Value2"] +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest +# responseDefinition: +# responseHeaders: +# - name: x-custom-header +# value: ["foo"] +# responseData: "dGVzdCByZXNwb25zZQ==" +# responseTrailers: +# - name: x-custom-trailer +# value: ["bing"] +# - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest +# requestData: "dGVzdCByZXNwb25zZQ==" +# - request: +# testName: client stream empty definition +# service: connectrpc.conformance.v1.ConformanceService +# method: ClientStream +# streamType: STREAM_TYPE_CLIENT_STREAM +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest +# responseDefinition: +# - request: +# testName: client stream no definition +# service: connectrpc.conformance.v1.ConformanceService +# method: ClientStream +# streamType: STREAM_TYPE_CLIENT_STREAM +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest +# # Server Stream Tests --------------------------------------------------------- +# - request: +# testName: server stream success +# service: connectrpc.conformance.v1.ConformanceService +# method: ServerStream +# streamType: STREAM_TYPE_SERVER_STREAM +# requestHeaders: +# - name: X-Conformance-Test +# value: ["Value1","Value2"] +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest +# responseDefinition: +# responseHeaders: +# - name: x-custom-header +# value: ["foo"] +# responseData: +# - "dGVzdCByZXNwb25zZQ==" +# - "dGVzdCByZXNwb25zZQ==" +# responseTrailers: +# - name: x-custom-trailer +# value: ["bing"] +# - request: +# testName: server stream no response +# service: connectrpc.conformance.v1.ConformanceService +# method: ServerStream +# streamType: STREAM_TYPE_SERVER_STREAM +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest +# responseDefinition: +# responseHeaders: +# - name: x-custom-header +# value: ["foo"] +# responseTrailers: +# - name: x-custom-trailer +# value: ["bing"] +# - request: +# testName: server stream no definition +# service: connectrpc.conformance.v1.ConformanceService +# method: ServerStream +# streamType: STREAM_TYPE_SERVER_STREAM +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest +# # Bidi Stream Tests ----------------------------------------------------------- +# - request: +# testName: bidi full duplex stream success +# service: connectrpc.conformance.v1.ConformanceService +# method: BidiStream +# streamType: STREAM_TYPE_FULL_DUPLEX_BIDI_STREAM +# requestHeaders: +# - name: X-Conformance-Test +# value: ["Value1","Value2"] +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest +# responseDefinition: +# responseHeaders: +# - name: x-custom-header +# value: ["foo"] +# responseData: +# - "dGVzdCByZXNwb25zZQ==" +# - "dGVzdCByZXNwb25zZQ==" +# responseTrailers: +# - name: x-custom-trailer +# value: ["bing"] +# fullDuplex: true +# - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest +# requestData: "dGVzdCByZXNwb25zZQ==" +# - request: +# testName: bidi half duplex stream success +# service: connectrpc.conformance.v1.ConformanceService +# method: BidiStream +# streamType: STREAM_TYPE_HALF_DUPLEX_BIDI_STREAM +# requestHeaders: +# - name: X-Conformance-Test +# value: ["Value1","Value2"] +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest +# responseDefinition: +# responseHeaders: +# - name: x-custom-header +# value: ["foo"] +# responseData: +# - "dGVzdCByZXNwb25zZQ==" +# - "dGVzdCByZXNwb25zZQ==" +# responseTrailers: +# - name: x-custom-trailer +# value: ["bing"] +# - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest +# requestData: "dGVzdCByZXNwb25zZQ==" +# - request: +# # Note there is no 'full duplex no definition set' test since that is +# # logically equivalent to this test and is therefore covered here. +# testName: bidi half duplex stream no definition set +# service: connectrpc.conformance.v1.ConformanceService +# method: BidiStream +# streamType: STREAM_TYPE_HALF_DUPLEX_BIDI_STREAM +# requestHeaders: +# - name: X-Conformance-Test +# value: ["Value1","Value2"] +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest +# # Misc Tests ------------------------------------------------------------------ +# - request: +# testName: unimplemented +# service: connectrpc.conformance.v1.ConformanceService +# method: Unimplemented +# streamType: STREAM_TYPE_UNARY +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnimplementedRequest +# # Override +# expectedResponse: +# error: +# code: 12 diff --git a/internal/app/referenceclient/client.go b/internal/app/referenceclient/client.go index d8a65521..3f9f79c1 100644 --- a/internal/app/referenceclient/client.go +++ b/internal/app/referenceclient/client.go @@ -24,7 +24,6 @@ import ( "net" "net/http" "net/url" - "os" "path/filepath" "runtime" "strconv" @@ -45,18 +44,6 @@ import ( "golang.org/x/sync/semaphore" ) -type ContextTracer struct { - tracer *tracer.Tracer - ctx context.Context -} - -func (t *ContextTracer) Complete(trace tracer.Trace) { - t.ctx = context.WithValue(t.ctx, "response", trace.Response.StatusCode) - fmt.Fprintln(os.Stderr, "Wrapped Trace %+v:", trace.Response) - - t.tracer.Complete(trace) -} - // Run runs the client according to a client config read from the 'in' reader. The result of the run // is written to the 'out' writer, including any errors encountered during the actual run. Any error // returned from this function is indicative of an issue with the reader or writer and should not be related @@ -230,12 +217,7 @@ func invoke(ctx context.Context, req *v1.ClientCompatRequest, trace *tracer.Trac return nil, errors.New("an HTTP version must be specified") } - ctx = context.WithValue(ctx, "response", nil) - // TODO - Should we always create the tracing roundtripper? - ct := &ContextTracer{ - tracer: trace, - ctx: ctx, - } + ct := tracer.NewContextTracer(trace) transport = tracer.TracingRoundTripper(transport, ct) if req.RawRequest != nil { diff --git a/internal/app/referenceclient/impl.go b/internal/app/referenceclient/impl.go index 06db2c1f..6078923a 100644 --- a/internal/app/referenceclient/impl.go +++ b/internal/app/referenceclient/impl.go @@ -35,13 +35,13 @@ const clientName = "connectconformance-referenceclient" type invoker struct { client conformancev1connect.ConformanceServiceClient - tracer *tracer.Tracer } func (i *invoker) Invoke( ctx context.Context, req *v1.ClientCompatRequest, ) (*v1.ClientResponseResult, error) { + // If a timeout was specified, create a derived context with that deadline if req.TimeoutMs != nil { deadlineCtx, cancel := context.WithDeadline(ctx, time.Now().Add(time.Duration(*req.TimeoutMs)*time.Millisecond)) @@ -120,13 +120,13 @@ func (i *invoker) unary( var trailers []*v1.Header payloads := make([]*v1.ConformancePayload, 0, 1) - fmt.Fprintln(os.Stderr, "Before unary") - fmt.Fprintln(os.Stderr, ctx.Value("response")) + ctx, wire := tracer.CaptureResp(ctx) + // Invoke the Unary call resp, err := i.client.Unary(ctx, request) - fmt.Fprintln(os.Stderr, "After unary") - fmt.Fprintln(os.Stderr, ctx.Value("response")) + fmt.Fprintln(os.Stderr, "WIRE: v") + fmt.Fprintln(os.Stderr, wire.Get()) if err != nil { // If an error was returned, first convert it to a Connect error // so that we can get the headers from the Meta property. Then, diff --git a/internal/tracer/middleware.go b/internal/tracer/middleware.go index f56063d4..d15feabb 100644 --- a/internal/tracer/middleware.go +++ b/internal/tracer/middleware.go @@ -19,6 +19,7 @@ import ( "fmt" "io" "net/http" + "os" "strconv" "strings" ) @@ -31,6 +32,15 @@ func TracingRoundTripper(transport http.RoundTripper, collector Collector) http. req = req.Clone(req.Context()) req.Body = newReader(req.Header, req.Body, true, builder) resp, err := transport.RoundTrip(req) + + // This is new --- can this be abstracted or moved into its own thing + respWrapper, ok := req.Context().Value(respKey{}).(*RespWrapper) + if err != nil || !ok { + return resp, err + } + respWrapper.val.Store(resp) + // This is new --- can this be abstracted + if err != nil { builder.add(&ResponseError{Err: err}) builder.build() @@ -91,6 +101,7 @@ func (t *tracingResponseWriter) Write(data []byte) (int, error) { n, err := t.respWriter.Write(data) t.dataTracer.trace(data[:n]) if err != nil { + fmt.Fprintln(os.Stderr, "Writing in the response writer") t.tryFinish(err) } return n, err @@ -158,6 +169,7 @@ func (t *tracingResponseWriter) Flush() { } func (t *tracingResponseWriter) tryFinish(err error) { + fmt.Fprintln(os.Stderr, "tryFinish in middleware") if t.finished { return // already finished } diff --git a/internal/tracer/reader.go b/internal/tracer/reader.go index a666eafb..8ac221c6 100644 --- a/internal/tracer/reader.go +++ b/internal/tracer/reader.go @@ -21,6 +21,7 @@ import ( "fmt" "io" "net/http" + "os" "strings" "sync/atomic" @@ -60,6 +61,7 @@ func (t *tracingReader) Read(data []byte) (n int, err error) { t.dataTracer.trace(data[:n]) if err != nil { if errors.Is(err, io.EOF) { + fmt.Fprintln(os.Stderr, "EOF") t.tryFinish(nil) } else { t.tryFinish(err) @@ -71,8 +73,10 @@ func (t *tracingReader) Read(data []byte) (n int, err error) { func (t *tracingReader) Close() error { err := t.reader.Close() if err != nil { + fmt.Fprintln(os.Stderr, "close noe error") t.tryFinish(fmt.Errorf("close: %w", err)) } else { + fmt.Fprintln(os.Stderr, "close before fully consumed error") t.tryFinish(errors.New("closed before fully consumed")) } return err @@ -92,6 +96,7 @@ func (t *tracingReader) tryFinish(err error) { // On the response side, when the body reaches the end, whole thing is done. t.builder.add(&ResponseBodyEnd{Err: err}) + fmt.Fprintln(os.Stderr, "BUILD called here") t.builder.build() } diff --git a/internal/tracer/resp_tracer.go b/internal/tracer/resp_tracer.go new file mode 100644 index 00000000..c58c2ef3 --- /dev/null +++ b/internal/tracer/resp_tracer.go @@ -0,0 +1,69 @@ +// Copyright 2023-2024 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tracer + +import ( + "context" + "fmt" + "net/http" + "os" + + "sync/atomic" +) + +type respKey struct{} + +type RespWrapper struct { + val atomic.Pointer[http.Response] +} + +// CaptureTrailers returns a context to be used with HTTP operations to capture trailers. +// Each HTTP operation used with the returned context will store its HTTP trailers into +// the returned *Trailers value. +func CaptureResp(ctx context.Context) (context.Context, *RespWrapper) { + wrappers := &RespWrapper{} + ctx = context.WithValue(ctx, respKey{}, wrappers) + return ctx, wrappers +} + +// Get returns the resp captured. Resps are not captured until the response body is +// exhausted. +func (t *RespWrapper) Get() *http.Response { + respPtr := t.val.Load() + if respPtr == nil { + return nil + } + return respPtr +} + +type contextTracer struct { + tracer *Tracer + // respWrapper RespWrapper +} + +func NewContextTracer(trace *Tracer) *contextTracer { + return &contextTracer{ + tracer: trace, + } +} + +func (t *contextTracer) Complete(trace Trace) { + // t.ctx = context.WithValue(t.ctx, "response", trace.Response.StatusCode) + fmt.Fprintln(os.Stderr, "Wrapped Trace %+v:", trace.Response) + + if t != nil { + t.tracer.Complete(trace) + } +} From 8106bf717422221463a1274bbc56f90e3ac34aff Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Sat, 27 Jan 2024 13:31:13 -0500 Subject: [PATCH 10/40] Update --- internal/app/referenceclient/impl.go | 9 ++++++--- internal/tracer/middleware.go | 8 ++------ internal/tracer/reader.go | 5 ----- internal/tracer/resp_tracer.go | 11 +---------- 4 files changed, 9 insertions(+), 24 deletions(-) diff --git a/internal/app/referenceclient/impl.go b/internal/app/referenceclient/impl.go index 6078923a..2d65c00e 100644 --- a/internal/app/referenceclient/impl.go +++ b/internal/app/referenceclient/impl.go @@ -120,13 +120,16 @@ func (i *invoker) unary( var trailers []*v1.Header payloads := make([]*v1.ConformancePayload, 0, 1) - ctx, wire := tracer.CaptureResp(ctx) + ctx, wire := tracer.WithResponseCapture(ctx) // Invoke the Unary call resp, err := i.client.Unary(ctx, request) - fmt.Fprintln(os.Stderr, "WIRE: v") - fmt.Fprintln(os.Stderr, wire.Get()) + httpResp := wire.Get() + if httpResp != nil { + fmt.Fprintln(os.Stderr, wire.Get()) + } + if err != nil { // If an error was returned, first convert it to a Connect error // so that we can get the headers from the Meta property. Then, diff --git a/internal/tracer/middleware.go b/internal/tracer/middleware.go index d15feabb..57b7eb4a 100644 --- a/internal/tracer/middleware.go +++ b/internal/tracer/middleware.go @@ -19,7 +19,6 @@ import ( "fmt" "io" "net/http" - "os" "strconv" "strings" ) @@ -35,10 +34,9 @@ func TracingRoundTripper(transport http.RoundTripper, collector Collector) http. // This is new --- can this be abstracted or moved into its own thing respWrapper, ok := req.Context().Value(respKey{}).(*RespWrapper) - if err != nil || !ok { - return resp, err + if ok { + respWrapper.val.Store(resp) } - respWrapper.val.Store(resp) // This is new --- can this be abstracted if err != nil { @@ -101,7 +99,6 @@ func (t *tracingResponseWriter) Write(data []byte) (int, error) { n, err := t.respWriter.Write(data) t.dataTracer.trace(data[:n]) if err != nil { - fmt.Fprintln(os.Stderr, "Writing in the response writer") t.tryFinish(err) } return n, err @@ -169,7 +166,6 @@ func (t *tracingResponseWriter) Flush() { } func (t *tracingResponseWriter) tryFinish(err error) { - fmt.Fprintln(os.Stderr, "tryFinish in middleware") if t.finished { return // already finished } diff --git a/internal/tracer/reader.go b/internal/tracer/reader.go index 8ac221c6..a666eafb 100644 --- a/internal/tracer/reader.go +++ b/internal/tracer/reader.go @@ -21,7 +21,6 @@ import ( "fmt" "io" "net/http" - "os" "strings" "sync/atomic" @@ -61,7 +60,6 @@ func (t *tracingReader) Read(data []byte) (n int, err error) { t.dataTracer.trace(data[:n]) if err != nil { if errors.Is(err, io.EOF) { - fmt.Fprintln(os.Stderr, "EOF") t.tryFinish(nil) } else { t.tryFinish(err) @@ -73,10 +71,8 @@ func (t *tracingReader) Read(data []byte) (n int, err error) { func (t *tracingReader) Close() error { err := t.reader.Close() if err != nil { - fmt.Fprintln(os.Stderr, "close noe error") t.tryFinish(fmt.Errorf("close: %w", err)) } else { - fmt.Fprintln(os.Stderr, "close before fully consumed error") t.tryFinish(errors.New("closed before fully consumed")) } return err @@ -96,7 +92,6 @@ func (t *tracingReader) tryFinish(err error) { // On the response side, when the body reaches the end, whole thing is done. t.builder.add(&ResponseBodyEnd{Err: err}) - fmt.Fprintln(os.Stderr, "BUILD called here") t.builder.build() } diff --git a/internal/tracer/resp_tracer.go b/internal/tracer/resp_tracer.go index c58c2ef3..ca5bc2f8 100644 --- a/internal/tracer/resp_tracer.go +++ b/internal/tracer/resp_tracer.go @@ -16,9 +16,7 @@ package tracer import ( "context" - "fmt" "net/http" - "os" "sync/atomic" ) @@ -29,10 +27,7 @@ type RespWrapper struct { val atomic.Pointer[http.Response] } -// CaptureTrailers returns a context to be used with HTTP operations to capture trailers. -// Each HTTP operation used with the returned context will store its HTTP trailers into -// the returned *Trailers value. -func CaptureResp(ctx context.Context) (context.Context, *RespWrapper) { +func WithResponseCapture(ctx context.Context) (context.Context, *RespWrapper) { wrappers := &RespWrapper{} ctx = context.WithValue(ctx, respKey{}, wrappers) return ctx, wrappers @@ -50,7 +45,6 @@ func (t *RespWrapper) Get() *http.Response { type contextTracer struct { tracer *Tracer - // respWrapper RespWrapper } func NewContextTracer(trace *Tracer) *contextTracer { @@ -60,9 +54,6 @@ func NewContextTracer(trace *Tracer) *contextTracer { } func (t *contextTracer) Complete(trace Trace) { - // t.ctx = context.WithValue(t.ctx, "response", trace.Response.StatusCode) - fmt.Fprintln(os.Stderr, "Wrapped Trace %+v:", trace.Response) - if t != nil { t.tracer.Complete(trace) } From ead36d492e4127f8b2accf91c97d9e9080ade8a8 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Mon, 29 Jan 2024 22:30:09 -0500 Subject: [PATCH 11/40] Raw json --- go.mod | 3 +- go.sum | 3 +- .../connectconformance/testsuites/basic.yaml | 62 +++++++++++++------ internal/app/referenceclient/impl.go | 48 ++++++++++++-- internal/tracer/builder.go | 1 + internal/tracer/middleware.go | 8 --- internal/tracer/resp_tracer.go | 49 +++++++++++++-- internal/tracer/tracer.go | 8 +++ 8 files changed, 140 insertions(+), 42 deletions(-) diff --git a/go.mod b/go.mod index ad8642bf..29098775 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( connectrpc.com/connect v1.14.0 github.com/andybalholm/brotli v1.1.0 github.com/bufbuild/protoyaml-go v0.1.7 + github.com/gogo/protobuf v1.2.1 github.com/golang/snappy v0.0.4 github.com/google/go-cmp v0.6.0 github.com/improbable-eng/grpc-web v0.15.0 @@ -15,7 +16,6 @@ require ( github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 - go.uber.org/atomic v1.5.0 golang.org/x/net v0.20.0 golang.org/x/sync v0.6.0 google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 @@ -43,7 +43,6 @@ require ( go.uber.org/mock v0.3.0 // indirect golang.org/x/crypto v0.18.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect - golang.org/x/lint v0.0.0-20190930215403-16217165b5de // indirect golang.org/x/mod v0.12.0 // indirect golang.org/x/sys v0.16.0 // indirect golang.org/x/text v0.14.0 // indirect diff --git a/go.sum b/go.sum index eaeb16c5..32828ecf 100644 --- a/go.sum +++ b/go.sum @@ -112,6 +112,7 @@ github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/E github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -375,7 +376,6 @@ go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo= go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= @@ -404,7 +404,6 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= diff --git a/internal/app/connectconformance/testsuites/basic.yaml b/internal/app/connectconformance/testsuites/basic.yaml index 372ba624..205124b7 100644 --- a/internal/app/connectconformance/testsuites/basic.yaml +++ b/internal/app/connectconformance/testsuites/basic.yaml @@ -28,6 +28,17 @@ testCases: responseTrailers: - name: x-custom-trailer value: ["bing"] +- request: + testName: unary canceled error + service: connectrpc.conformance.v1.ConformanceService + method: Unary + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 1 + message: "canceled" # - request: # testName: unary empty definition # service: connectrpc.conformance.v1.ConformanceService @@ -80,26 +91,37 @@ testCases: # requestMessages: # - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest # # Server Stream Tests --------------------------------------------------------- -# - request: -# testName: server stream success -# service: connectrpc.conformance.v1.ConformanceService -# method: ServerStream -# streamType: STREAM_TYPE_SERVER_STREAM -# requestHeaders: -# - name: X-Conformance-Test -# value: ["Value1","Value2"] -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest -# responseDefinition: -# responseHeaders: -# - name: x-custom-header -# value: ["foo"] -# responseData: -# - "dGVzdCByZXNwb25zZQ==" -# - "dGVzdCByZXNwb25zZQ==" -# responseTrailers: -# - name: x-custom-trailer -# value: ["bing"] +- request: + testName: stream canceled error + service: connectrpc.conformance.v1.ConformanceService + method: ServerStream + streamType: STREAM_TYPE_SERVER_STREAM + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest + responseDefinition: + error: + code: 1 + message: "canceled" +- request: + testName: server stream success + service: connectrpc.conformance.v1.ConformanceService + method: ServerStream + streamType: STREAM_TYPE_SERVER_STREAM + requestHeaders: + - name: X-Conformance-Test + value: ["Value1","Value2"] + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest + responseDefinition: + responseHeaders: + - name: x-custom-header + value: ["foo"] + responseData: + - "dGVzdCByZXNwb25zZQ==" + - "dGVzdCByZXNwb25zZQ==" + responseTrailers: + - name: x-custom-trailer + value: ["bing"] # - request: # testName: server stream no response # service: connectrpc.conformance.v1.ConformanceService diff --git a/internal/app/referenceclient/impl.go b/internal/app/referenceclient/impl.go index 2d65c00e..b234a05c 100644 --- a/internal/app/referenceclient/impl.go +++ b/internal/app/referenceclient/impl.go @@ -29,6 +29,7 @@ import ( "connectrpc.com/conformance/internal/gen/proto/go/connectrpc/conformance/v1/conformancev1connect" "connectrpc.com/conformance/internal/tracer" "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/structpb" ) const clientName = "connectconformance-referenceclient" @@ -125,9 +126,14 @@ func (i *invoker) unary( // Invoke the Unary call resp, err := i.client.Unary(ctx, request) - httpResp := wire.Get() - if httpResp != nil { - fmt.Fprintln(os.Stderr, wire.Get()) + var statusCode int32 + var jsonRaw *structpb.Struct + deets := wire.Get() + if deets != nil { + statusCode = deets.StatusCode + jsonRaw = deets.RawErrorDetails + + fmt.Fprintf(os.Stderr, "UNARY: %+v\n", deets) } if err != nil { @@ -152,7 +158,8 @@ func (i *invoker) unary( ResponseTrailers: trailers, Payloads: payloads, Error: protoErr, - ConnectErrorRaw: nil, // TODO + ActualStatusCode: statusCode, + ConnectErrorRaw: jsonRaw, }, nil } @@ -179,8 +186,21 @@ func (i *invoker) idempotentUnary( var trailers []*v1.Header payloads := make([]*v1.ConformancePayload, 0, 1) + ctx, wire := tracer.WithResponseCapture(ctx) + // Invoke the Unary call resp, err := i.client.IdempotentUnary(ctx, request) + + var statusCode int32 + var jsonRaw *structpb.Struct + deets := wire.Get() + if deets != nil { + statusCode = deets.StatusCode + jsonRaw = deets.RawErrorDetails + + fmt.Fprintf(os.Stderr, "Idempotent: %+v\n", deets) + } + if err != nil { // If an error was returned, first convert it to a Connect error // so that we can get the headers from the Meta property. Then, @@ -201,7 +221,8 @@ func (i *invoker) idempotentUnary( ResponseTrailers: trailers, Payloads: payloads, Error: protoErr, - ConnectErrorRaw: nil, // TODO + ActualStatusCode: statusCode, + ConnectErrorRaw: jsonRaw, }, nil } @@ -223,6 +244,8 @@ func (i *invoker) serverStream( // Add the specified request headers to the request internal.AddHeaders(req.RequestHeaders, request.Header()) + ctx, wire := tracer.WithResponseCapture(ctx) + stream, err := i.client.ServerStream(ctx, request) if err != nil { // If an error was returned, first convert it to a Connect error @@ -283,12 +306,25 @@ func (i *invoker) serverStream( protoErr = internal.ConvertErrorToProtoError(err) } } + var statusCode int32 + var jsonRaw *structpb.Struct + deets := wire.Get() + if deets != nil { + statusCode = deets.StatusCode + jsonRaw = deets.RawErrorDetails + + fmt.Fprintf(os.Stderr, "SERVER STREAM: %+v\n", deets) + } else { + fmt.Fprintf(os.Stderr, "SERVER SREAM NULLLLLLL") + } + return &v1.ClientResponseResult{ ResponseHeaders: headers, ResponseTrailers: trailers, Payloads: payloads, Error: protoErr, - ConnectErrorRaw: nil, // TODO + ActualStatusCode: statusCode, + ConnectErrorRaw: jsonRaw, }, nil } diff --git a/internal/tracer/builder.go b/internal/tracer/builder.go index 1a675f92..5d29157d 100644 --- a/internal/tracer/builder.go +++ b/internal/tracer/builder.go @@ -41,6 +41,7 @@ func newBuilder(req *http.Request, collector Collector) *builder { start: time.Now(), trace: Trace{ TestName: testName, + Request: req, Events: []Event{&RequestStart{Request: req}}, }, } diff --git a/internal/tracer/middleware.go b/internal/tracer/middleware.go index 57b7eb4a..f56063d4 100644 --- a/internal/tracer/middleware.go +++ b/internal/tracer/middleware.go @@ -31,14 +31,6 @@ func TracingRoundTripper(transport http.RoundTripper, collector Collector) http. req = req.Clone(req.Context()) req.Body = newReader(req.Header, req.Body, true, builder) resp, err := transport.RoundTrip(req) - - // This is new --- can this be abstracted or moved into its own thing - respWrapper, ok := req.Context().Value(respKey{}).(*RespWrapper) - if ok { - respWrapper.val.Store(resp) - } - // This is new --- can this be abstracted - if err != nil { builder.add(&ResponseError{Err: err}) builder.build() diff --git a/internal/tracer/resp_tracer.go b/internal/tracer/resp_tracer.go index ca5bc2f8..91a0a42d 100644 --- a/internal/tracer/resp_tracer.go +++ b/internal/tracer/resp_tracer.go @@ -16,15 +16,23 @@ package tracer import ( "context" - "net/http" + "encoding/json" "sync/atomic" + + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/types/known/structpb" ) type respKey struct{} +type WireDetails struct { + StatusCode int32 + RawErrorDetails *structpb.Struct +} + type RespWrapper struct { - val atomic.Pointer[http.Response] + val atomic.Pointer[*WireDetails] } func WithResponseCapture(ctx context.Context) (context.Context, *RespWrapper) { @@ -35,15 +43,21 @@ func WithResponseCapture(ctx context.Context) (context.Context, *RespWrapper) { // Get returns the resp captured. Resps are not captured until the response body is // exhausted. -func (t *RespWrapper) Get() *http.Response { +func (t *RespWrapper) Get() *WireDetails { respPtr := t.val.Load() if respPtr == nil { return nil } - return respPtr + return *respPtr +} + +type endStreamError struct { + Error json.RawMessage `json:"error"` } type contextTracer struct { + Tracer + tracer *Tracer } @@ -54,6 +68,33 @@ func NewContextTracer(trace *Tracer) *contextTracer { } func (t *contextTracer) Complete(trace Trace) { + respWrapper, ok := trace.Request.Context().Value(respKey{}).(*RespWrapper) + if ok { + wire := &WireDetails{ + StatusCode: int32(trace.Response.StatusCode), + } + + for _, ev := range trace.Events { + switch eventType := ev.(type) { + case *ResponseBodyEndStream: + var endStream endStreamError + json.Unmarshal([]byte(eventType.Content), &endStream) + + var jsonRaw structpb.Struct + if err := protojson.Unmarshal(endStream.Error, &jsonRaw); err != nil { + return + } + wire.RawErrorDetails = &jsonRaw + } + } + respWrapper.val.Store(&wire) + + } + + // p := internal.NewPrinter(os.Stderr) + + // trace.Print(p) + if t != nil { t.tracer.Complete(trace) } diff --git a/internal/tracer/tracer.go b/internal/tracer/tracer.go index 3cee78e0..64b11daa 100644 --- a/internal/tracer/tracer.go +++ b/internal/tracer/tracer.go @@ -172,6 +172,7 @@ type RequestStart struct { } func (r *RequestStart) print(printer internal.Printer) { + printer.Printf("REQUEST START") urlClone := *r.Request.URL if urlClone.Host == "" { urlClone.Host = "..." @@ -215,6 +216,7 @@ type RequestBodyData struct { } func (r *RequestBodyData) print(printer internal.Printer) { + printer.Printf("REQUEST BODY DATA") printData(requestPrefix, r.offsetMillis(), r.MessageIndex, r.Envelope, r.Len, printer) } @@ -230,6 +232,7 @@ type RequestBodyEnd struct { } func (r *RequestBodyEnd) print(printer internal.Printer) { + printer.Printf("REQUEST BODY END") if r.Err != nil { printer.Printf("%s %9.3fms body end (err=%v)", requestPrefix, r.offsetMillis(), r.Err) } else { @@ -247,6 +250,7 @@ type ResponseStart struct { } func (r *ResponseStart) print(printer internal.Printer) { + printer.Printf("RESPONSE START") printer.Printf("%s %9.3fms %s", responsePrefix, r.offsetMillis(), r.Response.Status) printHeaders(responsePrefix, r.Response.Header, printer) printer.Printf(responsePrefix) @@ -262,6 +266,7 @@ type ResponseError struct { } func (r *ResponseError) print(printer internal.Printer) { + printer.Printf("RESPONSE ERROR") printer.Printf("%s %9.3fms failed: %v", responsePrefix, r.offsetMillis(), r.Err) } @@ -294,6 +299,7 @@ type ResponseBodyData struct { } func (r *ResponseBodyData) print(printer internal.Printer) { + printer.Printf("RESPONSE BODY DATA") printData(responsePrefix, r.offsetMillis(), r.MessageIndex, r.Envelope, r.Len, printer) } @@ -308,6 +314,7 @@ type ResponseBodyEndStream struct { } func (r *ResponseBodyEndStream) print(printer internal.Printer) { + printer.Printf("RESPONSE BODY ENDSTREAM") lines := strings.Split(r.Content, "\n") for _, line := range lines { line = strings.Trim(line, "\r") @@ -327,6 +334,7 @@ type ResponseBodyEnd struct { } func (r *ResponseBodyEnd) print(printer internal.Printer) { + printer.Printf("RESPONSE BODY END") if r.Err != nil { printer.Printf("%s %9.3fms body end (err=%v)", responsePrefix, r.offsetMillis(), r.Err) } else { From 985292e74c9cea6f683d54d4e2326e7d65c59d81 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Tue, 30 Jan 2024 13:00:49 -0500 Subject: [PATCH 12/40] Content --- .../connectconformance/testsuites/basic.yaml | 98 +++++++++---------- internal/app/referenceclient/impl.go | 93 ++++++++++-------- internal/tracer/middleware.go | 18 +++- internal/tracer/resp_tracer.go | 62 +++++++++--- 4 files changed, 166 insertions(+), 105 deletions(-) diff --git a/internal/app/connectconformance/testsuites/basic.yaml b/internal/app/connectconformance/testsuites/basic.yaml index 205124b7..f3fc6e09 100644 --- a/internal/app/connectconformance/testsuites/basic.yaml +++ b/internal/app/connectconformance/testsuites/basic.yaml @@ -10,24 +10,24 @@ relevantHttpVersions: reliesOnTls: false testCases: # Unary Tests ----------------------------------------------------------------- -- request: - testName: unary success - service: connectrpc.conformance.v1.ConformanceService - method: Unary - streamType: STREAM_TYPE_UNARY - requestHeaders: - - name: X-Conformance-Test - value: ["Value1","Value2"] - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest - responseDefinition: - responseHeaders: - - name: x-custom-header - value: ["foo"] - responseData: "dGVzdCByZXNwb25zZQ==" - responseTrailers: - - name: x-custom-trailer - value: ["bing"] +# - request: +# testName: unary success +# service: connectrpc.conformance.v1.ConformanceService +# method: Unary +# streamType: STREAM_TYPE_UNARY +# requestHeaders: +# - name: X-Conformance-Test +# value: ["Value1","Value2"] +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest +# responseDefinition: +# responseHeaders: +# - name: x-custom-header +# value: ["foo"] +# responseData: "dGVzdCByZXNwb25zZQ==" +# responseTrailers: +# - name: x-custom-trailer +# value: ["bing"] - request: testName: unary canceled error service: connectrpc.conformance.v1.ConformanceService @@ -91,37 +91,37 @@ testCases: # requestMessages: # - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest # # Server Stream Tests --------------------------------------------------------- -- request: - testName: stream canceled error - service: connectrpc.conformance.v1.ConformanceService - method: ServerStream - streamType: STREAM_TYPE_SERVER_STREAM - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest - responseDefinition: - error: - code: 1 - message: "canceled" -- request: - testName: server stream success - service: connectrpc.conformance.v1.ConformanceService - method: ServerStream - streamType: STREAM_TYPE_SERVER_STREAM - requestHeaders: - - name: X-Conformance-Test - value: ["Value1","Value2"] - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest - responseDefinition: - responseHeaders: - - name: x-custom-header - value: ["foo"] - responseData: - - "dGVzdCByZXNwb25zZQ==" - - "dGVzdCByZXNwb25zZQ==" - responseTrailers: - - name: x-custom-trailer - value: ["bing"] +# - request: +# testName: stream canceled error +# service: connectrpc.conformance.v1.ConformanceService +# method: ServerStream +# streamType: STREAM_TYPE_SERVER_STREAM +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest +# responseDefinition: +# error: +# code: 1 +# message: "canceled" +# - request: +# testName: server stream success +# service: connectrpc.conformance.v1.ConformanceService +# method: ServerStream +# streamType: STREAM_TYPE_SERVER_STREAM +# requestHeaders: +# - name: X-Conformance-Test +# value: ["Value1","Value2"] +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest +# responseDefinition: +# responseHeaders: +# - name: x-custom-header +# value: ["foo"] +# responseData: +# - "dGVzdCByZXNwb25zZQ==" +# - "dGVzdCByZXNwb25zZQ==" +# responseTrailers: +# - name: x-custom-trailer +# value: ["bing"] # - request: # testName: server stream no response # service: connectrpc.conformance.v1.ConformanceService diff --git a/internal/app/referenceclient/impl.go b/internal/app/referenceclient/impl.go index b234a05c..86679489 100644 --- a/internal/app/referenceclient/impl.go +++ b/internal/app/referenceclient/impl.go @@ -126,14 +126,17 @@ func (i *invoker) unary( // Invoke the Unary call resp, err := i.client.Unary(ctx, request) - var statusCode int32 - var jsonRaw *structpb.Struct - deets := wire.Get() - if deets != nil { - statusCode = deets.StatusCode - jsonRaw = deets.RawErrorDetails - - fmt.Fprintf(os.Stderr, "UNARY: %+v\n", deets) + var actualStatusCode int32 + var connectErrorRaw *structpb.Struct + var actualTrailers []*v1.Header + wireDetails := wire.Get() + if wireDetails != nil { + fmt.Fprintf(os.Stderr, "UNARY WIRE DETAILS: %+v\n\n", wireDetails) + actualStatusCode = wireDetails.StatusCode + connectErrorRaw = wireDetails.RawErrorDetails + actualTrailers = wireDetails.Trailers + } else { + fmt.Fprintf(os.Stderr, "UNARY WIRE DETAILS are NULL") } if err != nil { @@ -154,12 +157,13 @@ func (i *invoker) unary( } return &v1.ClientResponseResult{ - ResponseHeaders: headers, - ResponseTrailers: trailers, - Payloads: payloads, - Error: protoErr, - ActualStatusCode: statusCode, - ConnectErrorRaw: jsonRaw, + ResponseHeaders: headers, + ResponseTrailers: trailers, + Payloads: payloads, + Error: protoErr, + ActualStatusCode: actualStatusCode, + ActualHttpTrailers: actualTrailers, + ConnectErrorRaw: connectErrorRaw, }, nil } @@ -191,14 +195,14 @@ func (i *invoker) idempotentUnary( // Invoke the Unary call resp, err := i.client.IdempotentUnary(ctx, request) - var statusCode int32 - var jsonRaw *structpb.Struct - deets := wire.Get() - if deets != nil { - statusCode = deets.StatusCode - jsonRaw = deets.RawErrorDetails - - fmt.Fprintf(os.Stderr, "Idempotent: %+v\n", deets) + var actualStatusCode int32 + var connectErrorRaw *structpb.Struct + var actualTrailers []*v1.Header + wireDetails := wire.Get() + if wireDetails != nil { + actualStatusCode = wireDetails.StatusCode + connectErrorRaw = wireDetails.RawErrorDetails + actualTrailers = wireDetails.Trailers } if err != nil { @@ -217,12 +221,13 @@ func (i *invoker) idempotentUnary( } return &v1.ClientResponseResult{ - ResponseHeaders: headers, - ResponseTrailers: trailers, - Payloads: payloads, - Error: protoErr, - ActualStatusCode: statusCode, - ConnectErrorRaw: jsonRaw, + ResponseHeaders: headers, + ResponseTrailers: trailers, + Payloads: payloads, + Error: protoErr, + ActualStatusCode: actualStatusCode, + ActualHttpTrailers: actualTrailers, + ConnectErrorRaw: connectErrorRaw, }, nil } @@ -306,25 +311,27 @@ func (i *invoker) serverStream( protoErr = internal.ConvertErrorToProtoError(err) } } - var statusCode int32 - var jsonRaw *structpb.Struct - deets := wire.Get() - if deets != nil { - statusCode = deets.StatusCode - jsonRaw = deets.RawErrorDetails - - fmt.Fprintf(os.Stderr, "SERVER STREAM: %+v\n", deets) + var actualStatusCode int32 + var connectErrorRaw *structpb.Struct + var actualTrailers []*v1.Header + wireDetails := wire.Get() + if wireDetails != nil { + fmt.Fprintf(os.Stderr, "SERVER STREAM WIRE DETAILS: %+v\n\n", wireDetails) + actualStatusCode = wireDetails.StatusCode + connectErrorRaw = wireDetails.RawErrorDetails + actualTrailers = wireDetails.Trailers } else { - fmt.Fprintf(os.Stderr, "SERVER SREAM NULLLLLLL") + fmt.Fprintf(os.Stderr, "SERVER STREAM WIRE DETAILS are NULL") } return &v1.ClientResponseResult{ - ResponseHeaders: headers, - ResponseTrailers: trailers, - Payloads: payloads, - Error: protoErr, - ActualStatusCode: statusCode, - ConnectErrorRaw: jsonRaw, + ResponseHeaders: headers, + ResponseTrailers: trailers, + Payloads: payloads, + Error: protoErr, + ActualStatusCode: actualStatusCode, + ActualHttpTrailers: actualTrailers, + ConnectErrorRaw: connectErrorRaw, }, nil } diff --git a/internal/tracer/middleware.go b/internal/tracer/middleware.go index 8b8c95d8..1389aafa 100644 --- a/internal/tracer/middleware.go +++ b/internal/tracer/middleware.go @@ -42,8 +42,24 @@ func TracingRoundTripper(transport http.RoundTripper, collector Collector) http. cancel() return nil, err } - builder.add(&ResponseStart{Response: resp}) + // body, _ := io.ReadAll(resp.Body) + // fmt.Fprintf(os.Stderr, "dagsboro: ", string(body)) + // resp.Body = io.NopCloser(bytes.NewReader(body)) + // booty, _ := io.ReadAll(resp.Body) + // fmt.Fprintf(os.Stderr, "dagster.io: ", string(booty)) + + respect := *resp + cardi, _ := io.ReadAll(respect.Body) + respect.Body = io.NopCloser(bytes.NewReader(cardi)) + + // bardy, _ := io.ReadAll(evt.Response.Body) + // fmt.Fprintf(os.Stderr, "dagestan", string(bardy)) + + builder.add(&ResponseStart{ + Response: &respect, + }) respClone := *resp + respClone.Body = newReader(resp.Header, resp.Body, false, builder, cancel) return &respClone, nil }) diff --git a/internal/tracer/resp_tracer.go b/internal/tracer/resp_tracer.go index 91a0a42d..513f3cff 100644 --- a/internal/tracer/resp_tracer.go +++ b/internal/tracer/resp_tracer.go @@ -17,9 +17,15 @@ package tracer import ( "context" "encoding/json" + "fmt" + "io" + "os" + "strings" "sync/atomic" + "connectrpc.com/conformance/internal" + v1 "connectrpc.com/conformance/internal/gen/proto/go/connectrpc/conformance/v1" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/types/known/structpb" ) @@ -29,6 +35,7 @@ type respKey struct{} type WireDetails struct { StatusCode int32 RawErrorDetails *structpb.Struct + Trailers []*v1.Header } type RespWrapper struct { @@ -67,34 +74,65 @@ func NewContextTracer(trace *Tracer) *contextTracer { } } +func isConnectStreaming(trace Trace) bool { + contentType := trace.Response.Header.Get("content-type") + return strings.HasPrefix(contentType, "application/connect+") +} +func isConnectUnary(trace Trace) bool { + contentType := trace.Response.Header.Get("content-type") + return contentType == "application/json" +} +func isConnect(trace Trace) bool { + return isConnectUnary(trace) || isConnectStreaming(trace) +} + func (t *contextTracer) Complete(trace Trace) { respWrapper, ok := trace.Request.Context().Value(respKey{}).(*RespWrapper) + p := internal.NewPrinter(os.Stderr) + + trace.Print(p) + if ok { + statusCode := int32(trace.Response.StatusCode) wire := &WireDetails{ - StatusCode: int32(trace.Response.StatusCode), + StatusCode: statusCode, + Trailers: internal.ConvertToProtoHeader(trace.Response.Trailer), } - for _, ev := range trace.Events { - switch eventType := ev.(type) { - case *ResponseBodyEndStream: - var endStream endStreamError - json.Unmarshal([]byte(eventType.Content), &endStream) - + if statusCode != 200 { + // Get raw Connect error details only if this is the Connect protocol + if isConnectUnary(trace) { + body, err := io.ReadAll(trace.Response.Body) + if err != nil { + return + } + fmt.Fprintf(os.Stderr, "Das booty", string(body)) var jsonRaw structpb.Struct - if err := protojson.Unmarshal(endStream.Error, &jsonRaw); err != nil { + if err := protojson.Unmarshal(body, &jsonRaw); err != nil { + fmt.Fprintf(os.Stderr, "OH HELL NO", err) return } wire.RawErrorDetails = &jsonRaw + } else if isConnectStreaming(trace) { + for _, ev := range trace.Events { + switch eventType := ev.(type) { + case *ResponseBodyEndStream: + var endStream endStreamError + json.Unmarshal([]byte(eventType.Content), &endStream) + + var jsonRaw structpb.Struct + if err := protojson.Unmarshal(endStream.Error, &jsonRaw); err != nil { + return + } + wire.RawErrorDetails = &jsonRaw + } + } } } respWrapper.val.Store(&wire) } - // p := internal.NewPrinter(os.Stderr) - - // trace.Print(p) - if t != nil { t.tracer.Complete(trace) } From c2448f1182d0a7057106cf774d2327fa3b0efeca Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Tue, 30 Jan 2024 15:30:19 -0500 Subject: [PATCH 13/40] Tracing --- .../connectconformance/testsuites/basic.yaml | 379 ++++++++---------- .../connectconformance/testsuites/errors.yaml | 65 ++- internal/app/referenceclient/client.go | 6 +- internal/app/referenceclient/impl.go | 57 ++- internal/app/referenceclient/wire_tracer.go | 131 ++++++ internal/tracer/builder.go | 4 + internal/tracer/middleware.go | 57 ++- internal/tracer/resp_tracer.go | 139 ------- 8 files changed, 439 insertions(+), 399 deletions(-) create mode 100644 internal/app/referenceclient/wire_tracer.go delete mode 100644 internal/tracer/resp_tracer.go diff --git a/internal/app/connectconformance/testsuites/basic.yaml b/internal/app/connectconformance/testsuites/basic.yaml index f3fc6e09..a0542806 100644 --- a/internal/app/connectconformance/testsuites/basic.yaml +++ b/internal/app/connectconformance/testsuites/basic.yaml @@ -1,215 +1,184 @@ name: Basic -relevantProtocols: - - PROTOCOL_CONNECT -relevantCodecs: - - CODEC_JSON -relevantCompressions: - - COMPRESSION_IDENTITY -relevantHttpVersions: - - HTTP_VERSION_1 -reliesOnTls: false testCases: # Unary Tests ----------------------------------------------------------------- -# - request: -# testName: unary success -# service: connectrpc.conformance.v1.ConformanceService -# method: Unary -# streamType: STREAM_TYPE_UNARY -# requestHeaders: -# - name: X-Conformance-Test -# value: ["Value1","Value2"] -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest -# responseDefinition: -# responseHeaders: -# - name: x-custom-header -# value: ["foo"] -# responseData: "dGVzdCByZXNwb25zZQ==" -# responseTrailers: -# - name: x-custom-trailer -# value: ["bing"] - request: - testName: unary canceled error + testName: unary success service: connectrpc.conformance.v1.ConformanceService method: Unary streamType: STREAM_TYPE_UNARY + requestHeaders: + - name: X-Conformance-Test + value: ["Value1","Value2"] requestMessages: - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest responseDefinition: - error: - code: 1 - message: "canceled" -# - request: -# testName: unary empty definition -# service: connectrpc.conformance.v1.ConformanceService -# method: Unary -# streamType: STREAM_TYPE_UNARY -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest -# responseDefinition: -# - request: -# testName: unary no definition -# service: connectrpc.conformance.v1.ConformanceService -# method: Unary -# streamType: STREAM_TYPE_UNARY -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest -# # Client Stream Tests --------------------------------------------------------- -# - request: -# testName: client stream success -# service: connectrpc.conformance.v1.ConformanceService -# method: ClientStream -# streamType: STREAM_TYPE_CLIENT_STREAM -# requestHeaders: -# - name: X-Conformance-Test -# value: ["Value1","Value2"] -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest -# responseDefinition: -# responseHeaders: -# - name: x-custom-header -# value: ["foo"] -# responseData: "dGVzdCByZXNwb25zZQ==" -# responseTrailers: -# - name: x-custom-trailer -# value: ["bing"] -# - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest -# requestData: "dGVzdCByZXNwb25zZQ==" -# - request: -# testName: client stream empty definition -# service: connectrpc.conformance.v1.ConformanceService -# method: ClientStream -# streamType: STREAM_TYPE_CLIENT_STREAM -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest -# responseDefinition: -# - request: -# testName: client stream no definition -# service: connectrpc.conformance.v1.ConformanceService -# method: ClientStream -# streamType: STREAM_TYPE_CLIENT_STREAM -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest -# # Server Stream Tests --------------------------------------------------------- -# - request: -# testName: stream canceled error -# service: connectrpc.conformance.v1.ConformanceService -# method: ServerStream -# streamType: STREAM_TYPE_SERVER_STREAM -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest -# responseDefinition: -# error: -# code: 1 -# message: "canceled" -# - request: -# testName: server stream success -# service: connectrpc.conformance.v1.ConformanceService -# method: ServerStream -# streamType: STREAM_TYPE_SERVER_STREAM -# requestHeaders: -# - name: X-Conformance-Test -# value: ["Value1","Value2"] -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest -# responseDefinition: -# responseHeaders: -# - name: x-custom-header -# value: ["foo"] -# responseData: -# - "dGVzdCByZXNwb25zZQ==" -# - "dGVzdCByZXNwb25zZQ==" -# responseTrailers: -# - name: x-custom-trailer -# value: ["bing"] -# - request: -# testName: server stream no response -# service: connectrpc.conformance.v1.ConformanceService -# method: ServerStream -# streamType: STREAM_TYPE_SERVER_STREAM -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest -# responseDefinition: -# responseHeaders: -# - name: x-custom-header -# value: ["foo"] -# responseTrailers: -# - name: x-custom-trailer -# value: ["bing"] -# - request: -# testName: server stream no definition -# service: connectrpc.conformance.v1.ConformanceService -# method: ServerStream -# streamType: STREAM_TYPE_SERVER_STREAM -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest -# # Bidi Stream Tests ----------------------------------------------------------- -# - request: -# testName: bidi full duplex stream success -# service: connectrpc.conformance.v1.ConformanceService -# method: BidiStream -# streamType: STREAM_TYPE_FULL_DUPLEX_BIDI_STREAM -# requestHeaders: -# - name: X-Conformance-Test -# value: ["Value1","Value2"] -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest -# responseDefinition: -# responseHeaders: -# - name: x-custom-header -# value: ["foo"] -# responseData: -# - "dGVzdCByZXNwb25zZQ==" -# - "dGVzdCByZXNwb25zZQ==" -# responseTrailers: -# - name: x-custom-trailer -# value: ["bing"] -# fullDuplex: true -# - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest -# requestData: "dGVzdCByZXNwb25zZQ==" -# - request: -# testName: bidi half duplex stream success -# service: connectrpc.conformance.v1.ConformanceService -# method: BidiStream -# streamType: STREAM_TYPE_HALF_DUPLEX_BIDI_STREAM -# requestHeaders: -# - name: X-Conformance-Test -# value: ["Value1","Value2"] -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest -# responseDefinition: -# responseHeaders: -# - name: x-custom-header -# value: ["foo"] -# responseData: -# - "dGVzdCByZXNwb25zZQ==" -# - "dGVzdCByZXNwb25zZQ==" -# responseTrailers: -# - name: x-custom-trailer -# value: ["bing"] -# - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest -# requestData: "dGVzdCByZXNwb25zZQ==" -# - request: -# # Note there is no 'full duplex no definition set' test since that is -# # logically equivalent to this test and is therefore covered here. -# testName: bidi half duplex stream no definition set -# service: connectrpc.conformance.v1.ConformanceService -# method: BidiStream -# streamType: STREAM_TYPE_HALF_DUPLEX_BIDI_STREAM -# requestHeaders: -# - name: X-Conformance-Test -# value: ["Value1","Value2"] -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest -# # Misc Tests ------------------------------------------------------------------ -# - request: -# testName: unimplemented -# service: connectrpc.conformance.v1.ConformanceService -# method: Unimplemented -# streamType: STREAM_TYPE_UNARY -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnimplementedRequest -# # Override -# expectedResponse: -# error: -# code: 12 + responseHeaders: + - name: x-custom-header + value: ["foo"] + responseData: "dGVzdCByZXNwb25zZQ==" + responseTrailers: + - name: x-custom-trailer + value: ["bing"] +- request: + testName: unary empty definition + service: connectrpc.conformance.v1.ConformanceService + method: Unary + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: +- request: + testName: unary no definition + service: connectrpc.conformance.v1.ConformanceService + method: Unary + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest +# Client Stream Tests --------------------------------------------------------- +- request: + testName: client stream success + service: connectrpc.conformance.v1.ConformanceService + method: ClientStream + streamType: STREAM_TYPE_CLIENT_STREAM + requestHeaders: + - name: X-Conformance-Test + value: ["Value1","Value2"] + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest + responseDefinition: + responseHeaders: + - name: x-custom-header + value: ["foo"] + responseData: "dGVzdCByZXNwb25zZQ==" + responseTrailers: + - name: x-custom-trailer + value: ["bing"] + - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest + requestData: "dGVzdCByZXNwb25zZQ==" +- request: + testName: client stream empty definition + service: connectrpc.conformance.v1.ConformanceService + method: ClientStream + streamType: STREAM_TYPE_CLIENT_STREAM + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest + responseDefinition: +- request: + testName: client stream no definition + service: connectrpc.conformance.v1.ConformanceService + method: ClientStream + streamType: STREAM_TYPE_CLIENT_STREAM + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest +# Server Stream Tests --------------------------------------------------------- +- request: + testName: server stream success + service: connectrpc.conformance.v1.ConformanceService + method: ServerStream + streamType: STREAM_TYPE_SERVER_STREAM + requestHeaders: + - name: X-Conformance-Test + value: ["Value1","Value2"] + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest + responseDefinition: + responseHeaders: + - name: x-custom-header + value: ["foo"] + responseData: + - "dGVzdCByZXNwb25zZQ==" + - "dGVzdCByZXNwb25zZQ==" + responseTrailers: + - name: x-custom-trailer + value: ["bing"] +- request: + testName: server stream no response + service: connectrpc.conformance.v1.ConformanceService + method: ServerStream + streamType: STREAM_TYPE_SERVER_STREAM + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest + responseDefinition: + responseHeaders: + - name: x-custom-header + value: ["foo"] + responseTrailers: + - name: x-custom-trailer + value: ["bing"] +- request: + testName: server stream no definition + service: connectrpc.conformance.v1.ConformanceService + method: ServerStream + streamType: STREAM_TYPE_SERVER_STREAM + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest +# Bidi Stream Tests ----------------------------------------------------------- +- request: + testName: bidi full duplex stream success + service: connectrpc.conformance.v1.ConformanceService + method: BidiStream + streamType: STREAM_TYPE_FULL_DUPLEX_BIDI_STREAM + requestHeaders: + - name: X-Conformance-Test + value: ["Value1","Value2"] + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest + responseDefinition: + responseHeaders: + - name: x-custom-header + value: ["foo"] + responseData: + - "dGVzdCByZXNwb25zZQ==" + - "dGVzdCByZXNwb25zZQ==" + responseTrailers: + - name: x-custom-trailer + value: ["bing"] + fullDuplex: true + - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest + requestData: "dGVzdCByZXNwb25zZQ==" +- request: + testName: bidi half duplex stream success + service: connectrpc.conformance.v1.ConformanceService + method: BidiStream + streamType: STREAM_TYPE_HALF_DUPLEX_BIDI_STREAM + requestHeaders: + - name: X-Conformance-Test + value: ["Value1","Value2"] + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest + responseDefinition: + responseHeaders: + - name: x-custom-header + value: ["foo"] + responseData: + - "dGVzdCByZXNwb25zZQ==" + - "dGVzdCByZXNwb25zZQ==" + responseTrailers: + - name: x-custom-trailer + value: ["bing"] + - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest + requestData: "dGVzdCByZXNwb25zZQ==" +- request: + # Note there is no 'full duplex no definition set' test since that is + # logically equivalent to this test and is therefore covered here. + testName: bidi half duplex stream no definition set + service: connectrpc.conformance.v1.ConformanceService + method: BidiStream + streamType: STREAM_TYPE_HALF_DUPLEX_BIDI_STREAM + requestHeaders: + - name: X-Conformance-Test + value: ["Value1","Value2"] + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest +# Misc Tests ------------------------------------------------------------------ +- request: + testName: unimplemented + service: connectrpc.conformance.v1.ConformanceService + method: Unimplemented + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnimplementedRequest + # Override + expectedResponse: + error: + code: 12 diff --git a/internal/app/connectconformance/testsuites/errors.yaml b/internal/app/connectconformance/testsuites/errors.yaml index 8a3123db..1311bcc4 100644 --- a/internal/app/connectconformance/testsuites/errors.yaml +++ b/internal/app/connectconformance/testsuites/errors.yaml @@ -412,33 +412,58 @@ testCases: responseTrailers: - name: x-custom-trailer value: ["bing"] -# Bidi Stream Tests ----------------------------------------------------------- +# Client Stream Tests ----------------------------------------------------------- - request: - testName: bidi full duplex stream error with responses + testName: client stream error one request service: connectrpc.conformance.v1.ConformanceService - method: BidiStream - streamType: STREAM_TYPE_FULL_DUPLEX_BIDI_STREAM - requestHeaders: - - name: X-Conformance-Test - value: ["Value1","Value2"] + method: ClientStream + streamType: STREAM_TYPE_CLIENT_STREAM requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest + - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest responseDefinition: - responseHeaders: - - name: x-custom-header - value: ["foo"] - responseData: - - "dGVzdCByZXNwb25zZQ==" - - "dGVzdCByZXNwb25zZQ==" error: code: 13 - message: "bidi full duplex stream failed" - responseTrailers: - - name: x-custom-trailer - value: ["bing"] - fullDuplex: true - - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest + message: "client stream failed" +- request: + testName: client stream error multiple requests + service: connectrpc.conformance.v1.ConformanceService + method: ClientStream + streamType: STREAM_TYPE_CLIENT_STREAM + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest + responseDefinition: + error: + code: 13 + message: "client stream failed" + - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest requestData: "dGVzdCByZXNwb25zZQ==" +# Bidi Stream Tests ----------------------------------------------------------- +- request: + # testName: bidi full duplex stream error with responses + # service: connectrpc.conformance.v1.ConformanceService + # method: BidiStream + # streamType: STREAM_TYPE_FULL_DUPLEX_BIDI_STREAM + # requestHeaders: + # - name: X-Conformance-Test + # value: ["Value1","Value2"] + # requestMessages: + # - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest + # responseDefinition: + # responseHeaders: + # - name: x-custom-header + # value: ["foo"] + # responseData: + # - "dGVzdCByZXNwb25zZQ==" + # - "dGVzdCByZXNwb25zZQ==" + # error: + # code: 13 + # message: "bidi full duplex stream failed" + # responseTrailers: + # - name: x-custom-trailer + # value: ["bing"] + # fullDuplex: true + # - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest + # requestData: "dGVzdCByZXNwb25zZQ==" - request: testName: bidi full duplex stream error no responses service: connectrpc.conformance.v1.ConformanceService diff --git a/internal/app/referenceclient/client.go b/internal/app/referenceclient/client.go index 3f9f79c1..4faf6d24 100644 --- a/internal/app/referenceclient/client.go +++ b/internal/app/referenceclient/client.go @@ -217,8 +217,10 @@ func invoke(ctx context.Context, req *v1.ClientCompatRequest, trace *tracer.Trac return nil, errors.New("an HTTP version must be specified") } - ct := tracer.NewContextTracer(trace) - transport = tracer.TracingRoundTripper(transport, ct) + // Create a new TracingRoundTripper with our WireTracer so that the tests can trace values on the + // wire. Note that 'trace' could be nil, in which case, any error traces will + // simply not be printed. + transport = tracer.TracingRoundTripper(transport, NewWireTracer(trace)) if req.RawRequest != nil { transport = &rawRequestSender{transport: transport, rawRequest: req.RawRequest} diff --git a/internal/app/referenceclient/impl.go b/internal/app/referenceclient/impl.go index 86679489..870473f8 100644 --- a/internal/app/referenceclient/impl.go +++ b/internal/app/referenceclient/impl.go @@ -21,13 +21,11 @@ import ( "io" "net/http" "net/url" - "os" "time" "connectrpc.com/conformance/internal" v1 "connectrpc.com/conformance/internal/gen/proto/go/connectrpc/conformance/v1" "connectrpc.com/conformance/internal/gen/proto/go/connectrpc/conformance/v1/conformancev1connect" - "connectrpc.com/conformance/internal/tracer" "connectrpc.com/connect" "google.golang.org/protobuf/types/known/structpb" ) @@ -121,7 +119,7 @@ func (i *invoker) unary( var trailers []*v1.Header payloads := make([]*v1.ConformancePayload, 0, 1) - ctx, wire := tracer.WithResponseCapture(ctx) + ctx, wire := WithWireCapture(ctx) // Invoke the Unary call resp, err := i.client.Unary(ctx, request) @@ -131,12 +129,9 @@ func (i *invoker) unary( var actualTrailers []*v1.Header wireDetails := wire.Get() if wireDetails != nil { - fmt.Fprintf(os.Stderr, "UNARY WIRE DETAILS: %+v\n\n", wireDetails) actualStatusCode = wireDetails.StatusCode - connectErrorRaw = wireDetails.RawErrorDetails actualTrailers = wireDetails.Trailers - } else { - fmt.Fprintf(os.Stderr, "UNARY WIRE DETAILS are NULL") + connectErrorRaw = wireDetails.ConnectErrorRaw } if err != nil { @@ -190,7 +185,7 @@ func (i *invoker) idempotentUnary( var trailers []*v1.Header payloads := make([]*v1.ConformancePayload, 0, 1) - ctx, wire := tracer.WithResponseCapture(ctx) + ctx, wire := WithWireCapture(ctx) // Invoke the Unary call resp, err := i.client.IdempotentUnary(ctx, request) @@ -201,8 +196,8 @@ func (i *invoker) idempotentUnary( wireDetails := wire.Get() if wireDetails != nil { actualStatusCode = wireDetails.StatusCode - connectErrorRaw = wireDetails.RawErrorDetails actualTrailers = wireDetails.Trailers + connectErrorRaw = wireDetails.ConnectErrorRaw } if err != nil { @@ -249,7 +244,7 @@ func (i *invoker) serverStream( // Add the specified request headers to the request internal.AddHeaders(req.RequestHeaders, request.Header()) - ctx, wire := tracer.WithResponseCapture(ctx) + ctx, wire := WithWireCapture(ctx) stream, err := i.client.ServerStream(ctx, request) if err != nil { @@ -316,12 +311,9 @@ func (i *invoker) serverStream( var actualTrailers []*v1.Header wireDetails := wire.Get() if wireDetails != nil { - fmt.Fprintf(os.Stderr, "SERVER STREAM WIRE DETAILS: %+v\n\n", wireDetails) actualStatusCode = wireDetails.StatusCode - connectErrorRaw = wireDetails.RawErrorDetails actualTrailers = wireDetails.Trailers - } else { - fmt.Fprintf(os.Stderr, "SERVER STREAM WIRE DETAILS are NULL") + connectErrorRaw = wireDetails.ConnectErrorRaw } return &v1.ClientResponseResult{ @@ -342,6 +334,7 @@ func (i *invoker) clientStream( ctx, cancel := context.WithCancel(ctx) defer cancel() + ctx, wire := WithWireCapture(ctx) stream := i.client.ClientStream(ctx) // Add the specified request headers to the request @@ -394,13 +387,24 @@ func (i *invoker) clientStream( headers = internal.ConvertToProtoHeader(resp.Header()) trailers = internal.ConvertToProtoHeader(resp.Trailer()) } + var actualStatusCode int32 + var connectErrorRaw *structpb.Struct + var actualTrailers []*v1.Header + wireDetails := wire.Get() + if wireDetails != nil { + actualStatusCode = wireDetails.StatusCode + actualTrailers = wireDetails.Trailers + connectErrorRaw = wireDetails.ConnectErrorRaw + } return &v1.ClientResponseResult{ - ResponseHeaders: headers, - ResponseTrailers: trailers, - Payloads: payloads, - Error: protoErr, - ConnectErrorRaw: nil, // TODO + ResponseHeaders: headers, + ResponseTrailers: trailers, + Payloads: payloads, + Error: protoErr, + ActualStatusCode: actualStatusCode, + ActualHttpTrailers: actualTrailers, + ConnectErrorRaw: connectErrorRaw, }, nil } @@ -415,6 +419,8 @@ func (i *invoker) bidiStream( ConnectErrorRaw: nil, // TODO } + ctx, wire := WithWireCapture(ctx) + stream := i.client.BidiStream(ctx) defer func() { if result != nil { @@ -520,6 +526,19 @@ func (i *invoker) bidiStream( result.Payloads = append(result.Payloads, msg.Payload) } + var actualStatusCode int32 + var connectErrorRaw *structpb.Struct + var actualTrailers []*v1.Header + wireDetails := wire.Get() + if wireDetails != nil { + actualStatusCode = wireDetails.StatusCode + actualTrailers = wireDetails.Trailers + connectErrorRaw = wireDetails.ConnectErrorRaw + } + result.ActualStatusCode = actualStatusCode + result.ActualHttpTrailers = actualTrailers + result.ConnectErrorRaw = connectErrorRaw + if protoErr != nil { result.Error = protoErr return result, nil diff --git a/internal/app/referenceclient/wire_tracer.go b/internal/app/referenceclient/wire_tracer.go new file mode 100644 index 00000000..6d78fcc8 --- /dev/null +++ b/internal/app/referenceclient/wire_tracer.go @@ -0,0 +1,131 @@ +// Copyright 2023-2024 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package referenceclient + +import ( + "context" + "encoding/json" + "io" + "strings" + + "sync/atomic" + + "connectrpc.com/conformance/internal" + v1 "connectrpc.com/conformance/internal/gen/proto/go/connectrpc/conformance/v1" + "connectrpc.com/conformance/internal/tracer" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/types/known/structpb" +) + +type wireKey struct{} + +type WireDetails struct { + // The actual HTTP status code observed. + StatusCode int32 + // The actual trailers observed. + Trailers []*v1.Header + // The actual JSON observed on the wire in case of an error from a Connect server. + // This will only be non-nil if the protocol is Connect and an error occurred. + ConnectErrorRaw *structpb.Struct +} + +type WireWrapper struct { + val atomic.Pointer[*WireDetails] +} + +func WithWireCapture(ctx context.Context) (context.Context, *WireWrapper) { + wrappers := &WireWrapper{} + ctx = context.WithValue(ctx, wireKey{}, wrappers) + return ctx, wrappers +} + +func (t *WireWrapper) Get() *WireDetails { + respPtr := t.val.Load() + if respPtr == nil { + return nil + } + return *respPtr +} + +type endStreamError struct { + Error json.RawMessage `json:"error"` +} + +type wireTracer struct { + tracer *tracer.Tracer +} + +func NewWireTracer(trace *tracer.Tracer) *wireTracer { + return &wireTracer{ + tracer: trace, + } +} + +func (t *wireTracer) Complete(trace tracer.Trace) { + // p := internal.NewPrinter(os.Stderr) + // trace.Print(p) + respWrapper, ok := trace.Request.Context().Value(wireKey{}).(*WireWrapper) + if ok { + if trace.Response != nil { + statusCode := int32(trace.Response.StatusCode) + + var jsonRaw structpb.Struct + contentType := trace.Response.Header.Get("content-type") + if contentType == "application/json" { + if statusCode != 200 { + // If this is a unary request, then use the entire response body + // as the wire error details. + body, err := io.ReadAll(trace.Response.Body) + if err != nil { + return + } + if err := protojson.Unmarshal(body, &jsonRaw); err != nil { + return + } + } + } else if strings.HasPrefix(contentType, "application/connect+") { + // If this is a streaming request, then look through the trace events + // for the ResponseBodyEndStream event and parse its content into an + // endStreamError to see if there are any error details. + for _, ev := range trace.Events { + switch eventType := ev.(type) { + case *tracer.ResponseBodyEndStream: + var endStream endStreamError + json.Unmarshal([]byte(eventType.Content), &endStream) + if err := protojson.Unmarshal(endStream.Error, &jsonRaw); err != nil { + return + } + break + } + } + } + + wire := &WireDetails{ + StatusCode: statusCode, + Trailers: internal.ConvertToProtoHeader(trace.Response.Trailer), + ConnectErrorRaw: &jsonRaw, + } + + respWrapper.val.Store(&wire) + // } else { + // p := internal.NewPrinter(os.Stderr) + // trace.Print(p) + } + } + + if t != nil { + t.tracer.Complete(trace) + } +} diff --git a/internal/tracer/builder.go b/internal/tracer/builder.go index 24d95c91..52e00ada 100644 --- a/internal/tracer/builder.go +++ b/internal/tracer/builder.go @@ -71,6 +71,10 @@ func (b *builder) add(event Event) { b.trace.Err = event.Err } case *ResponseStart: + // booty, _ := io.ReadAll(event.Response.Body) + // fmt.Fprintf(os.Stderr, "dagster.io: ", string(booty)) + // booty2, _ := io.ReadAll(event.Response.Body) + // fmt.Fprintf(os.Stderr, "dagster.iotwo: ", string(booty2)) b.trace.Response = event.Response case *ResponseError: b.trace.Err = event.Err diff --git a/internal/tracer/middleware.go b/internal/tracer/middleware.go index 1389aafa..abca07c6 100644 --- a/internal/tracer/middleware.go +++ b/internal/tracer/middleware.go @@ -24,6 +24,38 @@ import ( "strings" ) +type reusableReader struct { + io.Reader + readBuf *bytes.Buffer + backBuf *bytes.Buffer +} + +func newReusableReader(b []byte) io.Reader { + r := bytes.NewReader(b) + + readBuf := bytes.Buffer{} + readBuf.ReadFrom(r) // error handling ignored for brevity + backBuf := bytes.Buffer{} + + return reusableReader{ + io.TeeReader(&readBuf, &backBuf), + &readBuf, + &backBuf, + } +} + +func (r reusableReader) Read(p []byte) (int, error) { + n, err := r.Reader.Read(p) + if err == io.EOF { + r.reset() + } + return n, err +} + +func (r reusableReader) reset() { + io.Copy(r.readBuf, r.backBuf) // nolint: errcheck +} + // TracingRoundTripper applies tracing to the given transport. The returned // round tripper will record traces of all operations to the given tracer. func TracingRoundTripper(transport http.RoundTripper, collector Collector) http.RoundTripper { @@ -42,25 +74,22 @@ func TracingRoundTripper(transport http.RoundTripper, collector Collector) http. cancel() return nil, err } - // body, _ := io.ReadAll(resp.Body) - // fmt.Fprintf(os.Stderr, "dagsboro: ", string(body)) - // resp.Body = io.NopCloser(bytes.NewReader(body)) - // booty, _ := io.ReadAll(resp.Body) - // fmt.Fprintf(os.Stderr, "dagster.io: ", string(booty)) - - respect := *resp - cardi, _ := io.ReadAll(respect.Body) - respect.Body = io.NopCloser(bytes.NewReader(cardi)) - - // bardy, _ := io.ReadAll(evt.Response.Body) - // fmt.Fprintf(os.Stderr, "dagestan", string(bardy)) + // Read the response body in full and then replace it with a reusable reader + // so that we can read the contents multiple times in any trace or middleware. + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + resp.Body = io.NopCloser(newReusableReader(body)) builder.add(&ResponseStart{ - Response: &respect, + Response: resp, }) - respClone := *resp + respClone := *resp respClone.Body = newReader(resp.Header, resp.Body, false, builder, cancel) + return &respClone, nil }) } diff --git a/internal/tracer/resp_tracer.go b/internal/tracer/resp_tracer.go deleted file mode 100644 index 513f3cff..00000000 --- a/internal/tracer/resp_tracer.go +++ /dev/null @@ -1,139 +0,0 @@ -// Copyright 2023-2024 The Connect Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package tracer - -import ( - "context" - "encoding/json" - "fmt" - "io" - "os" - "strings" - - "sync/atomic" - - "connectrpc.com/conformance/internal" - v1 "connectrpc.com/conformance/internal/gen/proto/go/connectrpc/conformance/v1" - "google.golang.org/protobuf/encoding/protojson" - "google.golang.org/protobuf/types/known/structpb" -) - -type respKey struct{} - -type WireDetails struct { - StatusCode int32 - RawErrorDetails *structpb.Struct - Trailers []*v1.Header -} - -type RespWrapper struct { - val atomic.Pointer[*WireDetails] -} - -func WithResponseCapture(ctx context.Context) (context.Context, *RespWrapper) { - wrappers := &RespWrapper{} - ctx = context.WithValue(ctx, respKey{}, wrappers) - return ctx, wrappers -} - -// Get returns the resp captured. Resps are not captured until the response body is -// exhausted. -func (t *RespWrapper) Get() *WireDetails { - respPtr := t.val.Load() - if respPtr == nil { - return nil - } - return *respPtr -} - -type endStreamError struct { - Error json.RawMessage `json:"error"` -} - -type contextTracer struct { - Tracer - - tracer *Tracer -} - -func NewContextTracer(trace *Tracer) *contextTracer { - return &contextTracer{ - tracer: trace, - } -} - -func isConnectStreaming(trace Trace) bool { - contentType := trace.Response.Header.Get("content-type") - return strings.HasPrefix(contentType, "application/connect+") -} -func isConnectUnary(trace Trace) bool { - contentType := trace.Response.Header.Get("content-type") - return contentType == "application/json" -} -func isConnect(trace Trace) bool { - return isConnectUnary(trace) || isConnectStreaming(trace) -} - -func (t *contextTracer) Complete(trace Trace) { - respWrapper, ok := trace.Request.Context().Value(respKey{}).(*RespWrapper) - p := internal.NewPrinter(os.Stderr) - - trace.Print(p) - - if ok { - statusCode := int32(trace.Response.StatusCode) - wire := &WireDetails{ - StatusCode: statusCode, - Trailers: internal.ConvertToProtoHeader(trace.Response.Trailer), - } - - if statusCode != 200 { - // Get raw Connect error details only if this is the Connect protocol - if isConnectUnary(trace) { - body, err := io.ReadAll(trace.Response.Body) - if err != nil { - return - } - fmt.Fprintf(os.Stderr, "Das booty", string(body)) - var jsonRaw structpb.Struct - if err := protojson.Unmarshal(body, &jsonRaw); err != nil { - fmt.Fprintf(os.Stderr, "OH HELL NO", err) - return - } - wire.RawErrorDetails = &jsonRaw - } else if isConnectStreaming(trace) { - for _, ev := range trace.Events { - switch eventType := ev.(type) { - case *ResponseBodyEndStream: - var endStream endStreamError - json.Unmarshal([]byte(eventType.Content), &endStream) - - var jsonRaw structpb.Struct - if err := protojson.Unmarshal(endStream.Error, &jsonRaw); err != nil { - return - } - wire.RawErrorDetails = &jsonRaw - } - } - } - } - respWrapper.val.Store(&wire) - - } - - if t != nil { - t.tracer.Complete(trace) - } -} From 5ba1f02f819851f6e2e0f57361a48e68bef38fb0 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Tue, 30 Jan 2024 15:30:58 -0500 Subject: [PATCH 14/40] Revert go.mod --- go.mod | 1 - go.sum | 1 - 2 files changed, 2 deletions(-) diff --git a/go.mod b/go.mod index 5236c593..914a391a 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,6 @@ require ( connectrpc.com/connect v1.14.0 github.com/andybalholm/brotli v1.1.0 github.com/bufbuild/protoyaml-go v0.1.7 - github.com/gogo/protobuf v1.2.1 github.com/golang/snappy v0.0.4 github.com/google/go-cmp v0.6.0 github.com/improbable-eng/grpc-web v0.15.0 diff --git a/go.sum b/go.sum index 98042377..f34b2f52 100644 --- a/go.sum +++ b/go.sum @@ -112,7 +112,6 @@ github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/E github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= From aa57ce8a529fa02eabe171ade12a7a795aa8acb7 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Tue, 30 Jan 2024 15:32:07 -0500 Subject: [PATCH 15/40] Revert --- .../connectconformance/testsuites/errors.yaml | 50 +++++++++---------- internal/tracer/builder.go | 4 -- internal/tracer/tracer.go | 8 --- 3 files changed, 25 insertions(+), 37 deletions(-) diff --git a/internal/app/connectconformance/testsuites/errors.yaml b/internal/app/connectconformance/testsuites/errors.yaml index 1311bcc4..501d332d 100644 --- a/internal/app/connectconformance/testsuites/errors.yaml +++ b/internal/app/connectconformance/testsuites/errors.yaml @@ -439,31 +439,31 @@ testCases: requestData: "dGVzdCByZXNwb25zZQ==" # Bidi Stream Tests ----------------------------------------------------------- - request: - # testName: bidi full duplex stream error with responses - # service: connectrpc.conformance.v1.ConformanceService - # method: BidiStream - # streamType: STREAM_TYPE_FULL_DUPLEX_BIDI_STREAM - # requestHeaders: - # - name: X-Conformance-Test - # value: ["Value1","Value2"] - # requestMessages: - # - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest - # responseDefinition: - # responseHeaders: - # - name: x-custom-header - # value: ["foo"] - # responseData: - # - "dGVzdCByZXNwb25zZQ==" - # - "dGVzdCByZXNwb25zZQ==" - # error: - # code: 13 - # message: "bidi full duplex stream failed" - # responseTrailers: - # - name: x-custom-trailer - # value: ["bing"] - # fullDuplex: true - # - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest - # requestData: "dGVzdCByZXNwb25zZQ==" + testName: bidi full duplex stream error with responses + service: connectrpc.conformance.v1.ConformanceService + method: BidiStream + streamType: STREAM_TYPE_FULL_DUPLEX_BIDI_STREAM + requestHeaders: + - name: X-Conformance-Test + value: ["Value1","Value2"] + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest + responseDefinition: + responseHeaders: + - name: x-custom-header + value: ["foo"] + responseData: + - "dGVzdCByZXNwb25zZQ==" + - "dGVzdCByZXNwb25zZQ==" + error: + code: 13 + message: "bidi full duplex stream failed" + responseTrailers: + - name: x-custom-trailer + value: ["bing"] + fullDuplex: true + - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest + requestData: "dGVzdCByZXNwb25zZQ==" - request: testName: bidi full duplex stream error no responses service: connectrpc.conformance.v1.ConformanceService diff --git a/internal/tracer/builder.go b/internal/tracer/builder.go index 52e00ada..24d95c91 100644 --- a/internal/tracer/builder.go +++ b/internal/tracer/builder.go @@ -71,10 +71,6 @@ func (b *builder) add(event Event) { b.trace.Err = event.Err } case *ResponseStart: - // booty, _ := io.ReadAll(event.Response.Body) - // fmt.Fprintf(os.Stderr, "dagster.io: ", string(booty)) - // booty2, _ := io.ReadAll(event.Response.Body) - // fmt.Fprintf(os.Stderr, "dagster.iotwo: ", string(booty2)) b.trace.Response = event.Response case *ResponseError: b.trace.Err = event.Err diff --git a/internal/tracer/tracer.go b/internal/tracer/tracer.go index 133c32e5..3ad80b27 100644 --- a/internal/tracer/tracer.go +++ b/internal/tracer/tracer.go @@ -172,7 +172,6 @@ type RequestStart struct { } func (r *RequestStart) print(printer internal.Printer) { - printer.Printf("REQUEST START") urlClone := *r.Request.URL if urlClone.Host == "" { urlClone.Host = "..." @@ -219,7 +218,6 @@ type RequestBodyData struct { } func (r *RequestBodyData) print(printer internal.Printer) { - printer.Printf("REQUEST BODY DATA") printData(requestPrefix, r.offsetMillis(), r.MessageIndex, r.Envelope, r.Len, printer) } @@ -235,7 +233,6 @@ type RequestBodyEnd struct { } func (r *RequestBodyEnd) print(printer internal.Printer) { - printer.Printf("REQUEST BODY END") if r.Err != nil { printer.Printf("%s %9.3fms body end (err=%v)", requestPrefix, r.offsetMillis(), r.Err) } else { @@ -253,7 +250,6 @@ type ResponseStart struct { } func (r *ResponseStart) print(printer internal.Printer) { - printer.Printf("RESPONSE START") printer.Printf("%s %9.3fms %s", responsePrefix, r.offsetMillis(), r.Response.Status) printHeaders(responsePrefix, r.Response.Header, printer) if r.Response.ContentLength != -1 && len(r.Response.Header.Values("Content-Length")) == 0 { @@ -272,7 +268,6 @@ type ResponseError struct { } func (r *ResponseError) print(printer internal.Printer) { - printer.Printf("RESPONSE ERROR") printer.Printf("%s %9.3fms failed: %v", responsePrefix, r.offsetMillis(), r.Err) } @@ -305,7 +300,6 @@ type ResponseBodyData struct { } func (r *ResponseBodyData) print(printer internal.Printer) { - printer.Printf("RESPONSE BODY DATA") printData(responsePrefix, r.offsetMillis(), r.MessageIndex, r.Envelope, r.Len, printer) } @@ -320,7 +314,6 @@ type ResponseBodyEndStream struct { } func (r *ResponseBodyEndStream) print(printer internal.Printer) { - printer.Printf("RESPONSE BODY ENDSTREAM") lines := strings.Split(r.Content, "\n") for _, line := range lines { line = strings.Trim(line, "\r") @@ -340,7 +333,6 @@ type ResponseBodyEnd struct { } func (r *ResponseBodyEnd) print(printer internal.Printer) { - printer.Printf("RESPONSE BODY END") if r.Err != nil { printer.Printf("%s %9.3fms body end (err=%v)", responsePrefix, r.offsetMillis(), r.Err) } else { From 21b7974032cdfc8555c65635f04d38af70a7ca80 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Tue, 30 Jan 2024 17:15:00 -0500 Subject: [PATCH 16/40] Lint --- .../connectconformance/testsuites/basic.yaml | 46 +++++++++---------- internal/app/referenceclient/impl.go | 1 - internal/app/referenceclient/wire_tracer.go | 38 ++++++++------- internal/tracer/middleware.go | 42 ++--------------- 4 files changed, 46 insertions(+), 81 deletions(-) diff --git a/internal/app/connectconformance/testsuites/basic.yaml b/internal/app/connectconformance/testsuites/basic.yaml index a0542806..8773a483 100644 --- a/internal/app/connectconformance/testsuites/basic.yaml +++ b/internal/app/connectconformance/testsuites/basic.yaml @@ -113,29 +113,29 @@ testCases: requestMessages: - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest # Bidi Stream Tests ----------------------------------------------------------- -- request: - testName: bidi full duplex stream success - service: connectrpc.conformance.v1.ConformanceService - method: BidiStream - streamType: STREAM_TYPE_FULL_DUPLEX_BIDI_STREAM - requestHeaders: - - name: X-Conformance-Test - value: ["Value1","Value2"] - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest - responseDefinition: - responseHeaders: - - name: x-custom-header - value: ["foo"] - responseData: - - "dGVzdCByZXNwb25zZQ==" - - "dGVzdCByZXNwb25zZQ==" - responseTrailers: - - name: x-custom-trailer - value: ["bing"] - fullDuplex: true - - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest - requestData: "dGVzdCByZXNwb25zZQ==" +# - request: +# testName: bidi full duplex stream success +# service: connectrpc.conformance.v1.ConformanceService +# method: BidiStream +# streamType: STREAM_TYPE_FULL_DUPLEX_BIDI_STREAM +# requestHeaders: +# - name: X-Conformance-Test +# value: ["Value1","Value2"] +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest +# responseDefinition: +# responseHeaders: +# - name: x-custom-header +# value: ["foo"] +# responseData: +# - "dGVzdCByZXNwb25zZQ==" +# - "dGVzdCByZXNwb25zZQ==" +# responseTrailers: +# - name: x-custom-trailer +# value: ["bing"] +# fullDuplex: true +# - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest +# requestData: "dGVzdCByZXNwb25zZQ==" - request: testName: bidi half duplex stream success service: connectrpc.conformance.v1.ConformanceService diff --git a/internal/app/referenceclient/impl.go b/internal/app/referenceclient/impl.go index 870473f8..74040ec3 100644 --- a/internal/app/referenceclient/impl.go +++ b/internal/app/referenceclient/impl.go @@ -40,7 +40,6 @@ func (i *invoker) Invoke( ctx context.Context, req *v1.ClientCompatRequest, ) (*v1.ClientResponseResult, error) { - // If a timeout was specified, create a derived context with that deadline if req.TimeoutMs != nil { deadlineCtx, cancel := context.WithDeadline(ctx, time.Now().Add(time.Duration(*req.TimeoutMs)*time.Millisecond)) diff --git a/internal/app/referenceclient/wire_tracer.go b/internal/app/referenceclient/wire_tracer.go index 6d78fcc8..53a9b507 100644 --- a/internal/app/referenceclient/wire_tracer.go +++ b/internal/app/referenceclient/wire_tracer.go @@ -45,39 +45,37 @@ type WireWrapper struct { val atomic.Pointer[*WireDetails] } -func WithWireCapture(ctx context.Context) (context.Context, *WireWrapper) { - wrappers := &WireWrapper{} - ctx = context.WithValue(ctx, wireKey{}, wrappers) - return ctx, wrappers -} - -func (t *WireWrapper) Get() *WireDetails { - respPtr := t.val.Load() +func (w *WireWrapper) Get() *WireDetails { + respPtr := w.val.Load() if respPtr == nil { return nil } return *respPtr } +func WithWireCapture(ctx context.Context) (context.Context, *WireWrapper) { + wrappers := &WireWrapper{} + ctx = context.WithValue(ctx, wireKey{}, wrappers) + return ctx, wrappers +} + type endStreamError struct { Error json.RawMessage `json:"error"` } -type wireTracer struct { +type WireTracer struct { tracer *tracer.Tracer } -func NewWireTracer(trace *tracer.Tracer) *wireTracer { - return &wireTracer{ +func NewWireTracer(trace *tracer.Tracer) *WireTracer { + return &WireTracer{ tracer: trace, } } -func (t *wireTracer) Complete(trace tracer.Trace) { - // p := internal.NewPrinter(os.Stderr) - // trace.Print(p) +func (t *WireTracer) Complete(trace tracer.Trace) { respWrapper, ok := trace.Request.Context().Value(wireKey{}).(*WireWrapper) - if ok { + if ok { //nolint:nestif if trace.Response != nil { statusCode := int32(trace.Response.StatusCode) @@ -103,11 +101,14 @@ func (t *wireTracer) Complete(trace tracer.Trace) { switch eventType := ev.(type) { case *tracer.ResponseBodyEndStream: var endStream endStreamError - json.Unmarshal([]byte(eventType.Content), &endStream) + if err := json.Unmarshal([]byte(eventType.Content), &endStream); err != nil { + return + } if err := protojson.Unmarshal(endStream.Error, &jsonRaw); err != nil { return } - break + default: + // Do nothing } } } @@ -119,9 +120,6 @@ func (t *wireTracer) Complete(trace tracer.Trace) { } respWrapper.val.Store(&wire) - // } else { - // p := internal.NewPrinter(os.Stderr) - // trace.Print(p) } } diff --git a/internal/tracer/middleware.go b/internal/tracer/middleware.go index abca07c6..451b2c6f 100644 --- a/internal/tracer/middleware.go +++ b/internal/tracer/middleware.go @@ -24,44 +24,13 @@ import ( "strings" ) -type reusableReader struct { - io.Reader - readBuf *bytes.Buffer - backBuf *bytes.Buffer -} - -func newReusableReader(b []byte) io.Reader { - r := bytes.NewReader(b) - - readBuf := bytes.Buffer{} - readBuf.ReadFrom(r) // error handling ignored for brevity - backBuf := bytes.Buffer{} - - return reusableReader{ - io.TeeReader(&readBuf, &backBuf), - &readBuf, - &backBuf, - } -} - -func (r reusableReader) Read(p []byte) (int, error) { - n, err := r.Reader.Read(p) - if err == io.EOF { - r.reset() - } - return n, err -} - -func (r reusableReader) reset() { - io.Copy(r.readBuf, r.backBuf) // nolint: errcheck -} - // TracingRoundTripper applies tracing to the given transport. The returned // round tripper will record traces of all operations to the given tracer. func TracingRoundTripper(transport http.RoundTripper, collector Collector) http.RoundTripper { return roundTripperFunc(func(req *http.Request) (*http.Response, error) { builder := newBuilder(req, collector) ctx, cancel := context.WithCancel(req.Context()) + defer cancel() go func() { <-ctx.Done() builder.add(&RequestCanceled{}) @@ -71,24 +40,23 @@ func TracingRoundTripper(transport http.RoundTripper, collector Collector) http. resp, err := transport.RoundTrip(req) if err != nil { builder.add(&ResponseError{Err: err}) - cancel() return nil, err } - // Read the response body in full and then replace it with a reusable reader - // so that we can read the contents multiple times in any trace or middleware. defer resp.Body.Close() + // Read the response body in full and then replace it with a new in-memory reader + // so that we can read the contents multiple times in any trace or middleware. body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } - resp.Body = io.NopCloser(newReusableReader(body)) + resp.Body = io.NopCloser(bytes.NewReader(body)) builder.add(&ResponseStart{ Response: resp, }) respClone := *resp - respClone.Body = newReader(resp.Header, resp.Body, false, builder, cancel) + respClone.Body = newReader(resp.Header, io.NopCloser(bytes.NewReader(body)), false, builder, cancel) return &respClone, nil }) From c8db7a5643831d017c70e4904ad104dac7d1b3b4 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Tue, 30 Jan 2024 17:25:20 -0500 Subject: [PATCH 17/40] Docs --- internal/app/referenceclient/wire_tracer.go | 29 +++++++++++++-------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/internal/app/referenceclient/wire_tracer.go b/internal/app/referenceclient/wire_tracer.go index 53a9b507..3c83f540 100644 --- a/internal/app/referenceclient/wire_tracer.go +++ b/internal/app/referenceclient/wire_tracer.go @@ -19,7 +19,6 @@ import ( "encoding/json" "io" "strings" - "sync/atomic" "connectrpc.com/conformance/internal" @@ -31,6 +30,7 @@ import ( type wireKey struct{} +// WireDetails encapsulates the wire details to track for a roundtrip. type WireDetails struct { // The actual HTTP status code observed. StatusCode int32 @@ -45,6 +45,7 @@ type WireWrapper struct { val atomic.Pointer[*WireDetails] } +// Get returns the wire details func (w *WireWrapper) Get() *WireDetails { respPtr := w.val.Load() if respPtr == nil { @@ -53,26 +54,21 @@ func (w *WireWrapper) Get() *WireDetails { return *respPtr } +// WithWireCapture returns a new context which will contain wire details during +// a roundtrip. func WithWireCapture(ctx context.Context) (context.Context, *WireWrapper) { wrappers := &WireWrapper{} ctx = context.WithValue(ctx, wireKey{}, wrappers) return ctx, wrappers } -type endStreamError struct { - Error json.RawMessage `json:"error"` -} - type WireTracer struct { tracer *tracer.Tracer } -func NewWireTracer(trace *tracer.Tracer) *WireTracer { - return &WireTracer{ - tracer: trace, - } -} - +// Complete intercepts the Complete call for a tracer, extracting wire details +// from the passed trace. The wire details will be stored in the context acquired byte +// WithWireCapture and can be retrieved via WireWrapper.Get() func (t *WireTracer) Complete(trace tracer.Trace) { respWrapper, ok := trace.Request.Context().Value(wireKey{}).(*WireWrapper) if ok { //nolint:nestif @@ -94,6 +90,9 @@ func (t *WireTracer) Complete(trace tracer.Trace) { } } } else if strings.HasPrefix(contentType, "application/connect+") { + type endStreamError struct { + Error json.RawMessage `json:"error"` + } // If this is a streaming request, then look through the trace events // for the ResponseBodyEndStream event and parse its content into an // endStreamError to see if there are any error details. @@ -127,3 +126,11 @@ func (t *WireTracer) Complete(trace tracer.Trace) { t.tracer.Complete(trace) } } + +// NewWireTracer returns a new wire tracer with the given tracer. +// If trace is nil, all tracer operations will be bypassed. +func NewWireTracer(trace *tracer.Tracer) *WireTracer { + return &WireTracer{ + tracer: trace, + } +} From e4d2c2fc4d7b53022a073a3d2f789f84c6186dbd Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Tue, 30 Jan 2024 17:26:06 -0500 Subject: [PATCH 18/40] Revert --- .../connectconformance/testsuites/basic.yaml | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/internal/app/connectconformance/testsuites/basic.yaml b/internal/app/connectconformance/testsuites/basic.yaml index 8773a483..a0542806 100644 --- a/internal/app/connectconformance/testsuites/basic.yaml +++ b/internal/app/connectconformance/testsuites/basic.yaml @@ -113,29 +113,29 @@ testCases: requestMessages: - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest # Bidi Stream Tests ----------------------------------------------------------- -# - request: -# testName: bidi full duplex stream success -# service: connectrpc.conformance.v1.ConformanceService -# method: BidiStream -# streamType: STREAM_TYPE_FULL_DUPLEX_BIDI_STREAM -# requestHeaders: -# - name: X-Conformance-Test -# value: ["Value1","Value2"] -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest -# responseDefinition: -# responseHeaders: -# - name: x-custom-header -# value: ["foo"] -# responseData: -# - "dGVzdCByZXNwb25zZQ==" -# - "dGVzdCByZXNwb25zZQ==" -# responseTrailers: -# - name: x-custom-trailer -# value: ["bing"] -# fullDuplex: true -# - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest -# requestData: "dGVzdCByZXNwb25zZQ==" +- request: + testName: bidi full duplex stream success + service: connectrpc.conformance.v1.ConformanceService + method: BidiStream + streamType: STREAM_TYPE_FULL_DUPLEX_BIDI_STREAM + requestHeaders: + - name: X-Conformance-Test + value: ["Value1","Value2"] + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest + responseDefinition: + responseHeaders: + - name: x-custom-header + value: ["foo"] + responseData: + - "dGVzdCByZXNwb25zZQ==" + - "dGVzdCByZXNwb25zZQ==" + responseTrailers: + - name: x-custom-trailer + value: ["bing"] + fullDuplex: true + - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest + requestData: "dGVzdCByZXNwb25zZQ==" - request: testName: bidi half duplex stream success service: connectrpc.conformance.v1.ConformanceService From a132b49888724ed5c481b5b3b091cda65a5c1489 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Wed, 31 Jan 2024 21:17:19 -0500 Subject: [PATCH 19/40] Fixes --- internal/app/referenceclient/impl.go | 17 +++++++++- internal/app/referenceclient/wire_tracer.go | 37 +++++++++++++++------ 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/internal/app/referenceclient/impl.go b/internal/app/referenceclient/impl.go index 74040ec3..da7a3fa6 100644 --- a/internal/app/referenceclient/impl.go +++ b/internal/app/referenceclient/impl.go @@ -563,10 +563,25 @@ func (i *invoker) unimplemented( request := connect.NewRequest(ur) internal.AddHeaders(req.RequestHeaders, request.Header()) + ctx, wire := WithWireCapture(ctx) + // Invoke the Unary call _, err := i.client.Unimplemented(ctx, request) + + var actualStatusCode int32 + var connectErrorRaw *structpb.Struct + var actualTrailers []*v1.Header + wireDetails := wire.Get() + if wireDetails != nil { + actualStatusCode = wireDetails.StatusCode + actualTrailers = wireDetails.Trailers + connectErrorRaw = wireDetails.ConnectErrorRaw + } return &v1.ClientResponseResult{ - Error: internal.ConvertErrorToProtoError(err), + Error: internal.ConvertErrorToProtoError(err), + ActualStatusCode: actualStatusCode, + ActualHttpTrailers: actualTrailers, + ConnectErrorRaw: connectErrorRaw, }, nil } diff --git a/internal/app/referenceclient/wire_tracer.go b/internal/app/referenceclient/wire_tracer.go index 3c83f540..9e728c61 100644 --- a/internal/app/referenceclient/wire_tracer.go +++ b/internal/app/referenceclient/wire_tracer.go @@ -19,7 +19,7 @@ import ( "encoding/json" "io" "strings" - "sync/atomic" + "sync" "connectrpc.com/conformance/internal" v1 "connectrpc.com/conformance/internal/gen/proto/go/connectrpc/conformance/v1" @@ -42,24 +42,39 @@ type WireDetails struct { } type WireWrapper struct { - val atomic.Pointer[*WireDetails] + val *WireDetails + mtx sync.Mutex // serialize calls to sendRequest } // Get returns the wire details func (w *WireWrapper) Get() *WireDetails { - respPtr := w.val.Load() - if respPtr == nil { + if w.val == nil { return nil } - return *respPtr + return w.val +} + +// Lock acquires the internal lock for setting the wire details. This should +// always be called before calling Set. +func (w *WireWrapper) Lock() { + w.mtx.Lock() +} + +func (w *WireWrapper) Unlock() { + w.mtx.Unlock() +} + +// Set sets the wire details +func (w *WireWrapper) Set(details *WireDetails) { + w.val = details } // WithWireCapture returns a new context which will contain wire details during // a roundtrip. func WithWireCapture(ctx context.Context) (context.Context, *WireWrapper) { - wrappers := &WireWrapper{} - ctx = context.WithValue(ctx, wireKey{}, wrappers) - return ctx, wrappers + wrapper := &WireWrapper{} + ctx = context.WithValue(ctx, wireKey{}, wrapper) + return ctx, wrapper } type WireTracer struct { @@ -70,7 +85,9 @@ type WireTracer struct { // from the passed trace. The wire details will be stored in the context acquired byte // WithWireCapture and can be retrieved via WireWrapper.Get() func (t *WireTracer) Complete(trace tracer.Trace) { - respWrapper, ok := trace.Request.Context().Value(wireKey{}).(*WireWrapper) + wrapper, ok := trace.Request.Context().Value(wireKey{}).(*WireWrapper) + wrapper.Lock() + defer wrapper.Unlock() if ok { //nolint:nestif if trace.Response != nil { statusCode := int32(trace.Response.StatusCode) @@ -118,7 +135,7 @@ func (t *WireTracer) Complete(trace tracer.Trace) { ConnectErrorRaw: &jsonRaw, } - respWrapper.val.Store(&wire) + wrapper.Set(wire) } } From 411aa136c65ede9fc0344d45731b3c71ce1bd009 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Wed, 31 Jan 2024 21:21:47 -0500 Subject: [PATCH 20/40] Docs --- internal/app/referenceclient/wire_tracer.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/internal/app/referenceclient/wire_tracer.go b/internal/app/referenceclient/wire_tracer.go index 9e728c61..2b596e1e 100644 --- a/internal/app/referenceclient/wire_tracer.go +++ b/internal/app/referenceclient/wire_tracer.go @@ -43,11 +43,13 @@ type WireDetails struct { type WireWrapper struct { val *WireDetails - mtx sync.Mutex // serialize calls to sendRequest + mtx sync.Mutex } -// Get returns the wire details +// Get returns the wire details. func (w *WireWrapper) Get() *WireDetails { + w.mtx.Lock() + defer w.mtx.Unlock() if w.val == nil { return nil } @@ -60,11 +62,13 @@ func (w *WireWrapper) Lock() { w.mtx.Lock() } +// Lock releases the internal lock for setting the wire details. func (w *WireWrapper) Unlock() { w.mtx.Unlock() } -// Set sets the wire details +// Set sets the wire details. Note that calls to Set should always be +// preceded by calls to Lock. func (w *WireWrapper) Set(details *WireDetails) { w.val = details } @@ -83,9 +87,11 @@ type WireTracer struct { // Complete intercepts the Complete call for a tracer, extracting wire details // from the passed trace. The wire details will be stored in the context acquired byte -// WithWireCapture and can be retrieved via WireWrapper.Get() +// WithWireCapture and can be retrieved via WireWrapper.Get(). func (t *WireTracer) Complete(trace tracer.Trace) { wrapper, ok := trace.Request.Context().Value(wireKey{}).(*WireWrapper) + // Lock the mutex on the wrapper so that the client implementation isn't + // reading the wire details before we get a chance to populate them here. wrapper.Lock() defer wrapper.Unlock() if ok { //nolint:nestif From 8c46e10fd287e1f83ad001a78d1ad46f802406ea Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Thu, 1 Feb 2024 13:27:35 -0500 Subject: [PATCH 21/40] Race condition --- internal/app/connectconformance/results.go | 8 + .../connectconformance/test_case_library.go | 2 + .../connectconformance/testsuites/errors.yaml | 94 +++++----- .../connectconformance/testsuites/test.yaml | 26 +++ internal/app/referenceclient/client.go | 6 +- internal/app/referenceclient/impl.go | 28 +-- internal/app/referenceclient/wire_tracer.go | 169 ++++++++---------- 7 files changed, 178 insertions(+), 155 deletions(-) create mode 100644 internal/app/connectconformance/testsuites/test.yaml diff --git a/internal/app/connectconformance/results.go b/internal/app/connectconformance/results.go index 67318988..681f8cca 100644 --- a/internal/app/connectconformance/results.go +++ b/internal/app/connectconformance/results.go @@ -199,6 +199,14 @@ func (r *testResults) assert(testCase string, expected, actual *conformancev1.Cl } } + // If a status code is specified in the expected result, then verify it against the actual status code + if expected.ActualStatusCode != 0 { + diff := cmp.Diff(expected.ActualStatusCode, actual.ActualStatusCode, protocmp.Transform()) + if diff != "" { + errs = append(errs, fmt.Errorf("actual HTTP status code does not match: - wanted, + got\n%s", diff)) + } + } + r.setOutcome(testCase, false, errs.Result()) } diff --git a/internal/app/connectconformance/test_case_library.go b/internal/app/connectconformance/test_case_library.go index 3627b117..2e632808 100644 --- a/internal/app/connectconformance/test_case_library.go +++ b/internal/app/connectconformance/test_case_library.go @@ -600,6 +600,7 @@ func populateExpectedUnaryResponse(testCase *conformancev1.TestCase) error { payload.Data = respType.ResponseData } expected.Payloads = []*conformancev1.ConformancePayload{payload} + expected.ActualStatusCode = 200 default: return fmt.Errorf("provided UnaryRequest.Response has an unexpected type %T", respType) } @@ -643,6 +644,7 @@ func populateExpectedStreamResponse(testCase *conformancev1.TestCase) error { ResponseHeaders: def.ResponseHeaders, ResponseTrailers: def.ResponseTrailers, Error: def.Error, + ActualStatusCode: 200, // status codes should always be 200 for streaming responses (even errors) } // There should be one payload for every ResponseData the client specified diff --git a/internal/app/connectconformance/testsuites/errors.yaml b/internal/app/connectconformance/testsuites/errors.yaml index 501d332d..73175d6e 100644 --- a/internal/app/connectconformance/testsuites/errors.yaml +++ b/internal/app/connectconformance/testsuites/errors.yaml @@ -438,53 +438,53 @@ testCases: - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest requestData: "dGVzdCByZXNwb25zZQ==" # Bidi Stream Tests ----------------------------------------------------------- -- request: - testName: bidi full duplex stream error with responses - service: connectrpc.conformance.v1.ConformanceService - method: BidiStream - streamType: STREAM_TYPE_FULL_DUPLEX_BIDI_STREAM - requestHeaders: - - name: X-Conformance-Test - value: ["Value1","Value2"] - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest - responseDefinition: - responseHeaders: - - name: x-custom-header - value: ["foo"] - responseData: - - "dGVzdCByZXNwb25zZQ==" - - "dGVzdCByZXNwb25zZQ==" - error: - code: 13 - message: "bidi full duplex stream failed" - responseTrailers: - - name: x-custom-trailer - value: ["bing"] - fullDuplex: true - - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest - requestData: "dGVzdCByZXNwb25zZQ==" -- request: - testName: bidi full duplex stream error no responses - service: connectrpc.conformance.v1.ConformanceService - method: BidiStream - streamType: STREAM_TYPE_FULL_DUPLEX_BIDI_STREAM - requestHeaders: - - name: X-Conformance-Test - value: ["Value1","Value2"] - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest - responseDefinition: - responseHeaders: - - name: x-custom-header - value: ["foo"] - error: - code: 13 - message: "bidi full duplex stream failed" - responseTrailers: - - name: x-custom-trailer - value: ["bing"] - fullDuplex: true +# - request: +# testName: bidi full duplex stream error with responses +# service: connectrpc.conformance.v1.ConformanceService +# method: BidiStream +# streamType: STREAM_TYPE_FULL_DUPLEX_BIDI_STREAM +# requestHeaders: +# - name: X-Conformance-Test +# value: ["Value1","Value2"] +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest +# responseDefinition: +# responseHeaders: +# - name: x-custom-header +# value: ["foo"] +# responseData: +# - "dGVzdCByZXNwb25zZQ==" +# - "dGVzdCByZXNwb25zZQ==" +# error: +# code: 13 +# message: "bidi full duplex stream failed" +# responseTrailers: +# - name: x-custom-trailer +# value: ["bing"] +# fullDuplex: true +# - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest +# requestData: "dGVzdCByZXNwb25zZQ==" +# - request: +# testName: bidi full duplex stream error no responses +# service: connectrpc.conformance.v1.ConformanceService +# method: BidiStream +# streamType: STREAM_TYPE_FULL_DUPLEX_BIDI_STREAM +# requestHeaders: +# - name: X-Conformance-Test +# value: ["Value1","Value2"] +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest +# responseDefinition: +# responseHeaders: +# - name: x-custom-header +# value: ["foo"] +# error: +# code: 13 +# message: "bidi full duplex stream failed" +# responseTrailers: +# - name: x-custom-trailer +# value: ["bing"] +# fullDuplex: true - request: testName: bidi half duplex stream error with responses service: connectrpc.conformance.v1.ConformanceService diff --git a/internal/app/connectconformance/testsuites/test.yaml b/internal/app/connectconformance/testsuites/test.yaml new file mode 100644 index 00000000..60a16242 --- /dev/null +++ b/internal/app/connectconformance/testsuites/test.yaml @@ -0,0 +1,26 @@ +name: Test +relevantCompressions: + - COMPRESSION_IDENTITY +relevantCodecs: + - CODEC_JSON + - CODEC_PROTO +testCases: +# Unary Tests ----------------------------------------------------------------- +- request: + testName: unary success + service: connectrpc.conformance.v1.ConformanceService + method: Unary + streamType: STREAM_TYPE_UNARY + requestHeaders: + - name: X-Conformance-Test + value: ["Value1","Value2"] + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + responseHeaders: + - name: x-custom-header + value: ["foo"] + responseData: "dGVzdCByZXNwb25zZQ==" + responseTrailers: + - name: x-custom-trailer + value: ["bing"] diff --git a/internal/app/referenceclient/client.go b/internal/app/referenceclient/client.go index 4faf6d24..efd934ed 100644 --- a/internal/app/referenceclient/client.go +++ b/internal/app/referenceclient/client.go @@ -217,10 +217,12 @@ func invoke(ctx context.Context, req *v1.ClientCompatRequest, trace *tracer.Trac return nil, errors.New("an HTTP version must be specified") } - // Create a new TracingRoundTripper with our WireTracer so that the tests can trace values on the + // Create a new TracingRoundTripper with our wireTracer so that the tests can trace values on the // wire. Note that 'trace' could be nil, in which case, any error traces will // simply not be printed. - transport = tracer.TracingRoundTripper(transport, NewWireTracer(trace)) + transport = tracer.TracingRoundTripper(transport, &wireTracer{ + tracer: trace, + }) if req.RawRequest != nil { transport = &rawRequestSender{transport: transport, rawRequest: req.RawRequest} diff --git a/internal/app/referenceclient/impl.go b/internal/app/referenceclient/impl.go index da7a3fa6..f3dca375 100644 --- a/internal/app/referenceclient/impl.go +++ b/internal/app/referenceclient/impl.go @@ -21,6 +21,7 @@ import ( "io" "net/http" "net/url" + "os" "time" "connectrpc.com/conformance/internal" @@ -118,7 +119,7 @@ func (i *invoker) unary( var trailers []*v1.Header payloads := make([]*v1.ConformancePayload, 0, 1) - ctx, wire := WithWireCapture(ctx) + ctx = withWireCapture(ctx) // Invoke the Unary call resp, err := i.client.Unary(ctx, request) @@ -126,11 +127,14 @@ func (i *invoker) unary( var actualStatusCode int32 var connectErrorRaw *structpb.Struct var actualTrailers []*v1.Header - wireDetails := wire.Get() + fmt.Fprintf(os.Stderr, "Getting wire details in client impl for %s\n\n", req.TestName) + wireDetails := getWireDetails(ctx) if wireDetails != nil { actualStatusCode = wireDetails.StatusCode actualTrailers = wireDetails.Trailers connectErrorRaw = wireDetails.ConnectErrorRaw + } else { + fmt.Fprintf(os.Stderr, "Wire details for %s are NIL!\n\n", req.TestName) } if err != nil { @@ -184,7 +188,7 @@ func (i *invoker) idempotentUnary( var trailers []*v1.Header payloads := make([]*v1.ConformancePayload, 0, 1) - ctx, wire := WithWireCapture(ctx) + ctx = withWireCapture(ctx) // Invoke the Unary call resp, err := i.client.IdempotentUnary(ctx, request) @@ -192,7 +196,7 @@ func (i *invoker) idempotentUnary( var actualStatusCode int32 var connectErrorRaw *structpb.Struct var actualTrailers []*v1.Header - wireDetails := wire.Get() + wireDetails := getWireDetails(ctx) if wireDetails != nil { actualStatusCode = wireDetails.StatusCode actualTrailers = wireDetails.Trailers @@ -243,7 +247,7 @@ func (i *invoker) serverStream( // Add the specified request headers to the request internal.AddHeaders(req.RequestHeaders, request.Header()) - ctx, wire := WithWireCapture(ctx) + ctx = withWireCapture(ctx) stream, err := i.client.ServerStream(ctx, request) if err != nil { @@ -308,7 +312,7 @@ func (i *invoker) serverStream( var actualStatusCode int32 var connectErrorRaw *structpb.Struct var actualTrailers []*v1.Header - wireDetails := wire.Get() + wireDetails := getWireDetails(ctx) if wireDetails != nil { actualStatusCode = wireDetails.StatusCode actualTrailers = wireDetails.Trailers @@ -333,7 +337,7 @@ func (i *invoker) clientStream( ctx, cancel := context.WithCancel(ctx) defer cancel() - ctx, wire := WithWireCapture(ctx) + ctx = withWireCapture(ctx) stream := i.client.ClientStream(ctx) // Add the specified request headers to the request @@ -389,7 +393,7 @@ func (i *invoker) clientStream( var actualStatusCode int32 var connectErrorRaw *structpb.Struct var actualTrailers []*v1.Header - wireDetails := wire.Get() + wireDetails := getWireDetails(ctx) if wireDetails != nil { actualStatusCode = wireDetails.StatusCode actualTrailers = wireDetails.Trailers @@ -418,7 +422,7 @@ func (i *invoker) bidiStream( ConnectErrorRaw: nil, // TODO } - ctx, wire := WithWireCapture(ctx) + ctx = withWireCapture(ctx) stream := i.client.BidiStream(ctx) defer func() { @@ -528,7 +532,7 @@ func (i *invoker) bidiStream( var actualStatusCode int32 var connectErrorRaw *structpb.Struct var actualTrailers []*v1.Header - wireDetails := wire.Get() + wireDetails := getWireDetails(ctx) if wireDetails != nil { actualStatusCode = wireDetails.StatusCode actualTrailers = wireDetails.Trailers @@ -563,7 +567,7 @@ func (i *invoker) unimplemented( request := connect.NewRequest(ur) internal.AddHeaders(req.RequestHeaders, request.Header()) - ctx, wire := WithWireCapture(ctx) + ctx = withWireCapture(ctx) // Invoke the Unary call _, err := i.client.Unimplemented(ctx, request) @@ -571,7 +575,7 @@ func (i *invoker) unimplemented( var actualStatusCode int32 var connectErrorRaw *structpb.Struct var actualTrailers []*v1.Header - wireDetails := wire.Get() + wireDetails := getWireDetails(ctx) if wireDetails != nil { actualStatusCode = wireDetails.StatusCode actualTrailers = wireDetails.Trailers diff --git a/internal/app/referenceclient/wire_tracer.go b/internal/app/referenceclient/wire_tracer.go index 2b596e1e..efc593c5 100644 --- a/internal/app/referenceclient/wire_tracer.go +++ b/internal/app/referenceclient/wire_tracer.go @@ -17,9 +17,11 @@ package referenceclient import ( "context" "encoding/json" + "fmt" "io" + "os" "strings" - "sync" + "sync/atomic" "connectrpc.com/conformance/internal" v1 "connectrpc.com/conformance/internal/gen/proto/go/connectrpc/conformance/v1" @@ -28,10 +30,11 @@ import ( "google.golang.org/protobuf/types/known/structpb" ) -type wireKey struct{} +// The key associated with the wire details information stored in context. +type wireCtxKey struct{} -// WireDetails encapsulates the wire details to track for a roundtrip. -type WireDetails struct { +// wireDetails encapsulates the wire details to track for a roundtrip. +type wireDetails struct { // The actual HTTP status code observed. StatusCode int32 // The actual trailers observed. @@ -41,119 +44,97 @@ type WireDetails struct { ConnectErrorRaw *structpb.Struct } -type WireWrapper struct { - val *WireDetails - mtx sync.Mutex +type wireWrapper struct { + val atomic.Pointer[wireDetails] } -// Get returns the wire details. -func (w *WireWrapper) Get() *WireDetails { - w.mtx.Lock() - defer w.mtx.Unlock() - if w.val == nil { - return nil - } - return w.val -} - -// Lock acquires the internal lock for setting the wire details. This should -// always be called before calling Set. -func (w *WireWrapper) Lock() { - w.mtx.Lock() -} - -// Lock releases the internal lock for setting the wire details. -func (w *WireWrapper) Unlock() { - w.mtx.Unlock() +// withWireCapture returns a new context which will contain wire details during +// a roundtrip. +func withWireCapture(ctx context.Context) context.Context { + return context.WithValue(ctx, wireCtxKey{}, &wireWrapper{}) } -// Set sets the wire details. Note that calls to Set should always be -// preceded by calls to Lock. -func (w *WireWrapper) Set(details *WireDetails) { - w.val = details +// setWireDetails sets the given wire details in the given context. +func setWireDetails(ctx context.Context, details *wireDetails) { + wrapper, ok := ctx.Value(wireCtxKey{}).(*wireWrapper) + if !ok { + return + } + wrapper.val.Store(details) } -// WithWireCapture returns a new context which will contain wire details during -// a roundtrip. -func WithWireCapture(ctx context.Context) (context.Context, *WireWrapper) { - wrapper := &WireWrapper{} - ctx = context.WithValue(ctx, wireKey{}, wrapper) - return ctx, wrapper +func getWireDetails(ctx context.Context) *wireDetails { + wrapper, ok := ctx.Value(wireCtxKey{}).(*wireWrapper) + if !ok { + return nil + } + ptr := wrapper.val.Load() + if ptr == nil { + return nil + } + return ptr } -type WireTracer struct { +type wireTracer struct { tracer *tracer.Tracer } // Complete intercepts the Complete call for a tracer, extracting wire details -// from the passed trace. The wire details will be stored in the context acquired byte -// WithWireCapture and can be retrieved via WireWrapper.Get(). -func (t *WireTracer) Complete(trace tracer.Trace) { - wrapper, ok := trace.Request.Context().Value(wireKey{}).(*WireWrapper) - // Lock the mutex on the wrapper so that the client implementation isn't - // reading the wire details before we get a chance to populate them here. - wrapper.Lock() - defer wrapper.Unlock() - if ok { //nolint:nestif - if trace.Response != nil { - statusCode := int32(trace.Response.StatusCode) - - var jsonRaw structpb.Struct - contentType := trace.Response.Header.Get("content-type") - if contentType == "application/json" { - if statusCode != 200 { - // If this is a unary request, then use the entire response body - // as the wire error details. - body, err := io.ReadAll(trace.Response.Body) - if err != nil { +// from the passed trace. The wire details will be stored in the context acquired by +// withWireCapture and can be retrieved via getWireDetails. +func (t *wireTracer) Complete(trace tracer.Trace) { + if trace.Response != nil { //nolint:nestif + statusCode := int32(trace.Response.StatusCode) + + var jsonRaw structpb.Struct + contentType := trace.Response.Header.Get("content-type") + if contentType == "application/json" { + if statusCode != 200 { + // If this is a unary request, then use the entire response body + // as the wire error details. + body, err := io.ReadAll(trace.Response.Body) + if err != nil { + return + } + if err := protojson.Unmarshal(body, &jsonRaw); err != nil { + return + } + } + } else if strings.HasPrefix(contentType, "application/connect+") { + type endStreamError struct { + Error json.RawMessage `json:"error"` + } + // If this is a streaming request, then look through the trace events + // for the ResponseBodyEndStream event and parse its content into an + // endStreamError to see if there are any error details. + for _, ev := range trace.Events { + switch eventType := ev.(type) { + case *tracer.ResponseBodyEndStream: + var endStream endStreamError + if err := json.Unmarshal([]byte(eventType.Content), &endStream); err != nil { return } - if err := protojson.Unmarshal(body, &jsonRaw); err != nil { + if err := protojson.Unmarshal(endStream.Error, &jsonRaw); err != nil { return } - } - } else if strings.HasPrefix(contentType, "application/connect+") { - type endStreamError struct { - Error json.RawMessage `json:"error"` - } - // If this is a streaming request, then look through the trace events - // for the ResponseBodyEndStream event and parse its content into an - // endStreamError to see if there are any error details. - for _, ev := range trace.Events { - switch eventType := ev.(type) { - case *tracer.ResponseBodyEndStream: - var endStream endStreamError - if err := json.Unmarshal([]byte(eventType.Content), &endStream); err != nil { - return - } - if err := protojson.Unmarshal(endStream.Error, &jsonRaw); err != nil { - return - } - default: - // Do nothing - } + default: + // Do nothing } } + } - wire := &WireDetails{ - StatusCode: statusCode, - Trailers: internal.ConvertToProtoHeader(trace.Response.Trailer), - ConnectErrorRaw: &jsonRaw, - } - - wrapper.Set(wire) + wire := &wireDetails{ + StatusCode: statusCode, + Trailers: internal.ConvertToProtoHeader(trace.Response.Trailer), + ConnectErrorRaw: &jsonRaw, } + + fmt.Fprintf(os.Stderr, "Setting wire details for %s\n\n", trace.TestName) + setWireDetails(trace.Request.Context(), wire) + fmt.Fprintf(os.Stderr, "SET wire details for %s\n\n", trace.TestName) } if t != nil { t.tracer.Complete(trace) } } - -// NewWireTracer returns a new wire tracer with the given tracer. -// If trace is nil, all tracer operations will be bypassed. -func NewWireTracer(trace *tracer.Tracer) *WireTracer { - return &WireTracer{ - tracer: trace, - } -} From b47a71077ff887dad9b6c1dd97689d918666b78e Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Thu, 1 Feb 2024 13:29:48 -0500 Subject: [PATCH 22/40] Revert --- .../connectconformance/test_case_library.go | 1 - .../connectconformance/testsuites/errors.yaml | 97 +++++++------------ 2 files changed, 36 insertions(+), 62 deletions(-) diff --git a/internal/app/connectconformance/test_case_library.go b/internal/app/connectconformance/test_case_library.go index 2e632808..05a54f95 100644 --- a/internal/app/connectconformance/test_case_library.go +++ b/internal/app/connectconformance/test_case_library.go @@ -644,7 +644,6 @@ func populateExpectedStreamResponse(testCase *conformancev1.TestCase) error { ResponseHeaders: def.ResponseHeaders, ResponseTrailers: def.ResponseTrailers, Error: def.Error, - ActualStatusCode: 200, // status codes should always be 200 for streaming responses (even errors) } // There should be one payload for every ResponseData the client specified diff --git a/internal/app/connectconformance/testsuites/errors.yaml b/internal/app/connectconformance/testsuites/errors.yaml index 73175d6e..8a3123db 100644 --- a/internal/app/connectconformance/testsuites/errors.yaml +++ b/internal/app/connectconformance/testsuites/errors.yaml @@ -412,79 +412,54 @@ testCases: responseTrailers: - name: x-custom-trailer value: ["bing"] -# Client Stream Tests ----------------------------------------------------------- +# Bidi Stream Tests ----------------------------------------------------------- - request: - testName: client stream error one request + testName: bidi full duplex stream error with responses service: connectrpc.conformance.v1.ConformanceService - method: ClientStream - streamType: STREAM_TYPE_CLIENT_STREAM + method: BidiStream + streamType: STREAM_TYPE_FULL_DUPLEX_BIDI_STREAM + requestHeaders: + - name: X-Conformance-Test + value: ["Value1","Value2"] requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest + - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest responseDefinition: + responseHeaders: + - name: x-custom-header + value: ["foo"] + responseData: + - "dGVzdCByZXNwb25zZQ==" + - "dGVzdCByZXNwb25zZQ==" error: code: 13 - message: "client stream failed" + message: "bidi full duplex stream failed" + responseTrailers: + - name: x-custom-trailer + value: ["bing"] + fullDuplex: true + - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest + requestData: "dGVzdCByZXNwb25zZQ==" - request: - testName: client stream error multiple requests + testName: bidi full duplex stream error no responses service: connectrpc.conformance.v1.ConformanceService - method: ClientStream - streamType: STREAM_TYPE_CLIENT_STREAM + method: BidiStream + streamType: STREAM_TYPE_FULL_DUPLEX_BIDI_STREAM + requestHeaders: + - name: X-Conformance-Test + value: ["Value1","Value2"] requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest + - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest responseDefinition: + responseHeaders: + - name: x-custom-header + value: ["foo"] error: code: 13 - message: "client stream failed" - - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest - requestData: "dGVzdCByZXNwb25zZQ==" -# Bidi Stream Tests ----------------------------------------------------------- -# - request: -# testName: bidi full duplex stream error with responses -# service: connectrpc.conformance.v1.ConformanceService -# method: BidiStream -# streamType: STREAM_TYPE_FULL_DUPLEX_BIDI_STREAM -# requestHeaders: -# - name: X-Conformance-Test -# value: ["Value1","Value2"] -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest -# responseDefinition: -# responseHeaders: -# - name: x-custom-header -# value: ["foo"] -# responseData: -# - "dGVzdCByZXNwb25zZQ==" -# - "dGVzdCByZXNwb25zZQ==" -# error: -# code: 13 -# message: "bidi full duplex stream failed" -# responseTrailers: -# - name: x-custom-trailer -# value: ["bing"] -# fullDuplex: true -# - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest -# requestData: "dGVzdCByZXNwb25zZQ==" -# - request: -# testName: bidi full duplex stream error no responses -# service: connectrpc.conformance.v1.ConformanceService -# method: BidiStream -# streamType: STREAM_TYPE_FULL_DUPLEX_BIDI_STREAM -# requestHeaders: -# - name: X-Conformance-Test -# value: ["Value1","Value2"] -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest -# responseDefinition: -# responseHeaders: -# - name: x-custom-header -# value: ["foo"] -# error: -# code: 13 -# message: "bidi full duplex stream failed" -# responseTrailers: -# - name: x-custom-trailer -# value: ["bing"] -# fullDuplex: true + message: "bidi full duplex stream failed" + responseTrailers: + - name: x-custom-trailer + value: ["bing"] + fullDuplex: true - request: testName: bidi half duplex stream error with responses service: connectrpc.conformance.v1.ConformanceService From 57d8635accccbd1ebd3f26aece82830acb9be7a6 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Thu, 1 Feb 2024 16:08:08 -0500 Subject: [PATCH 23/40] Tracer --- .../connectconformance/test_case_library.go | 3 +- .../connectconformance/testsuites/test.yaml | 2 + internal/app/referenceclient/impl.go | 38 +++---- internal/app/referenceclient/middleware.txt | 51 +++++++++ internal/app/referenceclient/wire_tracer.go | 106 +++++++++--------- internal/tracer/middleware.go | 22 +--- 6 files changed, 130 insertions(+), 92 deletions(-) create mode 100644 internal/app/referenceclient/middleware.txt diff --git a/internal/app/connectconformance/test_case_library.go b/internal/app/connectconformance/test_case_library.go index 05a54f95..eb4e85f7 100644 --- a/internal/app/connectconformance/test_case_library.go +++ b/internal/app/connectconformance/test_case_library.go @@ -600,7 +600,7 @@ func populateExpectedUnaryResponse(testCase *conformancev1.TestCase) error { payload.Data = respType.ResponseData } expected.Payloads = []*conformancev1.ConformancePayload{payload} - expected.ActualStatusCode = 200 + // expected.ActualStatusCode = 200 default: return fmt.Errorf("provided UnaryRequest.Response has an unexpected type %T", respType) } @@ -644,6 +644,7 @@ func populateExpectedStreamResponse(testCase *conformancev1.TestCase) error { ResponseHeaders: def.ResponseHeaders, ResponseTrailers: def.ResponseTrailers, Error: def.Error, + ActualStatusCode: 200, // status codes should always be 200 for streaming responses (even errors) } // There should be one payload for every ResponseData the client specified diff --git a/internal/app/connectconformance/testsuites/test.yaml b/internal/app/connectconformance/testsuites/test.yaml index 60a16242..c3a73c95 100644 --- a/internal/app/connectconformance/testsuites/test.yaml +++ b/internal/app/connectconformance/testsuites/test.yaml @@ -1,6 +1,8 @@ name: Test relevantCompressions: - COMPRESSION_IDENTITY +relevantProtocols: + - PROTOCOL_GRPC relevantCodecs: - CODEC_JSON - CODEC_PROTO diff --git a/internal/app/referenceclient/impl.go b/internal/app/referenceclient/impl.go index f3dca375..1b4372b9 100644 --- a/internal/app/referenceclient/impl.go +++ b/internal/app/referenceclient/impl.go @@ -21,7 +21,6 @@ import ( "io" "net/http" "net/url" - "os" "time" "connectrpc.com/conformance/internal" @@ -119,23 +118,20 @@ func (i *invoker) unary( var trailers []*v1.Header payloads := make([]*v1.ConformancePayload, 0, 1) - ctx = withWireCapture(ctx) + // ctx = withWireCapture(ctx) // Invoke the Unary call resp, err := i.client.Unary(ctx, request) - var actualStatusCode int32 - var connectErrorRaw *structpb.Struct - var actualTrailers []*v1.Header - fmt.Fprintf(os.Stderr, "Getting wire details in client impl for %s\n\n", req.TestName) - wireDetails := getWireDetails(ctx) - if wireDetails != nil { - actualStatusCode = wireDetails.StatusCode - actualTrailers = wireDetails.Trailers - connectErrorRaw = wireDetails.ConnectErrorRaw - } else { - fmt.Fprintf(os.Stderr, "Wire details for %s are NIL!\n\n", req.TestName) - } + // var actualStatusCode int32 + // var connectErrorRaw *structpb.Struct + // var actualTrailers []*v1.Header + // wireDetails := getWireDetails(ctx) + // if wireDetails != nil { + // actualStatusCode = wireDetails.StatusCode + // actualTrailers = wireDetails.Trailers + // connectErrorRaw = wireDetails.ConnectErrorRaw + // } if err != nil { // If an error was returned, first convert it to a Connect error @@ -155,13 +151,13 @@ func (i *invoker) unary( } return &v1.ClientResponseResult{ - ResponseHeaders: headers, - ResponseTrailers: trailers, - Payloads: payloads, - Error: protoErr, - ActualStatusCode: actualStatusCode, - ActualHttpTrailers: actualTrailers, - ConnectErrorRaw: connectErrorRaw, + ResponseHeaders: headers, + ResponseTrailers: trailers, + Payloads: payloads, + Error: protoErr, + // ActualStatusCode: actualStatusCode, + // ActualHttpTrailers: actualTrailers, + // ConnectErrorRaw: connectErrorRaw, }, nil } diff --git a/internal/app/referenceclient/middleware.txt b/internal/app/referenceclient/middleware.txt new file mode 100644 index 00000000..9ff6b6b7 --- /dev/null +++ b/internal/app/referenceclient/middleware.txt @@ -0,0 +1,51 @@ +// Copyright 2023-2024 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package referenceclient + +import ( + "io" + "net/http" +) + +type WireInterceptor struct { + Transport http.RoundTripper +} + +func (w *WireInterceptor) RoundTrip(req *http.Request) (*http.Response, error) { + resp, err := w.Transport.RoundTrip(req) + wrapper, ok := req.Context().Value(wireCtxKey{}).(*wireWrapper) + if err != nil || !ok { + return resp, err + } + resp.Body = &capture{r: resp.Body, resp: resp, wrapper: wrapper} + return resp, nil +} + +type capture struct { + r io.ReadCloser + resp *http.Response + wrapper *wireWrapper +} + +func (c *capture) Read(p []byte) (int, error) { + // Capture bytes as they are read + c.wrapper.buf.Write(p) + + return c.r.Read(p) +} + +func (c *capture) Close() error { + return c.r.Close() +} diff --git a/internal/app/referenceclient/wire_tracer.go b/internal/app/referenceclient/wire_tracer.go index efc593c5..e8a3d241 100644 --- a/internal/app/referenceclient/wire_tracer.go +++ b/internal/app/referenceclient/wire_tracer.go @@ -15,18 +15,12 @@ package referenceclient import ( + "bytes" "context" - "encoding/json" - "fmt" - "io" - "os" - "strings" "sync/atomic" - "connectrpc.com/conformance/internal" v1 "connectrpc.com/conformance/internal/gen/proto/go/connectrpc/conformance/v1" "connectrpc.com/conformance/internal/tracer" - "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/types/known/structpb" ) @@ -46,6 +40,7 @@ type wireDetails struct { type wireWrapper struct { val atomic.Pointer[wireDetails] + buf *bytes.Buffer } // withWireCapture returns a new context which will contain wire details during @@ -83,56 +78,59 @@ type wireTracer struct { // from the passed trace. The wire details will be stored in the context acquired by // withWireCapture and can be retrieved via getWireDetails. func (t *wireTracer) Complete(trace tracer.Trace) { - if trace.Response != nil { //nolint:nestif - statusCode := int32(trace.Response.StatusCode) + // wrapper, ok := trace.Request.Context().Value(wireCtxKey{}).(*wireWrapper) + // if ok { + // if trace.Response != nil { //nolint:nestif + // statusCode := int32(trace.Response.StatusCode) - var jsonRaw structpb.Struct - contentType := trace.Response.Header.Get("content-type") - if contentType == "application/json" { - if statusCode != 200 { - // If this is a unary request, then use the entire response body - // as the wire error details. - body, err := io.ReadAll(trace.Response.Body) - if err != nil { - return - } - if err := protojson.Unmarshal(body, &jsonRaw); err != nil { - return - } - } - } else if strings.HasPrefix(contentType, "application/connect+") { - type endStreamError struct { - Error json.RawMessage `json:"error"` - } - // If this is a streaming request, then look through the trace events - // for the ResponseBodyEndStream event and parse its content into an - // endStreamError to see if there are any error details. - for _, ev := range trace.Events { - switch eventType := ev.(type) { - case *tracer.ResponseBodyEndStream: - var endStream endStreamError - if err := json.Unmarshal([]byte(eventType.Content), &endStream); err != nil { - return - } - if err := protojson.Unmarshal(endStream.Error, &jsonRaw); err != nil { - return - } - default: - // Do nothing - } - } - } + // var jsonRaw structpb.Struct + // contentType := trace.Response.Header.Get("content-type") + // if contentType == "application/json" { + // if statusCode != 200 { + // // If this is a unary request, then use the entire response body + // // as the wire error details. + // body, err := io.ReadAll(wrapper.buf) + // if err != nil { + // fmt.Fprintf(os.Stderr, "bang gang\n\n", err) + // return + // } + // if err := protojson.Unmarshal(body, &jsonRaw); err != nil { + // fmt.Fprintf(os.Stderr, "bang gang2\n\n", err) + // return + // } + // } + // } else if strings.HasPrefix(contentType, "application/connect+") { + // type endStreamError struct { + // Error json.RawMessage `json:"error"` + // } + // // If this is a streaming request, then look through the trace events + // // for the ResponseBodyEndStream event and parse its content into an + // // endStreamError to see if there are any error details. + // for _, ev := range trace.Events { + // switch eventType := ev.(type) { + // case *tracer.ResponseBodyEndStream: + // var endStream endStreamError + // if err := json.Unmarshal([]byte(eventType.Content), &endStream); err != nil { + // return + // } + // if err := protojson.Unmarshal(endStream.Error, &jsonRaw); err != nil { + // return + // } + // default: + // // Do nothing + // } + // } + // } - wire := &wireDetails{ - StatusCode: statusCode, - Trailers: internal.ConvertToProtoHeader(trace.Response.Trailer), - ConnectErrorRaw: &jsonRaw, - } + // wire := &wireDetails{ + // StatusCode: statusCode, + // Trailers: internal.ConvertToProtoHeader(trace.Response.Trailer), + // ConnectErrorRaw: &jsonRaw, + // } - fmt.Fprintf(os.Stderr, "Setting wire details for %s\n\n", trace.TestName) - setWireDetails(trace.Request.Context(), wire) - fmt.Fprintf(os.Stderr, "SET wire details for %s\n\n", trace.TestName) - } + // setWireDetails(trace.Request.Context(), wire) + // } + // } if t != nil { t.tracer.Complete(trace) diff --git a/internal/tracer/middleware.go b/internal/tracer/middleware.go index 451b2c6f..26250a77 100644 --- a/internal/tracer/middleware.go +++ b/internal/tracer/middleware.go @@ -20,6 +20,7 @@ import ( "fmt" "io" "net/http" + "os" "strconv" "strings" ) @@ -28,9 +29,9 @@ import ( // round tripper will record traces of all operations to the given tracer. func TracingRoundTripper(transport http.RoundTripper, collector Collector) http.RoundTripper { return roundTripperFunc(func(req *http.Request) (*http.Response, error) { + fmt.Fprintf(os.Stderr, "roundtrip start") builder := newBuilder(req, collector) ctx, cancel := context.WithCancel(req.Context()) - defer cancel() go func() { <-ctx.Done() builder.add(&RequestCanceled{}) @@ -40,24 +41,13 @@ func TracingRoundTripper(transport http.RoundTripper, collector Collector) http. resp, err := transport.RoundTrip(req) if err != nil { builder.add(&ResponseError{Err: err}) + cancel() return nil, err } - - defer resp.Body.Close() - // Read the response body in full and then replace it with a new in-memory reader - // so that we can read the contents multiple times in any trace or middleware. - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - resp.Body = io.NopCloser(bytes.NewReader(body)) - builder.add(&ResponseStart{ - Response: resp, - }) - + builder.add(&ResponseStart{Response: resp}) respClone := *resp - respClone.Body = newReader(resp.Header, io.NopCloser(bytes.NewReader(body)), false, builder, cancel) - + respClone.Body = newReader(resp.Header, resp.Body, false, builder, cancel) + fmt.Fprintf(os.Stderr, "roundtrip end") return &respClone, nil }) } From d53c5e4b268abe38d6fa0c2d01e2e78042927567 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Fri, 2 Feb 2024 10:21:13 -0500 Subject: [PATCH 24/40] Tests --- .../connectconformance/test_case_library.go | 2 +- .../{middleware.txt => middleware.go} | 0 internal/app/referenceclient/wire_tracer.go | 103 +++++++++--------- internal/tracer/middleware.go | 3 - 4 files changed, 54 insertions(+), 54 deletions(-) rename internal/app/referenceclient/{middleware.txt => middleware.go} (100%) diff --git a/internal/app/connectconformance/test_case_library.go b/internal/app/connectconformance/test_case_library.go index eb4e85f7..2e632808 100644 --- a/internal/app/connectconformance/test_case_library.go +++ b/internal/app/connectconformance/test_case_library.go @@ -600,7 +600,7 @@ func populateExpectedUnaryResponse(testCase *conformancev1.TestCase) error { payload.Data = respType.ResponseData } expected.Payloads = []*conformancev1.ConformancePayload{payload} - // expected.ActualStatusCode = 200 + expected.ActualStatusCode = 200 default: return fmt.Errorf("provided UnaryRequest.Response has an unexpected type %T", respType) } diff --git a/internal/app/referenceclient/middleware.txt b/internal/app/referenceclient/middleware.go similarity index 100% rename from internal/app/referenceclient/middleware.txt rename to internal/app/referenceclient/middleware.go diff --git a/internal/app/referenceclient/wire_tracer.go b/internal/app/referenceclient/wire_tracer.go index e8a3d241..501cb05a 100644 --- a/internal/app/referenceclient/wire_tracer.go +++ b/internal/app/referenceclient/wire_tracer.go @@ -17,10 +17,15 @@ package referenceclient import ( "bytes" "context" + "encoding/json" + "io" + "strings" "sync/atomic" + "connectrpc.com/conformance/internal" v1 "connectrpc.com/conformance/internal/gen/proto/go/connectrpc/conformance/v1" "connectrpc.com/conformance/internal/tracer" + "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/types/known/structpb" ) @@ -78,59 +83,57 @@ type wireTracer struct { // from the passed trace. The wire details will be stored in the context acquired by // withWireCapture and can be retrieved via getWireDetails. func (t *wireTracer) Complete(trace tracer.Trace) { - // wrapper, ok := trace.Request.Context().Value(wireCtxKey{}).(*wireWrapper) - // if ok { - // if trace.Response != nil { //nolint:nestif - // statusCode := int32(trace.Response.StatusCode) + wrapper, ok := trace.Request.Context().Value(wireCtxKey{}).(*wireWrapper) + if ok { + if trace.Response != nil { //nolint:nestif + statusCode := int32(trace.Response.StatusCode) - // var jsonRaw structpb.Struct - // contentType := trace.Response.Header.Get("content-type") - // if contentType == "application/json" { - // if statusCode != 200 { - // // If this is a unary request, then use the entire response body - // // as the wire error details. - // body, err := io.ReadAll(wrapper.buf) - // if err != nil { - // fmt.Fprintf(os.Stderr, "bang gang\n\n", err) - // return - // } - // if err := protojson.Unmarshal(body, &jsonRaw); err != nil { - // fmt.Fprintf(os.Stderr, "bang gang2\n\n", err) - // return - // } - // } - // } else if strings.HasPrefix(contentType, "application/connect+") { - // type endStreamError struct { - // Error json.RawMessage `json:"error"` - // } - // // If this is a streaming request, then look through the trace events - // // for the ResponseBodyEndStream event and parse its content into an - // // endStreamError to see if there are any error details. - // for _, ev := range trace.Events { - // switch eventType := ev.(type) { - // case *tracer.ResponseBodyEndStream: - // var endStream endStreamError - // if err := json.Unmarshal([]byte(eventType.Content), &endStream); err != nil { - // return - // } - // if err := protojson.Unmarshal(endStream.Error, &jsonRaw); err != nil { - // return - // } - // default: - // // Do nothing - // } - // } - // } + var jsonRaw structpb.Struct + contentType := trace.Response.Header.Get("content-type") + if contentType == "application/json" { + if statusCode != 200 { + // If this is a unary request, then use the entire response body + // as the wire error details. + body, err := io.ReadAll(wrapper.buf) + if err != nil { + return + } + if err := protojson.Unmarshal(body, &jsonRaw); err != nil { + return + } + } + } else if strings.HasPrefix(contentType, "application/connect+") { + type endStreamError struct { + Error json.RawMessage `json:"error"` + } + // If this is a streaming request, then look through the trace events + // for the ResponseBodyEndStream event and parse its content into an + // endStreamError to see if there are any error details. + for _, ev := range trace.Events { + switch eventType := ev.(type) { + case *tracer.ResponseBodyEndStream: + var endStream endStreamError + if err := json.Unmarshal([]byte(eventType.Content), &endStream); err != nil { + return + } + if err := protojson.Unmarshal(endStream.Error, &jsonRaw); err != nil { + return + } + default: + // Do nothing + } + } + } - // wire := &wireDetails{ - // StatusCode: statusCode, - // Trailers: internal.ConvertToProtoHeader(trace.Response.Trailer), - // ConnectErrorRaw: &jsonRaw, - // } + wire := &wireDetails{ + StatusCode: statusCode, + Trailers: internal.ConvertToProtoHeader(trace.Response.Trailer), + ConnectErrorRaw: &jsonRaw, + } - // setWireDetails(trace.Request.Context(), wire) - // } - // } + setWireDetails(trace.Request.Context(), wire) + } + } if t != nil { t.tracer.Complete(trace) diff --git a/internal/tracer/middleware.go b/internal/tracer/middleware.go index 26250a77..8b8c95d8 100644 --- a/internal/tracer/middleware.go +++ b/internal/tracer/middleware.go @@ -20,7 +20,6 @@ import ( "fmt" "io" "net/http" - "os" "strconv" "strings" ) @@ -29,7 +28,6 @@ import ( // round tripper will record traces of all operations to the given tracer. func TracingRoundTripper(transport http.RoundTripper, collector Collector) http.RoundTripper { return roundTripperFunc(func(req *http.Request) (*http.Response, error) { - fmt.Fprintf(os.Stderr, "roundtrip start") builder := newBuilder(req, collector) ctx, cancel := context.WithCancel(req.Context()) go func() { @@ -47,7 +45,6 @@ func TracingRoundTripper(transport http.RoundTripper, collector Collector) http. builder.add(&ResponseStart{Response: resp}) respClone := *resp respClone.Body = newReader(resp.Header, resp.Body, false, builder, cancel) - fmt.Fprintf(os.Stderr, "roundtrip end") return &respClone, nil }) } From 25337a767d755f8ed825bc1c334c2c89f5e89a59 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Fri, 2 Feb 2024 11:19:05 -0500 Subject: [PATCH 25/40] Tracer --- .../connectconformance/testsuites/test.yaml | 3 +- internal/app/referenceclient/client.go | 7 +++- internal/app/referenceclient/impl.go | 34 +++++++++---------- internal/app/referenceclient/wire_tracer.go | 7 +++- internal/tracer/middleware.go | 2 ++ 5 files changed, 32 insertions(+), 21 deletions(-) diff --git a/internal/app/connectconformance/testsuites/test.yaml b/internal/app/connectconformance/testsuites/test.yaml index c3a73c95..4622f02b 100644 --- a/internal/app/connectconformance/testsuites/test.yaml +++ b/internal/app/connectconformance/testsuites/test.yaml @@ -2,10 +2,9 @@ name: Test relevantCompressions: - COMPRESSION_IDENTITY relevantProtocols: - - PROTOCOL_GRPC + - PROTOCOL_CONNECT relevantCodecs: - CODEC_JSON - - CODEC_PROTO testCases: # Unary Tests ----------------------------------------------------------------- - request: diff --git a/internal/app/referenceclient/client.go b/internal/app/referenceclient/client.go index efd934ed..1c7c5127 100644 --- a/internal/app/referenceclient/client.go +++ b/internal/app/referenceclient/client.go @@ -220,7 +220,12 @@ func invoke(ctx context.Context, req *v1.ClientCompatRequest, trace *tracer.Trac // Create a new TracingRoundTripper with our wireTracer so that the tests can trace values on the // wire. Note that 'trace' could be nil, in which case, any error traces will // simply not be printed. - transport = tracer.TracingRoundTripper(transport, &wireTracer{ + // transport = &WireInterceptor{ + // Transport: tracer.TracingRoundTripper(transport, &wireTracer{ + // tracer: trace, + // }), + // } + transport = tracer.TracingRoundTripper(&WireInterceptor{Transport: transport}, &wireTracer{ tracer: trace, }) diff --git a/internal/app/referenceclient/impl.go b/internal/app/referenceclient/impl.go index 1b4372b9..040b5f08 100644 --- a/internal/app/referenceclient/impl.go +++ b/internal/app/referenceclient/impl.go @@ -118,20 +118,20 @@ func (i *invoker) unary( var trailers []*v1.Header payloads := make([]*v1.ConformancePayload, 0, 1) - // ctx = withWireCapture(ctx) + ctx = withWireCapture(ctx) // Invoke the Unary call resp, err := i.client.Unary(ctx, request) - // var actualStatusCode int32 - // var connectErrorRaw *structpb.Struct - // var actualTrailers []*v1.Header - // wireDetails := getWireDetails(ctx) - // if wireDetails != nil { - // actualStatusCode = wireDetails.StatusCode - // actualTrailers = wireDetails.Trailers - // connectErrorRaw = wireDetails.ConnectErrorRaw - // } + var actualStatusCode int32 + var connectErrorRaw *structpb.Struct + var actualTrailers []*v1.Header + wireDetails := getWireDetails(ctx) + if wireDetails != nil { + actualStatusCode = wireDetails.StatusCode + actualTrailers = wireDetails.Trailers + connectErrorRaw = wireDetails.ConnectErrorRaw + } if err != nil { // If an error was returned, first convert it to a Connect error @@ -151,13 +151,13 @@ func (i *invoker) unary( } return &v1.ClientResponseResult{ - ResponseHeaders: headers, - ResponseTrailers: trailers, - Payloads: payloads, - Error: protoErr, - // ActualStatusCode: actualStatusCode, - // ActualHttpTrailers: actualTrailers, - // ConnectErrorRaw: connectErrorRaw, + ResponseHeaders: headers, + ResponseTrailers: trailers, + Payloads: payloads, + Error: protoErr, + ActualStatusCode: actualStatusCode, + ActualHttpTrailers: actualTrailers, + ConnectErrorRaw: connectErrorRaw, }, nil } diff --git a/internal/app/referenceclient/wire_tracer.go b/internal/app/referenceclient/wire_tracer.go index 501cb05a..24f919de 100644 --- a/internal/app/referenceclient/wire_tracer.go +++ b/internal/app/referenceclient/wire_tracer.go @@ -18,7 +18,9 @@ import ( "bytes" "context" "encoding/json" + "fmt" "io" + "os" "strings" "sync/atomic" @@ -51,7 +53,9 @@ type wireWrapper struct { // withWireCapture returns a new context which will contain wire details during // a roundtrip. func withWireCapture(ctx context.Context) context.Context { - return context.WithValue(ctx, wireCtxKey{}, &wireWrapper{}) + return context.WithValue(ctx, wireCtxKey{}, &wireWrapper{ + buf: &bytes.Buffer{}, + }) } // setWireDetails sets the given wire details in the given context. @@ -130,6 +134,7 @@ func (t *wireTracer) Complete(trace tracer.Trace) { Trailers: internal.ConvertToProtoHeader(trace.Response.Trailer), ConnectErrorRaw: &jsonRaw, } + fmt.Fprintf(os.Stderr, "wire details %+v\n", wire) setWireDetails(trace.Request.Context(), wire) } diff --git a/internal/tracer/middleware.go b/internal/tracer/middleware.go index 8b8c95d8..b3d9b76f 100644 --- a/internal/tracer/middleware.go +++ b/internal/tracer/middleware.go @@ -20,6 +20,7 @@ import ( "fmt" "io" "net/http" + "os" "strconv" "strings" ) @@ -44,6 +45,7 @@ func TracingRoundTripper(transport http.RoundTripper, collector Collector) http. } builder.add(&ResponseStart{Response: resp}) respClone := *resp + fmt.Fprintf(os.Stderr, "setting response body in tracer mid========================\n") respClone.Body = newReader(resp.Header, resp.Body, false, builder, cancel) return &respClone, nil }) From 76a7496cea048d907f77804250cf23f5dc6c2d5e Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Fri, 2 Feb 2024 12:59:10 -0500 Subject: [PATCH 26/40] Tests --- .../connectconformance/testsuites/errors.yaml | 910 +++++++++--------- internal/app/referenceclient/impl.go | 26 +- internal/app/referenceclient/middleware.go | 6 +- internal/app/referenceclient/wire_tracer.go | 4 +- 4 files changed, 473 insertions(+), 473 deletions(-) diff --git a/internal/app/connectconformance/testsuites/errors.yaml b/internal/app/connectconformance/testsuites/errors.yaml index 8a3123db..7d980e8f 100644 --- a/internal/app/connectconformance/testsuites/errors.yaml +++ b/internal/app/connectconformance/testsuites/errors.yaml @@ -5,414 +5,414 @@ name: Errors # represented differently on the wire for each. testCases: # Unary Tests ----------------------------------------------------------------- -- request: - testName: unary canceled error - service: connectrpc.conformance.v1.ConformanceService - method: Unary - streamType: STREAM_TYPE_UNARY - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest - responseDefinition: - error: - code: 1 - message: "canceled" -- request: - testName: unary unknown error - service: connectrpc.conformance.v1.ConformanceService - method: Unary - streamType: STREAM_TYPE_UNARY - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest - responseDefinition: - error: - code: 2 - message: "unknown" -- request: - testName: unary invalid argument error - service: connectrpc.conformance.v1.ConformanceService - method: Unary - streamType: STREAM_TYPE_UNARY - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest - responseDefinition: - error: - code: 3 - message: "invalid argument" -- request: - testName: unary deadline exceeded error - service: connectrpc.conformance.v1.ConformanceService - method: Unary - streamType: STREAM_TYPE_UNARY - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest - responseDefinition: - error: - code: 4 - message: "deadline exceeded" -- request: - testName: unary not found error - service: connectrpc.conformance.v1.ConformanceService - method: Unary - streamType: STREAM_TYPE_UNARY - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest - responseDefinition: - error: - code: 5 - message: "not found" -- request: - testName: unary already exists error - service: connectrpc.conformance.v1.ConformanceService - method: Unary - streamType: STREAM_TYPE_UNARY - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest - responseDefinition: - error: - code: 6 - message: "already exists" -- request: - testName: unary permission denied error - service: connectrpc.conformance.v1.ConformanceService - method: Unary - streamType: STREAM_TYPE_UNARY - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest - responseDefinition: - error: - code: 7 - message: "permission denied" -- request: - testName: unary resource exhausted error - service: connectrpc.conformance.v1.ConformanceService - method: Unary - streamType: STREAM_TYPE_UNARY - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest - responseDefinition: - error: - code: 8 - message: "resource exhausted" -- request: - testName: unary failed precondition error - service: connectrpc.conformance.v1.ConformanceService - method: Unary - streamType: STREAM_TYPE_UNARY - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest - responseDefinition: - error: - code: 9 - message: "failed precondition" -- request: - testName: unary aborted error - service: connectrpc.conformance.v1.ConformanceService - method: Unary - streamType: STREAM_TYPE_UNARY - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest - responseDefinition: - error: - code: 10 - message: "aborted" -- request: - testName: unary out of range error - service: connectrpc.conformance.v1.ConformanceService - method: Unary - streamType: STREAM_TYPE_UNARY - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest - responseDefinition: - error: - code: 11 - message: "out of range" -- request: - testName: unary unimplemented error - service: connectrpc.conformance.v1.ConformanceService - method: Unary - streamType: STREAM_TYPE_UNARY - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest - responseDefinition: - error: - code: 12 - message: "unimplemented" -- request: - testName: unary internal error - service: connectrpc.conformance.v1.ConformanceService - method: Unary - streamType: STREAM_TYPE_UNARY - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest - responseDefinition: - error: - code: 13 - message: "internal" -- request: - testName: unary unavailable error - service: connectrpc.conformance.v1.ConformanceService - method: Unary - streamType: STREAM_TYPE_UNARY - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest - responseDefinition: - error: - code: 14 - message: "unavailable" -- request: - testName: unary data loss error - service: connectrpc.conformance.v1.ConformanceService - method: Unary - streamType: STREAM_TYPE_UNARY - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest - responseDefinition: - error: - code: 15 - message: "data loss" -- request: - testName: unary unauthenticated error - service: connectrpc.conformance.v1.ConformanceService - method: Unary - streamType: STREAM_TYPE_UNARY - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest - responseDefinition: - error: - code: 16 - message: "unauthenticated" -- request: - testName: unary error unicode - service: connectrpc.conformance.v1.ConformanceService - method: Unary - streamType: STREAM_TYPE_UNARY - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest - responseDefinition: - error: - code: 13 - message: "\ntest with whitespace\r\nand Unicode BMP ☺ and non-BMP 😈\n" -# Server Stream Tests --------------------------------------------------------- -- request: - testName: stream canceled error - service: connectrpc.conformance.v1.ConformanceService - method: ServerStream - streamType: STREAM_TYPE_SERVER_STREAM - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest - responseDefinition: - error: - code: 1 - message: "canceled" -- request: - testName: stream unknown error - service: connectrpc.conformance.v1.ConformanceService - method: ServerStream - streamType: STREAM_TYPE_SERVER_STREAM - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest - responseDefinition: - error: - code: 2 - message: "unknown" -- request: - testName: stream invalid argument error - service: connectrpc.conformance.v1.ConformanceService - method: ServerStream - streamType: STREAM_TYPE_SERVER_STREAM - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest - responseDefinition: - error: - code: 3 - message: "invalid argument" -- request: - testName: stream deadline exceeded error - service: connectrpc.conformance.v1.ConformanceService - method: ServerStream - streamType: STREAM_TYPE_SERVER_STREAM - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest - responseDefinition: - error: - code: 4 - message: "deadline exceeded" -- request: - testName: stream not found error - service: connectrpc.conformance.v1.ConformanceService - method: ServerStream - streamType: STREAM_TYPE_SERVER_STREAM - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest - responseDefinition: - error: - code: 5 - message: "not found" -- request: - testName: stream already exists error - service: connectrpc.conformance.v1.ConformanceService - method: ServerStream - streamType: STREAM_TYPE_SERVER_STREAM - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest - responseDefinition: - error: - code: 6 - message: "already exists" -- request: - testName: stream permission denied error - service: connectrpc.conformance.v1.ConformanceService - method: ServerStream - streamType: STREAM_TYPE_SERVER_STREAM - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest - responseDefinition: - error: - code: 7 - message: "permission denied" -- request: - testName: stream resource exhausted error - service: connectrpc.conformance.v1.ConformanceService - method: ServerStream - streamType: STREAM_TYPE_SERVER_STREAM - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest - responseDefinition: - error: - code: 8 - message: "resource exhausted" -- request: - testName: stream failed precondition error - service: connectrpc.conformance.v1.ConformanceService - method: ServerStream - streamType: STREAM_TYPE_SERVER_STREAM - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest - responseDefinition: - error: - code: 9 - message: "failed precondition" -- request: - testName: stream aborted error - service: connectrpc.conformance.v1.ConformanceService - method: ServerStream - streamType: STREAM_TYPE_SERVER_STREAM - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest - responseDefinition: - error: - code: 10 - message: "aborted" -- request: - testName: stream out of range error - service: connectrpc.conformance.v1.ConformanceService - method: ServerStream - streamType: STREAM_TYPE_SERVER_STREAM - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest - responseDefinition: - error: - code: 11 - message: "out of range" -- request: - testName: stream unimplemented error - service: connectrpc.conformance.v1.ConformanceService - method: ServerStream - streamType: STREAM_TYPE_SERVER_STREAM - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest - responseDefinition: - error: - code: 12 - message: "unimplemented" -- request: - testName: stream internal error - service: connectrpc.conformance.v1.ConformanceService - method: ServerStream - streamType: STREAM_TYPE_SERVER_STREAM - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest - responseDefinition: - error: - code: 13 - message: "internal" -- request: - testName: stream unavailable error - service: connectrpc.conformance.v1.ConformanceService - method: ServerStream - streamType: STREAM_TYPE_SERVER_STREAM - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest - responseDefinition: - error: - code: 14 - message: "unavailable" -- request: - testName: stream data loss error - service: connectrpc.conformance.v1.ConformanceService - method: ServerStream - streamType: STREAM_TYPE_SERVER_STREAM - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest - responseDefinition: - error: - code: 15 - message: "data loss" -- request: - testName: stream unauthenticated error - service: connectrpc.conformance.v1.ConformanceService - method: ServerStream - streamType: STREAM_TYPE_SERVER_STREAM - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest - responseDefinition: - error: - code: 16 - message: "unauthenticated" -- request: - testName: server stream error with responses - service: connectrpc.conformance.v1.ConformanceService - method: ServerStream - streamType: STREAM_TYPE_SERVER_STREAM - requestHeaders: - - name: X-Conformance-Test - value: ["Value1","Value2"] - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest - responseDefinition: - responseHeaders: - - name: x-custom-header - value: ["foo"] - responseData: - - "dGVzdCByZXNwb25zZQ==" - - "dGVzdCByZXNwb25zZQ==" - error: - code: 13 - message: "server stream failed" - responseTrailers: - - name: x-custom-trailer - value: ["bing"] -- request: - testName: server stream error with no responses - service: connectrpc.conformance.v1.ConformanceService - method: ServerStream - streamType: STREAM_TYPE_SERVER_STREAM - requestHeaders: - - name: X-Conformance-Test - value: ["Value1","Value2"] - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest - responseDefinition: - responseHeaders: - - name: x-custom-header - value: ["foo"] - error: - code: 13 - message: "server stream failed" - responseTrailers: - - name: x-custom-trailer - value: ["bing"] -# Bidi Stream Tests ----------------------------------------------------------- +# - request: +# testName: unary canceled error +# service: connectrpc.conformance.v1.ConformanceService +# method: Unary +# streamType: STREAM_TYPE_UNARY +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest +# responseDefinition: +# error: +# code: 1 +# message: "canceled" +# - request: +# testName: unary unknown error +# service: connectrpc.conformance.v1.ConformanceService +# method: Unary +# streamType: STREAM_TYPE_UNARY +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest +# responseDefinition: +# error: +# code: 2 +# message: "unknown" +# - request: +# testName: unary invalid argument error +# service: connectrpc.conformance.v1.ConformanceService +# method: Unary +# streamType: STREAM_TYPE_UNARY +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest +# responseDefinition: +# error: +# code: 3 +# message: "invalid argument" +# - request: +# testName: unary deadline exceeded error +# service: connectrpc.conformance.v1.ConformanceService +# method: Unary +# streamType: STREAM_TYPE_UNARY +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest +# responseDefinition: +# error: +# code: 4 +# message: "deadline exceeded" +# - request: +# testName: unary not found error +# service: connectrpc.conformance.v1.ConformanceService +# method: Unary +# streamType: STREAM_TYPE_UNARY +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest +# responseDefinition: +# error: +# code: 5 +# message: "not found" +# - request: +# testName: unary already exists error +# service: connectrpc.conformance.v1.ConformanceService +# method: Unary +# streamType: STREAM_TYPE_UNARY +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest +# responseDefinition: +# error: +# code: 6 +# message: "already exists" +# - request: +# testName: unary permission denied error +# service: connectrpc.conformance.v1.ConformanceService +# method: Unary +# streamType: STREAM_TYPE_UNARY +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest +# responseDefinition: +# error: +# code: 7 +# message: "permission denied" +# - request: +# testName: unary resource exhausted error +# service: connectrpc.conformance.v1.ConformanceService +# method: Unary +# streamType: STREAM_TYPE_UNARY +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest +# responseDefinition: +# error: +# code: 8 +# message: "resource exhausted" +# - request: +# testName: unary failed precondition error +# service: connectrpc.conformance.v1.ConformanceService +# method: Unary +# streamType: STREAM_TYPE_UNARY +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest +# responseDefinition: +# error: +# code: 9 +# message: "failed precondition" +# - request: +# testName: unary aborted error +# service: connectrpc.conformance.v1.ConformanceService +# method: Unary +# streamType: STREAM_TYPE_UNARY +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest +# responseDefinition: +# error: +# code: 10 +# message: "aborted" +# - request: +# testName: unary out of range error +# service: connectrpc.conformance.v1.ConformanceService +# method: Unary +# streamType: STREAM_TYPE_UNARY +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest +# responseDefinition: +# error: +# code: 11 +# message: "out of range" +# - request: +# testName: unary unimplemented error +# service: connectrpc.conformance.v1.ConformanceService +# method: Unary +# streamType: STREAM_TYPE_UNARY +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest +# responseDefinition: +# error: +# code: 12 +# message: "unimplemented" +# - request: +# testName: unary internal error +# service: connectrpc.conformance.v1.ConformanceService +# method: Unary +# streamType: STREAM_TYPE_UNARY +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest +# responseDefinition: +# error: +# code: 13 +# message: "internal" +# - request: +# testName: unary unavailable error +# service: connectrpc.conformance.v1.ConformanceService +# method: Unary +# streamType: STREAM_TYPE_UNARY +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest +# responseDefinition: +# error: +# code: 14 +# message: "unavailable" +# - request: +# testName: unary data loss error +# service: connectrpc.conformance.v1.ConformanceService +# method: Unary +# streamType: STREAM_TYPE_UNARY +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest +# responseDefinition: +# error: +# code: 15 +# message: "data loss" +# - request: +# testName: unary unauthenticated error +# service: connectrpc.conformance.v1.ConformanceService +# method: Unary +# streamType: STREAM_TYPE_UNARY +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest +# responseDefinition: +# error: +# code: 16 +# message: "unauthenticated" +# - request: +# testName: unary error unicode +# service: connectrpc.conformance.v1.ConformanceService +# method: Unary +# streamType: STREAM_TYPE_UNARY +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest +# responseDefinition: +# error: +# code: 13 +# message: "\ntest with whitespace\r\nand Unicode BMP ☺ and non-BMP 😈\n" +# # # Server Stream Tests --------------------------------------------------------- +# - request: +# testName: stream canceled error +# service: connectrpc.conformance.v1.ConformanceService +# method: ServerStream +# streamType: STREAM_TYPE_SERVER_STREAM +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest +# responseDefinition: +# error: +# code: 1 +# message: "canceled" +# - request: +# testName: stream unknown error +# service: connectrpc.conformance.v1.ConformanceService +# method: ServerStream +# streamType: STREAM_TYPE_SERVER_STREAM +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest +# responseDefinition: +# error: +# code: 2 +# message: "unknown" +# - request: +# testName: stream invalid argument error +# service: connectrpc.conformance.v1.ConformanceService +# method: ServerStream +# streamType: STREAM_TYPE_SERVER_STREAM +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest +# responseDefinition: +# error: +# code: 3 +# message: "invalid argument" +# - request: +# testName: stream deadline exceeded error +# service: connectrpc.conformance.v1.ConformanceService +# method: ServerStream +# streamType: STREAM_TYPE_SERVER_STREAM +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest +# responseDefinition: +# error: +# code: 4 +# message: "deadline exceeded" +# - request: +# testName: stream not found error +# service: connectrpc.conformance.v1.ConformanceService +# method: ServerStream +# streamType: STREAM_TYPE_SERVER_STREAM +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest +# responseDefinition: +# error: +# code: 5 +# message: "not found" +# - request: +# testName: stream already exists error +# service: connectrpc.conformance.v1.ConformanceService +# method: ServerStream +# streamType: STREAM_TYPE_SERVER_STREAM +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest +# responseDefinition: +# error: +# code: 6 +# message: "already exists" +# - request: +# testName: stream permission denied error +# service: connectrpc.conformance.v1.ConformanceService +# method: ServerStream +# streamType: STREAM_TYPE_SERVER_STREAM +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest +# responseDefinition: +# error: +# code: 7 +# message: "permission denied" +# - request: +# testName: stream resource exhausted error +# service: connectrpc.conformance.v1.ConformanceService +# method: ServerStream +# streamType: STREAM_TYPE_SERVER_STREAM +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest +# responseDefinition: +# error: +# code: 8 +# message: "resource exhausted" +# - request: +# testName: stream failed precondition error +# service: connectrpc.conformance.v1.ConformanceService +# method: ServerStream +# streamType: STREAM_TYPE_SERVER_STREAM +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest +# responseDefinition: +# error: +# code: 9 +# message: "failed precondition" +# - request: +# testName: stream aborted error +# service: connectrpc.conformance.v1.ConformanceService +# method: ServerStream +# streamType: STREAM_TYPE_SERVER_STREAM +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest +# responseDefinition: +# error: +# code: 10 +# message: "aborted" +# - request: +# testName: stream out of range error +# service: connectrpc.conformance.v1.ConformanceService +# method: ServerStream +# streamType: STREAM_TYPE_SERVER_STREAM +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest +# responseDefinition: +# error: +# code: 11 +# message: "out of range" +# - request: +# testName: stream unimplemented error +# service: connectrpc.conformance.v1.ConformanceService +# method: ServerStream +# streamType: STREAM_TYPE_SERVER_STREAM +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest +# responseDefinition: +# error: +# code: 12 +# message: "unimplemented" +# - request: +# testName: stream internal error +# service: connectrpc.conformance.v1.ConformanceService +# method: ServerStream +# streamType: STREAM_TYPE_SERVER_STREAM +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest +# responseDefinition: +# error: +# code: 13 +# message: "internal" +# - request: +# testName: stream unavailable error +# service: connectrpc.conformance.v1.ConformanceService +# method: ServerStream +# streamType: STREAM_TYPE_SERVER_STREAM +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest +# responseDefinition: +# error: +# code: 14 +# message: "unavailable" +# - request: +# testName: stream data loss error +# service: connectrpc.conformance.v1.ConformanceService +# method: ServerStream +# streamType: STREAM_TYPE_SERVER_STREAM +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest +# responseDefinition: +# error: +# code: 15 +# message: "data loss" +# - request: +# testName: stream unauthenticated error +# service: connectrpc.conformance.v1.ConformanceService +# method: ServerStream +# streamType: STREAM_TYPE_SERVER_STREAM +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest +# responseDefinition: +# error: +# code: 16 +# message: "unauthenticated" +# - request: +# testName: server stream error with responses +# service: connectrpc.conformance.v1.ConformanceService +# method: ServerStream +# streamType: STREAM_TYPE_SERVER_STREAM +# requestHeaders: +# - name: X-Conformance-Test +# value: ["Value1","Value2"] +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest +# responseDefinition: +# responseHeaders: +# - name: x-custom-header +# value: ["foo"] +# responseData: +# - "dGVzdCByZXNwb25zZQ==" +# - "dGVzdCByZXNwb25zZQ==" +# error: +# code: 13 +# message: "server stream failed" +# responseTrailers: +# - name: x-custom-trailer +# value: ["bing"] +# - request: +# testName: server stream error with no responses +# service: connectrpc.conformance.v1.ConformanceService +# method: ServerStream +# streamType: STREAM_TYPE_SERVER_STREAM +# requestHeaders: +# - name: X-Conformance-Test +# value: ["Value1","Value2"] +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest +# responseDefinition: +# responseHeaders: +# - name: x-custom-header +# value: ["foo"] +# error: +# code: 13 +# message: "server stream failed" +# responseTrailers: +# - name: x-custom-trailer +# value: ["bing"] +# # Bidi Stream Tests ----------------------------------------------------------- - request: testName: bidi full duplex stream error with responses service: connectrpc.conformance.v1.ConformanceService @@ -460,50 +460,50 @@ testCases: - name: x-custom-trailer value: ["bing"] fullDuplex: true -- request: - testName: bidi half duplex stream error with responses - service: connectrpc.conformance.v1.ConformanceService - method: BidiStream - streamType: STREAM_TYPE_HALF_DUPLEX_BIDI_STREAM - requestHeaders: - - name: X-Conformance-Test - value: ["Value1","Value2"] - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest - responseDefinition: - responseHeaders: - - name: x-custom-header - value: ["foo"] - responseData: - - "dGVzdCByZXNwb25zZQ==" - - "dGVzdCByZXNwb25zZQ==" - error: - code: 13 - message: "bidi half duplex stream failed" - responseTrailers: - - name: x-custom-trailer - value: ["bing"] - - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest - requestData: "dGVzdCByZXNwb25zZQ==" -- request: - testName: bidi half duplex stream error with no responses - service: connectrpc.conformance.v1.ConformanceService - method: BidiStream - streamType: STREAM_TYPE_HALF_DUPLEX_BIDI_STREAM - requestHeaders: - - name: X-Conformance-Test - value: ["Value1","Value2"] - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest - responseDefinition: - responseHeaders: - - name: x-custom-header - value: ["foo"] - error: - code: 13 - message: "bidi half duplex stream failed" - responseTrailers: - - name: x-custom-trailer - value: ["bing"] - - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest - requestData: "dGVzdCByZXNwb25zZQ==" +# - request: +# testName: bidi half duplex stream error with responses +# service: connectrpc.conformance.v1.ConformanceService +# method: BidiStream +# streamType: STREAM_TYPE_HALF_DUPLEX_BIDI_STREAM +# requestHeaders: +# - name: X-Conformance-Test +# value: ["Value1","Value2"] +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest +# responseDefinition: +# responseHeaders: +# - name: x-custom-header +# value: ["foo"] +# responseData: +# - "dGVzdCByZXNwb25zZQ==" +# - "dGVzdCByZXNwb25zZQ==" +# error: +# code: 13 +# message: "bidi half duplex stream failed" +# responseTrailers: +# - name: x-custom-trailer +# value: ["bing"] +# - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest +# requestData: "dGVzdCByZXNwb25zZQ==" +# - request: +# testName: bidi half duplex stream error with no responses +# service: connectrpc.conformance.v1.ConformanceService +# method: BidiStream +# streamType: STREAM_TYPE_HALF_DUPLEX_BIDI_STREAM +# requestHeaders: +# - name: X-Conformance-Test +# value: ["Value1","Value2"] +# requestMessages: +# - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest +# responseDefinition: +# responseHeaders: +# - name: x-custom-header +# value: ["foo"] +# error: +# code: 13 +# message: "bidi half duplex stream failed" +# responseTrailers: +# - name: x-custom-trailer +# value: ["bing"] +# - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest +# requestData: "dGVzdCByZXNwb25zZQ==" diff --git a/internal/app/referenceclient/impl.go b/internal/app/referenceclient/impl.go index 040b5f08..040e0b37 100644 --- a/internal/app/referenceclient/impl.go +++ b/internal/app/referenceclient/impl.go @@ -422,10 +422,23 @@ func (i *invoker) bidiStream( stream := i.client.BidiStream(ctx) defer func() { + var actualStatusCode int32 + var connectErrorRaw *structpb.Struct + var actualTrailers []*v1.Header if result != nil { // Read headers and trailers from the stream result.ResponseHeaders = internal.ConvertToProtoHeader(stream.ResponseHeader()) result.ResponseTrailers = internal.ConvertToProtoHeader(stream.ResponseTrailer()) + + wireDetails := getWireDetails(ctx) + if wireDetails != nil { + actualStatusCode = wireDetails.StatusCode + actualTrailers = wireDetails.Trailers + connectErrorRaw = wireDetails.ConnectErrorRaw + } + result.ActualStatusCode = actualStatusCode + result.ActualHttpTrailers = actualTrailers + result.ConnectErrorRaw = connectErrorRaw } }() @@ -525,19 +538,6 @@ func (i *invoker) bidiStream( result.Payloads = append(result.Payloads, msg.Payload) } - var actualStatusCode int32 - var connectErrorRaw *structpb.Struct - var actualTrailers []*v1.Header - wireDetails := getWireDetails(ctx) - if wireDetails != nil { - actualStatusCode = wireDetails.StatusCode - actualTrailers = wireDetails.Trailers - connectErrorRaw = wireDetails.ConnectErrorRaw - } - result.ActualStatusCode = actualStatusCode - result.ActualHttpTrailers = actualTrailers - result.ConnectErrorRaw = connectErrorRaw - if protoErr != nil { result.Error = protoErr return result, nil diff --git a/internal/app/referenceclient/middleware.go b/internal/app/referenceclient/middleware.go index 9ff6b6b7..4e528b06 100644 --- a/internal/app/referenceclient/middleware.go +++ b/internal/app/referenceclient/middleware.go @@ -40,10 +40,12 @@ type capture struct { } func (c *capture) Read(p []byte) (int, error) { + n, err := c.r.Read(p) + // Capture bytes as they are read - c.wrapper.buf.Write(p) + c.wrapper.buf.Write(p[:n]) - return c.r.Read(p) + return n, err } func (c *capture) Close() error { diff --git a/internal/app/referenceclient/wire_tracer.go b/internal/app/referenceclient/wire_tracer.go index 24f919de..4aa21878 100644 --- a/internal/app/referenceclient/wire_tracer.go +++ b/internal/app/referenceclient/wire_tracer.go @@ -18,9 +18,7 @@ import ( "bytes" "context" "encoding/json" - "fmt" "io" - "os" "strings" "sync/atomic" @@ -134,7 +132,7 @@ func (t *wireTracer) Complete(trace tracer.Trace) { Trailers: internal.ConvertToProtoHeader(trace.Response.Trailer), ConnectErrorRaw: &jsonRaw, } - fmt.Fprintf(os.Stderr, "wire details %+v\n", wire) + // fmt.Fprintf(os.Stderr, "wire details %+v\n", wire) setWireDetails(trace.Request.Context(), wire) } From 03bd905d75a5c93fc62c1e0af4565bd51831a417 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Fri, 2 Feb 2024 13:14:16 -0500 Subject: [PATCH 27/40] Docs --- .../connectconformance/testsuites/errors.yaml | 935 +++++++++--------- internal/app/referenceclient/client.go | 16 +- internal/app/referenceclient/middleware.go | 32 +- .../{wire_tracer.go => tracer.go} | 9 +- 4 files changed, 511 insertions(+), 481 deletions(-) rename internal/app/referenceclient/{wire_tracer.go => tracer.go} (96%) diff --git a/internal/app/connectconformance/testsuites/errors.yaml b/internal/app/connectconformance/testsuites/errors.yaml index 7d980e8f..501d332d 100644 --- a/internal/app/connectconformance/testsuites/errors.yaml +++ b/internal/app/connectconformance/testsuites/errors.yaml @@ -5,414 +5,439 @@ name: Errors # represented differently on the wire for each. testCases: # Unary Tests ----------------------------------------------------------------- -# - request: -# testName: unary canceled error -# service: connectrpc.conformance.v1.ConformanceService -# method: Unary -# streamType: STREAM_TYPE_UNARY -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest -# responseDefinition: -# error: -# code: 1 -# message: "canceled" -# - request: -# testName: unary unknown error -# service: connectrpc.conformance.v1.ConformanceService -# method: Unary -# streamType: STREAM_TYPE_UNARY -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest -# responseDefinition: -# error: -# code: 2 -# message: "unknown" -# - request: -# testName: unary invalid argument error -# service: connectrpc.conformance.v1.ConformanceService -# method: Unary -# streamType: STREAM_TYPE_UNARY -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest -# responseDefinition: -# error: -# code: 3 -# message: "invalid argument" -# - request: -# testName: unary deadline exceeded error -# service: connectrpc.conformance.v1.ConformanceService -# method: Unary -# streamType: STREAM_TYPE_UNARY -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest -# responseDefinition: -# error: -# code: 4 -# message: "deadline exceeded" -# - request: -# testName: unary not found error -# service: connectrpc.conformance.v1.ConformanceService -# method: Unary -# streamType: STREAM_TYPE_UNARY -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest -# responseDefinition: -# error: -# code: 5 -# message: "not found" -# - request: -# testName: unary already exists error -# service: connectrpc.conformance.v1.ConformanceService -# method: Unary -# streamType: STREAM_TYPE_UNARY -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest -# responseDefinition: -# error: -# code: 6 -# message: "already exists" -# - request: -# testName: unary permission denied error -# service: connectrpc.conformance.v1.ConformanceService -# method: Unary -# streamType: STREAM_TYPE_UNARY -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest -# responseDefinition: -# error: -# code: 7 -# message: "permission denied" -# - request: -# testName: unary resource exhausted error -# service: connectrpc.conformance.v1.ConformanceService -# method: Unary -# streamType: STREAM_TYPE_UNARY -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest -# responseDefinition: -# error: -# code: 8 -# message: "resource exhausted" -# - request: -# testName: unary failed precondition error -# service: connectrpc.conformance.v1.ConformanceService -# method: Unary -# streamType: STREAM_TYPE_UNARY -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest -# responseDefinition: -# error: -# code: 9 -# message: "failed precondition" -# - request: -# testName: unary aborted error -# service: connectrpc.conformance.v1.ConformanceService -# method: Unary -# streamType: STREAM_TYPE_UNARY -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest -# responseDefinition: -# error: -# code: 10 -# message: "aborted" -# - request: -# testName: unary out of range error -# service: connectrpc.conformance.v1.ConformanceService -# method: Unary -# streamType: STREAM_TYPE_UNARY -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest -# responseDefinition: -# error: -# code: 11 -# message: "out of range" -# - request: -# testName: unary unimplemented error -# service: connectrpc.conformance.v1.ConformanceService -# method: Unary -# streamType: STREAM_TYPE_UNARY -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest -# responseDefinition: -# error: -# code: 12 -# message: "unimplemented" -# - request: -# testName: unary internal error -# service: connectrpc.conformance.v1.ConformanceService -# method: Unary -# streamType: STREAM_TYPE_UNARY -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest -# responseDefinition: -# error: -# code: 13 -# message: "internal" -# - request: -# testName: unary unavailable error -# service: connectrpc.conformance.v1.ConformanceService -# method: Unary -# streamType: STREAM_TYPE_UNARY -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest -# responseDefinition: -# error: -# code: 14 -# message: "unavailable" -# - request: -# testName: unary data loss error -# service: connectrpc.conformance.v1.ConformanceService -# method: Unary -# streamType: STREAM_TYPE_UNARY -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest -# responseDefinition: -# error: -# code: 15 -# message: "data loss" -# - request: -# testName: unary unauthenticated error -# service: connectrpc.conformance.v1.ConformanceService -# method: Unary -# streamType: STREAM_TYPE_UNARY -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest -# responseDefinition: -# error: -# code: 16 -# message: "unauthenticated" -# - request: -# testName: unary error unicode -# service: connectrpc.conformance.v1.ConformanceService -# method: Unary -# streamType: STREAM_TYPE_UNARY -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest -# responseDefinition: -# error: -# code: 13 -# message: "\ntest with whitespace\r\nand Unicode BMP ☺ and non-BMP 😈\n" -# # # Server Stream Tests --------------------------------------------------------- -# - request: -# testName: stream canceled error -# service: connectrpc.conformance.v1.ConformanceService -# method: ServerStream -# streamType: STREAM_TYPE_SERVER_STREAM -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest -# responseDefinition: -# error: -# code: 1 -# message: "canceled" -# - request: -# testName: stream unknown error -# service: connectrpc.conformance.v1.ConformanceService -# method: ServerStream -# streamType: STREAM_TYPE_SERVER_STREAM -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest -# responseDefinition: -# error: -# code: 2 -# message: "unknown" -# - request: -# testName: stream invalid argument error -# service: connectrpc.conformance.v1.ConformanceService -# method: ServerStream -# streamType: STREAM_TYPE_SERVER_STREAM -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest -# responseDefinition: -# error: -# code: 3 -# message: "invalid argument" -# - request: -# testName: stream deadline exceeded error -# service: connectrpc.conformance.v1.ConformanceService -# method: ServerStream -# streamType: STREAM_TYPE_SERVER_STREAM -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest -# responseDefinition: -# error: -# code: 4 -# message: "deadline exceeded" -# - request: -# testName: stream not found error -# service: connectrpc.conformance.v1.ConformanceService -# method: ServerStream -# streamType: STREAM_TYPE_SERVER_STREAM -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest -# responseDefinition: -# error: -# code: 5 -# message: "not found" -# - request: -# testName: stream already exists error -# service: connectrpc.conformance.v1.ConformanceService -# method: ServerStream -# streamType: STREAM_TYPE_SERVER_STREAM -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest -# responseDefinition: -# error: -# code: 6 -# message: "already exists" -# - request: -# testName: stream permission denied error -# service: connectrpc.conformance.v1.ConformanceService -# method: ServerStream -# streamType: STREAM_TYPE_SERVER_STREAM -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest -# responseDefinition: -# error: -# code: 7 -# message: "permission denied" -# - request: -# testName: stream resource exhausted error -# service: connectrpc.conformance.v1.ConformanceService -# method: ServerStream -# streamType: STREAM_TYPE_SERVER_STREAM -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest -# responseDefinition: -# error: -# code: 8 -# message: "resource exhausted" -# - request: -# testName: stream failed precondition error -# service: connectrpc.conformance.v1.ConformanceService -# method: ServerStream -# streamType: STREAM_TYPE_SERVER_STREAM -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest -# responseDefinition: -# error: -# code: 9 -# message: "failed precondition" -# - request: -# testName: stream aborted error -# service: connectrpc.conformance.v1.ConformanceService -# method: ServerStream -# streamType: STREAM_TYPE_SERVER_STREAM -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest -# responseDefinition: -# error: -# code: 10 -# message: "aborted" -# - request: -# testName: stream out of range error -# service: connectrpc.conformance.v1.ConformanceService -# method: ServerStream -# streamType: STREAM_TYPE_SERVER_STREAM -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest -# responseDefinition: -# error: -# code: 11 -# message: "out of range" -# - request: -# testName: stream unimplemented error -# service: connectrpc.conformance.v1.ConformanceService -# method: ServerStream -# streamType: STREAM_TYPE_SERVER_STREAM -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest -# responseDefinition: -# error: -# code: 12 -# message: "unimplemented" -# - request: -# testName: stream internal error -# service: connectrpc.conformance.v1.ConformanceService -# method: ServerStream -# streamType: STREAM_TYPE_SERVER_STREAM -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest -# responseDefinition: -# error: -# code: 13 -# message: "internal" -# - request: -# testName: stream unavailable error -# service: connectrpc.conformance.v1.ConformanceService -# method: ServerStream -# streamType: STREAM_TYPE_SERVER_STREAM -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest -# responseDefinition: -# error: -# code: 14 -# message: "unavailable" -# - request: -# testName: stream data loss error -# service: connectrpc.conformance.v1.ConformanceService -# method: ServerStream -# streamType: STREAM_TYPE_SERVER_STREAM -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest -# responseDefinition: -# error: -# code: 15 -# message: "data loss" -# - request: -# testName: stream unauthenticated error -# service: connectrpc.conformance.v1.ConformanceService -# method: ServerStream -# streamType: STREAM_TYPE_SERVER_STREAM -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest -# responseDefinition: -# error: -# code: 16 -# message: "unauthenticated" -# - request: -# testName: server stream error with responses -# service: connectrpc.conformance.v1.ConformanceService -# method: ServerStream -# streamType: STREAM_TYPE_SERVER_STREAM -# requestHeaders: -# - name: X-Conformance-Test -# value: ["Value1","Value2"] -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest -# responseDefinition: -# responseHeaders: -# - name: x-custom-header -# value: ["foo"] -# responseData: -# - "dGVzdCByZXNwb25zZQ==" -# - "dGVzdCByZXNwb25zZQ==" -# error: -# code: 13 -# message: "server stream failed" -# responseTrailers: -# - name: x-custom-trailer -# value: ["bing"] -# - request: -# testName: server stream error with no responses -# service: connectrpc.conformance.v1.ConformanceService -# method: ServerStream -# streamType: STREAM_TYPE_SERVER_STREAM -# requestHeaders: -# - name: X-Conformance-Test -# value: ["Value1","Value2"] -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest -# responseDefinition: -# responseHeaders: -# - name: x-custom-header -# value: ["foo"] -# error: -# code: 13 -# message: "server stream failed" -# responseTrailers: -# - name: x-custom-trailer -# value: ["bing"] -# # Bidi Stream Tests ----------------------------------------------------------- +- request: + testName: unary canceled error + service: connectrpc.conformance.v1.ConformanceService + method: Unary + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 1 + message: "canceled" +- request: + testName: unary unknown error + service: connectrpc.conformance.v1.ConformanceService + method: Unary + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 2 + message: "unknown" +- request: + testName: unary invalid argument error + service: connectrpc.conformance.v1.ConformanceService + method: Unary + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 3 + message: "invalid argument" +- request: + testName: unary deadline exceeded error + service: connectrpc.conformance.v1.ConformanceService + method: Unary + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 4 + message: "deadline exceeded" +- request: + testName: unary not found error + service: connectrpc.conformance.v1.ConformanceService + method: Unary + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 5 + message: "not found" +- request: + testName: unary already exists error + service: connectrpc.conformance.v1.ConformanceService + method: Unary + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 6 + message: "already exists" +- request: + testName: unary permission denied error + service: connectrpc.conformance.v1.ConformanceService + method: Unary + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 7 + message: "permission denied" +- request: + testName: unary resource exhausted error + service: connectrpc.conformance.v1.ConformanceService + method: Unary + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 8 + message: "resource exhausted" +- request: + testName: unary failed precondition error + service: connectrpc.conformance.v1.ConformanceService + method: Unary + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 9 + message: "failed precondition" +- request: + testName: unary aborted error + service: connectrpc.conformance.v1.ConformanceService + method: Unary + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 10 + message: "aborted" +- request: + testName: unary out of range error + service: connectrpc.conformance.v1.ConformanceService + method: Unary + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 11 + message: "out of range" +- request: + testName: unary unimplemented error + service: connectrpc.conformance.v1.ConformanceService + method: Unary + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 12 + message: "unimplemented" +- request: + testName: unary internal error + service: connectrpc.conformance.v1.ConformanceService + method: Unary + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 13 + message: "internal" +- request: + testName: unary unavailable error + service: connectrpc.conformance.v1.ConformanceService + method: Unary + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 14 + message: "unavailable" +- request: + testName: unary data loss error + service: connectrpc.conformance.v1.ConformanceService + method: Unary + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 15 + message: "data loss" +- request: + testName: unary unauthenticated error + service: connectrpc.conformance.v1.ConformanceService + method: Unary + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 16 + message: "unauthenticated" +- request: + testName: unary error unicode + service: connectrpc.conformance.v1.ConformanceService + method: Unary + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 13 + message: "\ntest with whitespace\r\nand Unicode BMP ☺ and non-BMP 😈\n" +# Server Stream Tests --------------------------------------------------------- +- request: + testName: stream canceled error + service: connectrpc.conformance.v1.ConformanceService + method: ServerStream + streamType: STREAM_TYPE_SERVER_STREAM + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest + responseDefinition: + error: + code: 1 + message: "canceled" +- request: + testName: stream unknown error + service: connectrpc.conformance.v1.ConformanceService + method: ServerStream + streamType: STREAM_TYPE_SERVER_STREAM + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest + responseDefinition: + error: + code: 2 + message: "unknown" +- request: + testName: stream invalid argument error + service: connectrpc.conformance.v1.ConformanceService + method: ServerStream + streamType: STREAM_TYPE_SERVER_STREAM + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest + responseDefinition: + error: + code: 3 + message: "invalid argument" +- request: + testName: stream deadline exceeded error + service: connectrpc.conformance.v1.ConformanceService + method: ServerStream + streamType: STREAM_TYPE_SERVER_STREAM + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest + responseDefinition: + error: + code: 4 + message: "deadline exceeded" +- request: + testName: stream not found error + service: connectrpc.conformance.v1.ConformanceService + method: ServerStream + streamType: STREAM_TYPE_SERVER_STREAM + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest + responseDefinition: + error: + code: 5 + message: "not found" +- request: + testName: stream already exists error + service: connectrpc.conformance.v1.ConformanceService + method: ServerStream + streamType: STREAM_TYPE_SERVER_STREAM + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest + responseDefinition: + error: + code: 6 + message: "already exists" +- request: + testName: stream permission denied error + service: connectrpc.conformance.v1.ConformanceService + method: ServerStream + streamType: STREAM_TYPE_SERVER_STREAM + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest + responseDefinition: + error: + code: 7 + message: "permission denied" +- request: + testName: stream resource exhausted error + service: connectrpc.conformance.v1.ConformanceService + method: ServerStream + streamType: STREAM_TYPE_SERVER_STREAM + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest + responseDefinition: + error: + code: 8 + message: "resource exhausted" +- request: + testName: stream failed precondition error + service: connectrpc.conformance.v1.ConformanceService + method: ServerStream + streamType: STREAM_TYPE_SERVER_STREAM + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest + responseDefinition: + error: + code: 9 + message: "failed precondition" +- request: + testName: stream aborted error + service: connectrpc.conformance.v1.ConformanceService + method: ServerStream + streamType: STREAM_TYPE_SERVER_STREAM + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest + responseDefinition: + error: + code: 10 + message: "aborted" +- request: + testName: stream out of range error + service: connectrpc.conformance.v1.ConformanceService + method: ServerStream + streamType: STREAM_TYPE_SERVER_STREAM + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest + responseDefinition: + error: + code: 11 + message: "out of range" +- request: + testName: stream unimplemented error + service: connectrpc.conformance.v1.ConformanceService + method: ServerStream + streamType: STREAM_TYPE_SERVER_STREAM + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest + responseDefinition: + error: + code: 12 + message: "unimplemented" +- request: + testName: stream internal error + service: connectrpc.conformance.v1.ConformanceService + method: ServerStream + streamType: STREAM_TYPE_SERVER_STREAM + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest + responseDefinition: + error: + code: 13 + message: "internal" +- request: + testName: stream unavailable error + service: connectrpc.conformance.v1.ConformanceService + method: ServerStream + streamType: STREAM_TYPE_SERVER_STREAM + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest + responseDefinition: + error: + code: 14 + message: "unavailable" +- request: + testName: stream data loss error + service: connectrpc.conformance.v1.ConformanceService + method: ServerStream + streamType: STREAM_TYPE_SERVER_STREAM + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest + responseDefinition: + error: + code: 15 + message: "data loss" +- request: + testName: stream unauthenticated error + service: connectrpc.conformance.v1.ConformanceService + method: ServerStream + streamType: STREAM_TYPE_SERVER_STREAM + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest + responseDefinition: + error: + code: 16 + message: "unauthenticated" +- request: + testName: server stream error with responses + service: connectrpc.conformance.v1.ConformanceService + method: ServerStream + streamType: STREAM_TYPE_SERVER_STREAM + requestHeaders: + - name: X-Conformance-Test + value: ["Value1","Value2"] + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest + responseDefinition: + responseHeaders: + - name: x-custom-header + value: ["foo"] + responseData: + - "dGVzdCByZXNwb25zZQ==" + - "dGVzdCByZXNwb25zZQ==" + error: + code: 13 + message: "server stream failed" + responseTrailers: + - name: x-custom-trailer + value: ["bing"] +- request: + testName: server stream error with no responses + service: connectrpc.conformance.v1.ConformanceService + method: ServerStream + streamType: STREAM_TYPE_SERVER_STREAM + requestHeaders: + - name: X-Conformance-Test + value: ["Value1","Value2"] + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest + responseDefinition: + responseHeaders: + - name: x-custom-header + value: ["foo"] + error: + code: 13 + message: "server stream failed" + responseTrailers: + - name: x-custom-trailer + value: ["bing"] +# Client Stream Tests ----------------------------------------------------------- +- request: + testName: client stream error one request + service: connectrpc.conformance.v1.ConformanceService + method: ClientStream + streamType: STREAM_TYPE_CLIENT_STREAM + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest + responseDefinition: + error: + code: 13 + message: "client stream failed" +- request: + testName: client stream error multiple requests + service: connectrpc.conformance.v1.ConformanceService + method: ClientStream + streamType: STREAM_TYPE_CLIENT_STREAM + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest + responseDefinition: + error: + code: 13 + message: "client stream failed" + - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest + requestData: "dGVzdCByZXNwb25zZQ==" +# Bidi Stream Tests ----------------------------------------------------------- - request: testName: bidi full duplex stream error with responses service: connectrpc.conformance.v1.ConformanceService @@ -460,50 +485,50 @@ testCases: - name: x-custom-trailer value: ["bing"] fullDuplex: true -# - request: -# testName: bidi half duplex stream error with responses -# service: connectrpc.conformance.v1.ConformanceService -# method: BidiStream -# streamType: STREAM_TYPE_HALF_DUPLEX_BIDI_STREAM -# requestHeaders: -# - name: X-Conformance-Test -# value: ["Value1","Value2"] -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest -# responseDefinition: -# responseHeaders: -# - name: x-custom-header -# value: ["foo"] -# responseData: -# - "dGVzdCByZXNwb25zZQ==" -# - "dGVzdCByZXNwb25zZQ==" -# error: -# code: 13 -# message: "bidi half duplex stream failed" -# responseTrailers: -# - name: x-custom-trailer -# value: ["bing"] -# - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest -# requestData: "dGVzdCByZXNwb25zZQ==" -# - request: -# testName: bidi half duplex stream error with no responses -# service: connectrpc.conformance.v1.ConformanceService -# method: BidiStream -# streamType: STREAM_TYPE_HALF_DUPLEX_BIDI_STREAM -# requestHeaders: -# - name: X-Conformance-Test -# value: ["Value1","Value2"] -# requestMessages: -# - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest -# responseDefinition: -# responseHeaders: -# - name: x-custom-header -# value: ["foo"] -# error: -# code: 13 -# message: "bidi half duplex stream failed" -# responseTrailers: -# - name: x-custom-trailer -# value: ["bing"] -# - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest -# requestData: "dGVzdCByZXNwb25zZQ==" +- request: + testName: bidi half duplex stream error with responses + service: connectrpc.conformance.v1.ConformanceService + method: BidiStream + streamType: STREAM_TYPE_HALF_DUPLEX_BIDI_STREAM + requestHeaders: + - name: X-Conformance-Test + value: ["Value1","Value2"] + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest + responseDefinition: + responseHeaders: + - name: x-custom-header + value: ["foo"] + responseData: + - "dGVzdCByZXNwb25zZQ==" + - "dGVzdCByZXNwb25zZQ==" + error: + code: 13 + message: "bidi half duplex stream failed" + responseTrailers: + - name: x-custom-trailer + value: ["bing"] + - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest + requestData: "dGVzdCByZXNwb25zZQ==" +- request: + testName: bidi half duplex stream error with no responses + service: connectrpc.conformance.v1.ConformanceService + method: BidiStream + streamType: STREAM_TYPE_HALF_DUPLEX_BIDI_STREAM + requestHeaders: + - name: X-Conformance-Test + value: ["Value1","Value2"] + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest + responseDefinition: + responseHeaders: + - name: x-custom-header + value: ["foo"] + error: + code: 13 + message: "bidi half duplex stream failed" + responseTrailers: + - name: x-custom-trailer + value: ["bing"] + - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest + requestData: "dGVzdCByZXNwb25zZQ==" diff --git a/internal/app/referenceclient/client.go b/internal/app/referenceclient/client.go index 1c7c5127..6214cd56 100644 --- a/internal/app/referenceclient/client.go +++ b/internal/app/referenceclient/client.go @@ -217,17 +217,11 @@ func invoke(ctx context.Context, req *v1.ClientCompatRequest, trace *tracer.Trac return nil, errors.New("an HTTP version must be specified") } - // Create a new TracingRoundTripper with our wireTracer so that the tests can trace values on the - // wire. Note that 'trace' could be nil, in which case, any error traces will - // simply not be printed. - // transport = &WireInterceptor{ - // Transport: tracer.TracingRoundTripper(transport, &wireTracer{ - // tracer: trace, - // }), - // } - transport = tracer.TracingRoundTripper(&WireInterceptor{Transport: transport}, &wireTracer{ - tracer: trace, - }) + // Wrap the transport with a wire interceptor and an optional tracer. + // The wire interceptor wraps a TracingRoundTripper and intercepts values on the + // wire using the tracer framework. Note that 'trace' could be nil, in which case, + // any error traces will simply not be printed. The trace itself will still be built. + transport = newWireInterceptor(transport, trace) if req.RawRequest != nil { transport = &rawRequestSender{transport: transport, rawRequest: req.RawRequest} diff --git a/internal/app/referenceclient/middleware.go b/internal/app/referenceclient/middleware.go index 4e528b06..08949e04 100644 --- a/internal/app/referenceclient/middleware.go +++ b/internal/app/referenceclient/middleware.go @@ -17,37 +17,51 @@ package referenceclient import ( "io" "net/http" + + "connectrpc.com/conformance/internal/tracer" ) -type WireInterceptor struct { +type wireInterceptor struct { Transport http.RoundTripper } -func (w *WireInterceptor) RoundTrip(req *http.Request) (*http.Response, error) { +// RoundTrip replaces the response body with a wireReader which captures bytes +// as they are read. +func (w *wireInterceptor) RoundTrip(req *http.Request) (*http.Response, error) { resp, err := w.Transport.RoundTrip(req) wrapper, ok := req.Context().Value(wireCtxKey{}).(*wireWrapper) if err != nil || !ok { return resp, err } - resp.Body = &capture{r: resp.Body, resp: resp, wrapper: wrapper} + resp.Body = &wireReader{r: resp.Body, resp: resp, wrapper: wrapper} return resp, nil } -type capture struct { +// newWireInterceptor creates a new wireInterceptor which wraps the given transport +// in a TracingRoundTripper. +func newWireInterceptor(transport http.RoundTripper, trace *tracer.Tracer) http.RoundTripper { + return &wireInterceptor{ + Transport: tracer.TracingRoundTripper(transport, &wireTracer{ + tracer: trace, + }), + } +} + +type wireReader struct { r io.ReadCloser resp *http.Response wrapper *wireWrapper } -func (c *capture) Read(p []byte) (int, error) { - n, err := c.r.Read(p) +func (w *wireReader) Read(p []byte) (int, error) { + n, err := w.r.Read(p) // Capture bytes as they are read - c.wrapper.buf.Write(p[:n]) + w.wrapper.buf.Write(p[:n]) return n, err } -func (c *capture) Close() error { - return c.r.Close() +func (w *wireReader) Close() error { + return w.r.Close() } diff --git a/internal/app/referenceclient/wire_tracer.go b/internal/app/referenceclient/tracer.go similarity index 96% rename from internal/app/referenceclient/wire_tracer.go rename to internal/app/referenceclient/tracer.go index 4aa21878..c281e1c6 100644 --- a/internal/app/referenceclient/wire_tracer.go +++ b/internal/app/referenceclient/tracer.go @@ -45,6 +45,7 @@ type wireDetails struct { type wireWrapper struct { val atomic.Pointer[wireDetails] + // buf represents the read response body buf *bytes.Buffer } @@ -65,16 +66,13 @@ func setWireDetails(ctx context.Context, details *wireDetails) { wrapper.val.Store(details) } +// getWireDetails returns the wire details from the given context. func getWireDetails(ctx context.Context) *wireDetails { wrapper, ok := ctx.Value(wireCtxKey{}).(*wireWrapper) if !ok { return nil } - ptr := wrapper.val.Load() - if ptr == nil { - return nil - } - return ptr + return wrapper.val.Load() } type wireTracer struct { @@ -132,7 +130,6 @@ func (t *wireTracer) Complete(trace tracer.Trace) { Trailers: internal.ConvertToProtoHeader(trace.Response.Trailer), ConnectErrorRaw: &jsonRaw, } - // fmt.Fprintf(os.Stderr, "wire details %+v\n", wire) setWireDetails(trace.Request.Context(), wire) } From c8ba9c426fad3c65ae68bf6f88faeabd8ff37ade Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Fri, 2 Feb 2024 15:05:38 -0500 Subject: [PATCH 28/40] Revert --- internal/app/connectconformance/results.go | 6 ++--- .../connectconformance/test_case_library.go | 2 -- .../connectconformance/testsuites/test.yaml | 27 ------------------- internal/app/referenceclient/impl.go | 3 +++ internal/app/referenceclient/middleware.go | 9 +++---- internal/app/referenceclient/tracer.go | 12 ++++++--- 6 files changed, 18 insertions(+), 41 deletions(-) delete mode 100644 internal/app/connectconformance/testsuites/test.yaml diff --git a/internal/app/connectconformance/results.go b/internal/app/connectconformance/results.go index 681f8cca..01ca6ab4 100644 --- a/internal/app/connectconformance/results.go +++ b/internal/app/connectconformance/results.go @@ -199,8 +199,8 @@ func (r *testResults) assert(testCase string, expected, actual *conformancev1.Cl } } - // If a status code is specified in the expected result, then verify it against the actual status code - if expected.ActualStatusCode != 0 { + // If client didn't provide an actual status code, we skip this check. + if expected.ActualStatusCode != 0 && actual.ActualStatusCode != 0 { diff := cmp.Diff(expected.ActualStatusCode, actual.ActualStatusCode, protocmp.Transform()) if diff != "" { errs = append(errs, fmt.Errorf("actual HTTP status code does not match: - wanted, + got\n%s", diff)) @@ -494,7 +494,7 @@ func checkError(expected, actual *conformancev1.Error) multiErrors { // TODO: Should this be more lenient? Are we okay with a Connect implementation adding extra // error details transparently (such that the expected details would be a *subset* of // the actual details)? - errs = append(errs, fmt.Errorf("actual error contain %d details; expecing %d", + errs = append(errs, fmt.Errorf("actual error contain %d details; expecting %d", len(actual.Details), len(expected.Details))) } // Check as many as we can diff --git a/internal/app/connectconformance/test_case_library.go b/internal/app/connectconformance/test_case_library.go index 2e632808..3627b117 100644 --- a/internal/app/connectconformance/test_case_library.go +++ b/internal/app/connectconformance/test_case_library.go @@ -600,7 +600,6 @@ func populateExpectedUnaryResponse(testCase *conformancev1.TestCase) error { payload.Data = respType.ResponseData } expected.Payloads = []*conformancev1.ConformancePayload{payload} - expected.ActualStatusCode = 200 default: return fmt.Errorf("provided UnaryRequest.Response has an unexpected type %T", respType) } @@ -644,7 +643,6 @@ func populateExpectedStreamResponse(testCase *conformancev1.TestCase) error { ResponseHeaders: def.ResponseHeaders, ResponseTrailers: def.ResponseTrailers, Error: def.Error, - ActualStatusCode: 200, // status codes should always be 200 for streaming responses (even errors) } // There should be one payload for every ResponseData the client specified diff --git a/internal/app/connectconformance/testsuites/test.yaml b/internal/app/connectconformance/testsuites/test.yaml deleted file mode 100644 index 4622f02b..00000000 --- a/internal/app/connectconformance/testsuites/test.yaml +++ /dev/null @@ -1,27 +0,0 @@ -name: Test -relevantCompressions: - - COMPRESSION_IDENTITY -relevantProtocols: - - PROTOCOL_CONNECT -relevantCodecs: - - CODEC_JSON -testCases: -# Unary Tests ----------------------------------------------------------------- -- request: - testName: unary success - service: connectrpc.conformance.v1.ConformanceService - method: Unary - streamType: STREAM_TYPE_UNARY - requestHeaders: - - name: X-Conformance-Test - value: ["Value1","Value2"] - requestMessages: - - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest - responseDefinition: - responseHeaders: - - name: x-custom-header - value: ["foo"] - responseData: "dGVzdCByZXNwb25zZQ==" - responseTrailers: - - name: x-custom-trailer - value: ["bing"] diff --git a/internal/app/referenceclient/impl.go b/internal/app/referenceclient/impl.go index 040e0b37..2ad5507e 100644 --- a/internal/app/referenceclient/impl.go +++ b/internal/app/referenceclient/impl.go @@ -21,6 +21,7 @@ import ( "io" "net/http" "net/url" + "os" "time" "connectrpc.com/conformance/internal" @@ -394,6 +395,8 @@ func (i *invoker) clientStream( actualStatusCode = wireDetails.StatusCode actualTrailers = wireDetails.Trailers connectErrorRaw = wireDetails.ConnectErrorRaw + } else { + fmt.Fprintf(os.Stderr, "WIRE DETAILS IS NILLLLLLLLLLLLLLLLLLLL%+v....", wireDetails) } return &v1.ClientResponseResult{ diff --git a/internal/app/referenceclient/middleware.go b/internal/app/referenceclient/middleware.go index 08949e04..5039a6cf 100644 --- a/internal/app/referenceclient/middleware.go +++ b/internal/app/referenceclient/middleware.go @@ -33,7 +33,7 @@ func (w *wireInterceptor) RoundTrip(req *http.Request) (*http.Response, error) { if err != nil || !ok { return resp, err } - resp.Body = &wireReader{r: resp.Body, resp: resp, wrapper: wrapper} + resp.Body = &wireReader{body: resp.Body, wrapper: wrapper} return resp, nil } @@ -48,13 +48,12 @@ func newWireInterceptor(transport http.RoundTripper, trace *tracer.Tracer) http. } type wireReader struct { - r io.ReadCloser - resp *http.Response + body io.ReadCloser wrapper *wireWrapper } func (w *wireReader) Read(p []byte) (int, error) { - n, err := w.r.Read(p) + n, err := w.body.Read(p) // Capture bytes as they are read w.wrapper.buf.Write(p[:n]) @@ -63,5 +62,5 @@ func (w *wireReader) Read(p []byte) (int, error) { } func (w *wireReader) Close() error { - return w.r.Close() + return w.body.Close() } diff --git a/internal/app/referenceclient/tracer.go b/internal/app/referenceclient/tracer.go index c281e1c6..03c34bfd 100644 --- a/internal/app/referenceclient/tracer.go +++ b/internal/app/referenceclient/tracer.go @@ -84,8 +84,8 @@ type wireTracer struct { // withWireCapture and can be retrieved via getWireDetails. func (t *wireTracer) Complete(trace tracer.Trace) { wrapper, ok := trace.Request.Context().Value(wireCtxKey{}).(*wireWrapper) - if ok { - if trace.Response != nil { //nolint:nestif + if ok { //nolint:nestif + if trace.Response != nil { statusCode := int32(trace.Response.StatusCode) var jsonRaw structpb.Struct @@ -116,8 +116,12 @@ func (t *wireTracer) Complete(trace tracer.Trace) { if err := json.Unmarshal([]byte(eventType.Content), &endStream); err != nil { return } - if err := protojson.Unmarshal(endStream.Error, &jsonRaw); err != nil { - return + // If we unmarshalled any bytes into endStream.Error, then unmarshal _that_ + // into a Struct + if len(endStream.Error) > 0 { + if err := protojson.Unmarshal(endStream.Error, &jsonRaw); err != nil { + return + } } default: // Do nothing From b3baf6325f61fd054f69cf5b3572bc59b1e994da Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Fri, 2 Feb 2024 15:23:53 -0500 Subject: [PATCH 29/40] Tests --- .../testsuites/connect_to_http_code.yaml | 432 ++++++++++++++++++ 1 file changed, 432 insertions(+) create mode 100644 internal/app/connectconformance/testsuites/connect_to_http_code.yaml diff --git a/internal/app/connectconformance/testsuites/connect_to_http_code.yaml b/internal/app/connectconformance/testsuites/connect_to_http_code.yaml new file mode 100644 index 00000000..12a835d8 --- /dev/null +++ b/internal/app/connectconformance/testsuites/connect_to_http_code.yaml @@ -0,0 +1,432 @@ +name: Connect to HTTP Code Mapping +mode: TEST_MODE_CLIENT +relevantProtocols: + - PROTOCOL_CONNECT +# These tests verify that an explicit Connect code maps to the correct HTTP code +# according to the protocol. +testCases: +- request: + testName: unary canceled mapping + service: connectrpc.conformance.v1.ConformanceService + method: Unary + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 1 + expectedResponse: + actualStatusCode: 408 + error: + code: 1 + details: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ConformancePayload.RequestInfo + requests: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 1 +- request: + testName: unary unknown mapping + service: connectrpc.conformance.v1.ConformanceService + method: Unary + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 2 + expectedResponse: + actualStatusCode: 500 + error: + code: 2 + details: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ConformancePayload.RequestInfo + requests: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 2 +- request: + testName: unary invalid argument mapping + service: connectrpc.conformance.v1.ConformanceService + method: Unary + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 3 + expectedResponse: + actualStatusCode: 400 + error: + code: 3 + details: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ConformancePayload.RequestInfo + requests: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 3 +- request: + testName: unary deadline exceeded mapping + service: connectrpc.conformance.v1.ConformanceService + method: Unary + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 4 + expectedResponse: + actualStatusCode: 408 + error: + code: 4 + details: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ConformancePayload.RequestInfo + requests: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 4 +- request: + testName: unary not found mapping + service: connectrpc.conformance.v1.ConformanceService + method: Unary + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 5 + expectedResponse: + actualStatusCode: 404 + error: + code: 5 + details: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ConformancePayload.RequestInfo + requests: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 5 +- request: + testName: unary already exists mapping + service: connectrpc.conformance.v1.ConformanceService + method: Unary + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 6 + expectedResponse: + actualStatusCode: 409 + error: + code: 6 + details: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ConformancePayload.RequestInfo + requests: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 6 +- request: + testName: unary permission denied mapping + service: connectrpc.conformance.v1.ConformanceService + method: Unary + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 7 + expectedResponse: + actualStatusCode: 403 + error: + code: 7 + details: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ConformancePayload.RequestInfo + requests: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 7 +- request: + testName: unary resource exhausted mapping + service: connectrpc.conformance.v1.ConformanceService + method: Unary + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 8 + expectedResponse: + actualStatusCode: 429 + error: + code: 8 + details: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ConformancePayload.RequestInfo + requests: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 8 +- request: + testName: unary failed precondition mapping + service: connectrpc.conformance.v1.ConformanceService + method: Unary + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 9 + expectedResponse: + actualStatusCode: 412 + error: + code: 9 + details: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ConformancePayload.RequestInfo + requests: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 9 +- request: + testName: unary aborted mapping + service: connectrpc.conformance.v1.ConformanceService + method: Unary + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 10 + expectedResponse: + actualStatusCode: 409 + error: + code: 10 + details: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ConformancePayload.RequestInfo + requests: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 10 +- request: + testName: unary out of range mapping + service: connectrpc.conformance.v1.ConformanceService + method: Unary + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 11 + expectedResponse: + actualStatusCode: 400 + error: + code: 11 + details: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ConformancePayload.RequestInfo + requests: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 11 +- request: + testName: unary unimplemented mapping + service: connectrpc.conformance.v1.ConformanceService + method: Unary + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 12 + expectedResponse: + actualStatusCode: 404 + error: + code: 12 + details: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ConformancePayload.RequestInfo + requests: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 12 +- request: + testName: unary internal mapping + service: connectrpc.conformance.v1.ConformanceService + method: Unary + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 13 + expectedResponse: + actualStatusCode: 500 + error: + code: 13 + details: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ConformancePayload.RequestInfo + requests: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 13 +- request: + testName: unary unavailable mapping + service: connectrpc.conformance.v1.ConformanceService + method: Unary + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 14 + expectedResponse: + actualStatusCode: 503 + error: + code: 14 + details: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ConformancePayload.RequestInfo + requests: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 14 +- request: + testName: unary data loss mapping + service: connectrpc.conformance.v1.ConformanceService + method: Unary + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 15 + expectedResponse: + actualStatusCode: 500 + error: + code: 15 + details: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ConformancePayload.RequestInfo + requests: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 15 +- request: + testName: unary unauthenticated mapping + service: connectrpc.conformance.v1.ConformanceService + method: Unary + streamType: STREAM_TYPE_UNARY + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 16 + expectedResponse: + actualStatusCode: 401 + error: + code: 16 + details: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ConformancePayload.RequestInfo + requests: + - "@type": type.googleapis.com/connectrpc.conformance.v1.UnaryRequest + responseDefinition: + error: + code: 16 +# Server Stream Tests --------------------------------------------------------- +- request: + testName: server stream error returns success http code + service: connectrpc.conformance.v1.ConformanceService + method: ServerStream + streamType: STREAM_TYPE_SERVER_STREAM + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest + responseDefinition: + error: + code: 1 + expectedResponse: + actualStatusCode: 200 + error: + code: 1 + details: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ConformancePayload.RequestInfo + requests: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ServerStreamRequest + responseDefinition: + error: + code: 1 +# Client Stream Tests ----------------------------------------------------------- +- request: + testName: client stream error returns success http code + service: connectrpc.conformance.v1.ConformanceService + method: ClientStream + streamType: STREAM_TYPE_CLIENT_STREAM + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest + responseDefinition: + error: + code: 13 + expectedResponse: + actualStatusCode: 200 + error: + code: 13 + details: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ConformancePayload.RequestInfo + requests: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ClientStreamRequest + responseDefinition: + error: + code: 13 +# Bidi Stream Tests ----------------------------------------------------------- +- request: + testName: bidi full duplex stream error returns success http code + service: connectrpc.conformance.v1.ConformanceService + method: BidiStream + streamType: STREAM_TYPE_FULL_DUPLEX_BIDI_STREAM + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest + responseDefinition: + error: + code: 13 + fullDuplex: true + expectedResponse: + actualStatusCode: 200 + error: + code: 13 + details: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ConformancePayload.RequestInfo + requests: + - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest + responseDefinition: + error: + code: 13 + fullDuplex: true +- request: + testName: bidi half duplex stream error returns success http code + service: connectrpc.conformance.v1.ConformanceService + method: BidiStream + streamType: STREAM_TYPE_HALF_DUPLEX_BIDI_STREAM + requestMessages: + - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest + responseDefinition: + error: + code: 13 + expectedResponse: + actualStatusCode: 200 + error: + code: 13 + details: + - "@type": type.googleapis.com/connectrpc.conformance.v1.ConformancePayload.RequestInfo + requests: + - "@type": type.googleapis.com/connectrpc.conformance.v1.BidiStreamRequest + responseDefinition: + error: + code: 13 From 42639999fc55e86d6fd3841f0a89ca411cf8414f Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Fri, 2 Feb 2024 16:21:53 -0500 Subject: [PATCH 30/40] Remove debug --- internal/app/referenceclient/impl.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/internal/app/referenceclient/impl.go b/internal/app/referenceclient/impl.go index 2ad5507e..040e0b37 100644 --- a/internal/app/referenceclient/impl.go +++ b/internal/app/referenceclient/impl.go @@ -21,7 +21,6 @@ import ( "io" "net/http" "net/url" - "os" "time" "connectrpc.com/conformance/internal" @@ -395,8 +394,6 @@ func (i *invoker) clientStream( actualStatusCode = wireDetails.StatusCode actualTrailers = wireDetails.Trailers connectErrorRaw = wireDetails.ConnectErrorRaw - } else { - fmt.Fprintf(os.Stderr, "WIRE DETAILS IS NILLLLLLLLLLLLLLLLLLLL%+v....", wireDetails) } return &v1.ClientResponseResult{ From ff034863ed44d4a20006db3d7703dddec343c93e Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Fri, 2 Feb 2024 20:29:26 -0500 Subject: [PATCH 31/40] Simplify --- internal/app/referenceclient/impl.go | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/internal/app/referenceclient/impl.go b/internal/app/referenceclient/impl.go index 040e0b37..32718531 100644 --- a/internal/app/referenceclient/impl.go +++ b/internal/app/referenceclient/impl.go @@ -422,9 +422,6 @@ func (i *invoker) bidiStream( stream := i.client.BidiStream(ctx) defer func() { - var actualStatusCode int32 - var connectErrorRaw *structpb.Struct - var actualTrailers []*v1.Header if result != nil { // Read headers and trailers from the stream result.ResponseHeaders = internal.ConvertToProtoHeader(stream.ResponseHeader()) @@ -432,13 +429,10 @@ func (i *invoker) bidiStream( wireDetails := getWireDetails(ctx) if wireDetails != nil { - actualStatusCode = wireDetails.StatusCode - actualTrailers = wireDetails.Trailers - connectErrorRaw = wireDetails.ConnectErrorRaw + result.ActualStatusCode = wireDetails.StatusCode + result.ActualHttpTrailers = wireDetails.Trailers + result.ConnectErrorRaw = wireDetails.ConnectErrorRaw } - result.ActualStatusCode = actualStatusCode - result.ActualHttpTrailers = actualTrailers - result.ConnectErrorRaw = connectErrorRaw } }() From 0906002268c6f5c9e48bf8d36e196fa161343784 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Tue, 6 Feb 2024 11:47:24 -0500 Subject: [PATCH 32/40] Feedback --- internal/app/referenceclient/impl.go | 112 ++++++------- internal/app/referenceclient/middleware.go | 66 -------- internal/app/referenceclient/tracer.go | 179 +++++++++++++-------- 3 files changed, 160 insertions(+), 197 deletions(-) delete mode 100644 internal/app/referenceclient/middleware.go diff --git a/internal/app/referenceclient/impl.go b/internal/app/referenceclient/impl.go index 32718531..b5945a4e 100644 --- a/internal/app/referenceclient/impl.go +++ b/internal/app/referenceclient/impl.go @@ -27,7 +27,6 @@ import ( v1 "connectrpc.com/conformance/internal/gen/proto/go/connectrpc/conformance/v1" "connectrpc.com/conformance/internal/gen/proto/go/connectrpc/conformance/v1/conformancev1connect" "connectrpc.com/connect" - "google.golang.org/protobuf/types/known/structpb" ) const clientName = "connectconformance-referenceclient" @@ -123,16 +122,6 @@ func (i *invoker) unary( // Invoke the Unary call resp, err := i.client.Unary(ctx, request) - var actualStatusCode int32 - var connectErrorRaw *structpb.Struct - var actualTrailers []*v1.Header - wireDetails := getWireDetails(ctx) - if wireDetails != nil { - actualStatusCode = wireDetails.StatusCode - actualTrailers = wireDetails.Trailers - connectErrorRaw = wireDetails.ConnectErrorRaw - } - if err != nil { // If an error was returned, first convert it to a Connect error // so that we can get the headers from the Meta property. Then, @@ -150,14 +139,19 @@ func (i *invoker) unary( } } + wireDetails, err := getWireDetails(ctx) + if err != nil { + return nil, err + } + return &v1.ClientResponseResult{ ResponseHeaders: headers, ResponseTrailers: trailers, Payloads: payloads, Error: protoErr, - ActualStatusCode: actualStatusCode, - ActualHttpTrailers: actualTrailers, - ConnectErrorRaw: connectErrorRaw, + ActualStatusCode: wireDetails.StatusCode, + ActualHttpTrailers: wireDetails.Trailers, + ConnectErrorRaw: wireDetails.ConnectErrorRaw, }, nil } @@ -189,16 +183,6 @@ func (i *invoker) idempotentUnary( // Invoke the Unary call resp, err := i.client.IdempotentUnary(ctx, request) - var actualStatusCode int32 - var connectErrorRaw *structpb.Struct - var actualTrailers []*v1.Header - wireDetails := getWireDetails(ctx) - if wireDetails != nil { - actualStatusCode = wireDetails.StatusCode - actualTrailers = wireDetails.Trailers - connectErrorRaw = wireDetails.ConnectErrorRaw - } - if err != nil { // If an error was returned, first convert it to a Connect error // so that we can get the headers from the Meta property. Then, @@ -214,14 +198,18 @@ func (i *invoker) idempotentUnary( trailers = internal.ConvertToProtoHeader(resp.Trailer()) } + wireDetails, err := getWireDetails(ctx) + if err != nil { + return nil, err + } return &v1.ClientResponseResult{ ResponseHeaders: headers, ResponseTrailers: trailers, Payloads: payloads, Error: protoErr, - ActualStatusCode: actualStatusCode, - ActualHttpTrailers: actualTrailers, - ConnectErrorRaw: connectErrorRaw, + ActualStatusCode: wireDetails.StatusCode, + ActualHttpTrailers: wireDetails.Trailers, + ConnectErrorRaw: wireDetails.ConnectErrorRaw, }, nil } @@ -305,14 +293,10 @@ func (i *invoker) serverStream( protoErr = internal.ConvertErrorToProtoError(err) } } - var actualStatusCode int32 - var connectErrorRaw *structpb.Struct - var actualTrailers []*v1.Header - wireDetails := getWireDetails(ctx) - if wireDetails != nil { - actualStatusCode = wireDetails.StatusCode - actualTrailers = wireDetails.Trailers - connectErrorRaw = wireDetails.ConnectErrorRaw + + wireDetails, err := getWireDetails(ctx) + if err != nil { + return nil, err } return &v1.ClientResponseResult{ @@ -320,9 +304,9 @@ func (i *invoker) serverStream( ResponseTrailers: trailers, Payloads: payloads, Error: protoErr, - ActualStatusCode: actualStatusCode, - ActualHttpTrailers: actualTrailers, - ConnectErrorRaw: connectErrorRaw, + ActualStatusCode: wireDetails.StatusCode, + ActualHttpTrailers: wireDetails.Trailers, + ConnectErrorRaw: wireDetails.ConnectErrorRaw, }, nil } @@ -386,14 +370,10 @@ func (i *invoker) clientStream( headers = internal.ConvertToProtoHeader(resp.Header()) trailers = internal.ConvertToProtoHeader(resp.Trailer()) } - var actualStatusCode int32 - var connectErrorRaw *structpb.Struct - var actualTrailers []*v1.Header - wireDetails := getWireDetails(ctx) - if wireDetails != nil { - actualStatusCode = wireDetails.StatusCode - actualTrailers = wireDetails.Trailers - connectErrorRaw = wireDetails.ConnectErrorRaw + + wireDetails, err := getWireDetails(ctx) + if err != nil { + return nil, err } return &v1.ClientResponseResult{ @@ -401,16 +381,16 @@ func (i *invoker) clientStream( ResponseTrailers: trailers, Payloads: payloads, Error: protoErr, - ActualStatusCode: actualStatusCode, - ActualHttpTrailers: actualTrailers, - ConnectErrorRaw: connectErrorRaw, + ActualStatusCode: wireDetails.StatusCode, + ActualHttpTrailers: wireDetails.Trailers, + ConnectErrorRaw: wireDetails.ConnectErrorRaw, }, nil } func (i *invoker) bidiStream( ctx context.Context, req *v1.ClientCompatRequest, -) (result *v1.ClientResponseResult, _ error) { +) (result *v1.ClientResponseResult, err error) { ctx, cancel := context.WithCancel(ctx) defer cancel() @@ -422,17 +402,20 @@ func (i *invoker) bidiStream( stream := i.client.BidiStream(ctx) defer func() { + wireDetails, e := getWireDetails(ctx) + if e != nil { + result = nil + err = e + return + } if result != nil { // Read headers and trailers from the stream result.ResponseHeaders = internal.ConvertToProtoHeader(stream.ResponseHeader()) result.ResponseTrailers = internal.ConvertToProtoHeader(stream.ResponseTrailer()) - wireDetails := getWireDetails(ctx) - if wireDetails != nil { - result.ActualStatusCode = wireDetails.StatusCode - result.ActualHttpTrailers = wireDetails.Trailers - result.ConnectErrorRaw = wireDetails.ConnectErrorRaw - } + result.ActualStatusCode = wireDetails.StatusCode + result.ActualHttpTrailers = wireDetails.Trailers + result.ConnectErrorRaw = wireDetails.ConnectErrorRaw } }() @@ -562,20 +545,15 @@ func (i *invoker) unimplemented( // Invoke the Unary call _, err := i.client.Unimplemented(ctx, request) - var actualStatusCode int32 - var connectErrorRaw *structpb.Struct - var actualTrailers []*v1.Header - wireDetails := getWireDetails(ctx) - if wireDetails != nil { - actualStatusCode = wireDetails.StatusCode - actualTrailers = wireDetails.Trailers - connectErrorRaw = wireDetails.ConnectErrorRaw + wireDetails, err := getWireDetails(ctx) + if err != nil { + return nil, err } return &v1.ClientResponseResult{ Error: internal.ConvertErrorToProtoError(err), - ActualStatusCode: actualStatusCode, - ActualHttpTrailers: actualTrailers, - ConnectErrorRaw: connectErrorRaw, + ActualStatusCode: wireDetails.StatusCode, + ActualHttpTrailers: wireDetails.Trailers, + ConnectErrorRaw: wireDetails.ConnectErrorRaw, }, nil } diff --git a/internal/app/referenceclient/middleware.go b/internal/app/referenceclient/middleware.go deleted file mode 100644 index 5039a6cf..00000000 --- a/internal/app/referenceclient/middleware.go +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2023-2024 The Connect Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package referenceclient - -import ( - "io" - "net/http" - - "connectrpc.com/conformance/internal/tracer" -) - -type wireInterceptor struct { - Transport http.RoundTripper -} - -// RoundTrip replaces the response body with a wireReader which captures bytes -// as they are read. -func (w *wireInterceptor) RoundTrip(req *http.Request) (*http.Response, error) { - resp, err := w.Transport.RoundTrip(req) - wrapper, ok := req.Context().Value(wireCtxKey{}).(*wireWrapper) - if err != nil || !ok { - return resp, err - } - resp.Body = &wireReader{body: resp.Body, wrapper: wrapper} - return resp, nil -} - -// newWireInterceptor creates a new wireInterceptor which wraps the given transport -// in a TracingRoundTripper. -func newWireInterceptor(transport http.RoundTripper, trace *tracer.Tracer) http.RoundTripper { - return &wireInterceptor{ - Transport: tracer.TracingRoundTripper(transport, &wireTracer{ - tracer: trace, - }), - } -} - -type wireReader struct { - body io.ReadCloser - wrapper *wireWrapper -} - -func (w *wireReader) Read(p []byte) (int, error) { - n, err := w.body.Read(p) - - // Capture bytes as they are read - w.wrapper.buf.Write(p[:n]) - - return n, err -} - -func (w *wireReader) Close() error { - return w.body.Close() -} diff --git a/internal/app/referenceclient/tracer.go b/internal/app/referenceclient/tracer.go index 03c34bfd..8d5c5175 100644 --- a/internal/app/referenceclient/tracer.go +++ b/internal/app/referenceclient/tracer.go @@ -18,7 +18,9 @@ import ( "bytes" "context" "encoding/json" + "errors" "io" + "net/http" "strings" "sync/atomic" @@ -43,8 +45,57 @@ type wireDetails struct { ConnectErrorRaw *structpb.Struct } +type wireInterceptor struct { + Transport http.RoundTripper +} + +// newWireInterceptor creates a new wireInterceptor which wraps the given transport +// in a TracingRoundTripper. +func newWireInterceptor(transport http.RoundTripper, trace *tracer.Tracer) http.RoundTripper { + return &wireInterceptor{ + Transport: tracer.TracingRoundTripper(transport, &wireTracer{ + tracer: trace, + }), + } +} + +// RoundTrip replaces the response body with a wireReader which captures bytes +// as they are read. +func (w *wireInterceptor) RoundTrip(req *http.Request) (*http.Response, error) { + resp, err := w.Transport.RoundTrip(req) + wrapper, ok := req.Context().Value(wireCtxKey{}).(*wireWrapper) + if err != nil || !ok { + return resp, err + } + // If this is a unary error with JSON body, replace the body with a reader + // that will save off the body bytes as they are read so that we can access + // the body contents in the tracer + if resp.StatusCode != 200 && resp.Header.Get("content-type") == "application/json" { + resp.Body = &wireReader{body: resp.Body, wrapper: wrapper} + } + return resp, nil +} + +type wireReader struct { + body io.ReadCloser + wrapper *wireWrapper +} + +func (w *wireReader) Read(p []byte) (int, error) { + n, err := w.body.Read(p) + + // Capture bytes as they are read + w.wrapper.buf.Write(p[:n]) + + return n, err +} + +func (w *wireReader) Close() error { + return w.body.Close() +} + type wireWrapper struct { - val atomic.Pointer[wireDetails] + val atomic.Pointer[tracer.Trace] // buf represents the read response body buf *bytes.Buffer } @@ -57,22 +108,76 @@ func withWireCapture(ctx context.Context) context.Context { }) } -// setWireDetails sets the given wire details in the given context. -func setWireDetails(ctx context.Context, details *wireDetails) { +// setWireTrace sets the given trace in the given context. +func setWireTrace(ctx context.Context, trace *tracer.Trace) { wrapper, ok := ctx.Value(wireCtxKey{}).(*wireWrapper) if !ok { return } - wrapper.val.Store(details) + wrapper.val.Store(trace) } -// getWireDetails returns the wire details from the given context. -func getWireDetails(ctx context.Context) *wireDetails { +// getWireDetails returns the wire details from the trace in the given context. +func getWireDetails(ctx context.Context) (*wireDetails, error) { wrapper, ok := ctx.Value(wireCtxKey{}).(*wireWrapper) if !ok { - return nil + return nil, errors.New("wireWrapper not found in context") + } + trace := wrapper.val.Load() + if trace.Response == nil { + return nil, errors.New("trace response was nil") } - return wrapper.val.Load() + statusCode := int32(trace.Response.StatusCode) + + var jsonRaw structpb.Struct + contentType := trace.Response.Header.Get("content-type") + if contentType == "application/json" { + if statusCode != 200 { + // If this is a unary request, then use the entire response body + // as the wire error details. + body, err := io.ReadAll(wrapper.buf) + if err != nil { + return nil, err + } + if err := protojson.Unmarshal(body, &jsonRaw); err != nil { + return nil, err + } + } + } else if strings.HasPrefix(contentType, "application/connect+") { + type endStreamError struct { + Error json.RawMessage `json:"error"` + } + // If this is a streaming request, then look through the trace events + // for the ResponseBodyEndStream event and parse its content into an + // endStreamError to see if there are any error details. + for _, ev := range trace.Events { + switch eventType := ev.(type) { + case *tracer.ResponseBodyEndStream: + var endStream endStreamError + if err := json.Unmarshal([]byte(eventType.Content), &endStream); err != nil { + return nil, err + } + // If we unmarshalled any bytes into endStream.Error, then unmarshal _that_ + // into a Struct + if len(endStream.Error) > 0 { + if err := protojson.Unmarshal(endStream.Error, &jsonRaw); err != nil { + return nil, err + } + } + default: + // Do nothing + } + } + } + + wire := &wireDetails{ + StatusCode: statusCode, + Trailers: internal.ConvertToProtoHeader(trace.Response.Trailer), + ConnectErrorRaw: &jsonRaw, + } + + return wire, nil + } type wireTracer struct { @@ -83,63 +188,9 @@ type wireTracer struct { // from the passed trace. The wire details will be stored in the context acquired by // withWireCapture and can be retrieved via getWireDetails. func (t *wireTracer) Complete(trace tracer.Trace) { - wrapper, ok := trace.Request.Context().Value(wireCtxKey{}).(*wireWrapper) - if ok { //nolint:nestif - if trace.Response != nil { - statusCode := int32(trace.Response.StatusCode) - - var jsonRaw structpb.Struct - contentType := trace.Response.Header.Get("content-type") - if contentType == "application/json" { - if statusCode != 200 { - // If this is a unary request, then use the entire response body - // as the wire error details. - body, err := io.ReadAll(wrapper.buf) - if err != nil { - return - } - if err := protojson.Unmarshal(body, &jsonRaw); err != nil { - return - } - } - } else if strings.HasPrefix(contentType, "application/connect+") { - type endStreamError struct { - Error json.RawMessage `json:"error"` - } - // If this is a streaming request, then look through the trace events - // for the ResponseBodyEndStream event and parse its content into an - // endStreamError to see if there are any error details. - for _, ev := range trace.Events { - switch eventType := ev.(type) { - case *tracer.ResponseBodyEndStream: - var endStream endStreamError - if err := json.Unmarshal([]byte(eventType.Content), &endStream); err != nil { - return - } - // If we unmarshalled any bytes into endStream.Error, then unmarshal _that_ - // into a Struct - if len(endStream.Error) > 0 { - if err := protojson.Unmarshal(endStream.Error, &jsonRaw); err != nil { - return - } - } - default: - // Do nothing - } - } - } - - wire := &wireDetails{ - StatusCode: statusCode, - Trailers: internal.ConvertToProtoHeader(trace.Response.Trailer), - ConnectErrorRaw: &jsonRaw, - } - - setWireDetails(trace.Request.Context(), wire) - } - } + setWireTrace(trace.Request.Context(), &trace) - if t != nil { + if t.tracer != nil { t.tracer.Complete(trace) } } From aa0a862bcd5647d88c8825848b5f3d509003ea30 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Tue, 6 Feb 2024 13:06:56 -0500 Subject: [PATCH 33/40] Impl --- internal/app/referenceclient/impl.go | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/internal/app/referenceclient/impl.go b/internal/app/referenceclient/impl.go index b5945a4e..f21e5c31 100644 --- a/internal/app/referenceclient/impl.go +++ b/internal/app/referenceclient/impl.go @@ -402,20 +402,22 @@ func (i *invoker) bidiStream( stream := i.client.BidiStream(ctx) defer func() { - wireDetails, e := getWireDetails(ctx) - if e != nil { - result = nil - err = e - return - } if result != nil { + if err == nil { + wireDetails, e := getWireDetails(ctx) + if e != nil { + result = nil + err = e + return + } + result.ActualStatusCode = wireDetails.StatusCode + result.ActualHttpTrailers = wireDetails.Trailers + result.ConnectErrorRaw = wireDetails.ConnectErrorRaw + } // Read headers and trailers from the stream result.ResponseHeaders = internal.ConvertToProtoHeader(stream.ResponseHeader()) result.ResponseTrailers = internal.ConvertToProtoHeader(stream.ResponseTrailer()) - result.ActualStatusCode = wireDetails.StatusCode - result.ActualHttpTrailers = wireDetails.Trailers - result.ConnectErrorRaw = wireDetails.ConnectErrorRaw } }() From 67fe5d294f0973c669f4b2652148c9abcff564de Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Tue, 6 Feb 2024 15:29:31 -0500 Subject: [PATCH 34/40] Wire Details --- .../conformance/v1/client_compat_pb.d.ts | 50 ++- .../conformance/v1/client_compat_pb.js | 386 +++++++++++++----- internal/app/connectconformance/results.go | 14 +- .../testsuites/connect_to_http_code.yaml | 60 ++- internal/app/grpcclient/impl.go | 13 +- internal/app/referenceclient/impl.go | 62 ++- internal/app/referenceclient/tracer.go | 30 +- .../conformance/v1/client_compat.pb.go | 278 ++++++++----- .../conformance/v1/client_compat.proto | 30 +- 9 files changed, 589 insertions(+), 334 deletions(-) diff --git a/grpcwebclient/gen/proto/connectrpc/conformance/v1/client_compat_pb.d.ts b/grpcwebclient/gen/proto/connectrpc/conformance/v1/client_compat_pb.d.ts index d17ba665..e861f2de 100644 --- a/grpcwebclient/gen/proto/connectrpc/conformance/v1/client_compat_pb.d.ts +++ b/grpcwebclient/gen/proto/connectrpc/conformance/v1/client_compat_pb.d.ts @@ -267,18 +267,10 @@ export class ClientResponseResult extends jspb.Message { getNumUnsentRequests(): number; setNumUnsentRequests(value: number): ClientResponseResult; - getActualStatusCode(): number; - setActualStatusCode(value: number): ClientResponseResult; - - getConnectErrorRaw(): google_protobuf_struct_pb.Struct | undefined; - setConnectErrorRaw(value?: google_protobuf_struct_pb.Struct): ClientResponseResult; - hasConnectErrorRaw(): boolean; - clearConnectErrorRaw(): ClientResponseResult; - - getActualHttpTrailersList(): Array; - setActualHttpTrailersList(value: Array): ClientResponseResult; - clearActualHttpTrailersList(): ClientResponseResult; - addActualHttpTrailers(value?: connectrpc_conformance_v1_service_pb.Header, index?: number): connectrpc_conformance_v1_service_pb.Header; + getWireDetails(): WireDetails | undefined; + setWireDetails(value?: WireDetails): ClientResponseResult; + hasWireDetails(): boolean; + clearWireDetails(): ClientResponseResult; serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): ClientResponseResult.AsObject; @@ -295,9 +287,7 @@ export namespace ClientResponseResult { error?: connectrpc_conformance_v1_service_pb.Error.AsObject, responseTrailersList: Array, numUnsentRequests: number, - actualStatusCode: number, - connectErrorRaw?: google_protobuf_struct_pb.Struct.AsObject, - actualHttpTrailersList: Array, + wireDetails?: WireDetails.AsObject, } } @@ -319,3 +309,33 @@ export namespace ClientErrorResult { } } +export class WireDetails extends jspb.Message { + getActualStatusCode(): number; + setActualStatusCode(value: number): WireDetails; + + getConnectErrorRaw(): google_protobuf_struct_pb.Struct | undefined; + setConnectErrorRaw(value?: google_protobuf_struct_pb.Struct): WireDetails; + hasConnectErrorRaw(): boolean; + clearConnectErrorRaw(): WireDetails; + + getActualHttpTrailersList(): Array; + setActualHttpTrailersList(value: Array): WireDetails; + clearActualHttpTrailersList(): WireDetails; + addActualHttpTrailers(value?: connectrpc_conformance_v1_service_pb.Header, index?: number): connectrpc_conformance_v1_service_pb.Header; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): WireDetails.AsObject; + static toObject(includeInstance: boolean, msg: WireDetails): WireDetails.AsObject; + static serializeBinaryToWriter(message: WireDetails, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): WireDetails; + static deserializeBinaryFromReader(message: WireDetails, reader: jspb.BinaryReader): WireDetails; +} + +export namespace WireDetails { + export type AsObject = { + actualStatusCode: number, + connectErrorRaw?: google_protobuf_struct_pb.Struct.AsObject, + actualHttpTrailersList: Array, + } +} + diff --git a/grpcwebclient/gen/proto/connectrpc/conformance/v1/client_compat_pb.js b/grpcwebclient/gen/proto/connectrpc/conformance/v1/client_compat_pb.js index 3d2c5c6a..13f84e15 100644 --- a/grpcwebclient/gen/proto/connectrpc/conformance/v1/client_compat_pb.js +++ b/grpcwebclient/gen/proto/connectrpc/conformance/v1/client_compat_pb.js @@ -53,6 +53,7 @@ goog.exportSymbol('proto.connectrpc.conformance.v1.ClientCompatResponse', null, goog.exportSymbol('proto.connectrpc.conformance.v1.ClientCompatResponse.ResultCase', null, global); goog.exportSymbol('proto.connectrpc.conformance.v1.ClientErrorResult', null, global); goog.exportSymbol('proto.connectrpc.conformance.v1.ClientResponseResult', null, global); +goog.exportSymbol('proto.connectrpc.conformance.v1.WireDetails', null, global); /** * Generated by JsPbCodeGenerator. * @param {Array=} opt_data Optional initial data array, typically from a @@ -179,6 +180,27 @@ if (goog.DEBUG && !COMPILED) { */ proto.connectrpc.conformance.v1.ClientErrorResult.displayName = 'proto.connectrpc.conformance.v1.ClientErrorResult'; } +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.connectrpc.conformance.v1.WireDetails = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, proto.connectrpc.conformance.v1.WireDetails.repeatedFields_, null); +}; +goog.inherits(proto.connectrpc.conformance.v1.WireDetails, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.connectrpc.conformance.v1.WireDetails.displayName = 'proto.connectrpc.conformance.v1.WireDetails'; +} /** * List of repeated fields within this message type. @@ -1839,7 +1861,7 @@ proto.connectrpc.conformance.v1.ClientCompatResponse.prototype.clearFeedbackList * @private {!Array} * @const */ -proto.connectrpc.conformance.v1.ClientResponseResult.repeatedFields_ = [1,2,4,8]; +proto.connectrpc.conformance.v1.ClientResponseResult.repeatedFields_ = [1,2,4]; @@ -1880,10 +1902,7 @@ proto.connectrpc.conformance.v1.ClientResponseResult.toObject = function(include responseTrailersList: jspb.Message.toObjectList(msg.getResponseTrailersList(), connectrpc_conformance_v1_service_pb.Header.toObject, includeInstance), numUnsentRequests: jspb.Message.getFieldWithDefault(msg, 5, 0), - actualStatusCode: jspb.Message.getFieldWithDefault(msg, 6, 0), - connectErrorRaw: (f = msg.getConnectErrorRaw()) && google_protobuf_struct_pb.Struct.toObject(includeInstance, f), - actualHttpTrailersList: jspb.Message.toObjectList(msg.getActualHttpTrailersList(), - connectrpc_conformance_v1_service_pb.Header.toObject, includeInstance) + wireDetails: (f = msg.getWireDetails()) && proto.connectrpc.conformance.v1.WireDetails.toObject(includeInstance, f) }; if (includeInstance) { @@ -1945,18 +1964,9 @@ proto.connectrpc.conformance.v1.ClientResponseResult.deserializeBinaryFromReader msg.setNumUnsentRequests(value); break; case 6: - var value = /** @type {number} */ (reader.readInt32()); - msg.setActualStatusCode(value); - break; - case 7: - var value = new google_protobuf_struct_pb.Struct; - reader.readMessage(value,google_protobuf_struct_pb.Struct.deserializeBinaryFromReader); - msg.setConnectErrorRaw(value); - break; - case 8: - var value = new connectrpc_conformance_v1_service_pb.Header; - reader.readMessage(value,connectrpc_conformance_v1_service_pb.Header.deserializeBinaryFromReader); - msg.addActualHttpTrailers(value); + var value = new proto.connectrpc.conformance.v1.WireDetails; + reader.readMessage(value,proto.connectrpc.conformance.v1.WireDetails.deserializeBinaryFromReader); + msg.setWireDetails(value); break; default: reader.skipField(); @@ -2026,27 +2036,12 @@ proto.connectrpc.conformance.v1.ClientResponseResult.serializeBinaryToWriter = f f ); } - f = message.getActualStatusCode(); - if (f !== 0) { - writer.writeInt32( - 6, - f - ); - } - f = message.getConnectErrorRaw(); + f = message.getWireDetails(); if (f != null) { writer.writeMessage( - 7, - f, - google_protobuf_struct_pb.Struct.serializeBinaryToWriter - ); - } - f = message.getActualHttpTrailersList(); - if (f.length > 0) { - writer.writeRepeatedMessage( - 8, + 6, f, - connectrpc_conformance_v1_service_pb.Header.serializeBinaryToWriter + proto.connectrpc.conformance.v1.WireDetails.serializeBinaryToWriter ); } }; @@ -2222,39 +2217,21 @@ proto.connectrpc.conformance.v1.ClientResponseResult.prototype.setNumUnsentReque /** - * optional int32 actual_status_code = 6; - * @return {number} + * optional WireDetails wire_details = 6; + * @return {?proto.connectrpc.conformance.v1.WireDetails} */ -proto.connectrpc.conformance.v1.ClientResponseResult.prototype.getActualStatusCode = function() { - return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 6, 0)); +proto.connectrpc.conformance.v1.ClientResponseResult.prototype.getWireDetails = function() { + return /** @type{?proto.connectrpc.conformance.v1.WireDetails} */ ( + jspb.Message.getWrapperField(this, proto.connectrpc.conformance.v1.WireDetails, 6)); }; /** - * @param {number} value - * @return {!proto.connectrpc.conformance.v1.ClientResponseResult} returns this - */ -proto.connectrpc.conformance.v1.ClientResponseResult.prototype.setActualStatusCode = function(value) { - return jspb.Message.setProto3IntField(this, 6, value); -}; - - -/** - * optional google.protobuf.Struct connect_error_raw = 7; - * @return {?proto.google.protobuf.Struct} - */ -proto.connectrpc.conformance.v1.ClientResponseResult.prototype.getConnectErrorRaw = function() { - return /** @type{?proto.google.protobuf.Struct} */ ( - jspb.Message.getWrapperField(this, google_protobuf_struct_pb.Struct, 7)); -}; - - -/** - * @param {?proto.google.protobuf.Struct|undefined} value + * @param {?proto.connectrpc.conformance.v1.WireDetails|undefined} value * @return {!proto.connectrpc.conformance.v1.ClientResponseResult} returns this */ -proto.connectrpc.conformance.v1.ClientResponseResult.prototype.setConnectErrorRaw = function(value) { - return jspb.Message.setWrapperField(this, 7, value); +proto.connectrpc.conformance.v1.ClientResponseResult.prototype.setWireDetails = function(value) { + return jspb.Message.setWrapperField(this, 6, value); }; @@ -2262,8 +2239,8 @@ proto.connectrpc.conformance.v1.ClientResponseResult.prototype.setConnectErrorRa * Clears the message field making it undefined. * @return {!proto.connectrpc.conformance.v1.ClientResponseResult} returns this */ -proto.connectrpc.conformance.v1.ClientResponseResult.prototype.clearConnectErrorRaw = function() { - return this.setConnectErrorRaw(undefined); +proto.connectrpc.conformance.v1.ClientResponseResult.prototype.clearWireDetails = function() { + return this.setWireDetails(undefined); }; @@ -2271,46 +2248,8 @@ proto.connectrpc.conformance.v1.ClientResponseResult.prototype.clearConnectError * Returns whether this field is set. * @return {boolean} */ -proto.connectrpc.conformance.v1.ClientResponseResult.prototype.hasConnectErrorRaw = function() { - return jspb.Message.getField(this, 7) != null; -}; - - -/** - * repeated Header actual_http_trailers = 8; - * @return {!Array} - */ -proto.connectrpc.conformance.v1.ClientResponseResult.prototype.getActualHttpTrailersList = function() { - return /** @type{!Array} */ ( - jspb.Message.getRepeatedWrapperField(this, connectrpc_conformance_v1_service_pb.Header, 8)); -}; - - -/** - * @param {!Array} value - * @return {!proto.connectrpc.conformance.v1.ClientResponseResult} returns this -*/ -proto.connectrpc.conformance.v1.ClientResponseResult.prototype.setActualHttpTrailersList = function(value) { - return jspb.Message.setRepeatedWrapperField(this, 8, value); -}; - - -/** - * @param {!proto.connectrpc.conformance.v1.Header=} opt_value - * @param {number=} opt_index - * @return {!proto.connectrpc.conformance.v1.Header} - */ -proto.connectrpc.conformance.v1.ClientResponseResult.prototype.addActualHttpTrailers = function(opt_value, opt_index) { - return jspb.Message.addToRepeatedWrapperField(this, 8, opt_value, proto.connectrpc.conformance.v1.Header, opt_index); -}; - - -/** - * Clears the list making it empty but non-null. - * @return {!proto.connectrpc.conformance.v1.ClientResponseResult} returns this - */ -proto.connectrpc.conformance.v1.ClientResponseResult.prototype.clearActualHttpTrailersList = function() { - return this.setActualHttpTrailersList([]); +proto.connectrpc.conformance.v1.ClientResponseResult.prototype.hasWireDetails = function() { + return jspb.Message.getField(this, 6) != null; }; @@ -2444,4 +2383,245 @@ proto.connectrpc.conformance.v1.ClientErrorResult.prototype.setMessage = functio }; + +/** + * List of repeated fields within this message type. + * @private {!Array} + * @const + */ +proto.connectrpc.conformance.v1.WireDetails.repeatedFields_ = [3]; + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.connectrpc.conformance.v1.WireDetails.prototype.toObject = function(opt_includeInstance) { + return proto.connectrpc.conformance.v1.WireDetails.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.connectrpc.conformance.v1.WireDetails} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.connectrpc.conformance.v1.WireDetails.toObject = function(includeInstance, msg) { + var f, obj = { + actualStatusCode: jspb.Message.getFieldWithDefault(msg, 1, 0), + connectErrorRaw: (f = msg.getConnectErrorRaw()) && google_protobuf_struct_pb.Struct.toObject(includeInstance, f), + actualHttpTrailersList: jspb.Message.toObjectList(msg.getActualHttpTrailersList(), + connectrpc_conformance_v1_service_pb.Header.toObject, includeInstance) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.connectrpc.conformance.v1.WireDetails} + */ +proto.connectrpc.conformance.v1.WireDetails.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.connectrpc.conformance.v1.WireDetails; + return proto.connectrpc.conformance.v1.WireDetails.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.connectrpc.conformance.v1.WireDetails} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.connectrpc.conformance.v1.WireDetails} + */ +proto.connectrpc.conformance.v1.WireDetails.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {number} */ (reader.readInt32()); + msg.setActualStatusCode(value); + break; + case 2: + var value = new google_protobuf_struct_pb.Struct; + reader.readMessage(value,google_protobuf_struct_pb.Struct.deserializeBinaryFromReader); + msg.setConnectErrorRaw(value); + break; + case 3: + var value = new connectrpc_conformance_v1_service_pb.Header; + reader.readMessage(value,connectrpc_conformance_v1_service_pb.Header.deserializeBinaryFromReader); + msg.addActualHttpTrailers(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.connectrpc.conformance.v1.WireDetails.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.connectrpc.conformance.v1.WireDetails.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.connectrpc.conformance.v1.WireDetails} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.connectrpc.conformance.v1.WireDetails.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getActualStatusCode(); + if (f !== 0) { + writer.writeInt32( + 1, + f + ); + } + f = message.getConnectErrorRaw(); + if (f != null) { + writer.writeMessage( + 2, + f, + google_protobuf_struct_pb.Struct.serializeBinaryToWriter + ); + } + f = message.getActualHttpTrailersList(); + if (f.length > 0) { + writer.writeRepeatedMessage( + 3, + f, + connectrpc_conformance_v1_service_pb.Header.serializeBinaryToWriter + ); + } +}; + + +/** + * optional int32 actual_status_code = 1; + * @return {number} + */ +proto.connectrpc.conformance.v1.WireDetails.prototype.getActualStatusCode = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 1, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.connectrpc.conformance.v1.WireDetails} returns this + */ +proto.connectrpc.conformance.v1.WireDetails.prototype.setActualStatusCode = function(value) { + return jspb.Message.setProto3IntField(this, 1, value); +}; + + +/** + * optional google.protobuf.Struct connect_error_raw = 2; + * @return {?proto.google.protobuf.Struct} + */ +proto.connectrpc.conformance.v1.WireDetails.prototype.getConnectErrorRaw = function() { + return /** @type{?proto.google.protobuf.Struct} */ ( + jspb.Message.getWrapperField(this, google_protobuf_struct_pb.Struct, 2)); +}; + + +/** + * @param {?proto.google.protobuf.Struct|undefined} value + * @return {!proto.connectrpc.conformance.v1.WireDetails} returns this +*/ +proto.connectrpc.conformance.v1.WireDetails.prototype.setConnectErrorRaw = function(value) { + return jspb.Message.setWrapperField(this, 2, value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.connectrpc.conformance.v1.WireDetails} returns this + */ +proto.connectrpc.conformance.v1.WireDetails.prototype.clearConnectErrorRaw = function() { + return this.setConnectErrorRaw(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.connectrpc.conformance.v1.WireDetails.prototype.hasConnectErrorRaw = function() { + return jspb.Message.getField(this, 2) != null; +}; + + +/** + * repeated Header actual_http_trailers = 3; + * @return {!Array} + */ +proto.connectrpc.conformance.v1.WireDetails.prototype.getActualHttpTrailersList = function() { + return /** @type{!Array} */ ( + jspb.Message.getRepeatedWrapperField(this, connectrpc_conformance_v1_service_pb.Header, 3)); +}; + + +/** + * @param {!Array} value + * @return {!proto.connectrpc.conformance.v1.WireDetails} returns this +*/ +proto.connectrpc.conformance.v1.WireDetails.prototype.setActualHttpTrailersList = function(value) { + return jspb.Message.setRepeatedWrapperField(this, 3, value); +}; + + +/** + * @param {!proto.connectrpc.conformance.v1.Header=} opt_value + * @param {number=} opt_index + * @return {!proto.connectrpc.conformance.v1.Header} + */ +proto.connectrpc.conformance.v1.WireDetails.prototype.addActualHttpTrailers = function(opt_value, opt_index) { + return jspb.Message.addToRepeatedWrapperField(this, 3, opt_value, proto.connectrpc.conformance.v1.Header, opt_index); +}; + + +/** + * Clears the list making it empty but non-null. + * @return {!proto.connectrpc.conformance.v1.WireDetails} returns this + */ +proto.connectrpc.conformance.v1.WireDetails.prototype.clearActualHttpTrailersList = function() { + return this.setActualHttpTrailersList([]); +}; + + goog.object.extend(exports, proto.connectrpc.conformance.v1); diff --git a/internal/app/connectconformance/results.go b/internal/app/connectconformance/results.go index 01ca6ab4..b8f6cc0a 100644 --- a/internal/app/connectconformance/results.go +++ b/internal/app/connectconformance/results.go @@ -191,18 +191,14 @@ func (r *testResults) assert(testCase string, expected, actual *conformancev1.Cl errs = append(errs, checkPayloads(expected.Payloads, actual.Payloads)...) errs = append(errs, checkError(expected.Error, actual.Error)...) - // If client didn't provide actual raw error, we skip this check. - if expected.ConnectErrorRaw != nil && actual.ConnectErrorRaw != nil { - diff := cmp.Diff(expected.ConnectErrorRaw, actual.ConnectErrorRaw, protocmp.Transform()) - if diff != "" { + expectedWire := expected.WireDetails + actualWire := actual.WireDetails + if expectedWire != nil && actualWire != nil { + if diff := cmp.Diff(expectedWire.ConnectErrorRaw, actualWire.ConnectErrorRaw, protocmp.Transform()); diff != "" { errs = append(errs, fmt.Errorf("raw Connect error does not match: - wanted, + got\n%s", diff)) } - } - // If client didn't provide an actual status code, we skip this check. - if expected.ActualStatusCode != 0 && actual.ActualStatusCode != 0 { - diff := cmp.Diff(expected.ActualStatusCode, actual.ActualStatusCode, protocmp.Transform()) - if diff != "" { + if diff := cmp.Diff(expectedWire.ActualStatusCode, actualWire.ActualStatusCode, protocmp.Transform()); diff != "" { errs = append(errs, fmt.Errorf("actual HTTP status code does not match: - wanted, + got\n%s", diff)) } } diff --git a/internal/app/connectconformance/testsuites/connect_to_http_code.yaml b/internal/app/connectconformance/testsuites/connect_to_http_code.yaml index 12a835d8..563a37f4 100644 --- a/internal/app/connectconformance/testsuites/connect_to_http_code.yaml +++ b/internal/app/connectconformance/testsuites/connect_to_http_code.yaml @@ -16,7 +16,8 @@ testCases: error: code: 1 expectedResponse: - actualStatusCode: 408 + wireDetails: + actualStatusCode: 408 error: code: 1 details: @@ -37,7 +38,8 @@ testCases: error: code: 2 expectedResponse: - actualStatusCode: 500 + wireDetails: + actualStatusCode: 500 error: code: 2 details: @@ -58,7 +60,8 @@ testCases: error: code: 3 expectedResponse: - actualStatusCode: 400 + wireDetails: + actualStatusCode: 400 error: code: 3 details: @@ -79,7 +82,8 @@ testCases: error: code: 4 expectedResponse: - actualStatusCode: 408 + wireDetails: + actualStatusCode: 408 error: code: 4 details: @@ -100,7 +104,8 @@ testCases: error: code: 5 expectedResponse: - actualStatusCode: 404 + wireDetails: + actualStatusCode: 404 error: code: 5 details: @@ -121,7 +126,8 @@ testCases: error: code: 6 expectedResponse: - actualStatusCode: 409 + wireDetails: + actualStatusCode: 409 error: code: 6 details: @@ -142,7 +148,8 @@ testCases: error: code: 7 expectedResponse: - actualStatusCode: 403 + wireDetails: + actualStatusCode: 403 error: code: 7 details: @@ -163,7 +170,8 @@ testCases: error: code: 8 expectedResponse: - actualStatusCode: 429 + wireDetails: + actualStatusCode: 429 error: code: 8 details: @@ -184,7 +192,8 @@ testCases: error: code: 9 expectedResponse: - actualStatusCode: 412 + wireDetails: + actualStatusCode: 412 error: code: 9 details: @@ -205,7 +214,8 @@ testCases: error: code: 10 expectedResponse: - actualStatusCode: 409 + wireDetails: + actualStatusCode: 409 error: code: 10 details: @@ -226,7 +236,8 @@ testCases: error: code: 11 expectedResponse: - actualStatusCode: 400 + wireDetails: + actualStatusCode: 400 error: code: 11 details: @@ -247,7 +258,8 @@ testCases: error: code: 12 expectedResponse: - actualStatusCode: 404 + wireDetails: + actualStatusCode: 404 error: code: 12 details: @@ -268,7 +280,8 @@ testCases: error: code: 13 expectedResponse: - actualStatusCode: 500 + wireDetails: + actualStatusCode: 500 error: code: 13 details: @@ -289,7 +302,8 @@ testCases: error: code: 14 expectedResponse: - actualStatusCode: 503 + wireDetails: + actualStatusCode: 503 error: code: 14 details: @@ -310,7 +324,8 @@ testCases: error: code: 15 expectedResponse: - actualStatusCode: 500 + wireDetails: + actualStatusCode: 500 error: code: 15 details: @@ -331,7 +346,8 @@ testCases: error: code: 16 expectedResponse: - actualStatusCode: 401 + wireDetails: + actualStatusCode: 401 error: code: 16 details: @@ -353,7 +369,8 @@ testCases: error: code: 1 expectedResponse: - actualStatusCode: 200 + wireDetails: + actualStatusCode: 200 error: code: 1 details: @@ -375,7 +392,8 @@ testCases: error: code: 13 expectedResponse: - actualStatusCode: 200 + wireDetails: + actualStatusCode: 200 error: code: 13 details: @@ -398,7 +416,8 @@ testCases: code: 13 fullDuplex: true expectedResponse: - actualStatusCode: 200 + wireDetails: + actualStatusCode: 200 error: code: 13 details: @@ -420,7 +439,8 @@ testCases: error: code: 13 expectedResponse: - actualStatusCode: 200 + wireDetails: + actualStatusCode: 200 error: code: 13 details: diff --git a/internal/app/grpcclient/impl.go b/internal/app/grpcclient/impl.go index e61e2fce..6f811d1c 100644 --- a/internal/app/grpcclient/impl.go +++ b/internal/app/grpcclient/impl.go @@ -127,7 +127,6 @@ func (i *invoker) unary( ResponseTrailers: trailers, Payloads: payloads, Error: protoErr, - ConnectErrorRaw: nil, // TODO }, nil } @@ -138,9 +137,7 @@ func (i *invoker) serverStream( ctx, cancel := context.WithCancel(ctx) defer cancel() - result = &v1.ClientResponseResult{ - ConnectErrorRaw: nil, // TODO - } + result = &v1.ClientResponseResult{} msg := ccr.RequestMessages[0] req := &v1.ServerStreamRequest{} @@ -208,9 +205,7 @@ func (i *invoker) clientStream( ctx, cancel := context.WithCancel(ctx) defer cancel() - result := &v1.ClientResponseResult{ - ConnectErrorRaw: nil, // TODO - } + result := &v1.ClientResponseResult{} // Add the specified request headers to the request ctx = grpcutil.AppendToOutgoingContext(ctx, ccr.RequestHeaders) @@ -274,9 +269,7 @@ func (i *invoker) bidiStream( ctx, cancel := context.WithCancel(ctx) defer cancel() - result = &v1.ClientResponseResult{ - ConnectErrorRaw: nil, // TODO - } + result = &v1.ClientResponseResult{} // Add the specified request headers to the request ctx = grpcutil.AppendToOutgoingContext(ctx, ccr.RequestHeaders) diff --git a/internal/app/referenceclient/impl.go b/internal/app/referenceclient/impl.go index f21e5c31..e33ebd96 100644 --- a/internal/app/referenceclient/impl.go +++ b/internal/app/referenceclient/impl.go @@ -145,13 +145,11 @@ func (i *invoker) unary( } return &v1.ClientResponseResult{ - ResponseHeaders: headers, - ResponseTrailers: trailers, - Payloads: payloads, - Error: protoErr, - ActualStatusCode: wireDetails.StatusCode, - ActualHttpTrailers: wireDetails.Trailers, - ConnectErrorRaw: wireDetails.ConnectErrorRaw, + ResponseHeaders: headers, + ResponseTrailers: trailers, + Payloads: payloads, + Error: protoErr, + WireDetails: wireDetails, }, nil } @@ -203,13 +201,11 @@ func (i *invoker) idempotentUnary( return nil, err } return &v1.ClientResponseResult{ - ResponseHeaders: headers, - ResponseTrailers: trailers, - Payloads: payloads, - Error: protoErr, - ActualStatusCode: wireDetails.StatusCode, - ActualHttpTrailers: wireDetails.Trailers, - ConnectErrorRaw: wireDetails.ConnectErrorRaw, + ResponseHeaders: headers, + ResponseTrailers: trailers, + Payloads: payloads, + Error: protoErr, + WireDetails: wireDetails, }, nil } @@ -300,13 +296,11 @@ func (i *invoker) serverStream( } return &v1.ClientResponseResult{ - ResponseHeaders: headers, - ResponseTrailers: trailers, - Payloads: payloads, - Error: protoErr, - ActualStatusCode: wireDetails.StatusCode, - ActualHttpTrailers: wireDetails.Trailers, - ConnectErrorRaw: wireDetails.ConnectErrorRaw, + ResponseHeaders: headers, + ResponseTrailers: trailers, + Payloads: payloads, + Error: protoErr, + WireDetails: wireDetails, }, nil } @@ -377,13 +371,11 @@ func (i *invoker) clientStream( } return &v1.ClientResponseResult{ - ResponseHeaders: headers, - ResponseTrailers: trailers, - Payloads: payloads, - Error: protoErr, - ActualStatusCode: wireDetails.StatusCode, - ActualHttpTrailers: wireDetails.Trailers, - ConnectErrorRaw: wireDetails.ConnectErrorRaw, + ResponseHeaders: headers, + ResponseTrailers: trailers, + Payloads: payloads, + Error: protoErr, + WireDetails: wireDetails, }, nil } @@ -394,9 +386,7 @@ func (i *invoker) bidiStream( ctx, cancel := context.WithCancel(ctx) defer cancel() - result = &v1.ClientResponseResult{ - ConnectErrorRaw: nil, // TODO - } + result = &v1.ClientResponseResult{} ctx = withWireCapture(ctx) @@ -410,9 +400,7 @@ func (i *invoker) bidiStream( err = e return } - result.ActualStatusCode = wireDetails.StatusCode - result.ActualHttpTrailers = wireDetails.Trailers - result.ConnectErrorRaw = wireDetails.ConnectErrorRaw + result.WireDetails = wireDetails } // Read headers and trailers from the stream result.ResponseHeaders = internal.ConvertToProtoHeader(stream.ResponseHeader()) @@ -552,10 +540,8 @@ func (i *invoker) unimplemented( return nil, err } return &v1.ClientResponseResult{ - Error: internal.ConvertErrorToProtoError(err), - ActualStatusCode: wireDetails.StatusCode, - ActualHttpTrailers: wireDetails.Trailers, - ConnectErrorRaw: wireDetails.ConnectErrorRaw, + Error: internal.ConvertErrorToProtoError(err), + WireDetails: wireDetails, }, nil } diff --git a/internal/app/referenceclient/tracer.go b/internal/app/referenceclient/tracer.go index 8d5c5175..8a8fd3d0 100644 --- a/internal/app/referenceclient/tracer.go +++ b/internal/app/referenceclient/tracer.go @@ -34,17 +34,6 @@ import ( // The key associated with the wire details information stored in context. type wireCtxKey struct{} -// wireDetails encapsulates the wire details to track for a roundtrip. -type wireDetails struct { - // The actual HTTP status code observed. - StatusCode int32 - // The actual trailers observed. - Trailers []*v1.Header - // The actual JSON observed on the wire in case of an error from a Connect server. - // This will only be non-nil if the protocol is Connect and an error occurred. - ConnectErrorRaw *structpb.Struct -} - type wireInterceptor struct { Transport http.RoundTripper } @@ -118,14 +107,16 @@ func setWireTrace(ctx context.Context, trace *tracer.Trace) { } // getWireDetails returns the wire details from the trace in the given context. -func getWireDetails(ctx context.Context) (*wireDetails, error) { +func getWireDetails(ctx context.Context) (*v1.WireDetails, error) { wrapper, ok := ctx.Value(wireCtxKey{}).(*wireWrapper) if !ok { return nil, errors.New("wireWrapper not found in context") } trace := wrapper.val.Load() + // A nil response in the trace is valid if the HTTP round trip failed. + // In that case, we don't want to return any error, just empty wire details. if trace.Response == nil { - return nil, errors.New("trace response was nil") + return &v1.WireDetails{}, nil } statusCode := int32(trace.Response.StatusCode) @@ -170,14 +161,11 @@ func getWireDetails(ctx context.Context) (*wireDetails, error) { } } - wire := &wireDetails{ - StatusCode: statusCode, - Trailers: internal.ConvertToProtoHeader(trace.Response.Trailer), - ConnectErrorRaw: &jsonRaw, - } - - return wire, nil - + return &v1.WireDetails{ + ActualStatusCode: statusCode, + ActualHttpTrailers: internal.ConvertToProtoHeader(trace.Response.Trailer), + ConnectErrorRaw: &jsonRaw, + }, nil } type wireTracer struct { diff --git a/internal/gen/proto/go/connectrpc/conformance/v1/client_compat.pb.go b/internal/gen/proto/go/connectrpc/conformance/v1/client_compat.pb.go index f3471c7e..8bd7e537 100644 --- a/internal/gen/proto/go/connectrpc/conformance/v1/client_compat.pb.go +++ b/internal/gen/proto/go/connectrpc/conformance/v1/client_compat.pb.go @@ -385,15 +385,10 @@ type ClientResponseResult struct { // The number of messages that were present in the request but that could not be // sent because an error occurred before finishing the upload. NumUnsentRequests int32 `protobuf:"varint,5,opt,name=num_unsent_requests,json=numUnsentRequests,proto3" json:"num_unsent_requests,omitempty"` - // The HTTP status code of the response. - ActualStatusCode int32 `protobuf:"varint,6,opt,name=actual_status_code,json=actualStatusCode,proto3" json:"actual_status_code,omitempty"` - // When processing an error from a Connect server, this should contain - // the actual JSON received on the wire. - ConnectErrorRaw *structpb.Struct `protobuf:"bytes,7,opt,name=connect_error_raw,json=connectErrorRaw,proto3" json:"connect_error_raw,omitempty"` - // Any HTTP trailers observed after the response body. These do NOT - // include trailers that conveyed via the body, as done in the gRPC-Web - // and Connect streaming protocols. - ActualHttpTrailers []*Header `protobuf:"bytes,8,rep,name=actual_http_trailers,json=actualHttpTrailers,proto3" json:"actual_http_trailers,omitempty"` + // The following field is only used by the reference client. If + // you are implementing a client under test, you may ignore this + // field and should not populate it. + WireDetails *WireDetails `protobuf:"bytes,6,opt,name=wire_details,json=wireDetails,proto3" json:"wire_details,omitempty"` } func (x *ClientResponseResult) Reset() { @@ -463,23 +458,9 @@ func (x *ClientResponseResult) GetNumUnsentRequests() int32 { return 0 } -func (x *ClientResponseResult) GetActualStatusCode() int32 { - if x != nil { - return x.ActualStatusCode - } - return 0 -} - -func (x *ClientResponseResult) GetConnectErrorRaw() *structpb.Struct { - if x != nil { - return x.ConnectErrorRaw - } - return nil -} - -func (x *ClientResponseResult) GetActualHttpTrailers() []*Header { +func (x *ClientResponseResult) GetWireDetails() *WireDetails { if x != nil { - return x.ActualHttpTrailers + return x.WireDetails } return nil } @@ -534,6 +515,75 @@ func (x *ClientErrorResult) GetMessage() string { return "" } +type WireDetails struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The HTTP status code of the response. + ActualStatusCode int32 `protobuf:"varint,1,opt,name=actual_status_code,json=actualStatusCode,proto3" json:"actual_status_code,omitempty"` + // When processing an error from a Connect server, this should contain + // the actual JSON received on the wire. + ConnectErrorRaw *structpb.Struct `protobuf:"bytes,2,opt,name=connect_error_raw,json=connectErrorRaw,proto3" json:"connect_error_raw,omitempty"` + // Any HTTP trailers observed after the response body. These do NOT + // include trailers that conveyed via the body, as done in the gRPC-Web + // and Connect streaming protocols. + ActualHttpTrailers []*Header `protobuf:"bytes,3,rep,name=actual_http_trailers,json=actualHttpTrailers,proto3" json:"actual_http_trailers,omitempty"` +} + +func (x *WireDetails) Reset() { + *x = WireDetails{} + if protoimpl.UnsafeEnabled { + mi := &file_connectrpc_conformance_v1_client_compat_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *WireDetails) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WireDetails) ProtoMessage() {} + +func (x *WireDetails) ProtoReflect() protoreflect.Message { + mi := &file_connectrpc_conformance_v1_client_compat_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WireDetails.ProtoReflect.Descriptor instead. +func (*WireDetails) Descriptor() ([]byte, []int) { + return file_connectrpc_conformance_v1_client_compat_proto_rawDescGZIP(), []int{4} +} + +func (x *WireDetails) GetActualStatusCode() int32 { + if x != nil { + return x.ActualStatusCode + } + return 0 +} + +func (x *WireDetails) GetConnectErrorRaw() *structpb.Struct { + if x != nil { + return x.ConnectErrorRaw + } + return nil +} + +func (x *WireDetails) GetActualHttpTrailers() []*Header { + if x != nil { + return x.ActualHttpTrailers + } + return nil +} + type ClientCompatRequest_TLSCreds struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -546,7 +596,7 @@ type ClientCompatRequest_TLSCreds struct { func (x *ClientCompatRequest_TLSCreds) Reset() { *x = ClientCompatRequest_TLSCreds{} if protoimpl.UnsafeEnabled { - mi := &file_connectrpc_conformance_v1_client_compat_proto_msgTypes[4] + mi := &file_connectrpc_conformance_v1_client_compat_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -559,7 +609,7 @@ func (x *ClientCompatRequest_TLSCreds) String() string { func (*ClientCompatRequest_TLSCreds) ProtoMessage() {} func (x *ClientCompatRequest_TLSCreds) ProtoReflect() protoreflect.Message { - mi := &file_connectrpc_conformance_v1_client_compat_proto_msgTypes[4] + mi := &file_connectrpc_conformance_v1_client_compat_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -611,7 +661,7 @@ type ClientCompatRequest_Cancel struct { func (x *ClientCompatRequest_Cancel) Reset() { *x = ClientCompatRequest_Cancel{} if protoimpl.UnsafeEnabled { - mi := &file_connectrpc_conformance_v1_client_compat_proto_msgTypes[5] + mi := &file_connectrpc_conformance_v1_client_compat_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -624,7 +674,7 @@ func (x *ClientCompatRequest_Cancel) String() string { func (*ClientCompatRequest_Cancel) ProtoMessage() {} func (x *ClientCompatRequest_Cancel) ProtoReflect() protoreflect.Message { - mi := &file_connectrpc_conformance_v1_client_compat_proto_msgTypes[5] + mi := &file_connectrpc_conformance_v1_client_compat_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -818,7 +868,7 @@ var file_connectrpc_conformance_v1_client_compat_proto_rawDesc = []byte{ 0x6c, 0x74, 0x48, 0x00, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x65, 0x65, 0x64, 0x62, 0x61, 0x63, 0x6b, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x66, 0x65, 0x65, 0x64, 0x62, 0x61, 0x63, 0x6b, 0x42, 0x08, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, - 0x74, 0x22, 0xaf, 0x04, 0x0a, 0x14, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, + 0x74, 0x22, 0xb2, 0x03, 0x0a, 0x14, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x4c, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x72, 0x70, @@ -840,40 +890,46 @@ var file_connectrpc_conformance_v1_client_compat_proto_rawDesc = []byte{ 0x6e, 0x73, 0x65, 0x54, 0x72, 0x61, 0x69, 0x6c, 0x65, 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x13, 0x6e, 0x75, 0x6d, 0x5f, 0x75, 0x6e, 0x73, 0x65, 0x6e, 0x74, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, 0x11, 0x6e, 0x75, 0x6d, 0x55, 0x6e, 0x73, - 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x61, - 0x63, 0x74, 0x75, 0x61, 0x6c, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x5f, 0x63, 0x6f, 0x64, - 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x10, 0x61, 0x63, 0x74, 0x75, 0x61, 0x6c, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x43, 0x0a, 0x11, 0x63, 0x6f, 0x6e, - 0x6e, 0x65, 0x63, 0x74, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x72, 0x61, 0x77, 0x18, 0x07, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x0f, 0x63, - 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x61, 0x77, 0x12, 0x53, - 0x0a, 0x14, 0x61, 0x63, 0x74, 0x75, 0x61, 0x6c, 0x5f, 0x68, 0x74, 0x74, 0x70, 0x5f, 0x74, 0x72, - 0x61, 0x69, 0x6c, 0x65, 0x72, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x63, - 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x6f, 0x72, - 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x52, - 0x12, 0x61, 0x63, 0x74, 0x75, 0x61, 0x6c, 0x48, 0x74, 0x74, 0x70, 0x54, 0x72, 0x61, 0x69, 0x6c, - 0x65, 0x72, 0x73, 0x22, 0x2d, 0x0a, 0x11, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x45, 0x72, 0x72, - 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, - 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, - 0x67, 0x65, 0x42, 0x92, 0x02, 0x0a, 0x1d, 0x63, 0x6f, 0x6d, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, - 0x63, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, - 0x65, 0x2e, 0x76, 0x31, 0x42, 0x11, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6d, 0x70, - 0x61, 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x58, 0x63, 0x6f, 0x6e, 0x6e, 0x65, - 0x63, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x6f, 0x72, - 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, - 0x65, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x6f, 0x2f, 0x63, 0x6f, 0x6e, 0x6e, - 0x65, 0x63, 0x74, 0x72, 0x70, 0x63, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, - 0x63, 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x63, 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, - 0x65, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x43, 0x43, 0x58, 0xaa, 0x02, 0x19, 0x43, 0x6f, 0x6e, 0x6e, - 0x65, 0x63, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, - 0x63, 0x65, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x19, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x72, - 0x70, 0x63, 0x5c, 0x43, 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x5c, 0x56, - 0x31, 0xe2, 0x02, 0x25, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x72, 0x70, 0x63, 0x5c, 0x43, - 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, - 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x1b, 0x43, 0x6f, 0x6e, 0x6e, - 0x65, 0x63, 0x74, 0x72, 0x70, 0x63, 0x3a, 0x3a, 0x43, 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, - 0x6e, 0x63, 0x65, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x12, 0x49, 0x0a, 0x0c, 0x77, + 0x69, 0x72, 0x65, 0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x26, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x63, + 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x69, + 0x72, 0x65, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x0b, 0x77, 0x69, 0x72, 0x65, 0x44, + 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x22, 0x2d, 0x0a, 0x11, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x6d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0xd5, 0x01, 0x0a, 0x0b, 0x57, 0x69, 0x72, 0x65, 0x44, 0x65, + 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x61, 0x63, 0x74, 0x75, 0x61, 0x6c, 0x5f, + 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x05, 0x52, 0x10, 0x61, 0x63, 0x74, 0x75, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x43, + 0x6f, 0x64, 0x65, 0x12, 0x43, 0x0a, 0x11, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x5f, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x72, 0x61, 0x77, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x61, 0x77, 0x12, 0x53, 0x0a, 0x14, 0x61, 0x63, 0x74, 0x75, + 0x61, 0x6c, 0x5f, 0x68, 0x74, 0x74, 0x70, 0x5f, 0x74, 0x72, 0x61, 0x69, 0x6c, 0x65, 0x72, 0x73, + 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x72, 0x70, 0x63, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, + 0x76, 0x31, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x52, 0x12, 0x61, 0x63, 0x74, 0x75, 0x61, + 0x6c, 0x48, 0x74, 0x74, 0x70, 0x54, 0x72, 0x61, 0x69, 0x6c, 0x65, 0x72, 0x73, 0x42, 0x92, 0x02, + 0x0a, 0x1d, 0x63, 0x6f, 0x6d, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x72, 0x70, 0x63, + 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x76, 0x31, 0x42, + 0x11, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x61, 0x74, 0x50, 0x72, 0x6f, + 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x58, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x72, 0x70, 0x63, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, + 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x6f, 0x2f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x72, 0x70, + 0x63, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2f, 0x76, 0x31, + 0x3b, 0x63, 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x76, 0x31, 0xa2, 0x02, + 0x03, 0x43, 0x43, 0x58, 0xaa, 0x02, 0x19, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x72, 0x70, + 0x63, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x56, 0x31, + 0xca, 0x02, 0x19, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x72, 0x70, 0x63, 0x5c, 0x43, 0x6f, + 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x25, 0x43, + 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x72, 0x70, 0x63, 0x5c, 0x43, 0x6f, 0x6e, 0x66, 0x6f, 0x72, + 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x1b, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x72, 0x70, + 0x63, 0x3a, 0x3a, 0x43, 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x3a, 0x3a, + 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -888,52 +944,54 @@ func file_connectrpc_conformance_v1_client_compat_proto_rawDescGZIP() []byte { return file_connectrpc_conformance_v1_client_compat_proto_rawDescData } -var file_connectrpc_conformance_v1_client_compat_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_connectrpc_conformance_v1_client_compat_proto_msgTypes = make([]protoimpl.MessageInfo, 7) var file_connectrpc_conformance_v1_client_compat_proto_goTypes = []interface{}{ (*ClientCompatRequest)(nil), // 0: connectrpc.conformance.v1.ClientCompatRequest (*ClientCompatResponse)(nil), // 1: connectrpc.conformance.v1.ClientCompatResponse (*ClientResponseResult)(nil), // 2: connectrpc.conformance.v1.ClientResponseResult (*ClientErrorResult)(nil), // 3: connectrpc.conformance.v1.ClientErrorResult - (*ClientCompatRequest_TLSCreds)(nil), // 4: connectrpc.conformance.v1.ClientCompatRequest.TLSCreds - (*ClientCompatRequest_Cancel)(nil), // 5: connectrpc.conformance.v1.ClientCompatRequest.Cancel - (HTTPVersion)(0), // 6: connectrpc.conformance.v1.HTTPVersion - (Protocol)(0), // 7: connectrpc.conformance.v1.Protocol - (Codec)(0), // 8: connectrpc.conformance.v1.Codec - (Compression)(0), // 9: connectrpc.conformance.v1.Compression - (StreamType)(0), // 10: connectrpc.conformance.v1.StreamType - (*Header)(nil), // 11: connectrpc.conformance.v1.Header - (*anypb.Any)(nil), // 12: google.protobuf.Any - (*RawHTTPRequest)(nil), // 13: connectrpc.conformance.v1.RawHTTPRequest - (*ConformancePayload)(nil), // 14: connectrpc.conformance.v1.ConformancePayload - (*Error)(nil), // 15: connectrpc.conformance.v1.Error - (*structpb.Struct)(nil), // 16: google.protobuf.Struct - (*emptypb.Empty)(nil), // 17: google.protobuf.Empty + (*WireDetails)(nil), // 4: connectrpc.conformance.v1.WireDetails + (*ClientCompatRequest_TLSCreds)(nil), // 5: connectrpc.conformance.v1.ClientCompatRequest.TLSCreds + (*ClientCompatRequest_Cancel)(nil), // 6: connectrpc.conformance.v1.ClientCompatRequest.Cancel + (HTTPVersion)(0), // 7: connectrpc.conformance.v1.HTTPVersion + (Protocol)(0), // 8: connectrpc.conformance.v1.Protocol + (Codec)(0), // 9: connectrpc.conformance.v1.Codec + (Compression)(0), // 10: connectrpc.conformance.v1.Compression + (StreamType)(0), // 11: connectrpc.conformance.v1.StreamType + (*Header)(nil), // 12: connectrpc.conformance.v1.Header + (*anypb.Any)(nil), // 13: google.protobuf.Any + (*RawHTTPRequest)(nil), // 14: connectrpc.conformance.v1.RawHTTPRequest + (*ConformancePayload)(nil), // 15: connectrpc.conformance.v1.ConformancePayload + (*Error)(nil), // 16: connectrpc.conformance.v1.Error + (*structpb.Struct)(nil), // 17: google.protobuf.Struct + (*emptypb.Empty)(nil), // 18: google.protobuf.Empty } var file_connectrpc_conformance_v1_client_compat_proto_depIdxs = []int32{ - 6, // 0: connectrpc.conformance.v1.ClientCompatRequest.http_version:type_name -> connectrpc.conformance.v1.HTTPVersion - 7, // 1: connectrpc.conformance.v1.ClientCompatRequest.protocol:type_name -> connectrpc.conformance.v1.Protocol - 8, // 2: connectrpc.conformance.v1.ClientCompatRequest.codec:type_name -> connectrpc.conformance.v1.Codec - 9, // 3: connectrpc.conformance.v1.ClientCompatRequest.compression:type_name -> connectrpc.conformance.v1.Compression - 4, // 4: connectrpc.conformance.v1.ClientCompatRequest.client_tls_creds:type_name -> connectrpc.conformance.v1.ClientCompatRequest.TLSCreds - 10, // 5: connectrpc.conformance.v1.ClientCompatRequest.stream_type:type_name -> connectrpc.conformance.v1.StreamType - 11, // 6: connectrpc.conformance.v1.ClientCompatRequest.request_headers:type_name -> connectrpc.conformance.v1.Header - 12, // 7: connectrpc.conformance.v1.ClientCompatRequest.request_messages:type_name -> google.protobuf.Any - 5, // 8: connectrpc.conformance.v1.ClientCompatRequest.cancel:type_name -> connectrpc.conformance.v1.ClientCompatRequest.Cancel - 13, // 9: connectrpc.conformance.v1.ClientCompatRequest.raw_request:type_name -> connectrpc.conformance.v1.RawHTTPRequest + 7, // 0: connectrpc.conformance.v1.ClientCompatRequest.http_version:type_name -> connectrpc.conformance.v1.HTTPVersion + 8, // 1: connectrpc.conformance.v1.ClientCompatRequest.protocol:type_name -> connectrpc.conformance.v1.Protocol + 9, // 2: connectrpc.conformance.v1.ClientCompatRequest.codec:type_name -> connectrpc.conformance.v1.Codec + 10, // 3: connectrpc.conformance.v1.ClientCompatRequest.compression:type_name -> connectrpc.conformance.v1.Compression + 5, // 4: connectrpc.conformance.v1.ClientCompatRequest.client_tls_creds:type_name -> connectrpc.conformance.v1.ClientCompatRequest.TLSCreds + 11, // 5: connectrpc.conformance.v1.ClientCompatRequest.stream_type:type_name -> connectrpc.conformance.v1.StreamType + 12, // 6: connectrpc.conformance.v1.ClientCompatRequest.request_headers:type_name -> connectrpc.conformance.v1.Header + 13, // 7: connectrpc.conformance.v1.ClientCompatRequest.request_messages:type_name -> google.protobuf.Any + 6, // 8: connectrpc.conformance.v1.ClientCompatRequest.cancel:type_name -> connectrpc.conformance.v1.ClientCompatRequest.Cancel + 14, // 9: connectrpc.conformance.v1.ClientCompatRequest.raw_request:type_name -> connectrpc.conformance.v1.RawHTTPRequest 2, // 10: connectrpc.conformance.v1.ClientCompatResponse.response:type_name -> connectrpc.conformance.v1.ClientResponseResult 3, // 11: connectrpc.conformance.v1.ClientCompatResponse.error:type_name -> connectrpc.conformance.v1.ClientErrorResult - 11, // 12: connectrpc.conformance.v1.ClientResponseResult.response_headers:type_name -> connectrpc.conformance.v1.Header - 14, // 13: connectrpc.conformance.v1.ClientResponseResult.payloads:type_name -> connectrpc.conformance.v1.ConformancePayload - 15, // 14: connectrpc.conformance.v1.ClientResponseResult.error:type_name -> connectrpc.conformance.v1.Error - 11, // 15: connectrpc.conformance.v1.ClientResponseResult.response_trailers:type_name -> connectrpc.conformance.v1.Header - 16, // 16: connectrpc.conformance.v1.ClientResponseResult.connect_error_raw:type_name -> google.protobuf.Struct - 11, // 17: connectrpc.conformance.v1.ClientResponseResult.actual_http_trailers:type_name -> connectrpc.conformance.v1.Header - 17, // 18: connectrpc.conformance.v1.ClientCompatRequest.Cancel.before_close_send:type_name -> google.protobuf.Empty - 19, // [19:19] is the sub-list for method output_type - 19, // [19:19] is the sub-list for method input_type - 19, // [19:19] is the sub-list for extension type_name - 19, // [19:19] is the sub-list for extension extendee - 0, // [0:19] is the sub-list for field type_name + 12, // 12: connectrpc.conformance.v1.ClientResponseResult.response_headers:type_name -> connectrpc.conformance.v1.Header + 15, // 13: connectrpc.conformance.v1.ClientResponseResult.payloads:type_name -> connectrpc.conformance.v1.ConformancePayload + 16, // 14: connectrpc.conformance.v1.ClientResponseResult.error:type_name -> connectrpc.conformance.v1.Error + 12, // 15: connectrpc.conformance.v1.ClientResponseResult.response_trailers:type_name -> connectrpc.conformance.v1.Header + 4, // 16: connectrpc.conformance.v1.ClientResponseResult.wire_details:type_name -> connectrpc.conformance.v1.WireDetails + 17, // 17: connectrpc.conformance.v1.WireDetails.connect_error_raw:type_name -> google.protobuf.Struct + 12, // 18: connectrpc.conformance.v1.WireDetails.actual_http_trailers:type_name -> connectrpc.conformance.v1.Header + 18, // 19: connectrpc.conformance.v1.ClientCompatRequest.Cancel.before_close_send:type_name -> google.protobuf.Empty + 20, // [20:20] is the sub-list for method output_type + 20, // [20:20] is the sub-list for method input_type + 20, // [20:20] is the sub-list for extension type_name + 20, // [20:20] is the sub-list for extension extendee + 0, // [0:20] is the sub-list for field type_name } func init() { file_connectrpc_conformance_v1_client_compat_proto_init() } @@ -993,7 +1051,7 @@ func file_connectrpc_conformance_v1_client_compat_proto_init() { } } file_connectrpc_conformance_v1_client_compat_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ClientCompatRequest_TLSCreds); i { + switch v := v.(*WireDetails); i { case 0: return &v.state case 1: @@ -1005,6 +1063,18 @@ func file_connectrpc_conformance_v1_client_compat_proto_init() { } } file_connectrpc_conformance_v1_client_compat_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ClientCompatRequest_TLSCreds); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_connectrpc_conformance_v1_client_compat_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ClientCompatRequest_Cancel); i { case 0: return &v.state @@ -1022,7 +1092,7 @@ func file_connectrpc_conformance_v1_client_compat_proto_init() { (*ClientCompatResponse_Response)(nil), (*ClientCompatResponse_Error)(nil), } - file_connectrpc_conformance_v1_client_compat_proto_msgTypes[5].OneofWrappers = []interface{}{ + file_connectrpc_conformance_v1_client_compat_proto_msgTypes[6].OneofWrappers = []interface{}{ (*ClientCompatRequest_Cancel_BeforeCloseSend)(nil), (*ClientCompatRequest_Cancel_AfterCloseSendMs)(nil), (*ClientCompatRequest_Cancel_AfterNumResponses)(nil), @@ -1033,7 +1103,7 @@ func file_connectrpc_conformance_v1_client_compat_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_connectrpc_conformance_v1_client_compat_proto_rawDesc, NumEnums: 0, - NumMessages: 6, + NumMessages: 7, NumExtensions: 0, NumServices: 0, }, diff --git a/proto/connectrpc/conformance/v1/client_compat.proto b/proto/connectrpc/conformance/v1/client_compat.proto index 57a5fdb8..3ec2724b 100644 --- a/proto/connectrpc/conformance/v1/client_compat.proto +++ b/proto/connectrpc/conformance/v1/client_compat.proto @@ -140,20 +140,10 @@ message ClientResponseResult { // sent because an error occurred before finishing the upload. int32 num_unsent_requests = 5; - // The following fields are only used by the reference client. If - // you are implementing a client under test, you may ignore these - // fields and do not need to populate them. If your client does - // populate them, the conformance test runner will ignore them. - - // The HTTP status code of the response. - int32 actual_status_code = 6; - // When processing an error from a Connect server, this should contain - // the actual JSON received on the wire. - google.protobuf.Struct connect_error_raw = 7; - // Any HTTP trailers observed after the response body. These do NOT - // include trailers that conveyed via the body, as done in the gRPC-Web - // and Connect streaming protocols. - repeated Header actual_http_trailers = 8; + // The following field is only used by the reference client. If + // you are implementing a client under test, you may ignore this + // field and should not populate it. + WireDetails wire_details = 6; } // The client is not able to fulfill the ClientCompatRequest. This may be due @@ -162,3 +152,15 @@ message ClientResponseResult { message ClientErrorResult { string message = 1; } + +message WireDetails { + // The HTTP status code of the response. + int32 actual_status_code = 1; + // When processing an error from a Connect server, this should contain + // the actual JSON received on the wire. + google.protobuf.Struct connect_error_raw = 2; + // Any HTTP trailers observed after the response body. These do NOT + // include trailers that conveyed via the body, as done in the gRPC-Web + // and Connect streaming protocols. + repeated Header actual_http_trailers = 3; +} From 0122e396834b9c9a34924e3b307a5b6b5d1f16ce Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Tue, 6 Feb 2024 15:49:15 -0500 Subject: [PATCH 35/40] Lint --- internal/app/referenceclient/impl.go | 45 +++------------- internal/app/referenceclient/tracer.go | 54 ++++++++++--------- .../conformance/v1/client_compat.proto | 2 +- 3 files changed, 38 insertions(+), 63 deletions(-) diff --git a/internal/app/referenceclient/impl.go b/internal/app/referenceclient/impl.go index e33ebd96..d9f2863a 100644 --- a/internal/app/referenceclient/impl.go +++ b/internal/app/referenceclient/impl.go @@ -139,17 +139,12 @@ func (i *invoker) unary( } } - wireDetails, err := getWireDetails(ctx) - if err != nil { - return nil, err - } - return &v1.ClientResponseResult{ ResponseHeaders: headers, ResponseTrailers: trailers, Payloads: payloads, Error: protoErr, - WireDetails: wireDetails, + WireDetails: getWireDetails(ctx), }, nil } @@ -196,16 +191,12 @@ func (i *invoker) idempotentUnary( trailers = internal.ConvertToProtoHeader(resp.Trailer()) } - wireDetails, err := getWireDetails(ctx) - if err != nil { - return nil, err - } return &v1.ClientResponseResult{ ResponseHeaders: headers, ResponseTrailers: trailers, Payloads: payloads, Error: protoErr, - WireDetails: wireDetails, + WireDetails: getWireDetails(ctx), }, nil } @@ -290,17 +281,12 @@ func (i *invoker) serverStream( } } - wireDetails, err := getWireDetails(ctx) - if err != nil { - return nil, err - } - return &v1.ClientResponseResult{ ResponseHeaders: headers, ResponseTrailers: trailers, Payloads: payloads, Error: protoErr, - WireDetails: wireDetails, + WireDetails: getWireDetails(ctx), }, nil } @@ -365,17 +351,12 @@ func (i *invoker) clientStream( trailers = internal.ConvertToProtoHeader(resp.Trailer()) } - wireDetails, err := getWireDetails(ctx) - if err != nil { - return nil, err - } - return &v1.ClientResponseResult{ ResponseHeaders: headers, ResponseTrailers: trailers, Payloads: payloads, Error: protoErr, - WireDetails: wireDetails, + WireDetails: getWireDetails(ctx), }, nil } @@ -393,19 +374,11 @@ func (i *invoker) bidiStream( stream := i.client.BidiStream(ctx) defer func() { if result != nil { - if err == nil { - wireDetails, e := getWireDetails(ctx) - if e != nil { - result = nil - err = e - return - } - result.WireDetails = wireDetails - } + result.WireDetails = getWireDetails(ctx) + // Read headers and trailers from the stream result.ResponseHeaders = internal.ConvertToProtoHeader(stream.ResponseHeader()) result.ResponseTrailers = internal.ConvertToProtoHeader(stream.ResponseTrailer()) - } }() @@ -535,13 +508,9 @@ func (i *invoker) unimplemented( // Invoke the Unary call _, err := i.client.Unimplemented(ctx, request) - wireDetails, err := getWireDetails(ctx) - if err != nil { - return nil, err - } return &v1.ClientResponseResult{ Error: internal.ConvertErrorToProtoError(err), - WireDetails: wireDetails, + WireDetails: getWireDetails(ctx), }, nil } diff --git a/internal/app/referenceclient/tracer.go b/internal/app/referenceclient/tracer.go index 8a8fd3d0..a4bed10c 100644 --- a/internal/app/referenceclient/tracer.go +++ b/internal/app/referenceclient/tracer.go @@ -18,7 +18,6 @@ import ( "bytes" "context" "encoding/json" - "errors" "io" "net/http" "strings" @@ -59,7 +58,7 @@ func (w *wireInterceptor) RoundTrip(req *http.Request) (*http.Response, error) { // If this is a unary error with JSON body, replace the body with a reader // that will save off the body bytes as they are read so that we can access // the body contents in the tracer - if resp.StatusCode != 200 && resp.Header.Get("content-type") == "application/json" { + if isUnaryJSONError(resp.Header.Get("content-type"), int32(resp.StatusCode)) { resp.Body = &wireReader{body: resp.Body, wrapper: wrapper} } return resp, nil @@ -107,52 +106,53 @@ func setWireTrace(ctx context.Context, trace *tracer.Trace) { } // getWireDetails returns the wire details from the trace in the given context. -func getWireDetails(ctx context.Context) (*v1.WireDetails, error) { +func getWireDetails(ctx context.Context) *v1.WireDetails { + // TODO - Note that this function swallows any errors experienced when processing + // the response, instead opting for just returning a nil wire details. + // It might be worth revisiting using the feedback approach to surface these errors. wrapper, ok := ctx.Value(wireCtxKey{}).(*wireWrapper) if !ok { - return nil, errors.New("wireWrapper not found in context") + return nil } trace := wrapper.val.Load() // A nil response in the trace is valid if the HTTP round trip failed. - // In that case, we don't want to return any error, just empty wire details. - if trace.Response == nil { - return &v1.WireDetails{}, nil + if trace == nil || trace.Response == nil { + return nil } statusCode := int32(trace.Response.StatusCode) var jsonRaw structpb.Struct contentType := trace.Response.Header.Get("content-type") - if contentType == "application/json" { - if statusCode != 200 { - // If this is a unary request, then use the entire response body - // as the wire error details. - body, err := io.ReadAll(wrapper.buf) - if err != nil { - return nil, err - } - if err := protojson.Unmarshal(body, &jsonRaw); err != nil { - return nil, err - } + + // If this is a unary request that returned an error, then use the entire + // response body as the wire error details. + if isUnaryJSONError(contentType, statusCode) { //nolint:nestif + body, err := io.ReadAll(wrapper.buf) + if err != nil { + return nil } - } else if strings.HasPrefix(contentType, "application/connect+") { - type endStreamError struct { - Error json.RawMessage `json:"error"` + if err := protojson.Unmarshal(body, &jsonRaw); err != nil { + return nil } + } else if strings.HasPrefix(contentType, "application/connect+") { // If this is a streaming request, then look through the trace events // for the ResponseBodyEndStream event and parse its content into an // endStreamError to see if there are any error details. + type endStreamError struct { + Error json.RawMessage `json:"error"` + } for _, ev := range trace.Events { switch eventType := ev.(type) { case *tracer.ResponseBodyEndStream: var endStream endStreamError if err := json.Unmarshal([]byte(eventType.Content), &endStream); err != nil { - return nil, err + return nil } // If we unmarshalled any bytes into endStream.Error, then unmarshal _that_ // into a Struct if len(endStream.Error) > 0 { if err := protojson.Unmarshal(endStream.Error, &jsonRaw); err != nil { - return nil, err + return nil } } default: @@ -165,7 +165,7 @@ func getWireDetails(ctx context.Context) (*v1.WireDetails, error) { ActualStatusCode: statusCode, ActualHttpTrailers: internal.ConvertToProtoHeader(trace.Response.Trailer), ConnectErrorRaw: &jsonRaw, - }, nil + } } type wireTracer struct { @@ -182,3 +182,9 @@ func (t *wireTracer) Complete(trace tracer.Trace) { t.tracer.Complete(trace) } } + +// isUnaryJSONError returns whether the given content type and HTTP status code +// represents a unary JSON error. +func isUnaryJSONError(contentType string, statusCode int32) bool { + return contentType == "application/json" && statusCode != 200 +} diff --git a/proto/connectrpc/conformance/v1/client_compat.proto b/proto/connectrpc/conformance/v1/client_compat.proto index 3ec2724b..2eac256a 100644 --- a/proto/connectrpc/conformance/v1/client_compat.proto +++ b/proto/connectrpc/conformance/v1/client_compat.proto @@ -142,7 +142,7 @@ message ClientResponseResult { // The following field is only used by the reference client. If // you are implementing a client under test, you may ignore this - // field and should not populate it. + // field and should not populate it. WireDetails wire_details = 6; } From f8cd911c48417f6f69e7c95a41d909791eb443e8 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Tue, 6 Feb 2024 15:59:35 -0500 Subject: [PATCH 36/40] Add TODO --- internal/app/connectconformance/results.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/internal/app/connectconformance/results.go b/internal/app/connectconformance/results.go index b8f6cc0a..3f2dcba2 100644 --- a/internal/app/connectconformance/results.go +++ b/internal/app/connectconformance/results.go @@ -194,9 +194,11 @@ func (r *testResults) assert(testCase string, expected, actual *conformancev1.Cl expectedWire := expected.WireDetails actualWire := actual.WireDetails if expectedWire != nil && actualWire != nil { - if diff := cmp.Diff(expectedWire.ConnectErrorRaw, actualWire.ConnectErrorRaw, protocmp.Transform()); diff != "" { - errs = append(errs, fmt.Errorf("raw Connect error does not match: - wanted, + got\n%s", diff)) - } + // TODO - Add comparison (and tests) for connecterrorraw and actual http trailers + + // if diff := cmp.Diff(expectedWire.ConnectErrorRaw, actualWire.ConnectErrorRaw, protocmp.Transform()); diff != "" { + // errs = append(errs, fmt.Errorf("raw Connect error does not match: - wanted, + got\n%s", diff)) + // } if diff := cmp.Diff(expectedWire.ActualStatusCode, actualWire.ActualStatusCode, protocmp.Transform()); diff != "" { errs = append(errs, fmt.Errorf("actual HTTP status code does not match: - wanted, + got\n%s", diff)) From 2537552ab0a3864fdaea4317d272339da244323d Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Tue, 6 Feb 2024 21:17:44 -0500 Subject: [PATCH 37/40] Feedback --- .github/workflows/buf.yaml | 19 ++++++++++--------- internal/app/referenceclient/impl.go | 14 ++++++++------ 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/.github/workflows/buf.yaml b/.github/workflows/buf.yaml index 026db3bf..e3d299dc 100644 --- a/.github/workflows/buf.yaml +++ b/.github/workflows/buf.yaml @@ -16,15 +16,16 @@ jobs: - uses: bufbuild/buf-lint-action@v1.1.0 with: input: proto - breaking: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: bufbuild/buf-setup-action@v1 - - uses: bufbuild/buf-breaking-action@v1.1.3 - with: - input: proto - against: 'buf.build/connectrpc/conformance' + # TODO - Reenable after pushing breaking proto changes to BSR + # breaking: + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v4 + # - uses: bufbuild/buf-setup-action@v1 + # - uses: bufbuild/buf-breaking-action@v1.1.3 + # with: + # input: proto + # against: 'buf.build/connectrpc/conformance' push: runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' diff --git a/internal/app/referenceclient/impl.go b/internal/app/referenceclient/impl.go index d9f2863a..a0135511 100644 --- a/internal/app/referenceclient/impl.go +++ b/internal/app/referenceclient/impl.go @@ -373,13 +373,15 @@ func (i *invoker) bidiStream( stream := i.client.BidiStream(ctx) defer func() { - if result != nil { - result.WireDetails = getWireDetails(ctx) - - // Read headers and trailers from the stream - result.ResponseHeaders = internal.ConvertToProtoHeader(stream.ResponseHeader()) - result.ResponseTrailers = internal.ConvertToProtoHeader(stream.ResponseTrailer()) + if err != nil { + return } + + result.WireDetails = getWireDetails(ctx) + + // Read headers and trailers from the stream + result.ResponseHeaders = internal.ConvertToProtoHeader(stream.ResponseHeader()) + result.ResponseTrailers = internal.ConvertToProtoHeader(stream.ResponseTrailer()) }() // Add the specified request headers to the request From b4bba187a867e0be41da096410ceb40dd860f011 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Tue, 6 Feb 2024 21:40:28 -0500 Subject: [PATCH 38/40] Temp disable breaking step --- .github/workflows/buf.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/buf.yaml b/.github/workflows/buf.yaml index e3d299dc..34048cba 100644 --- a/.github/workflows/buf.yaml +++ b/.github/workflows/buf.yaml @@ -31,7 +31,8 @@ jobs: if: github.ref == 'refs/heads/main' needs: - lint - - breaking + # TODO - Reenable + # - breaking steps: - uses: actions/checkout@v4 - uses: bufbuild/buf-setup-action@v1 From a2e3f6dce527341097ca85d45636dcf32bbf9607 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Wed, 7 Feb 2024 10:31:06 -0500 Subject: [PATCH 39/40] Slim down the breaking comments --- .github/workflows/buf.yaml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/buf.yaml b/.github/workflows/buf.yaml index 34048cba..fe93e374 100644 --- a/.github/workflows/buf.yaml +++ b/.github/workflows/buf.yaml @@ -16,12 +16,12 @@ jobs: - uses: bufbuild/buf-lint-action@v1.1.0 with: input: proto + breaking: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: bufbuild/buf-setup-action@v1 # TODO - Reenable after pushing breaking proto changes to BSR - # breaking: - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v4 - # - uses: bufbuild/buf-setup-action@v1 # - uses: bufbuild/buf-breaking-action@v1.1.3 # with: # input: proto @@ -31,8 +31,7 @@ jobs: if: github.ref == 'refs/heads/main' needs: - lint - # TODO - Reenable - # - breaking + - breaking steps: - uses: actions/checkout@v4 - uses: bufbuild/buf-setup-action@v1 From 50df580b9486356bb1e470f1391a0a8cf05f5ea8 Mon Sep 17 00:00:00 2001 From: Joshua Humphries <2035234+jhump@users.noreply.github.com> Date: Wed, 7 Feb 2024 11:04:50 -0500 Subject: [PATCH 40/40] Relaxes assertions on query parameters (#771) Previously, computing the expected response would add an expected query parameter for the `message` param, with an encoded value. But the problem is that not all clients would necessarily encode the message in exactly the same way, and that's okay. Some will choose to base64-encode (which is usually more efficient with binary payloads). Also, there's no _canonical_ protobuf binary format, so there could be subtle differences in the byte output, even for equivalent messages. Finally, there's no _canonical_ encoding for compressed payloads -- different default compression settings or other subtle differences in implementation can lead to more than one valid compressed encoding of the same data. --- internal/app/connectconformance/results.go | 16 ++++---- .../app/connectconformance/results_test.go | 6 +-- .../connectconformance/test_case_library.go | 31 ++++++++------- .../test_case_library_test.go | 38 ------------------- internal/codec.go | 33 ++-------------- 5 files changed, 29 insertions(+), 95 deletions(-) diff --git a/internal/app/connectconformance/results.go b/internal/app/connectconformance/results.go index 195dbb3f..d6411b58 100644 --- a/internal/app/connectconformance/results.go +++ b/internal/app/connectconformance/results.go @@ -159,6 +159,9 @@ func (r *testResults) failed(testCase string, err *conformancev1.ClientErrorResu func (r *testResults) assert(testCase string, expected, actual *conformancev1.ClientResponseResult) { var errs multiErrors + errs = append(errs, checkError(expected.Error, actual.Error)...) + errs = append(errs, checkPayloads(expected.Payloads, actual.Payloads)...) + // TODO - This check is for trailers-only and should really only apply to gRPC and gRPC-Web protocols. // Previously, it checked for error != nil, which is compliant with gRPC. But gRPC-Web does trailers-only // responses with no errors also. @@ -188,9 +191,6 @@ func (r *testResults) assert(testCase string, expected, actual *conformancev1.Cl errs = append(errs, checkHeaders("response trailers", expected.ResponseTrailers, actual.ResponseTrailers)...) } - errs = append(errs, checkPayloads(expected.Payloads, actual.Payloads)...) - errs = append(errs, checkError(expected.Error, actual.Error)...) - expectedWire := expected.WireDetails actualWire := actual.WireDetails if expectedWire != nil && actualWire != nil { @@ -409,12 +409,12 @@ func checkRequestInfo(expected, actual *conformancev1.ConformancePayload_Request if actual == nil || actual.TimeoutMs == nil { errs = append(errs, fmt.Errorf("server did not echo back a timeout but one was expected (%d ms)", expected.GetTimeoutMs())) } else { - max := expected.GetTimeoutMs() - min := max - timeoutCheckGracePeriodMillis - if min < 0 { - min = 0 + maxAllowed := expected.GetTimeoutMs() + minAllowed := maxAllowed - timeoutCheckGracePeriodMillis + if minAllowed < 0 { + minAllowed = 0 } - if actual.GetTimeoutMs() > max || actual.GetTimeoutMs() < min { + if actual.GetTimeoutMs() > maxAllowed || actual.GetTimeoutMs() < minAllowed { errs = append(errs, fmt.Errorf("server echoed back a timeout (%d ms) that did not match expected (%d ms)", actual.GetTimeoutMs(), expected.GetTimeoutMs())) } } diff --git a/internal/app/connectconformance/results_test.go b/internal/app/connectconformance/results_test.go index a59a2ac2..64b17a04 100644 --- a/internal/app/connectconformance/results_test.go +++ b/internal/app/connectconformance/results_test.go @@ -671,15 +671,15 @@ func TestResults_Assert_ReportsAllErrors(t *testing.T) { }`, // It tries to describe everything wrong, all in one shot. expectedErrors: []string{ - `actual response headers missing "abc"`, - `actual response trailers missing "xyz"`, + `expecting an error but received none`, `expecting 3 response messages but instead got 1`, `response #1: expecting data 69b71d79f821, got d76df8`, `actual request headers missing "foo"`, `server did not echo back a timeout but one was expected (12345 ms)`, `expecting 1 request messages to be described but instead got 2`, `request #1: did not survive round-trip`, - `expecting an error but received none`, + `actual response headers missing "abc"`, + `actual response trailers missing "xyz"`, }, }, } diff --git a/internal/app/connectconformance/test_case_library.go b/internal/app/connectconformance/test_case_library.go index e673a25f..533b288b 100644 --- a/internal/app/connectconformance/test_case_library.go +++ b/internal/app/connectconformance/test_case_library.go @@ -15,7 +15,6 @@ package connectconformance import ( - "encoding/base64" "errors" "fmt" "math" @@ -524,28 +523,28 @@ func populateExpectedUnaryResponse(testCase *conformancev1.TestCase) error { // If this is a GET test, then the request should be marshalled and in the query params if testCase.Request.UseGetHttpMethod { - isJSON := testCase.Request.Codec == conformancev1.Codec_CODEC_JSON - // Build a codec based on what is used in the request - codec := internal.NewCodec(isJSON) - reqAsBytes, err := codec.MarshalStable(definer) - if err != nil { - return err - } - var value string var encoding string - if isJSON { - value = string(reqAsBytes) + if testCase.Request.Codec == conformancev1.Codec_CODEC_JSON { encoding = "json" } else { - value = base64.RawURLEncoding.EncodeToString(reqAsBytes) encoding = "proto" } + // We intentionally exclude the "message" query param: the RPC response, which + // echos back the request data, will suffice to check that. Also, we can't + // indicate a specific sequence of bytes to expect for "message" because there + // isn't a *canonical* encoding for protobuf nor is there necessarily a + // canonical encoding of compressed messages. So we just verify that it has + // the required "encoding" parameter and then will check the echoed-back request + // to verify that the message was otherwise correctly encoded (by virtue of the + // fact that it could be correctly decoded by the server). + // + // We also exclude the "base64" parameter, allowing implementations flexibility + // on when to use it. It's mainly valuable to use for binary data, where the + // alternative (to just rely on the percent-encoding of URL query strings) will + // be more verbose and lead to a larger request line (potentially one that is + // too long to fit in a URI). reqInfo.ConnectGetInfo = &conformancev1.ConformancePayload_ConnectGetInfo{ QueryParams: []*conformancev1.Header{ - { - Name: "message", - Value: []string{value}, - }, { Name: "encoding", Value: []string{encoding}, diff --git a/internal/app/connectconformance/test_case_library_test.go b/internal/app/connectconformance/test_case_library_test.go index 04d4a0c2..219cdcf5 100644 --- a/internal/app/connectconformance/test_case_library_test.go +++ b/internal/app/connectconformance/test_case_library_test.go @@ -15,11 +15,9 @@ package connectconformance import ( - "encoding/base64" "sort" "testing" - "connectrpc.com/conformance/internal" "connectrpc.com/conformance/internal/app/connectconformance/testsuites" conformancev1 "connectrpc.com/conformance/internal/gen/proto/go/connectrpc/conformance/v1" "connectrpc.com/connect" @@ -922,10 +920,6 @@ func TestPopulateExpectedResponse(t *testing.T) { Requests: asAnySlice(t, unarySuccessReq), ConnectGetInfo: &conformancev1.ConformancePayload_ConnectGetInfo{ QueryParams: []*conformancev1.Header{ - { - Name: "message", - Value: []string{marshalToString(t, true, unarySuccessReq)}, - }, { Name: "encoding", Value: []string{"json"}, @@ -961,10 +955,6 @@ func TestPopulateExpectedResponse(t *testing.T) { Requests: asAnySlice(t, unarySuccessReq), ConnectGetInfo: &conformancev1.ConformancePayload_ConnectGetInfo{ QueryParams: []*conformancev1.Header{ - { - Name: "message", - Value: []string{marshalToString(t, false, unarySuccessReq)}, - }, { Name: "encoding", Value: []string{"proto"}, @@ -1000,10 +990,6 @@ func TestPopulateExpectedResponse(t *testing.T) { Requests: asAnySlice(t, unaryErrorReq), ConnectGetInfo: &conformancev1.ConformancePayload_ConnectGetInfo{ QueryParams: []*conformancev1.Header{ - { - Name: "message", - Value: []string{marshalToString(t, true, unaryErrorReq)}, - }, { Name: "encoding", Value: []string{"json"}, @@ -1038,10 +1024,6 @@ func TestPopulateExpectedResponse(t *testing.T) { Requests: asAnySlice(t, unaryErrorReq), ConnectGetInfo: &conformancev1.ConformancePayload_ConnectGetInfo{ QueryParams: []*conformancev1.Header{ - { - Name: "message", - Value: []string{marshalToString(t, false, unaryErrorReq)}, - }, { Name: "encoding", Value: []string{"proto"}, @@ -1791,23 +1773,3 @@ func asAnySlice(t *testing.T, msgs ...proto.Message) []*anypb.Any { } return arr } - -// marshalToString marshals the given proto message to a string mirroring the -// logic that Connect specifies for GET requests. -// If asJSON is true, the message is first marshalled to JSON and the bytes are -// then converted to a string. -// If asJSON is false, the message is marshalled to binary and the bytes are then -// base64-encoded as a string. -func marshalToString(t *testing.T, asJSON bool, msg proto.Message) string { - t.Helper() - codec := internal.NewCodec(asJSON) - - bytes, err := codec.MarshalStable(msg) - require.NoError(t, err) - - if asJSON { - return string(bytes) - } - - return base64.RawURLEncoding.EncodeToString(bytes) -} diff --git a/internal/codec.go b/internal/codec.go index b3cafb57..7a86b2d9 100644 --- a/internal/codec.go +++ b/internal/codec.go @@ -15,7 +15,6 @@ package internal import ( - "bytes" "encoding/json" "errors" "fmt" @@ -38,15 +37,14 @@ type StreamEncoder interface { Encode(msg proto.Message) error } -// codec describes anything that can marshal and unmarshal proto messages. -type codec interface { +// Codec describes anything that can marshal and unmarshal proto messages. +type Codec interface { NewDecoder(io.Reader) StreamDecoder NewEncoder(io.Writer) StreamEncoder - MarshalStable(msg proto.Message) ([]byte, error) } // NewCodec returns a new Codec. -func NewCodec(json bool) codec { +func NewCodec(json bool) Codec { if json { return &jsonCodec{MarshalOptions: protojson.MarshalOptions{Multiline: true}} } @@ -112,22 +110,6 @@ func (j *jsonEncoder) Encode(msg proto.Message) error { return nil } -// This function is lifted from connect-go since this is the logic it uses -// to stably marshal a request message into JSON for putting into query params -// of GET requests. -// See https://github.com/connectrpc/connect-go/blob/main/codec.go -func (c *jsonCodec) MarshalStable(msg proto.Message) ([]byte, error) { - messageJSON, err := c.Marshal(msg) - if err != nil { - return nil, fmt.Errorf("failed to marshal message to JSON: %w", err) - } - compactedJSON := bytes.NewBuffer(messageJSON[:0]) - if err = json.Compact(compactedJSON, messageJSON); err != nil { - return nil, fmt.Errorf("failed to compact marshaled JSON: %w", err) - } - return compactedJSON.Bytes(), nil -} - // protoCodec marshals and unmarshals the Protobuf binary format. type protoCodec struct { proto.MarshalOptions @@ -148,15 +130,6 @@ func (c *protoCodec) NewEncoder(out io.Writer) StreamEncoder { } } -// This function is lifted from connect-go since this is the logic it uses -// to stably marshal a request message into binary for putting into query params -// of GET requests. -// See https://github.com/connectrpc/connect-go/blob/main/codec.go -func (c *protoCodec) MarshalStable(msg proto.Message) ([]byte, error) { - options := proto.MarshalOptions{Deterministic: true} - return options.Marshal(msg) -} - type protoDecoder struct { opts proto.UnmarshalOptions in io.Reader