-
Notifications
You must be signed in to change notification settings - Fork 100
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 WithConditionalHandlerOptions for conditional options #538
Changes from 1 commit
cec2a2a
b9ed035
11df7c4
4ef063d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -166,6 +166,25 @@ func WithRequireConnectProtocolHeader() HandlerOption { | |
return &requireConnectProtocolHeaderOption{} | ||
} | ||
|
||
// WithHandlerOptional is a function that returns a HandlerOption. It's used | ||
// to conditionally apply HandlerOption to a Handler based on the Spec. | ||
// For example, to apply a HandlerOption only to a specific procedure: | ||
// | ||
// connect.WithHandlerOptional(func(spec connect.Spec) connect.HandlerOption { | ||
// if spec.Procedure == pingv1connect.PingServicePingProcedure { | ||
// return connect.WithReadMaxBytes(1024) | ||
// } | ||
// return nil | ||
// }) | ||
emcfarlane marked this conversation as resolved.
Show resolved
Hide resolved
|
||
type WithHandlerOptional func(Spec) HandlerOption | ||
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. This strikes me a little odd that this one is a type and not a function that returns an option. For consistency and "path of least surprise", maybe this should be a function that returns an unexported Also, the name is not particularly intuitive or self-describing, and the way this works feels a little clunky. I can see why you may not want to just have the user provide a procedure name since they may want to do something more clever in the predicate, such as including matching on stream type instead of procedure name, or matching multiple procedure names. But I think then the predicate may be an explicit parameter, separate from the options. Something like so: func WithHandlerOptions(predicate func(Spec) bool, opts ... HandlerOption) HandlerOption To apply this to a single procedure, the code snippet then looks something like so: connect.WithHandlerOptions(
func(spec connect.Spec) bool {
return spec.Procedure == pingv1connect.PingServicePingProcedure
},
connect.WithReadMaxBytes(1024),
) I find this a little more intuitive than a function that can optionally return a non-nil option. It also makes it easier to configure multiple options for the same procedure(s). WDYT? 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. Unfortunately, we've already got a What do you think of the name If we keep the current approach, I agree with Josh that this is surprisingly different from the other option constructor type signatures. Because of that, it won't get grouped nicely in the GoDoc. If we stick with this approach, I'd go for something like this: func WithHandlerOptional(f func(Spec) HandlerOption) HandlerOption {
return handlerOptionalFunc(f)
} 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. I think the single connect.WithHandlerOption(func(spec connect.Spec) connect.HandlerOption {
var options []connect.HandlerOption
if cfg, ok := config[spec.Procedure]; ok {
if cfg.ReadMaxLimit > 0 {
options = append(options, connect.WithReadMaxBytes(cfg.ReadMaxBytes)
}
// ...
}
return connect.WithHandlerOptions(options...)
}) To avoid the nil case maybe the signature should be: func WithHandlerOptional(f func(Spec) []HandlerOption) HandlerOption Which helps point out that there can be zero or more options returned. 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. That signature feels a little overly tailored to the above use case (where you want to produce per-method options from some separate config source), and might be a little less intuitive for simpler usages. But if we think that's a likely usage pattern, then it is indeed simpler to configure with that signature. So having the function return a slice, along with a name like |
||
|
||
func (f WithHandlerOptional) applyToHandler(h *handlerConfig) { | ||
spec := h.newSpec() | ||
if option := f(spec); option != nil { | ||
option.applyToHandler(h) | ||
} | ||
} | ||
|
||
// Option implements both [ClientOption] and [HandlerOption], so it can be | ||
// applied both client-side and server-side. | ||
type Option interface { | ||
|
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.
Is
StreamType
a bit mask? Why are all bits set here? Would it work just as well to pass an actual validStreamType
(i.e.StreamTypeUnary
) and put a comment that it's really just a sentinel value here?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 like a problematic corner of the design. We're using the options here to know the codecs registered, so that we can understand the content-types that the handler supports. If we're now allowing codecs to be registered conditionally depending on the stream type, we'll have to make sure that the stream type we supply here is correct. I'm not sure how to do that.
The best options I can come up with are:
WithStreamType
handler option and document that it only affectsErrorWriter
.ErrorWriter.SetStreamType
method, which alters the default and reconfigures the error writer.Neither of those feel great to me. If I had to choose, I'd probably pick the last option. Can anyone think of a nicer, backward-compatible alternative?
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.
StreamType
is a bit mask with the zero type equal toStreamTypeUnary
. Maybe we could document an unknown sentinel type:type StreamTypeUnknown StreamType = ^StreamType(0)
? I wanted the zero value not to match real ones to avoid confusing cases where applying options with aspec.StreamType == StreamTypeUnary
could be applied to aStreamTypeBidi
under error conditions. Probably not an issue though.A non backward-compatible alternative is to move http handler middlerware onto the handler with an option like:
So each error handler is aware of the protocol and can deduce the
StreamType
. This maybe has too many sharp edges, but it would integrate nicely on the level ofHandler
.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.
Would header middleware be fine for most applications?
Then the auth example here could be written as:
And going further with the
Optional
types we can implement per RPC checks based on the Spec. I guess this is too close to interceptors, the only part that is different is not having the messages read.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.
Is that intentional? It's also a simple, sequential enum where the zero value is "unary", so it's unclear if it was meant to be used as a bitmask. Do we actually expect code to use the client and server stream constants as masks to decode whether the stream type supports client and server streaming?
FWIW, this still doesn't make sense to me to use all bits set. That implies that we want bit-wise tests of whether this is a client or server stream to treat it as a bidi stream.
We are past v1.0, so I think incompatible changes are non-starters.
Although TBH, I don't see how that quite solves the issue at hand.
As far as @akshayjshah's proposals, I can think of a fourth one if the shape of
WithConditionalHandlerOptions
was a predicated and associated options instead of a function that produces options:NewErrorWriter
could actually crawl all of the options, using type assertions to find the codec options. For conditional options, it could recursively crawl the set of associated options (no need to apply the predicate). At that point, it has a union of all supported codecs, which should be sufficient to correctly configure the writer.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.
I see what you mean, it looks like it would be checked for a streaming client or streaming response which then wouldn't make sense to have either bits set.
I don't understand how this works. You would have all the non conditional options and union them with the conditional?
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.
Correct. The error writer really just needs to know what content types are supported. Thinking about this more, I guess this would be problematic if the conditional options were used to override a codec, such that the union of all codecs was ambiguous regarding which codec to use for a particular content type.
I think the only way we could handle that sort of setup would be to provide the
Spec
somehow to the error writer, similar to what Akshay suggested above. Though I'm now a little confused why the suggestions above focus solely on stream type, and not the whole spec, since the conditional options might have logic based on procedure name, too.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.
I think we can easily get the procedure name from the path, given a valid request, but stream type would be harder. I don't think that information is available.
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.
That's only true when using protos (and gRPC conventions). But that's not actually a requirement in the Connect protocol spec. So, used with some other service description convention, the
Procedure-Name
production in the spec could map to something with fewer or more than two path components, in which case this assumption means we'd compute the wrong string.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.
Set stream type to unary and added a check in applying conditionals to ignore options when the
Spec
is missing (procedure is an empty string).