Class GrpcHttp3Adapter

java.lang.Object
org.mockserver.netty.http3.GrpcHttp3Adapter

public final class GrpcHttp3Adapter extends Object
Adapter that bridges gRPC request/response framing for the HTTP/3 path.

Reuses the existing GrpcFrameCodec, GrpcProtoDescriptorStore, and GrpcJsonMessageConverter from mockserver-core -- no logic is duplicated. The adapter handles:

  • Inbound: detecting gRPC content-type, decoding the 5-byte length-prefixed gRPC frame(s) from the request body, converting protobuf to JSON via the descriptor store, and tagging the request with x-grpc-service / x-grpc-method so the response path can re-encode.
  • Outbound: converting the matched response's JSON body back to gRPC-framed protobuf, building the initial HTTP/3 HEADERS frame (without grpc-status), the DATA frame (gRPC framed body), and a separate trailing HEADERS frame with grpc-status/grpc-message and any user-authored trailing metadata -- the correct wire framing that gRPC clients expect over HTTP/3.

This class is stateless and thread-safe; all state is passed via method parameters.

  • Method Details

    • isGrpcRequest

      public static boolean isGrpcRequest(String contentType)
      Detect whether the given content-type indicates a gRPC request. Delegates to GrpcStatusMapper.isGrpcContentType(String).
    • transformGrpcRequest

      public static HttpRequest transformGrpcRequest(HttpRequest request, GrpcProtoDescriptorStore descriptorStore)
      Transform a gRPC request for the MockServer matching pipeline: decode the gRPC length-prefixed message(s), convert protobuf to JSON via the descriptor store, and tag the request with service/method markers.
      Parameters:
      request - the raw HttpRequest with gRPC-framed binary body
      descriptorStore - the gRPC proto descriptor store
      Returns:
      a transformed HttpRequest with a JSON body suitable for matching
      Throws:
      GrpcException - if the gRPC path or framing is invalid
    • transformGrpcResponse

      public static GrpcHttp3Adapter.GrpcResponseParts transformGrpcResponse(HttpResponse response, String serviceName, String methodName, GrpcProtoDescriptorStore descriptorStore)
      Convert the response from the matching pipeline back to a gRPC-framed response. Encodes the JSON body to protobuf, wraps it in a gRPC length-prefixed frame, and determines the grpc-status.
      Parameters:
      response - the matched HttpResponse (JSON body, grpc-status-name header)
      serviceName - the gRPC service name from the original request
      methodName - the gRPC method name from the original request
      descriptorStore - the gRPC proto descriptor store
      Returns:
      a GrpcHttp3Adapter.GrpcResponseParts with the initial headers, body bytes, and trailing headers
    • errorResponse

      public static GrpcHttp3Adapter.GrpcResponseParts errorResponse(GrpcStatusMapper.GrpcStatusCode statusCode, String message)
      Build an error response (trailers-only pattern) for gRPC over HTTP/3. Used when gRPC request processing fails before matching.
      Parameters:
      statusCode - the gRPC status code
      message - the error message
      Returns:
      a GrpcHttp3Adapter.GrpcResponseParts with no body and the error status
    • buildInitialHeadersFrame

      public static io.netty.handler.codec.http3.DefaultHttp3HeadersFrame buildInitialHeadersFrame()
      Build the initial HTTP/3 HEADERS frame for a gRPC response. Contains :status=200 and content-type=application/grpc but NOT grpc-status (which belongs in the trailing HEADERS frame).
    • buildInitialHeadersFrame

      public static io.netty.handler.codec.http3.DefaultHttp3HeadersFrame buildInitialHeadersFrame(HttpResponse response)
      As buildInitialHeadersFrame(), additionally copying the matched response's own headers through to the client.

      Without this, a unary HTTP/3 gRPC response silently dropped every header the expectation set -- withHeader("x-tenant-id", "acme") simply never arrived. HTTP/2 preserves them by cloning the response, and the HTTP/3 server-streaming path already copies them via addConfiguredHeaders, so unary HTTP/3 was the only path that lost them. gRPC protocol metadata is excluded (GrpcResponseStatusResolver.isGrpcProtocolMetadata(java.lang.String)) because this frame emits :status and content-type itself, and grpc-status/grpc-message belong in the trailing HEADERS frame.

      Parameters:
      response - the matched response, may be null
    • buildTrailingHeadersFrame

      public static io.netty.handler.codec.http3.DefaultHttp3HeadersFrame buildTrailingHeadersFrame(String grpcStatus, String grpcMessage)
      Build the trailing HTTP/3 HEADERS frame for a gRPC response. Contains grpc-status and optionally grpc-message. No pseudo-headers (no :status) because this is a trailing HEADERS frame.
    • buildTrailingHeadersFrame

      public static io.netty.handler.codec.http3.DefaultHttp3HeadersFrame buildTrailingHeadersFrame(String grpcStatus, String grpcMessage, HttpResponse response)
      As buildTrailingHeadersFrame(String, String), additionally emitting the matched response's user-authored trailers as gRPC trailing metadata.

      Without this, HTTP/3 silently dropped every trailer an expectation set: response().withTrailer("x-request-cost", "42") and the gRPC chaos profile's customTrailers simply never reached the client, on both the body and the body-less branch. HTTP/1.1 and HTTP/2 emit them from the response model via MockServerHttpResponseToFullHttpResponse.mapResponseWithTrailers; the HTTP/3 gRPC writer builds its frames by hand and carried only grpc-status/grpc-message.

      The custom metadata rides the same terminal frame as the status, so the framing is unchanged -- there is still exactly one trailing HEADERS frame, written with SHUTDOWN_OUTPUT. This is why HTTP/3 never had the HTTP/2 Trailers-Only defect that GrpcToHttpResponseHandler.asTrailersOnlyIfHttp2 guards against.

      Parameters:
      response - the matched response, may be null for a transport-synthesized status (a grpc-timeout deadline, an encoding failure) that has no user-authored trailers
    • buildDataFrame

      public static io.netty.handler.codec.http3.DefaultHttp3DataFrame buildDataFrame(byte[] grpcFrameBytes)
      Build an HTTP/3 DATA frame from gRPC-framed bytes. Returns null if the frame bytes are null or empty.
    • buildTrailersOnlyFrame

      public static io.netty.handler.codec.http3.DefaultHttp3HeadersFrame buildTrailersOnlyFrame(String grpcStatus, String grpcMessage)
      Build a "trailers-only" HTTP/3 HEADERS frame that combines :status, content-type, and grpc-status/grpc-message in a single HEADERS frame (no DATA frame follows). This is used for error responses where there is no message body.
    • buildTrailersOnlyFrame

      public static io.netty.handler.codec.http3.DefaultHttp3HeadersFrame buildTrailersOnlyFrame(String grpcStatus, String grpcMessage, HttpResponse response)
      As buildTrailersOnlyFrame(String, String), additionally copying the matched response's own headers and trailers through -- a trailers-only response is the client's only frame, so dropping either loses it entirely.

      Folding the user-authored trailers into this frame is the correct gRPC shape rather than a compromise: the Trailers-Only form is defined as HTTP-Status Content-Type Trailers, and Trailers includes custom metadata. A gRPC client reads this single end-of-stream frame as the call's trailing metadata. Emitting a second frame after it instead would be wrong twice over -- the frame is already written with SHUTDOWN_OUTPUT, and a non-terminal initial frame carrying grpc-status is exactly the HTTP/2 defect fixed in GrpcToHttpResponseHandler.asTrailersOnlyIfHttp2.