diff --git a/.github/workflows/buf.yaml b/.github/workflows/buf.yaml index 026db3bf..fe93e374 100644 --- a/.github/workflows/buf.yaml +++ b/.github/workflows/buf.yaml @@ -21,10 +21,11 @@ jobs: 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 + # - 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/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 c7cf0db6..d6411b58 100644 --- a/internal/app/connectconformance/results.go +++ b/internal/app/connectconformance/results.go @@ -191,11 +191,17 @@ func (r *testResults) assert(testCase string, expected, actual *conformancev1.Cl errs = append(errs, checkHeaders("response trailers", expected.ResponseTrailers, actual.ResponseTrailers)...) } - // 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 != "" { - errs = append(errs, fmt.Errorf("raw Connect error does not match: - wanted, + got\n%s", diff)) + expectedWire := expected.WireDetails + actualWire := actual.WireDetails + if expectedWire != nil && actualWire != nil { + // 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)) } } @@ -486,7 +492,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/testsuites/connect_to_http_code.yaml b/internal/app/connectconformance/testsuites/connect_to_http_code.yaml new file mode 100644 index 00000000..563a37f4 --- /dev/null +++ b/internal/app/connectconformance/testsuites/connect_to_http_code.yaml @@ -0,0 +1,452 @@ +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: + wireDetails: + 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: + wireDetails: + 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: + wireDetails: + 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: + wireDetails: + 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: + wireDetails: + 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: + wireDetails: + 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: + wireDetails: + 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: + wireDetails: + 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: + wireDetails: + 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: + wireDetails: + 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: + wireDetails: + 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: + wireDetails: + 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: + wireDetails: + 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: + wireDetails: + 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: + wireDetails: + 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: + wireDetails: + 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: + wireDetails: + 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: + wireDetails: + 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: + wireDetails: + 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: + wireDetails: + 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 diff --git a/internal/app/connectconformance/testsuites/errors.yaml b/internal/app/connectconformance/testsuites/errors.yaml index 8a3123db..501d332d 100644 --- a/internal/app/connectconformance/testsuites/errors.yaml +++ b/internal/app/connectconformance/testsuites/errors.yaml @@ -412,6 +412,31 @@ testCases: 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 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/client.go b/internal/app/referenceclient/client.go index 0fc7fa45..6214cd56 100644 --- a/internal/app/referenceclient/client.go +++ b/internal/app/referenceclient/client.go @@ -216,9 +216,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) - } + + // 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/impl.go b/internal/app/referenceclient/impl.go index acafe4c6..a0135511 100644 --- a/internal/app/referenceclient/impl.go +++ b/internal/app/referenceclient/impl.go @@ -117,8 +117,11 @@ func (i *invoker) unary( var trailers []*v1.Header payloads := make([]*v1.ConformancePayload, 0, 1) + ctx = withWireCapture(ctx) + // 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, @@ -141,7 +144,7 @@ func (i *invoker) unary( ResponseTrailers: trailers, Payloads: payloads, Error: protoErr, - ConnectErrorRaw: nil, // TODO + WireDetails: getWireDetails(ctx), }, nil } @@ -168,8 +171,11 @@ func (i *invoker) idempotentUnary( var trailers []*v1.Header payloads := make([]*v1.ConformancePayload, 0, 1) + ctx = withWireCapture(ctx) + // Invoke the Unary call resp, err := i.client.IdempotentUnary(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, @@ -190,7 +196,7 @@ func (i *invoker) idempotentUnary( ResponseTrailers: trailers, Payloads: payloads, Error: protoErr, - ConnectErrorRaw: nil, // TODO + WireDetails: getWireDetails(ctx), }, nil } @@ -212,6 +218,8 @@ func (i *invoker) serverStream( // Add the specified request headers to the request internal.AddHeaders(req.RequestHeaders, request.Header()) + ctx = withWireCapture(ctx) + stream, err := i.client.ServerStream(ctx, request) if err != nil { // If an error was returned, first convert it to a Connect error @@ -272,12 +280,13 @@ func (i *invoker) serverStream( protoErr = internal.ConvertErrorToProtoError(err) } } + return &v1.ClientResponseResult{ ResponseHeaders: headers, ResponseTrailers: trailers, Payloads: payloads, Error: protoErr, - ConnectErrorRaw: nil, // TODO + WireDetails: getWireDetails(ctx), }, nil } @@ -288,6 +297,7 @@ func (i *invoker) clientStream( ctx, cancel := context.WithCancel(ctx) defer cancel() + ctx = withWireCapture(ctx) stream := i.client.ClientStream(ctx) // Add the specified request headers to the request @@ -346,28 +356,32 @@ func (i *invoker) clientStream( ResponseTrailers: trailers, Payloads: payloads, Error: protoErr, - ConnectErrorRaw: nil, // TODO + WireDetails: getWireDetails(ctx), }, 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() - result = &v1.ClientResponseResult{ - ConnectErrorRaw: nil, // TODO - } + result = &v1.ClientResponseResult{} + + ctx = withWireCapture(ctx) stream := i.client.BidiStream(ctx) defer func() { - if result != nil { - // 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 @@ -491,10 +505,14 @@ func (i *invoker) unimplemented( request := connect.NewRequest(ur) internal.AddHeaders(req.RequestHeaders, request.Header()) + ctx = withWireCapture(ctx) + // Invoke the Unary call _, err := i.client.Unimplemented(ctx, request) + return &v1.ClientResponseResult{ - Error: internal.ConvertErrorToProtoError(err), + Error: internal.ConvertErrorToProtoError(err), + WireDetails: getWireDetails(ctx), }, nil } diff --git a/internal/app/referenceclient/tracer.go b/internal/app/referenceclient/tracer.go new file mode 100644 index 00000000..a4bed10c --- /dev/null +++ b/internal/app/referenceclient/tracer.go @@ -0,0 +1,190 @@ +// 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 ( + "bytes" + "context" + "encoding/json" + "io" + "net/http" + "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" +) + +// The key associated with the wire details information stored in context. +type wireCtxKey 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 isUnaryJSONError(resp.Header.Get("content-type"), int32(resp.StatusCode)) { + 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[tracer.Trace] + // buf represents the read response body + buf *bytes.Buffer +} + +// 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{ + buf: &bytes.Buffer{}, + }) +} + +// 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(trace) +} + +// getWireDetails returns the wire details from the trace in the given context. +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 + } + trace := wrapper.val.Load() + // A nil response in the trace is valid if the HTTP round trip failed. + 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 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 + } + 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 + } + // 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 + } + } + default: + // Do nothing + } + } + } + + return &v1.WireDetails{ + ActualStatusCode: statusCode, + ActualHttpTrailers: internal.ConvertToProtoHeader(trace.Response.Trailer), + ConnectErrorRaw: &jsonRaw, + } +} + +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 by +// withWireCapture and can be retrieved via getWireDetails. +func (t *wireTracer) Complete(trace tracer.Trace) { + setWireTrace(trace.Request.Context(), &trace) + + if t.tracer != nil { + 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/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/internal/tracer/builder.go b/internal/tracer/builder.go index 9ee2036d..24d95c91 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/proto/connectrpc/conformance/v1/client_compat.proto b/proto/connectrpc/conformance/v1/client_compat.proto index 57a5fdb8..2eac256a 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; +}