From e3bf9bc9137e6b9365894d6912e5d3539be9baa4 Mon Sep 17 00:00:00 2001 From: Krishnanand G <118352827+Krishnanand-G@users.noreply.github.com> Date: Mon, 29 Jun 2026 11:28:10 +0530 Subject: [PATCH] feat: add structured X-Client-Info header (closes #62) --- client.go | 2 ++ client_info.go | 45 +++++++++++++++++++++++++++++++++++ client_info_test.go | 57 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+) create mode 100644 client_info.go create mode 100644 client_info_test.go diff --git a/client.go b/client.go index 5baffa1..ab3be9e 100644 --- a/client.go +++ b/client.go @@ -60,6 +60,8 @@ func NewClient(url, key string, options *ClientOptions) (*Client, error) { } } + applyXClientInfoHeader(headers) + client := &Client{} client.options.url = url // map is pass by reference, so this gets updated by rest of function diff --git a/client_info.go b/client_info.go new file mode 100644 index 0000000..8dc20ac --- /dev/null +++ b/client_info.go @@ -0,0 +1,45 @@ +package supabase + +import ( + "fmt" + "runtime" + "strings" +) + +const ( + clientInfoHeader = "X-Client-Info" + // Version is the supabase-go library version sent in X-Client-Info. + Version = "0.0.5" +) + +func buildXClientInfo() string { + parts := []string{fmt.Sprintf("supabase-go/%s", Version)} + + if platform := runtime.GOOS; platform != "" { + parts = append(parts, "platform="+platform) + } + + parts = append(parts, "runtime=go") + + if v := strings.TrimPrefix(runtime.Version(), "go"); v != "" { + parts = append(parts, "runtime-version="+v) + } + + return strings.Join(parts, "; ") +} + +func applyXClientInfoHeader(headers map[string]string) { + if hasClientInfoHeader(headers) { + return + } + headers[clientInfoHeader] = buildXClientInfo() +} + +func hasClientInfoHeader(headers map[string]string) bool { + for k := range headers { + if strings.EqualFold(k, clientInfoHeader) { + return true + } + } + return false +} diff --git a/client_info_test.go b/client_info_test.go new file mode 100644 index 0000000..770b938 --- /dev/null +++ b/client_info_test.go @@ -0,0 +1,57 @@ +package supabase + +import ( + "strings" + "testing" +) + +func TestBuildXClientInfo(t *testing.T) { + info := buildXClientInfo() + + if !strings.HasPrefix(info, "supabase-go/"+Version) { + t.Fatalf("expected library token supabase-go/%s, got %q", Version, info) + } + + required := []string{"platform=", "runtime=go", "runtime-version="} + for _, fragment := range required { + if !strings.Contains(info, fragment) { + t.Errorf("expected %q in X-Client-Info, got %q", fragment, info) + } + } +} + +func TestApplyXClientInfoHeaderSetsDefault(t *testing.T) { + headers := map[string]string{} + applyXClientInfoHeader(headers) + + got := headers[clientInfoHeader] + if got == "" { + t.Fatal("expected X-Client-Info to be set") + } + if !strings.HasPrefix(got, "supabase-go/") { + t.Fatalf("unexpected header value: %q", got) + } +} + +func TestApplyXClientInfoHeaderPreservesUserValue(t *testing.T) { + custom := "my-app/1.0; platform=edge" + headers := map[string]string{clientInfoHeader: custom} + applyXClientInfoHeader(headers) + + if headers[clientInfoHeader] != custom { + t.Fatalf("expected user header to be preserved, got %q", headers[clientInfoHeader]) + } +} + +func TestApplyXClientInfoHeaderPreservesUserValueCaseInsensitive(t *testing.T) { + custom := "my-app/1.0" + headers := map[string]string{"x-client-info": custom} + applyXClientInfoHeader(headers) + + if headers["x-client-info"] != custom { + t.Fatalf("expected user header to be preserved, got %q", headers["x-client-info"]) + } + if _, ok := headers[clientInfoHeader]; ok { + t.Fatal("expected canonical header key not to be added when user header exists") + } +}