vanguard

package module
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 26, 2024 License: Apache-2.0 Imports: 29 Imported by: 10

README

⚔️ Vanguard

Build Report Card GoDoc Slack

Vanguard is a powerful library for Go net/http servers that enables seamless transcoding between REST and RPC protocols. Whether you need to bridge the gap between gRPC, gRPC-Web, Connect, or REST, Vanguard has got you covered. With support for Google's HTTP transcoding options, it can effortlessly translate protocols using strongly typed Protobuf definitions.

See an example in action!

Why Vanguard?

Vanguard offers a range of compelling use cases that make it an invaluable addition to your services:

  1. RESTful Transformation: By leveraging HTTP transcoding annotations, you can effortlessly support REST clients. This feature is especially handy during the migration from a REST API to a schema-driven RPC API. With the right annotations, your existing REST clients can seamlessly access your API, even as you transition your server implementations to Protobuf and RPC.

  2. Efficiency and Code Generation: Unlike traditional approaches like gRPC-Gateway, Vanguard operates efficiently within Go servers, compatible with various servers such as Connect and gRPC. It doesn't rely on extensive code generation, eliminating the need for additional code generation steps. This flexibility ensures that your code can adapt dynamically, loading service definitions from configuration, schema registries, or via gRPC Server Reflection, making it a perfect fit for proxies without the hassle of recompilation and redeployment each time an RPC service schema changes.

  3. Legacy Compatibility: The HTTP transcoding annotations also empower you to support legacy REST API servers when clients are accustomed to using Protobuf RPC. This lets you embrace RPC in specific teams, such as for web or mobile clients, without the prerequisite of migrating all backend API services.

  4. Seamless Protocol Bridging: If your organization is transitioning from gRPC to Connect, Vanguard acts as a bridge between the protocols. This facilitates the use of your existing gRPC service handlers with Connect clients, allowing you to smoothly adapt to Connect's enhanced usability and inspectability with web browsers and mobile devices. No need to overhaul your server handler logic before migrating clients to Connect.

Usage

Vanguard is straight-forward to configure and is used to wrap other HTTP handlers. In that regard, a Vanguard transcoder acts kind of like HTTP middleware. The kinds of HTTP handlers you'll typically be wrapping are:

  1. gRPC handlers: After configuring a *grpc.Server, instead of calling its Start method, it can be mounted as a handler of an http.Server or http.ServeMux. This allows you to decorate the gRPC handler with the Vanguard middleware.
  2. Connect handlers: With Connect, handlers already implement http.Handler and are thus trivial to wrap with Vanguard.
  3. Proxy handlers: In some cases, your Go service may act as a proxy and forward requests to a different backend server. To support legacy REST API servers, the Go server can proxy requests to those legacy backends, and Vanguard can transcode incoming RPC requests to a form that the legacy backends can understand.
Defining Services

A Service in Vanguard is the configuration for a single Protobuf RPC service. This configuration includes a schema for the service, which empowers the format translations and is also the typical source HTTP annotations, for mapping an RPC to REST-ful conventions. It also includes a handler, which is an http.Handler that implements the service. Finally, it can include options that configure the protocols and formats that the handler can accept.

You create one by calling vanguard.NewService, supplying the service's fully-qualified name (or URI path, which is the fully-qualified name with a leading and trailing slash) and the corresponding handler.

If you supply no other options, a service's default configuration supports a Connect handler. So if you use the protoc-gen-connect-go Protobuf plugin and use the generated factory function for creating a handler, it will work perfectly with Vanguard.

// A Connect handler factory function returns the service path and the HTTP
// handler. So you can pass the result directly to NewService:
myService := vanguard.NewService(
    myservicev1connect.NewMyServiceHandler(&myServiceImpl{}),
)

// If not using a Connect handler, you can still use this function and
// directly refer to the service's name.
myService = vanguard.NewService(
    myservicev1connect.MyServiceName,
	someOtherHTTPHandler,
)

With the vanguard.NewService function, the service must be known to the program. That means that Go code has been generated for the Protobuf file that defines the RPC service, using the protoc-gen-go plugin, and then imported into the program. That generated Go code registers the service's schema in a global registry named protoregistry.GlobalFiles.

If you are using Connect or gRPC handlers, there is nothing to worry about: the use of generated Go code for Connect and gRPC stubs and handler interfaces ensures that the services are known.

Dynamic Schemas

If you want to handle a service whose schema is not known to the program at compile-time, then you can use vanguard.NewServiceWithSchema and supply a protoreflect.ServiceDescriptor that defines the RPC schema. This descriptor could have been loaded from a configuration file or file descriptor set, downloaded from a server, such as a Buf Schema Registry, or even compiled from Protobuf sources.

Service Options

Both vanguard.NewService and vanguard.NewServiceWithSchema functions accept options for configuring how a transcoder will process requests for that service. In particular, you can configure the protocols, the codecs/message formats, and compression formats that the service handler accepts.

As mentioned previously, the default configuration -- with no options -- is designed to work out-of-the-box with Connect handlers: it assumes the handler can process Connect, gRPC, and gRPC-Web protocols; that it can process "proto" and "json" message formats; and that it can handle "gzip" compression.

You can change this, including adding more message formats or compression algorithms, by supplying options:

// In this example, we'll assume that proxyHandler functions as a reverse proxy
// to a gRPC backend that only supports the gRPC protocol and the "proto" message
// format.
otherService := vanguard.NewService(
	"some.other.Service",
	proxyHandler,
	vanguard.WithTargetProtocols(vanguard.ProtocolGRPC),
	vanguard.WithTargetCodecs(vanguard.CodecProto)
)
Building the Transcoder

The thing that does all of the work in Vanguard is a *vanguard.Transcoder. When the transcoder is instantiated, you provide the set of services. The transcoder implements http.Handler and handles dispatching requests to the various configured service handlers, and it also handles translating requests into a form that the handler understands. It then also translates responses into the form that the client is expecting.

transcoder := vanguard.NewTranscoder([]*vanguard.Service{
    myService,
    otherService,
})
Transcoder Options

When creating a transcoder, you can supply options to customize it:

  • You can provide a custom handler for unknown endpoints. Without this, requests for unrecognized URI paths will result in a simple "404 Not Found" response.
  • You can provide separate HTTP annotations. This is to provide REST-ful mappings for RPC services that do not define the mapping in their Protobuf sources. It also allows you to add mappings to a service that does already have annotations.
  • You can add support for extra codecs, beyond "proto" and "json", and compression algorithms, beyond "gzip".
  • You can supply a set of default service options that will apply to every service (unless overridden via other options in a call to NewService).
transcoder = vanguard.NewTranscoder(
	[]*vanguard.Service{
        myService,
        otherService,
    },
	vanguard.WithUnknownHandler(custom404handler),
	vanguard.WithCodec(myCustomMessageFormat{}),
)

The use of WithCodec and WithCompression can also be used to replace the default implementations of the "proto" and "json" codec or the "gzip" compression algorithm. Note that when replacing the "json" codec, you should use *vanguard.JSONCodec as the implementation if you want to also support the REST protocol. REST-ful mappings can require message formatting features that are implemented by *vanguard.JSONCodec but not in the base vanguard.Codec interface.

gRPC Handlers

You can also wrap gRPC handlers with a Transcoder. This works a little differently than wrapping Connect handlers. The generated Go code for gRPC does not include a handler factory like Connect uses. It's generated functions instead register handler information with a *grpc.Server.

So you would register the various service implementations with a *grpc.Server, which implements http.Handler and can then be wrapped with a transcoder. But you can also use the vanguardgrpc package to do everything in a single function call:

transcoder = vanguardgrpc.NewTranscoder(grpcServer)

When you use vanguardgrpc.NewTranscoder, it automatically creates a *vanguard.Service for each service registered with the gRPC service, and supplies service options that tell it to transcode to the gRPC protocol and the "proto" codec. If you also install a "json" codec for gRPC, it will configure the transcoder to also allow sending the "json" codec to the gRPC server, which makes handling of JSON Connect requests much more efficient.

Wiring up to a server

Finally, you can register the transcoder with an http.Server or http.ServeMux.

// The Mux can be used as the sole handler for an HTTP server.
err := http.Serve(listener, transcoder)

// Or it can be used alongside other handlers, all registered with
// the same http.ServeMux.
mux := http.NewServeMux()
mux.Handle("/", transcoder)
err := http.Serve(listener, mux)

The above example registers the transcoder for the root path. This is useful to support REST requests for the service, which could have very different path URLs from those used for Connect and gRPC. Using a pattern this broad means the transcoder can handle all paths that might correspond to a method, without having to explicitly configure the ServeMux for the various paths named in HTTP transcoding annotations.

For the same reason, it is best to use a single vanguard transcoder, even if your service supports many exposed RPC services, so that the transcoder can dispatch to the correct service based on the request. You can register many services with the same transcoder. And if any need different configuration, that can be handled by providing override options when the service is created.

Status: Alpha

Vanguard is undergoing initial development and is not yet stable.

Offered under the Apache 2 license.

Documentation

Overview

Package vanguard provides a transcoder that acts like middleware for your RPC handlers, augmenting them to support additional protocols or message formats, including REST+JSON. The transcoder also acts as a router, handling dispatch of configured REST-ful URI paths to the right RPC handlers.

Use NewService or NewServiceWithSchema to create Service definitions wrap your existing HTTP and/or RPC handlers. Then pass those services to NewTranscoder.

Example (RestClientToRpcServer)
package main

import (
	"bytes"
	"context"
	"io"
	"log"
	"net/http"
	"net/http/httptest"
	"os"
	"strings"
	"time"

	"connectrpc.com/connect"
	"connectrpc.com/vanguard"

	testv1 "connectrpc.com/vanguard/internal/gen/vanguard/test/v1"
	"connectrpc.com/vanguard/internal/gen/vanguard/test/v1/testv1connect"
	"google.golang.org/protobuf/encoding/protojson"
	"google.golang.org/protobuf/types/known/timestamppb"
)

func main() {
	// This example shows Vanguard adding REST support to an RPC server built
	// with Connect. (To add REST, gRPC-Web, and Connect support to servers built
	// with grpc-go, use the connectrpc.com/vanguard/vanguardgrpc sub-package.)
	logger := log.New(os.Stdout, "" /* prefix */, 0 /* flags */)

	// libraryRPC is an implementation of the testv1connect.LibraryService RPC
	// server. It's a pure RPC server, without any hand-written translation to or
	// from RESTful semantics.
	svc := &libraryRPC{}
	rpcRoute, rpcHandler := testv1connect.NewLibraryServiceHandler(svc)

	// Using Vanguard, the server can also accept RESTful requests. The Vanguard
	// Transcoder handles both REST and RPC traffic, so there's no need to mount
	// the RPC-only handler.
	services := []*vanguard.Service{vanguard.NewService(rpcRoute, rpcHandler)}
	transcoder, err := vanguard.NewTranscoder(services)
	if err != nil {
		logger.Println(err)
		return
	}

	// We can use any server that works with http.Handlers. Since this is a
	// testable example, we're using httptest.
	server := httptest.NewServer(transcoder)
	defer server.Close()

	// With the server running, we can make a RESTful call.
	client := server.Client()
	book := &testv1.Book{
		Title:       "2001: A Space Odyssey",
		Author:      "Arthur C. Clarke",
		Description: "A space voyage to Jupiter awakens the crew's intelligence.",
		Labels: map[string]string{
			"genre": "science fiction",
		},
	}
	body, err := protojson.Marshal(book)
	if err != nil {
		logger.Println(err)
		return
	}

	req, err := http.NewRequestWithContext(
		context.Background(), http.MethodPost,
		server.URL+"/v1/shelves/top/books",
		bytes.NewReader(body),
	)
	if err != nil {
		logger.Println(err)
		return
	}
	req.Header.Set("Content-Type", "application/json")
	req.URL.RawQuery = "book_id=2&request_id=123"

	rsp, err := client.Do(req)
	if err != nil {
		logger.Println(err)
		return
	}
	defer rsp.Body.Close()
	logger.Println(rsp.Status)
	logger.Println(rsp.Header.Get("Content-Type"))

	body, err = io.ReadAll(rsp.Body)
	if err != nil {
		logger.Println(err)
		return
	}
	if err := protojson.Unmarshal(body, book); err != nil {
		logger.Println(err)
		return
	}
	logger.Println(book.GetAuthor())
}

type libraryRPC struct {
	testv1connect.UnimplementedLibraryServiceHandler
}

func (s *libraryRPC) GetBook(_ context.Context, req *connect.Request[testv1.GetBookRequest]) (*connect.Response[testv1.Book], error) {
	msg := req.Msg
	rsp := connect.NewResponse(&testv1.Book{
		Name:        msg.GetName(),
		Parent:      strings.Join(strings.Split(msg.GetName(), "/")[:2], "/"),
		CreateTime:  timestamppb.New(time.Date(1968, 1, 1, 0, 0, 0, 0, time.UTC)),
		Title:       "Do Androids Dream of Electric Sheep?",
		Author:      "Philip K. Dick",
		Description: "Have you seen Blade Runner?",
		Labels: map[string]string{
			"genre": "science fiction",
		},
	})
	return rsp, nil
}

func (s *libraryRPC) CreateBook(_ context.Context, req *connect.Request[testv1.CreateBookRequest]) (*connect.Response[testv1.Book], error) {
	msg := req.Msg
	book := req.Msg.GetBook()
	rsp := connect.NewResponse(&testv1.Book{
		Name:        strings.Join([]string{msg.GetParent(), "books", msg.GetBookId()}, "/"),
		Parent:      msg.GetParent(),
		CreateTime:  timestamppb.New(time.Date(1968, 1, 1, 0, 0, 0, 0, time.UTC)),
		Title:       book.GetTitle(),
		Author:      book.GetAuthor(),
		Description: book.GetDescription(),
		Labels:      book.GetLabels(),
	})
	return rsp, nil
}
Output:

200 OK
application/json
Arthur C. Clarke
Example (RpcClientToRestServer)
package main

import (
	"context"
	"errors"
	"io"
	"log"
	"net/http"
	"net/http/httptest"
	"os"
	"regexp"
	"strconv"
	"strings"
	"time"

	"connectrpc.com/connect"
	"connectrpc.com/vanguard"

	testv1 "connectrpc.com/vanguard/internal/gen/vanguard/test/v1"
	"connectrpc.com/vanguard/internal/gen/vanguard/test/v1/testv1connect"
	"google.golang.org/protobuf/encoding/protojson"
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/types/known/timestamppb"
)

func main() {
	// This example shows Vanguard adding RPC support to an REST server. This
	// lets organizations use RPC clients in new codebases without rewriting
	// existing REST services.
	logger := log.New(os.Stdout, "" /* prefix */, 0 /* flags */)

	// libraryREST is an http.Handler that implements a RESTful server. The
	// implementation doesn't use Protobuf or RPC directly.
	restHandler := &libraryREST{}

	// Using Vanguard, the server can also accept RPC traffic. The Vanguard
	// Transcoder handles both REST and RPC traffic, so there's no need to mount
	// the REST-only handler.
	services := []*vanguard.Service{vanguard.NewService(
		testv1connect.LibraryServiceName,
		restHandler,
		// This tells vanguard that the service implementation only supports REST.
		vanguard.WithTargetProtocols(vanguard.ProtocolREST),
	)}
	transcoder, err := vanguard.NewTranscoder(services)
	if err != nil {
		logger.Println(err)
		return
	}

	// We can serve RPC and REST traffic using any server that works with
	// http.Handlers. Since this is a testable example, we're using httptest.
	server := httptest.NewServer(transcoder)
	defer server.Close()

	// With the server running, we can make an RPC call using a generated client.
	client := testv1connect.NewLibraryServiceClient(server.Client(), server.URL)
	rsp, err := client.GetBook(
		context.Background(),
		connect.NewRequest(&testv1.GetBookRequest{
			Name: "shelves/top/books/123",
		}),
	)
	if err != nil {
		logger.Println(err)
		return
	}
	logger.Println(rsp.Msg.GetDescription())
}

type libraryREST struct {
	libraryRPC
}

func (s *libraryREST) ServeHTTP(rsp http.ResponseWriter, req *http.Request) {
	urlPath := []byte(req.URL.Path)
	ctx := req.Context()
	var msg proto.Message
	var err error
	switch req.Method {
	case http.MethodGet:
		switch {
		case regexp.MustCompile("/v1/shelves/.*/books/.*").Match(urlPath):
			got, gotErr := s.GetBook(ctx, connect.NewRequest(&testv1.GetBookRequest{
				Name: req.URL.Path[len("/v1/"):],
			}))
			msg, err = got.Msg, gotErr
		default:
			err = connect.NewError(connect.CodeNotFound, errors.New("method not found"))
		}
	case http.MethodPost:
		switch {
		case regexp.MustCompile("/v1/shelves/.*").Match(urlPath):
			var book testv1.Book
			body, _ := io.ReadAll(req.Body)
			_ = protojson.Unmarshal(body, &book)
			got, gotErr := s.CreateBook(ctx, connect.NewRequest(&testv1.CreateBookRequest{
				Parent:    req.URL.Path[len("/v1/"):],
				BookId:    req.URL.Query().Get("book_id"),
				Book:      &book,
				RequestId: req.URL.Query().Get("request_id"),
			}))
			msg, err = got.Msg, gotErr
		default:
			err = connect.NewError(connect.CodeNotFound, errors.New("method not found"))
		}
	default:
		err = connect.NewError(connect.CodeNotFound, errors.New("method not found"))
	}
	rsp.Header().Set("Content-Type", "application/json")
	var body []byte
	if err != nil {
		code := connect.CodeInternal
		if ce := (*connect.Error)(nil); errors.As(err, &ce) {
			code = ce.Code()
		}
		body = []byte(`{"code":` + strconv.Itoa(int(code)) +
			`, "message":"` + err.Error() + `"}`)
	} else {
		body, _ = protojson.Marshal(msg)
	}
	rsp.WriteHeader(http.StatusOK)
	_, _ = rsp.Write(body)
}

type libraryRPC struct {
	testv1connect.UnimplementedLibraryServiceHandler
}

func (s *libraryRPC) GetBook(_ context.Context, req *connect.Request[testv1.GetBookRequest]) (*connect.Response[testv1.Book], error) {
	msg := req.Msg
	rsp := connect.NewResponse(&testv1.Book{
		Name:        msg.GetName(),
		Parent:      strings.Join(strings.Split(msg.GetName(), "/")[:2], "/"),
		CreateTime:  timestamppb.New(time.Date(1968, 1, 1, 0, 0, 0, 0, time.UTC)),
		Title:       "Do Androids Dream of Electric Sheep?",
		Author:      "Philip K. Dick",
		Description: "Have you seen Blade Runner?",
		Labels: map[string]string{
			"genre": "science fiction",
		},
	})
	return rsp, nil
}

func (s *libraryRPC) CreateBook(_ context.Context, req *connect.Request[testv1.CreateBookRequest]) (*connect.Response[testv1.Book], error) {
	msg := req.Msg
	book := req.Msg.GetBook()
	rsp := connect.NewResponse(&testv1.Book{
		Name:        strings.Join([]string{msg.GetParent(), "books", msg.GetBookId()}, "/"),
		Parent:      msg.GetParent(),
		CreateTime:  timestamppb.New(time.Date(1968, 1, 1, 0, 0, 0, 0, time.UTC)),
		Title:       book.GetTitle(),
		Author:      book.GetAuthor(),
		Description: book.GetDescription(),
		Labels:      book.GetLabels(),
	})
	return rsp, nil
}
Output:

Have you seen Blade Runner?

Index

Examples

Constants

View Source
const (
	// ProtocolConnect indicates the Connect protocol. This protocol supports
	// unary and streaming endpoints. However, bidirectional streams are only
	// supported when combined with HTTP/2.
	ProtocolConnect = Protocol(iota + 1)
	// ProtocolGRPC indicates the gRPC protocol. This protocol can only be
	// used in combination with HTTP/2. It supports unary and all kinds of
	// streaming endpoints.
	ProtocolGRPC
	// ProtocolGRPCWeb indicates the gRPC-Web protocol. This is a tweak of the
	// gRPC protocol to support HTTP 1.1. This protocol supports unary and
	// streaming endpoints. However, bidirectional streams are only supported
	// when combined with HTTP/2.
	ProtocolGRPCWeb
	// ProtocolREST indicates the REST+JSON protocol. This protocol often
	// requires non-trivial transformations between HTTP requests and responses
	// and Protobuf request and response messages.
	//
	// Only methods that have the google.api.http annotation can be invoked
	// with this protocol. The annotation defines the "shape" of the HTTP
	// request and response, such as the URI path, HTTP method, and how URI
	// path components, query string parameters, and an optional request
	// body are mapped to the Protobuf request message.
	//
	// This protocol only supports unary and server-stream endpoints.
	ProtocolREST
)
View Source
const (
	// CompressionGzip is the name of the gzip compression algorithm.
	CompressionGzip = "gzip"
	// CompressionIdentity is the name of the "identity" compression algorithm,
	// which is the default and indicates no compression.
	CompressionIdentity = "identity"

	// CodecProto is the name of the protobuf codec.
	CodecProto = "proto"
	// CodecJSON is the name of the JSON codec.
	CodecJSON = "json"

	// DefaultMaxMessageBufferBytes is the default value for the maximum number
	// of bytes that can be buffered for a request or response payload. If a
	// payload exceeds this limit, the RPC will fail with a "resource exhausted"
	// error.
	DefaultMaxMessageBufferBytes = math.MaxUint32
	// DefaultMaxGetURLBytes is the default value for the maximum number of bytes
	// that can be used for a URL with the Connect unary protocol using the GET
	// HTTP method. If a URL's length would exceed this limit, the POST HTTP method
	// will be used instead (and the request contents moved from the URL to the body).
	DefaultMaxGetURLBytes = 8 * 1024
)

Variables

This section is empty.

Functions

This section is empty.

Types

type Codec

type Codec interface {
	// Name returns the name of this codec. This is used in content-type
	// strings to indicate this codec in the various RPC protocols.
	Name() string
	// MarshalAppend marshals the given message to bytes, appended to the
	// given base byte slice. The given slice may be empty, but its
	// capacity should be used when marshalling to bytes to reduce
	// additional allocations.
	MarshalAppend(base []byte, msg proto.Message) ([]byte, error)
	// Unmarshal unmarshals the given data into the given target message.
	Unmarshal(data []byte, msg proto.Message) error
}

Codec is a message encoding format. It handles unmarshalling messages from bytes and back.

type JSONCodec

type JSONCodec struct {
	MarshalOptions   protojson.MarshalOptions
	UnmarshalOptions protojson.UnmarshalOptions
}

JSONCodec implements Codec, StableCodec, and RESTCodec for the JSON format. It uses the protojson package for its implementation.

func NewJSONCodec

func NewJSONCodec(res TypeResolver) *JSONCodec

NewJSONCodec is the default codec factory used for the codec named "json". The given resolver is used to unmarshal extensions and also to marshal and unmarshal instances of google.protobuf.Any.

By default, the returned codec is configured to emit unpopulated fields when marshalling and to discard unknown fields when unmarshalling.

func (JSONCodec) IsBinary

func (j JSONCodec) IsBinary() bool

IsBinary returns false, indicating that JSON is a text format. Implements StableCodec.

func (JSONCodec) MarshalAppend

func (j JSONCodec) MarshalAppend(base []byte, msg proto.Message) ([]byte, error)

MarshalAppend implements Codec.

func (JSONCodec) MarshalAppendField

func (j JSONCodec) MarshalAppendField(base []byte, msg proto.Message, field protoreflect.FieldDescriptor) ([]byte, error)

MarshalAppendField implements RESTCodec.

func (JSONCodec) MarshalAppendStable

func (j JSONCodec) MarshalAppendStable(base []byte, msg proto.Message) ([]byte, error)

MarshalAppendStable implements StableCodec.

func (JSONCodec) Name

func (j JSONCodec) Name() string

Name returns "json". Implements Codec.

func (JSONCodec) Unmarshal

func (j JSONCodec) Unmarshal(bytes []byte, msg proto.Message) error

Unmarshal implements Codec.

func (JSONCodec) UnmarshalField

func (j JSONCodec) UnmarshalField(data []byte, msg proto.Message, field protoreflect.FieldDescriptor) error

UnmarshalField implements RESTCodec.

type ProtoCodec

type ProtoCodec struct {
	// contains filtered or unexported fields
}

ProtoCodec implements Codec and StableCodec for the binary Protobuf format. It uses the proto package for its implementation.

func NewProtoCodec

func NewProtoCodec(res TypeResolver) *ProtoCodec

NewProtoCodec is the default codec factory used for the codec name "proto". The given resolver is used to unmarshal extensions.

func (*ProtoCodec) IsBinary

func (p *ProtoCodec) IsBinary() bool

IsBinary returns true, indicating that Protobuf is a binary format. Implements StableCodec.

func (*ProtoCodec) MarshalAppend

func (p *ProtoCodec) MarshalAppend(base []byte, msg proto.Message) ([]byte, error)

MarshalAppend implements Codec.

func (*ProtoCodec) MarshalAppendStable

func (p *ProtoCodec) MarshalAppendStable(base []byte, msg proto.Message) ([]byte, error)

MarshalAppendStable implements StableCodec.

func (*ProtoCodec) Name

func (p *ProtoCodec) Name() string

Name returns "proto". Implements Codec.

func (*ProtoCodec) Unmarshal

func (p *ProtoCodec) Unmarshal(bytes []byte, msg proto.Message) error

Unmarshal implements Codec.

type Protocol

type Protocol int

Protocol represents an on-the-wire protocol for RPCs.

func (Protocol) String

func (p Protocol) String() string

type RESTCodec

type RESTCodec interface {
	Codec

	// MarshalAppendField marshals just the given field of the given message to
	// bytes, and appends it to the given base byte slice.
	MarshalAppendField(base []byte, msg proto.Message, field protoreflect.FieldDescriptor) ([]byte, error)
	// UnmarshalField unmarshals the given data into the given field of the given
	// message.
	UnmarshalField(data []byte, msg proto.Message, field protoreflect.FieldDescriptor) error
}

RESTCodec is a Codec with additional methods for marshalling and unmarshalling individual fields of a message. This is necessary to support query string variables and request and response bodies whose value is a specific field, not an entire message. The extra methods are only used by the REST protocol.

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service represents the configuration for a single RPC service.

func NewService

func NewService(servicePath string, handler http.Handler, opts ...ServiceOption) *Service

NewService creates a new service definition for the given service path and handler. The service path must be the service's fully-qualified name, with an optional leading and trailing slash. This means you can provide generated constants for service names or you can provide the path returned by a New*Handler function generated by the [Protobuf plugin for Connect]. In fact, if you do not need to specify any service-specific options, you can directly wrap the call to the generated constructor with NewService:

vanguard.NewService(elizav1connect.NewElizaServiceHandler(elizaImpl))

If the given service path does not correspond to a known service (one whose schema is registered with the Protobuf runtime, usually from generated code), NewTranscoder will return an error. For these cases, where the corresponding service schema may be dynamically retrieved, use NewServiceWithSchema instead.

func NewServiceWithSchema

func NewServiceWithSchema(schema protoreflect.ServiceDescriptor, handler http.Handler, opts ...ServiceOption) *Service

NewServiceWithSchema creates a new service using the given schema and handler. This option is appropriate for use with dynamic schemas.

The default type resolver for the service will use protoregistry.GlobalTypes if the given service matches a descriptor of the same name registered in protoregistry.GlobalFiles. Otherwise, the default resolver will use dynamic messages for the given service's request and response types. In either case, the default resolver will fall back to protoregistry.GlobalTypes for resolving extensions and message types for messages inside anypb.Any values.

type ServiceOption

type ServiceOption interface {
	// contains filtered or unexported methods
}

A ServiceOption configures how a Transcoder handles requests to a particular RPC service. ServiceOptions can be passed to NewService and NewServiceWithSchema. Default ServiceOptions, that apply to all services, can be defined by passing a WithDefaultServiceOptions option to NewTranscoder. This is useful when all or many services use the same options.

func WithMaxGetURLBytes

func WithMaxGetURLBytes(limit uint32) ServiceOption

WithMaxGetURLBytes returns a service option that limits the size of URLs with the Connect unary protocol using the GET HTTP method. If a URL's length would exceed this limit, the POST HTTP method will be used instead (and the request contents moved from the URL to the body).

If set to zero or a negative value, a limit of 8 KB will be used.

func WithMaxMessageBufferBytes

func WithMaxMessageBufferBytes(limit uint32) ServiceOption

WithMaxMessageBufferBytes returns a service option that limits buffering of data when handling the service to the given limit. If any payload in a request or response exceeds this, the RPC will fail with a "resource exhausted" error.

If set to zero or a negative value, a limit of 4 GB will be used.

func WithNoTargetCompression

func WithNoTargetCompression() ServiceOption

WithNoTargetCompression returns a service option indicating that the server handler does not support compression.

func WithTargetCodecs

func WithTargetCodecs(names ...string) ServiceOption

WithTargetCodecs returns a service option indicating that the service handler supports the given codecs. By default, the handler is assumed only to support the "proto" codec.

func WithTargetCompression

func WithTargetCompression(names ...string) ServiceOption

WithTargetCompression returns a service option indicating that the service handler supports the given compression algorithms. By default, the handler is assumed only to support the "gzip" compression algorithm.

To configure the handler to not use any compression, one could use this option and supply no names. However, to make this scenario more readable, prefer WithNoTargetCompression instead.

func WithTargetProtocols

func WithTargetProtocols(protocols ...Protocol) ServiceOption

WithTargetProtocols returns a service option indicating that the service handler supports the listed protocols. By default, the handler is assumed to support all but the REST protocol, which is true if the handler is a Connect handler (created using generated code from the protoc-gen-connect-go plugin or an explicit call to connect.NewUnaryHandler or its streaming equivalents).

func WithTypeResolver

func WithTypeResolver(resolver TypeResolver) ServiceOption

WithTypeResolver returns a service option to use the given resolver when serializing and de-serializing messages. If not specified, this defaults to protoregistry.GlobalTypes.

type StableCodec

type StableCodec interface {
	Codec

	// MarshalAppendStable is the same as MarshalAppend except that the
	// bytes produced must be deterministic and stable. Ideally, the
	// produced bytes represent a *canonical* encoding. But this is not
	// required as many codecs (including binary Protobuf and JSON) do
	// not have a well-defined canonical encoding format.
	MarshalAppendStable(b []byte, msg proto.Message) ([]byte, error)
	// IsBinary returns true for non-text formats. This is used to decide
	// whether the message query string parameter should be base64-encoded.
	IsBinary() bool
}

StableCodec is an encoding format that can produce stable, deterministic output when marshalling data. This stable form is the result of the MarshalAppendStable method. So the codec's MarshalAppend method is free to produce unstable/non-deterministic output, if useful for improved performance. The performance penalty of stable output will only be taken when necessary.

This is used to encode messages that end up in the URL query string, for the Connect protocol when unary methods use the HTTP GET method. If the codec in use does not implement StableCodec then HTTP GET methods will not be used; a Transcoder will send all unary RPCs that use the Connect protocol and that codec as POST requests.

type Transcoder

type Transcoder struct {
	// contains filtered or unexported fields
}

Transcoder is a Vanguard handler which acts like a router and a middleware. It transforms all supported input protocols (Connect, gRPC, gRPC-Web, REST) into a protocol that the service handlers support. It can do simple routing based on RPC method name, for simple protocols like Connect, gRPC, and gRPC-Web; but it can also route based on REST-ful URI paths configured with HTTP transcoding annotations.

See the package-level examples for sample usage.

func NewTranscoder

func NewTranscoder(services []*Service, opts ...TranscoderOption) (*Transcoder, error)

NewTranscoder creates a new transcoder that handles the given services, with the configuration described by the given options. A non-nil error is returned if there is an issue with this configuration.

The returned handler does the routing and dispatch to the RPC handlers associated with each provided service. Routing supports more than just the service path provided to NewService since HTTP transcoding annotations are used to also support REST-ful URI paths for each method.

The returned handler also acts like a middleware, transparently "upgrading" the RPC handlers to support incoming request protocols they wouldn't otherwise support. This can be used to upgrade Connect handlers to support REST requests (based on HTTP transcoding configuration) or gRPC handlers to support Connect, gRPC-Web, or REST. This can even be used with a reverse proxy handler, to translate all incoming requests to a single protocol that another backend server supports.

If any options given implement ServiceOption, they are treated as default service options and apply to all configured services, unless overridden by a particular service.

func (*Transcoder) ServeHTTP

func (t *Transcoder) ServeHTTP(writer http.ResponseWriter, request *http.Request)

ServeHTTP implements http.Handler, dispatching requests for configured services and transcoding protocols and message encoding as needed.

type TranscoderOption

type TranscoderOption interface {
	// contains filtered or unexported methods
}

TranscoderOption is an option used to configure a Transcoder. See NewTranscoder.

func WithCodec

func WithCodec(newCodec func(TypeResolver) Codec) TranscoderOption

WithCodec returns an option that instructs the transcoder to use the given function for instantiating codec implementations. The function is immediately invoked in order to determine the name of the codec. The name reported by codecs created with the function should all return the same name. (Otherwise, behavior is undefined.)

By default, "proto" and "json" codecs are supported using default options. This option can be used to support additional codecs or to override the default implementations (such as to change serialization or de-serialization options).

func WithCompression

func WithCompression(name string, newCompressor func() connect.Compressor, newDecompressor func() connect.Decompressor) TranscoderOption

WithCompression returns an option that instructs the transcoder to use the given functions to instantiate compressors and decompressors for the given compression algorithm name.

By default, "gzip" compression is supported using default options. This option can be used to support additional compression algorithms or to override the default "gzip" implementation (such as to change the compression level).

func WithDefaultServiceOptions

func WithDefaultServiceOptions(serviceOptions ...ServiceOption) TranscoderOption

WithDefaultServiceOptions returns an option that configures the given service options as defaults. They will apply to all services passed to NewTranscoder, except where overridden via an explicit ServiceOption passed to NewService / NewServiceWithSchema.

Providing multiple instances of this option will be cumulative: the union of all defaults are used with later options overriding any previous options.

func WithRules

func WithRules(rules ...*annotations.HttpRule) TranscoderOption

WithRules returns an option that adds HTTP transcoding configuration to the set of configured services. The given rules must have a selector defined, and the selector must match at least one configured method. Otherwise, NewTranscoder will report a configuration error.

func WithUnknownHandler

func WithUnknownHandler(unknownHandler http.Handler) TranscoderOption

WithUnknownHandler returns an option that instructs the transcoder to delegate to the given handler when a request arrives for an unknown endpoint. If no such option is used, the transcoder will reply with a simple "404 Not Found" error.

type TypeResolver

TypeResolver can resolve message and extension types and is used to instantiate messages as needed for the middleware to serialize/de-serialize request and response payloads.

Implementations of this interface should be comparable, so they can be used as map keys. Typical implementations are pointers to structs, which are suitable.

Directories

Path Synopsis
internal
examples/pets/internal/gen/io/swagger/petstore/v2/petstorev2connect
The service defined herein comes from v2 of the Petstore service, which is used as an example for Swagger/OpenAPI.
The service defined herein comes from v2 of the Petstore service, which is used as an example for Swagger/OpenAPI.
gen/io/swagger/petstore/v2/petstorev2connect
The service defined herein comes from v2 of the Petstore service, which is used as an example for Swagger/OpenAPI.
The service defined herein comes from v2 of the Petstore service, which is used as an example for Swagger/OpenAPI.
Package vanguardgrpc provides convenience functions to make it easy to wrap your [grpc.Server] with a [vanguard.Transcoder], to upgrade it to supporting Connect, gRPC-Web, and REST+JSON protocols.
Package vanguardgrpc provides convenience functions to make it easy to wrap your [grpc.Server] with a [vanguard.Transcoder], to upgrade it to supporting Connect, gRPC-Web, and REST+JSON protocols.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL