-
Notifications
You must be signed in to change notification settings - Fork 79
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 headerbp integration #669
Conversation
this will be used to write middleware to automatically propagate baseplate headers received by a v0 service, but not allow sending new ones
httpbp/client_middlewares.go
Outdated
|
||
// only add the middleware to forward baseplate headers if the client is for internal calls | ||
if config.InternalOnly { | ||
defaults = append(defaults, ClientHeaderBPMiddleware(config.Slug)) | ||
} |
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.
Since we don't have a distinct "internal vs non-internal" client type, I was thinking it would probably be good to have some sort of flag that you have to manually set to have HTTP clients automatically propagate headers?
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.
the test passed for go 1.22 but failed for go 1.23, I think that just means the test is flaky? I don't think you used anything that will cause a difference between go 1.22 and 1.23.
if len(key) < len(headerPrefixLower) { | ||
return false | ||
} | ||
prefix := key[:len(headerPrefixLower)] | ||
return prefix == headerPrefixLower || prefix == headerPrefixCanonicalHTTP |
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.
nit: just use:
if len(key) < len(headerPrefixLower) { | |
return false | |
} | |
prefix := key[:len(headerPrefixLower)] | |
return prefix == headerPrefixLower || prefix == headerPrefixCanonicalHTTP | |
return strings.HasPrefix(key, headerPrefixLower) || strings.HasPrefix(key, headerPrefixCanonicalHTTP) |
?
this way you no longer rely on headerPrefixCanonicalHTTP
and headerPrefixLower
being the same length.
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.
the strings.HasPrefix
approach is significantly slower (relatively speaking)
func BenchmarkIsBaseplateHeader(b *testing.B) {
// the question of using string.HasPrefix has come up a couple of times, benchmark the two approaches to see if there
// is a significant enough differenc between the two.
isBaseplateHeader_startswith := func(key string) bool {
return strings.HasPrefix(key, HeaderPrefixLower) || strings.HasPrefix(key, HeaderPrefixCanonicalHTTP)
}
b.Run("startswith", func(b *testing.B) {
for i := 0; i < b.N; i++ {
isBaseplateHeader_startswith("X-Bp-Header")
}
})
b.Run("==", func(b *testing.B) {
for i := 0; i < b.N; i++ {
IsBaseplateHeader("X-Bp-Header")
}
})
}
BenchmarkIsBaseplateHeader
BenchmarkIsBaseplateHeader/startswith
BenchmarkIsBaseplateHeader/startswith-16 299018905 3.799 ns/op
BenchmarkIsBaseplateHeader/==
BenchmarkIsBaseplateHeader/==-16 1000000000 0.2518 ns/op
PASS
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 way you no longer rely on headerPrefixCanonicalHTTP and headerPrefixLower being the same length.
They must always be the same since they are just the same string with different cases
internal/headerbp/metrics.go
Outdated
|
||
var ( | ||
clientHeadersRejectedTotal = promauto.With(prometheusbpint.GlobalRegistry).NewCounterVec(prometheus.CounterOpts{ | ||
Name: "baseplate_client_headers_rejected_total", |
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.
nit: _rejected_headers_total
, the last word before total should be the unit in plural form ("headers")
internal/headerbp/metrics.go
Outdated
}) | ||
|
||
clientHeadersSentTotal = promauto.With(prometheusbpint.GlobalRegistry).NewHistogramVec(prometheus.HistogramOpts{ | ||
Name: "baseplate_client_headers_sent_total", |
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.
same comment here, _sent_headers_total
.
internal/headerbp/metrics.go
Outdated
}) | ||
|
||
clientHeadersSentSize = promauto.With(prometheusbpint.GlobalRegistry).NewHistogramVec(prometheus.HistogramOpts{ | ||
Name: "baseplate_client_headers_sent_size_total", |
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.
_sent_size_bytes
, _total
is only for counters (this is a histogram), and there the unit is bytes.
internal/headerbp/metrics.go
Outdated
}) | ||
|
||
serverHeadersReceivedTotal = promauto.With(prometheusbpint.GlobalRegistry).NewCounterVec(prometheus.CounterOpts{ | ||
Name: "baseplate_service_headers_received_total", |
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.
_received_headers_total
.
internal/headerbp/metrics.go
Outdated
}) | ||
|
||
serverHeadersReceivedSize = promauto.With(prometheusbpint.GlobalRegistry).NewHistogramVec(prometheus.HistogramOpts{ | ||
Name: "baseplate_server_headers_received_size_total", |
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.
_received_size_bytes
.
} else if _, ok := thrift.GetHeader(ctx, headerbp.IsUntrustedRequestHeaderCanonicalHTTP); ok { | ||
untrusted = true | ||
} | ||
if untrusted { |
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 wonder if we should add a x-bp-
version of the deadline propagation header, so that we can still propagate deadline even in a untrusted chain of calls?
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 wonder if we should add a x-bp- version of the deadline propagation header, so that we can still propagate deadline even in a untrusted chain of calls?
I'm not sure I understand, this specifically would discard that header if the request was marked as untrusted? 😅
thriftbp/server_middlewares.go
Outdated
} | ||
|
||
headers := headerbp.NewIncomingHeaders( | ||
headerbp.WithThriftService("", name), |
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.
nit: for readability:
headerbp.WithThriftService("", name), | |
headerbp.WithThriftService("" /* service */, name), |
(and I assume we use ""
for service because we don't really have a notion of service name in thrift servers?)
It does seem like the test is flaky, but I think it's flaky because it can't call the test service I'm setting up. Is there something I'm missing there? |
t.Cleanup(func() { | ||
originServer.Close() | ||
}) | ||
go originServer.Serve() |
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.
you can probably add a small sleep after this go
to give it time to actually start the server, not sure if it's worth it though 🤷 looks like the failed test is just very unlucky.
httpbp/client_middlewares.go
Outdated
// ClientHeaderBPMiddleware is a middleware that forwards baseplate headers from the context to the outgoing request. | ||
// | ||
// If it detects any new baseplate headers set on the request, it will reject the request and return an error. | ||
func ClientHeaderBPMiddleware(client string) ClientMiddleware { |
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.
func ClientHeaderBPMiddleware(client string) ClientMiddleware { | |
func ClientHeaderBaseplateMiddleware(client string) ClientMiddleware { |
🔕 maybe best spelled out since we have Baseplate
more often in function or method names than BP
.
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.
headerbp
is the name of the actual library so I figured it was better to keep it consistent with that?
for k := range req.Header { | ||
if err := headerbp.CheckClientHeader(k, | ||
headerbp.WithHTTPClient("", client, ""), | ||
); err != nil { | ||
return nil, err | ||
} |
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.
for k := range req.Header { | |
if err := headerbp.CheckClientHeader(k, | |
headerbp.WithHTTPClient("", client, ""), | |
); err != nil { | |
return nil, err | |
} | |
for key := range req.Header { | |
if err := headerbp.CheckClientHeader( | |
key, | |
headerbp.WithHTTPClient("", client, ""), | |
); err != nil { | |
return nil, err | |
} |
🔕 I find this slightly more readable 🙂
} | ||
|
||
func GetUntrustedBaseplateHeaders(ctx context.Context) (map[string]string, bool) { | ||
h, ok := ctx.Value(untrustedHeadersKey{}).(map[string]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.
I'm a big fan of always checking if the assertion was successful, though I got the impression that if we tightly control it, it's okay to not check to make the API more user-friendly 🤔
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 don't think that really does anything here though
internalv2compat/compat.go
Outdated
} | ||
|
||
func SetV2BaseplateHeadersLookup(setter func(context.Context, map[string]string) context.Context) { | ||
headerbp.SetV2Context = setter |
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.
One thing I noticed with our internal2compat
primarily sets on structs local to this package but then makes it accessible through a separate getter. Would it make sense to define this locally here too and then call V2Context()
to get the function outside of this package?
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, that makes sense, updated.
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.
Only requesting changes for the open internal2compat question/discussion since it seems like it won't clear my review queue unless I approve or request changes 😅
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.
💸 TL;DR
Add integrations to automatically propagate baseplate headers received from other services and forbids sending new baseplate headers in client calls.
✅ Checks