This is an automated email from the ASF dual-hosted git repository.

kevinjqliu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-go.git


The following commit(s) were added to refs/heads/main by this push:
     new 4a70225e feat(catalog): add WithHeaders function (#652)
4a70225e is described below

commit 4a70225e8859180be3332271abb400c2cbcb6b63
Author: Alex Stephen <[email protected]>
AuthorDate: Fri Jan 9 08:57:15 2026 -0800

    feat(catalog): add WithHeaders function (#652)
    
    Closes #532
    
    This adds support for sending custom headers for IRC requests.
    
    I'm happy to add a `WithClient` function as well (for sending a custom
    HTTP client), but it's unclear when the custom client would be used
    versus the options the user has sent.
    
    ---------
    
    Co-authored-by: Kevin Liu <[email protected]>
    Co-authored-by: Kevin Liu <[email protected]>
---
 catalog/rest/options.go   |   7 ++++
 catalog/rest/rest.go      |  16 ++++---
 catalog/rest/rest_test.go | 104 ++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 122 insertions(+), 5 deletions(-)

diff --git a/catalog/rest/options.go b/catalog/rest/options.go
index c14ec1f2..3308c510 100644
--- a/catalog/rest/options.go
+++ b/catalog/rest/options.go
@@ -40,6 +40,12 @@ func WithOAuthToken(token string) Option {
        }
 }
 
+func WithHeaders(headers map[string]string) Option {
+       return func(o *options) {
+               o.headers = headers
+       }
+}
+
 func WithTLSConfig(config *tls.Config) Option {
        return func(o *options) {
                o.tlsConfig = config
@@ -132,6 +138,7 @@ type options struct {
        authUri           *url.URL
        scope             string
        transport         http.RoundTripper
+       headers           map[string]string
 
        additionalProps iceberg.Properties
 }
diff --git a/catalog/rest/rest.go b/catalog/rest/rest.go
index 6c1f92b9..8c48d675 100644
--- a/catalog/rest/rest.go
+++ b/catalog/rest/rest.go
@@ -593,6 +593,17 @@ func (r *Catalog) createSession(ctx context.Context, opts 
*options) (*http.Clien
        }
        cl := &http.Client{Transport: session}
 
+       for k, v := range opts.headers {
+               session.defaultHeaders.Set(k, v)
+       }
+
+       session.defaultHeaders.Set("X-Client-Version", icebergRestSpecVersion)
+       session.defaultHeaders.Set("Content-Type", "application/json")
+       session.defaultHeaders.Set("User-Agent", "GoIceberg/"+iceberg.Version())
+       if session.defaultHeaders.Get("X-Iceberg-Access-Delegation") == "" {
+               session.defaultHeaders.Set("X-Iceberg-Access-Delegation", 
"vended-credentials")
+       }
+
        token := opts.oauthToken
        if token == "" && opts.credential != "" {
                var err error
@@ -605,11 +616,6 @@ func (r *Catalog) createSession(ctx context.Context, opts 
*options) (*http.Clien
                session.defaultHeaders.Set(authorizationHeader, bearerPrefix+" 
"+token)
        }
 
-       session.defaultHeaders.Set("X-Client-Version", icebergRestSpecVersion)
-       session.defaultHeaders.Set("Content-Type", "application/json")
-       session.defaultHeaders.Set("User-Agent", "GoIceberg/"+iceberg.Version())
-       session.defaultHeaders.Set("X-Iceberg-Access-Delegation", 
"vended-credentials")
-
        if opts.enableSigv4 {
                cfg := opts.awsConfig
                if !opts.awsConfigSet {
diff --git a/catalog/rest/rest_test.go b/catalog/rest/rest_test.go
index c515b360..8135a864 100644
--- a/catalog/rest/rest_test.go
+++ b/catalog/rest/rest_test.go
@@ -235,6 +235,110 @@ func (r *RestCatalogSuite) TestToken401() {
        r.ErrorContains(err, "invalid_client: credentials for key invalid_key 
do not match")
 }
 
+func (r *RestCatalogSuite) TestWithHeaders() {
+       namespace := "examples"
+       customHeaders := map[string]string{
+               "X-Custom-Header": "custom-value",
+               "Another-Header":  "another-value",
+       }
+
+       r.mux.HandleFunc("/v1/namespaces/"+namespace+"/tables", func(w 
http.ResponseWriter, req *http.Request) {
+               r.Require().Equal(http.MethodGet, req.Method)
+
+               // Check for standard headers
+               for k, v := range TestHeaders {
+                       r.Equal(v, req.Header.Values(k))
+               }
+
+               // Check for custom headers
+               for k, v := range customHeaders {
+                       r.Equal(v, req.Header.Get(k))
+               }
+
+               json.NewEncoder(w).Encode(map[string]any{
+                       "identifiers": []any{},
+               })
+       })
+
+       cat, err := rest.NewCatalog(context.Background(), "rest", r.srv.URL,
+               rest.WithOAuthToken(TestToken),
+               rest.WithHeaders(customHeaders))
+       r.Require().NoError(err)
+
+       iter := cat.ListTables(context.Background(), 
catalog.ToIdentifier(namespace))
+       for _, err := range iter {
+               r.Require().NoError(err)
+       }
+}
+
+func (r *RestCatalogSuite) TestWithHeadersOnOAuthRoute() {
+       customHeaders := map[string]string{
+               "X-Custom-Header": "custom-value",
+               "Another-Header":  "another-value",
+       }
+
+       r.mux.HandleFunc("/v1/oauth/tokens", func(w http.ResponseWriter, req 
*http.Request) {
+               r.Equal(http.MethodPost, req.Method)
+
+               // Check that custom headers are present on the OAuth token 
request
+               for k, v := range customHeaders {
+                       r.Equal(v, req.Header.Get(k))
+               }
+
+               w.WriteHeader(http.StatusOK)
+
+               json.NewEncoder(w).Encode(map[string]any{
+                       "access_token":      TestToken,
+                       "token_type":        "Bearer",
+                       "expires_in":        86400,
+                       "issued_token_type": 
"urn:ietf:params:oauth:token-type:access_token",
+               })
+       })
+
+       cat, err := rest.NewCatalog(context.Background(), "rest", r.srv.URL,
+               rest.WithCredential(TestCreds),
+               rest.WithHeaders(customHeaders))
+       r.Require().NoError(err)
+
+       r.NotNil(cat)
+}
+
+func (r *RestCatalogSuite) TestWithHeadersOnAuthURLRoute() {
+       customHeaders := map[string]string{
+               "X-Custom-Header": "custom-value",
+               "Another-Header":  "another-value",
+       }
+
+       r.mux.HandleFunc("/custom-auth-url", func(w http.ResponseWriter, req 
*http.Request) {
+               r.Equal(http.MethodPost, req.Method)
+
+               // Check that custom headers are present on the custom auth URL 
request
+               for k, v := range customHeaders {
+                       r.Equal(v, req.Header.Get(k))
+               }
+
+               w.WriteHeader(http.StatusOK)
+
+               json.NewEncoder(w).Encode(map[string]any{
+                       "access_token":      TestToken,
+                       "token_type":        "Bearer",
+                       "expires_in":        86400,
+                       "issued_token_type": 
"urn:ietf:params:oauth:token-type:access_token",
+               })
+       })
+
+       authUri, err := url.Parse(r.srv.URL)
+       r.Require().NoError(err)
+
+       cat, err := rest.NewCatalog(context.Background(), "rest", r.srv.URL,
+               rest.WithCredential(TestCreds),
+               rest.WithHeaders(customHeaders),
+               rest.WithAuthURI(authUri.JoinPath("custom-auth-url")))
+       r.Require().NoError(err)
+
+       r.NotNil(cat)
+}
+
 func (r *RestCatalogSuite) TestListTables200() {
        namespace := "examples"
        customPageSize := 100

Reply via email to