runtime: publish load-balanced http client#135
Conversation
PR SummaryLow Risk Overview Introduces Reviewed by Cursor Bugbot for commit 149c846. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Test calls t.Fatalf from HTTP handler goroutine
- Replaced the handler's goroutine-unsafe t.Fatalf call with t.Errorf so request validation still fails the test safely.
Preview (89aef70a0b)
diff --git a/go.mod b/go.mod
--- a/go.mod
+++ b/go.mod
@@ -28,6 +28,7 @@
require (
github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op // indirect
github.com/beorn7/perks v1.0.1 // indirect
+ github.com/bufbuild/httplb v0.4.1 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
diff --git a/go.sum b/go.sum
--- a/go.sum
+++ b/go.sum
@@ -18,6 +18,8 @@
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
+github.com/bufbuild/httplb v0.4.1 h1:f8dMp7tx2aJfMX2UcOId1A58QDiBag7Dv6BA1OtV/YA=
+github.com/bufbuild/httplb v0.4.1/go.mod h1:9XDjl/3UvlkOQUKthLlKn92C1/1SuZ3UCiekxZbenck=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
diff --git a/httpclient/httplb.go b/httpclient/httplb.go
new file mode 100644
--- /dev/null
+++ b/httpclient/httplb.go
@@ -1,0 +1,130 @@
+package httpclient
+
+import (
+ "crypto/tls"
+ "net/http"
+ "time"
+
+ "github.com/bufbuild/httplb"
+)
+
+const (
+ DefaultRequestTimeout = 30 * time.Second
+ DefaultIdleConnTimeout = 90 * time.Second
+ DefaultMaxIdleConns = 64
+ DefaultMaxIdleConnsPerHost = 16
+ DefaultMaxConnsPerHost = 32
+)
+
+// LoadBalancedOptions controls EvalOps' default outbound HTTP client.
+type LoadBalancedOptions struct {
+ RequestTimeout time.Duration
+ IdleConnTimeout time.Duration
+ MaxIdleConns int
+ MaxIdleConnsPerHost int
+ MaxConnsPerHost int
+ MaxResponseHeaderSize int
+ TLSClientConfig *tls.Config
+ TLSHandshakeTimeout time.Duration
+ DisableCompression bool
+}
+
+// LoadBalancedClient owns an httplb client and exposes its standard *http.Client.
+type LoadBalancedClient struct {
+ *http.Client
+
+ client *httplb.Client
+}
+
+var sharedDefaultClient = NewLoadBalanced(LoadBalancedOptions{})
+
+// DefaultClient returns a shared load-balanced client for nil-client fallbacks.
+func DefaultClient() *http.Client {
+ return sharedDefaultClient.Client
+}
+
+// NewLoadBalanced creates an HTTP client with DNS-backed client-side balancing.
+func NewLoadBalanced(options LoadBalancedOptions) *LoadBalancedClient {
+ options = options.withDefaults()
+ lbOptions := []httplb.ClientOption{
+ httplb.WithRequestTimeout(options.RequestTimeout),
+ httplb.WithIdleConnectionTimeout(options.IdleConnTimeout),
+ httplb.WithTransport("http", loadBalancedTransport{options: options}),
+ httplb.WithTransport("https", loadBalancedTransport{options: options}),
+ }
+ if options.TLSClientConfig != nil {
+ lbOptions = append(
+ lbOptions,
+ httplb.WithTLSConfig(options.TLSClientConfig, options.TLSHandshakeTimeout),
+ )
+ }
+ if options.MaxResponseHeaderSize > 0 {
+ lbOptions = append(lbOptions, httplb.WithMaxResponseHeaderBytes(options.MaxResponseHeaderSize))
+ }
+ if options.DisableCompression {
+ lbOptions = append(lbOptions, httplb.WithDisableCompression(true))
+ }
+
+ client := httplb.NewClient(lbOptions...)
+ return &LoadBalancedClient{
+ Client: client.Client,
+ client: client,
+ }
+}
+
+// Close releases resolver and transport resources owned by the httplb client.
+func (c *LoadBalancedClient) Close() error {
+ if c == nil || c.client == nil {
+ return nil
+ }
+ return c.client.Close()
+}
+
+func (o LoadBalancedOptions) withDefaults() LoadBalancedOptions {
+ if o.RequestTimeout <= 0 {
+ o.RequestTimeout = DefaultRequestTimeout
+ }
+ if o.IdleConnTimeout <= 0 {
+ o.IdleConnTimeout = DefaultIdleConnTimeout
+ }
+ if o.MaxIdleConns <= 0 {
+ o.MaxIdleConns = DefaultMaxIdleConns
+ }
+ if o.MaxIdleConnsPerHost <= 0 {
+ o.MaxIdleConnsPerHost = DefaultMaxIdleConnsPerHost
+ }
+ if o.MaxConnsPerHost <= 0 {
+ o.MaxConnsPerHost = DefaultMaxConnsPerHost
+ }
+ return o
+}
+
+type loadBalancedTransport struct {
+ options LoadBalancedOptions
+}
+
+func (t loadBalancedTransport) NewRoundTripper(
+ _ string,
+ _ string,
+ config httplb.TransportConfig,
+) httplb.RoundTripperResult {
+ transport := &http.Transport{
+ Proxy: config.ProxyFunc,
+ GetProxyConnectHeader: config.ProxyConnectHeadersFunc,
+ DialContext: config.DialFunc,
+ ForceAttemptHTTP2: true,
+ MaxIdleConns: t.options.MaxIdleConns,
+ MaxIdleConnsPerHost: t.options.MaxIdleConnsPerHost,
+ MaxConnsPerHost: t.options.MaxConnsPerHost,
+ IdleConnTimeout: config.IdleConnTimeout,
+ TLSHandshakeTimeout: config.TLSHandshakeTimeout,
+ TLSClientConfig: config.TLSClientConfig,
+ MaxResponseHeaderBytes: config.MaxResponseHeaderBytes,
+ ExpectContinueTimeout: time.Second,
+ DisableCompression: config.DisableCompression,
+ }
+ return httplb.RoundTripperResult{
+ RoundTripper: transport,
+ Close: transport.CloseIdleConnections,
+ }
+}
diff --git a/httpclient/httplb_test.go b/httpclient/httplb_test.go
new file mode 100644
--- /dev/null
+++ b/httpclient/httplb_test.go
@@ -1,0 +1,82 @@
+package httpclient
+
+import (
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+)
+
+func TestNewLoadBalancedUsesDefaults(t *testing.T) {
+ t.Parallel()
+
+ client := NewLoadBalanced(LoadBalancedOptions{})
+ t.Cleanup(func() {
+ if err := client.Close(); err != nil {
+ t.Fatalf("Close() error = %v", err)
+ }
+ })
+
+ if client.Client == nil {
+ t.Fatal("expected embedded http client")
+ }
+ if client.Transport == nil {
+ t.Fatal("expected httplb transport")
+ }
+ if client.Timeout != 0 {
+ t.Fatalf("Timeout = %s, want httplb request timeout only", client.Timeout)
+ }
+}
+
+func TestNewLoadBalancedHonorsCustomOptions(t *testing.T) {
+ t.Parallel()
+
+ client := NewLoadBalanced(LoadBalancedOptions{
+ RequestTimeout: 2 * time.Second,
+ IdleConnTimeout: time.Second,
+ MaxIdleConns: 8,
+ MaxIdleConnsPerHost: 4,
+ MaxConnsPerHost: 6,
+ })
+ t.Cleanup(func() {
+ if err := client.Close(); err != nil {
+ t.Fatalf("Close() error = %v", err)
+ }
+ })
+
+ if client.Timeout != 0 {
+ t.Fatalf("Timeout = %s, want httplb request timeout only", client.Timeout)
+ }
+}
+
+func TestLoadBalancedClientSendsRequests(t *testing.T) {
+ t.Parallel()
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if got := r.URL.Path; got != "/healthz" {
+ t.Errorf("path = %q, want /healthz", got)
+ }
+ _, _ = fmt.Fprint(w, "ok")
+ }))
+ defer server.Close()
+
+ client := NewLoadBalanced(LoadBalancedOptions{RequestTimeout: time.Second})
+ t.Cleanup(func() {
+ if err := client.Close(); err != nil {
+ t.Fatalf("Close() error = %v", err)
+ }
+ })
+
+ response, err := client.Get(server.URL + "/healthz")
+ if err != nil {
+ t.Fatalf("Get() error = %v", err)
+ }
+ defer func() {
+ _ = response.Body.Close()
+ }()
+
+ if response.StatusCode != http.StatusOK {
+ t.Fatalf("status = %d, want %d", response.StatusCode, http.StatusOK)
+ }
+}
diff --git a/vendor/github.com/bufbuild/httplb/.gitignore b/vendor/github.com/bufbuild/httplb/.gitignore
new file mode 100644
--- /dev/null
+++ b/vendor/github.com/bufbuild/httplb/.gitignore
@@ -1,0 +1,4 @@
+/.tmp/
+*.pprof
+*.svg
+cover.out
diff --git a/vendor/github.com/bufbuild/httplb/.golangci.yml b/vendor/github.com/bufbuild/httplb/.golangci.yml
new file mode 100644
--- /dev/null
+++ b/vendor/github.com/bufbuild/httplb/.golangci.yml
@@ -1,0 +1,81 @@
+linters-settings:
+ errcheck:
+ check-type-assertions: true
+ forbidigo:
+ forbid:
+ - '^fmt\.Print'
+ - '^log\.'
+ - '^print$'
+ - '^println$'
+ - '^panic$'
+ - '^clocktest\.'
+ - '^clockwork\.'
+ godox:
+ # TODO, OPT, etc. comments are fine to commit. Use FIXME comments for
+ # temporary hacks, and use godox to prevent committing them.
+ keywords: [FIXME, NOCOMMIT]
+ varnamelen:
+ ignore-decls:
+ - T any
+ - i int
+ - wg sync.WaitGroup
+ - ok bool
+ - w http.ResponseWriter
+ - r *http.Request
+linters:
+ enable-all: true
+ disable:
+ - cyclop # covered by gocyclo
+ - depguard # unnecessary for small libraries
+ - exhaustruct # not helpful, prevents idiomatic struct literals
+ - funlen # rely on code review to limit function length
+ - gocognit # dubious "cognitive overhead" quantification
+ - gofumpt # prefer standard gofmt
+ - goimports # rely on gci instead
+ - inamedparam # convention is not followed
+ - ireturn # "accept interfaces, return structs" isn't ironclad
+ - lll # don't want hard limits for line length
+ - maintidx # covered by gocyclo
+ - mnd # some unnamed constants are okay
+ - nlreturn # generous whitespace violates house style
+ - nonamedreturns # named returns are fine; it's *bare* returns that are bad
+ - tenv # deprecated in golangci v1.64.0
+ - testpackage # internal tests are fine
+ - wrapcheck # don't _always_ need to wrap errors
+ - wsl # generous whitespace violates house style
+issues:
+ exclude-dirs-use-default: false
+ exclude:
+ # Don't ban use of fmt.Errorf to create new errors, but the remaining
+ # checks from err113 are useful.
+ - "do not define dynamic errors.*"
+ # This gosec error is noisy with false positives.
+ - "G115: integer overflow conversion"
+ exclude-rules:
+ # Needlessly verbose inside of test-only code.
+ - path: (.+)_test\.go
+ linters:
+ - forcetypeassert
+ # Allow dot imports for testing.
+ - path: (.+)_test\.go
+ text: "^dot-imports: should not use dot imports"
+ linters:
+ - revive
+ # Allow clocktest within tests
+ - path: (.+)_test\.go
+ text: "^use of `clocktest\\."
+ linters:
+ - forbidigo
+ # Allow clockwork within clocktest
+ - path: internal/clocktest
+ text: "^use of `clockwork\\."
+ linters:
+ - forbidigo
+ - path: client_test\.go
+ linters:
+ # These tests examine goroutines to make sure all resources
+ # are cleaned up, which doesn't work when run in parallel.
+ - paralleltest
+ # For some reason, this linter is coming up with several
+ # false positives in this file ¯\_(ツ)_/¯
+ - bodyclose
diff --git a/vendor/github.com/bufbuild/httplb/LICENSE b/vendor/github.com/bufbuild/httplb/LICENSE
new file mode 100644
--- /dev/null
+++ b/vendor/github.com/bufbuild/httplb/LICENSE
@@ -1,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2023-2025 Buf Technologies, Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/vendor/github.com/bufbuild/httplb/Makefile b/vendor/github.com/bufbuild/httplb/Makefile
new file mode 100644
--- /dev/null
+++ b/vendor/github.com/bufbuild/httplb/Makefile
@@ -1,0 +1,78 @@
+# See https://tech.davis-hansson.com/p/make/
+SHELL := bash
+.DELETE_ON_ERROR:
+.SHELLFLAGS := -eu -o pipefail -c
+.DEFAULT_GOAL := all
+MAKEFLAGS += --warn-undefined-variables
+MAKEFLAGS += --no-builtin-rules
+MAKEFLAGS += --no-print-directory
+BIN := .tmp/bin
+export PATH := $(BIN):$(PATH)
+export GOBIN := $(abspath $(BIN))
+COPYRIGHT_YEARS := 2023-2025
+LICENSE_IGNORE := --ignore /testdata/
+
+.PHONY: help
+help: ## Describe useful make targets
+ @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "%-30s %s\n", $$1, $$2}'
+
+.PHONY: all
+all: ## Build, test, and lint (default)
+ $(MAKE) test
+ $(MAKE) lint
+
+.PHONY: clean
+clean: ## Delete intermediate build artifacts
+ @# -X only removes untracked files, -d recurses into directories, -f actually removes files/dirs
+ git clean -Xdf
+
+.PHONY: test
+test: build ## Run unit tests
+ go test -vet=off -race -cover ./...
+
+.PHONY: build
+build: generate ## Build all packages
+ go build ./...
+
+.PHONY: generate
+generate: $(BIN)/license-header ## Regenerate code and licenses
+ go mod tidy
+ license-header \
+ --license-type apache \
+ --copyright-holder "Buf Technologies, Inc." \
+ --year-range "$(COPYRIGHT_YEARS)" $(LICENSE_IGNORE)
+
+.PHONY: lint
+lint: $(BIN)/golangci-lint $(BIN)/checklocks ## Lint
+ go vet ./...
+ #go vet -vettool=$(BIN)/checklocks ./...
+ golangci-lint run
+
+.PHONY: lintfix
+lintfix: $(BIN)/golangci-lint ## Automatically fix some lint errors
+ golangci-lint run --fix
+
+.PHONY: install
+install: ## Install all binaries
+ go install ./...
+
+.PHONY: upgrade
+upgrade: ## Upgrade dependencies
+ go get -u -t ./... && go mod tidy -v
+
+.PHONY: checkgenerate
+checkgenerate:
+ @# Used in CI to verify that `make generate` doesn't produce a diff.
+ test -z "$$(git status --porcelain | tee /dev/stderr)"
+
+$(BIN)/license-header: Makefile
+ @mkdir -p $(@D)
+ go install github.com/bufbuild/buf/private/pkg/licenseheader/cmd/[email protected]
+
+$(BIN)/golangci-lint: Makefile
+ @mkdir -p $(@D)
+ go install github.com/golangci/golangci-lint/cmd/[email protected]
+
+$(BIN)/checklocks: Makefile
+ @mkdir -p $(@D)
+ go install gvisor.dev/gvisor/tools/checklocks/cmd/[email protected]
diff --git a/vendor/github.com/bufbuild/httplb/README.md b/vendor/github.com/bufbuild/httplb/README.md
new file mode 100644
--- /dev/null
+++ b/vendor/github.com/bufbuild/httplb/README.md
@@ -1,0 +1,100 @@
+# httplb
+
+[](https://github.com/bufbuild/httplb/actions/workflows/ci.yaml)
+[](https://goreportcard.com/report/github.com/bufbuild/httplb)
+[](https://pkg.go.dev/github.com/bufbuild/httplb)
+
+[`httplb`](https://pkg.go.dev/github.com/bufbuild/httplb)
+provides client-side load balancing for `net/http` clients. By default,
+clients are designed for server-to-server and RPC workloads:
+
+* They support HTTP/1.1, HTTP/2, and [H2C](https://en.wikipedia.org/wiki/HTTP/2#Encryption).
+* They periodically re-resolve names using DNS.
+* They use a round-robin load balancing policy.
+
+Random, fewest-pending, and power-of-two load balancing policies are also available.
+Clients with more complex needs can customize the underlying transports, name
+resolution, and load balancing. They can also add subsetting and active health
+checking.
+
+`httplb` takes care to build all this functionality underneath the standard library's
+`*http.Client`, so `httplb` is usable anywhere you're currently using `net/http`.
+
+## Example
+
+Here's a quick example of how to get started with `httplb`:
+
+```go
+package main
+
+import (
+ "log"
+ "net"
+ "time"
+
+ "github.com/bufbuild/httplb"
+ "github.com/bufbuild/httplb/picker"
+)
+
+func main() {
+ client := httplb.NewClient(
+ // Switch from the default round-robin policy to power-of-two.
+ httplb.WithPicker(picker.NewPowerOfTwo),
+ )
+ defer client.Close()
+ resp, err := client.Get("https://example.com")
+ if err != nil {
+ log.Fatalln(err)
+ }
+ defer resp.Body.Close()
+ log.Println(resp.Status)
+}
+```
+
+If you're using [Connect](https://github.com/connectrpc/connect-go), you can
+use `httplb` for your RPC clients:
+
+```go
+func main() {
+ client := httplb.NewClient()
+ defer client.Close()
+ pingClient := pingv1connect.NewPingServiceClient(
+ client,
+ "http://localhost:8080/",
+ )
+ req := connect.NewRequest(&pingv1.PingRequest{
+ Number: 42,
+ })
+ res, err := pingClient.Ping(context.Background(), req)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ log.Println(res.Msg)
+}
+```
+
+If you know the server supports HTTP/2 without TLS (HTTP/2 over cleartext, or H2C for short), use
+the `h2c` scheme in your URLs instead of `http`:
+
+```go
+ pingClient := pingv1connect.NewPingServiceClient(
+ client,
+ "h2c://localhost:8080/",
+ )
+```
+
+For more information on how to use `httplb`, especially for advanced use cases, take
+a look at the [full documentation](https://pkg.go.dev/github.com/bufbuild/httplb).
+
+## Ecosystem
+
+* [connect-go](https://github.com/bufbuild/connect-go): RPC library, compatible with `httplb`.
+
+## Status: Alpha
+
+This project is currently in **alpha**. The API should be considered unstable and likely to change.
+
+## Legal
+
+Offered under the [Apache 2 license](LICENSE).
+
diff --git a/vendor/github.com/bufbuild/httplb/attribute/attribute.go b/vendor/github.com/bufbuild/httplb/attribute/attribute.go
new file mode 100644
--- /dev/null
+++ b/vendor/github.com/bufbuild/httplb/attribute/attribute.go
@@ -1,0 +1,112 @@
+// Copyright 2023-2025 Buf Technologies, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package attribute provides a type-safe container of custom attributes
+// named Values. This can be used to add custom metadata to a resolved
+// address. Custom attributes are declared using [NewKey] to create a
+// strongly-typed key. The values can then be defined using the key's
+// Value method.
+//
+// The following example declares two custom attributes, a floating point
+// "weight", and a string "geographic region". It then constructs a new
+// resolved Address that has values for each of them.
+//
+// var (
+// Weight = attribute.NewKey[float64]()
+// GeographicRegion = attribute.NewKey[string]()
+//
+// Address = resolver.Address{
+// HostPort: "111.222.123.234:5432",
+// Attributes: attribute.NewValues(
+// Weight.Value(1.25),
+// GeographicRegion.Value("us-east1"),
+// ),
+// }
+// )
+//
+// Custom [Resolver] implementations can attach any kind of metadata
+// to an address this way. This can be combined with a [custom picker]
+// that uses the metadata, which can access the properties in a type-safe way
+// using the [GetValue] function.
+//
+// Such metadata can be used to implement regional affinity or to implement
+// a weighted round-robin or random selection strategy (where a weight could
+// be used to send more traffic to an address that has more available
+// resources, such as more compute, memory, or network bandwidth).
+//
+// [Resolver]: https://pkg.go.dev/github.com/bufbuild/httplb/resolver#Resolver
+// [custom picker]: https://pkg.go.dev/github.com/bufbuild/httplb/picker#Picker
+package attribute
+
+// Values is a collection of type-safe custom metadata values.
... diff truncated: showing 800 of 5225 linesYou can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 8f8cd48. Configure here.

Summary
httpclientruntime package used by platform agent-mcpgithub.com/bufbuild/httplbso the repo builds with its existing vendor modeTest plan
go test ./httpclientgo test ./...git diff --check