-
Notifications
You must be signed in to change notification settings - Fork 16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add default HTTP route for all methods #124
base: main
Are you sure you want to change the base?
Changes from 3 commits
53b81e5
244db9f
6cb215f
71205ee
6ca7a80
d8738f3
8fd3335
07867f3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -71,19 +71,26 @@ const ( | |
// 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. | ||
// used to also support RESTful URI paths for each method. | ||
// | ||
// The returned handler also acts like a middleware, transparently "upgrading" | ||
// RESTful routing can be established either through google.api.http annotations in the | ||
// service's schema or by configuration using [WithRules]. These annotations define mappings | ||
// between HTTP methods and paths to RPC methods. Refer to the [annotations.HttpRule] message | ||
// for detailed information. By default, transcoding is provided for a POST request to the | ||
// service's fully-qualified name and method name. This is effectively the mapping of a | ||
// request of POST /GRPC_SERVICE_FULL_NAME/METHOD_NAME. No additional mappings conflicting | ||
// with this default mapping may be added. | ||
// | ||
// Additionally, the returned handler also acts as 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. | ||
// Any options implementing ServiceOption are considered default service options and are applied | ||
// to all configured services, unless overridden by a specific service. | ||
func NewTranscoder(services []*Service, opts ...TranscoderOption) (*Transcoder, error) { | ||
for _, svc := range services { | ||
if svc.err != nil { | ||
|
@@ -127,7 +134,6 @@ func NewTranscoder(services []*Service, opts ...TranscoderOption) (*Transcoder, | |
methods: map[string]*methodConfig{}, | ||
} | ||
|
||
var restOnlyServices []protoreflect.ServiceDescriptor | ||
for _, svc := range services { | ||
resolvedOpts := defaultServiceOptions | ||
for _, opt := range svc.opts { | ||
|
@@ -136,33 +142,10 @@ func NewTranscoder(services []*Service, opts ...TranscoderOption) (*Transcoder, | |
if err := transcoder.registerService(svc, resolvedOpts); err != nil { | ||
return nil, err | ||
} | ||
if len(resolvedOpts.protocols) == 1 { | ||
_, ok := resolvedOpts.protocols[ProtocolREST] | ||
if ok { | ||
restOnlyServices = append(restOnlyServices, svc.schema) | ||
} | ||
} | ||
} | ||
if err := transcoder.registerRules(transcoderOpts.rules); err != nil { | ||
return nil, err | ||
} | ||
|
||
// Finally, check that any services with only REST as target protocol | ||
// actually have at least one method with REST mappings. | ||
for _, svcDesc := range restOnlyServices { | ||
methods := svcDesc.Methods() | ||
var numSupportedMethods int | ||
for i, length := 0, methods.Len(); i < length; i++ { | ||
methodDesc := methods.Get(i) | ||
if transcoder.methods[methodPath(methodDesc)].httpRule != nil { | ||
numSupportedMethods++ | ||
} | ||
} | ||
if numSupportedMethods == 0 { | ||
return nil, fmt.Errorf("service %s only supports REST target protocol but has no methods with HTTP rules", svcDesc.FullName()) | ||
} | ||
} | ||
Comment on lines
-150
to
-164
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We still need a check here -- if the service only has client and/or bidi stream methods that do not use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We were missing checks to ensure a non http body couldn't be a target. I've moved to checking at runtime that the stream can be handled by the protocol. Any method can bind, but non http body streams will fail with an unsupported error. We may be able to relax this restriction in future versions. This feels more consistent than special casing REST rules. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
REST is a special case because it can't handle all kinds of methods whereas the other protocols can. Failing at runtime is a terrible developer experience. If we know the config cannot work, we should fail fast. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The config is not always controllable. For example these stream proto files are valid rules but we don't support the behaviour. I think we should still be able to register this service as this config may be valid behaviour in the future. I also don't see the terrible developer experience. For example if you register a service with one non-streaming and the other streaming endpoint the current behaviour will register the one and silently discard the other method. So now if you test the endpoints one will work and the other will return a 404. For this case I think it's much clearer to the user to fail with an explicit error that this streaming behaviour is not currently supported. The routes are valid but return an error. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Sure, that situation would not be an error. What I said was "if the service only has client and/or bidi stream methods that do not use google.api.HttpBody, then we will not have any REST routes for it." What I am saying is that it should be an error if there are zero valid/supported routes. That is also what this existing check was looking for.
If I configure a transcoder to always translate to REST, and yet the transcoder will always 404 because there are zero supported routes, that would be frustrating as a user to have that deploy and startup successfully and then not work at all. It is obvious that a transcoder with zero routes is not their intent. Failing fast means they can trivially check that the server is correctly configured in CI and also prevent bad roll-outs. |
||
|
||
return transcoder, nil | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems wrong. We always add the default first and we always fail on error. So if the source annotations or separate provided annotations also include the default mapping (but configured explicitly), this will fail. That seems bad.
It also seems bad that a default mapping could otherwise prevent registration of an explicit one. I could see a case (albeit not a good practice) where a user wants to rename/split an RPC in two, and in effect needs to use a route that looks like the default mapping of method A, but have it route to method B. This prevents that.
So I think we'll probably want special knowledge for these rules in the route trie that they are defaut rules. If a conflict is encountered, always discard the default rule and use the explicitly configured one instead. Only report conflicts between two explicitly configured rules, not between an explicitly configured one and a default. Does that make sense?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added an implicit rule which may be overridden, PTAL.