Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions catalog/rest/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -132,6 +138,7 @@ type options struct {
authUri *url.URL
scope string
transport http.RoundTripper
headers map[string]string

additionalProps iceberg.Properties
}
16 changes: 11 additions & 5 deletions catalog/rest/rest.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it desirable to be able to override this as well?

Copy link
Contributor

Choose a reason for hiding this comment

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

yea good point. for python, we allow overriding X-Iceberg-Access-Delegation but not User-Agent and Content-Type.

https://github.com/apache/iceberg-python/blob/3855f64b2ef5552483c377abeed95d2b9872777b/pyiceberg/catalog/rest/__init__.py#L615-L620

Copy link
Contributor

Choose a reason for hiding this comment

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

the order currently would not allow User-Agent to be overridden. I think thats the right behavior

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
Expand All @@ -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 {
Expand Down
104 changes: 104 additions & 0 deletions catalog/rest/rest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading