Skip to content
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

Merged
merged 10 commits into from
Jan 21, 2025
Merged

Add headerbp integration #669

merged 10 commits into from
Jan 21, 2025

Conversation

pacejackson
Copy link
Contributor

@pacejackson pacejackson commented Jan 9, 2025

💸 TL;DR

Add integrations to automatically propagate baseplate headers received from other services and forbids sending new baseplate headers in client calls.

✅ Checks

  • CI tests (if present) are passing
  • Adheres to code style for repo
  • Contributor License Agreement (CLA) completed if not a Reddit employee

this will be used to write middleware to automatically propagate baseplate headers received by a v0 service, but not allow sending new ones
@pacejackson pacejackson changed the title Headerbp Add headerbp integration Jan 10, 2025
@pacejackson pacejackson marked this pull request as ready for review January 10, 2025 03:12
@pacejackson pacejackson requested a review from a team as a code owner January 10, 2025 03:12
@pacejackson pacejackson requested review from fishy, kylelemons and konradreiche and removed request for a team January 10, 2025 03:12
Comment on lines 92 to 96

// only add the middleware to forward baseplate headers if the client is for internal calls
if config.InternalOnly {
defaults = append(defaults, ClientHeaderBPMiddleware(config.Slug))
}
Copy link
Contributor Author

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?

Copy link
Member

@fishy fishy left a 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.

Comment on lines +28 to +32
if len(key) < len(headerPrefixLower) {
return false
}
prefix := key[:len(headerPrefixLower)]
return prefix == headerPrefixLower || prefix == headerPrefixCanonicalHTTP
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: just use:

Suggested change
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.

Copy link
Contributor Author

@pacejackson pacejackson Jan 17, 2025

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

Copy link
Contributor Author

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


var (
clientHeadersRejectedTotal = promauto.With(prometheusbpint.GlobalRegistry).NewCounterVec(prometheus.CounterOpts{
Name: "baseplate_client_headers_rejected_total",
Copy link
Member

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")

})

clientHeadersSentTotal = promauto.With(prometheusbpint.GlobalRegistry).NewHistogramVec(prometheus.HistogramOpts{
Name: "baseplate_client_headers_sent_total",
Copy link
Member

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.

})

clientHeadersSentSize = promauto.With(prometheusbpint.GlobalRegistry).NewHistogramVec(prometheus.HistogramOpts{
Name: "baseplate_client_headers_sent_size_total",
Copy link
Member

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.

})

serverHeadersReceivedTotal = promauto.With(prometheusbpint.GlobalRegistry).NewCounterVec(prometheus.CounterOpts{
Name: "baseplate_service_headers_received_total",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_received_headers_total.

})

serverHeadersReceivedSize = promauto.With(prometheusbpint.GlobalRegistry).NewHistogramVec(prometheus.HistogramOpts{
Name: "baseplate_server_headers_received_size_total",
Copy link
Member

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 {
Copy link
Member

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?

Copy link
Contributor Author

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? 😅

}

headers := headerbp.NewIncomingHeaders(
headerbp.WithThriftService("", name),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: for readability:

Suggested change
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?)

@pacejackson pacejackson requested a review from fishy January 17, 2025 22:01
@pacejackson
Copy link
Contributor Author

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.

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()
Copy link
Member

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.

// 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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.

Copy link
Contributor Author

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?

Comment on lines +365 to +370
for k := range req.Header {
if err := headerbp.CheckClientHeader(k,
headerbp.WithHTTPClient("", client, ""),
); err != nil {
return nil, err
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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)
Copy link
Member

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 🤔

Copy link
Contributor Author

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

}

func SetV2BaseplateHeadersLookup(setter func(context.Context, map[string]string) context.Context) {
headerbp.SetV2Context = setter
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

@konradreiche konradreiche left a 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 😅

Copy link
Member

@konradreiche konradreiche left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

@pacejackson pacejackson merged commit 39862e6 into reddit:master Jan 21, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

3 participants