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

CORE: Allow HTTPClient to parse headers from properties. #12595

Open
wants to merge 12 commits into
base: main
Choose a base branch
from

Conversation

wolflex888
Copy link

@wolflex888 wolflex888 commented Mar 21, 2025

Currently, a header can be set within the catalog properties with a prefix header.. We are using this property to pass in properties like header.X-Iceberg-Access-Delegation to enable vended-credential. The base headers were defined here back in 1.7.x, but this behavior disappeared when we refactored the code to use AuthManager.

I want to have HTTPClient parse the headers from the configs it got from RestSessionCatalog instead of having RestSessionCatalog set it every time it wants to make a request.

@github-actions github-actions bot added the core label Mar 21, 2025
@adutra
Copy link
Contributor

adutra commented Mar 21, 2025

@wolflex888 thank you so much for reporting this.

I was initially surprised because this functionality was supposed to work just as before. After digging a bit, I found the culprit: in this commit I inadvertently removed one line that should not have been removed:

77a25fa#diff-86450612dbe323d6d06cbc3846aa1913f042eaedadc0ca027c36bfbe08d3a46cL174

I think the proper "fix" would be to just put that line back. For example:

  public RESTSessionCatalog() {
    this(
        config -> {
          HTTPClient.Builder builder =
              HTTPClient.builder(config).uri(config.get(CatalogProperties.URI));
          configHeaders(config).forEach(builder::withHeader);
          return builder.build();
        },
        null);
  }

I am sorry for the inconvenience.

@wolflex888
Copy link
Author

wolflex888 commented Mar 21, 2025

@wolflex888 thank you so much for reporting this.

I was initially surprised because this functionality was supposed to work just as before. After digging a bit, I found the culprit: in this commit I inadvertently removed one line that should not have been removed:

77a25fa#diff-86450612dbe323d6d06cbc3846aa1913f042eaedadc0ca027c36bfbe08d3a46cL174

I think the proper "fix" would be to just put that line back. For example:

  public RESTSessionCatalog() {
    this(
        config -> {
          HTTPClient.Builder builder =
              HTTPClient.builder(config).uri(config.get(CatalogProperties.URI));
          configHeaders(config).forEach(builder::withHeader);
          return builder.build();
        },
        null);
  }

I am sorry for the inconvenience.

Let me test that a bit. I think i did something similar and it did not work as expected because there's also this

@@ -55,7 +55,7 @@ public class RESTCatalog
public RESTCatalog() {
this(
SessionCatalog.SessionContext.createEmpty(),
config -> HTTPClient.builder(config).uri(config.get(CatalogProperties.URI)).build());
config -> HTTPClient.builder(config).uri(config.get(CatalogProperties.URI)).withHeaders(RESTUtil.configHeaders(config)).build());
Copy link
Author

@wolflex888 wolflex888 Mar 21, 2025

Choose a reason for hiding this comment

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

I am moving out the function configHeaders to RESTUtil so RESTCatalog can use it here. It's passing the clientBuilder to RESTSessionCatalog in class construction; therefore, only doing this in RESTSessionCatalog would not work.

Copy link
Contributor

@adutra adutra left a comment

Choose a reason for hiding this comment

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

Thanks @wolflex888 for fixing this!

\cc @nastra

@nastra nastra self-requested a review March 24, 2025 16:03
@amogh-jahagirdar amogh-jahagirdar added this to the Iceberg 1.9.0 milestone Mar 25, 2025
@@ -55,7 +55,11 @@ public class RESTCatalog
public RESTCatalog() {
this(
SessionCatalog.SessionContext.createEmpty(),
config -> HTTPClient.builder(config).uri(config.get(CatalogProperties.URI)).build());
config ->
Copy link
Contributor

Choose a reason for hiding this comment

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

we haven't been setting the headers here previously which might be because it was missed earlier. I believe it should be fine to set them here

Copy link
Contributor

Choose a reason for hiding this comment

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

can you please add a test to TestRESTCatalog that make sure that headers are properly passed from the properties to the underlying HTTP client when a request is being done?

@nastra nastra requested a review from danielcweeks March 25, 2025 16:02
Copy link
Contributor

@danielcweeks danielcweeks left a comment

Choose a reason for hiding this comment

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

Thanks @wolflex888 for catching this. I'm +1 pending addition of tests per @nastra

@@ -53,9 +54,11 @@ public class RESTCatalog
private final ViewCatalog viewSessionCatalog;

public RESTCatalog() {
this(
SessionCatalog.SessionContext.createEmpty(),
Copy link
Contributor

Choose a reason for hiding this comment

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

all of this refactoring shouldn't really be necessary

@@ -38,6 +40,12 @@ public class RESTUtil {
private static final Joiner NAMESPACE_ESCAPED_JOINER = Joiner.on(NAMESPACE_ESCAPED_SEPARATOR);
private static final Splitter NAMESPACE_ESCAPED_SPLITTER =
Splitter.on(NAMESPACE_ESCAPED_SEPARATOR);
public static final Function<Map<String, String>, RESTClient> DEFAULT_CLIENT_BUILDER =
Copy link
Contributor

Choose a reason for hiding this comment

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

this shouldn't be necessary. Can you please revert the refactorings from 1536b7f and only add the new tests?

@@ -32,6 +32,7 @@ private CatalogProperties() {}
public static final String VIEW_DEFAULT_PREFIX = "view-default.";
public static final String VIEW_OVERRIDE_PREFIX = "view-override.";
public static final String METRICS_REPORTER_IMPL = "metrics-reporter-impl";
public static final String HTTP_HEADER_PREFIX = "header.";
Copy link
Contributor

Choose a reason for hiding this comment

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

this is specific to REST, so shouldn't be in CatalogProperties. RESTUtil would be the right place but I'm currently not sure whether we should be exposing this

@@ -332,6 +330,27 @@ public void testInitializeWithBadArguments() throws IOException {
restCat.close();
}

@Test
public void testCatalogProperties() {
Copy link
Contributor

Choose a reason for hiding this comment

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

I was thinking of something similar to this, where we provide a custom client that we spy on and check what headers are sent:

@Test
  public void testDefaultHeadersPropagated() {
    Map<String, String> catalogHeaders =
        ImmutableMap.of("Authorization", "Bearer client-credentials-token:sub=catalog");

    Map<String, String> properties =
        ImmutableMap.of(
            "header.test-header",
            "test-value",
            CatalogProperties.URI,
            httpServer.getURI().toString());
    Map<String, String> expectedHeaders = ImmutableMap.of("test-header", "test-value");

    HTTPClient client =
        Mockito.spy(
            HTTPClient.builder(properties)
                .withHeaders(RESTUtil.merge(RESTUtil.configHeaders(properties), catalogHeaders))
                .uri(properties.get(CatalogProperties.URI))
                .build());
    RESTCatalog catalog =
        new RESTCatalog(SessionCatalog.SessionContext.createEmpty(), (config) -> client);
    catalog.initialize("test", properties);

    assertThat(catalog.namespaceExists(Namespace.of("non-existing"))).isFalse();

    Mockito.verify(client)
        .execute(
            reqMatcher(HTTPMethod.GET, "v1/config", expectedHeaders, Map.of()),
            eq(ConfigResponse.class),
            any(),
            any());
    Mockito.verify(client)
        .execute(
            reqMatcher(HTTPMethod.HEAD, "v1/namespaces/non-existing", expectedHeaders, Map.of()),
            any(),
            any(),
            any());
  }

I haven't executed/tried this, so you might need to tune/adjust this test a little bit

.uri(properties.get(CatalogProperties.URI))
.build());
RESTCatalog catalog =
new RESTCatalog(SessionCatalog.SessionContext.createEmpty(), (config) -> client);
Copy link
Author

@wolflex888 wolflex888 Mar 28, 2025

Choose a reason for hiding this comment

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

Still trying to figure out why passing in HTTPClient using a lambda function here will close the client.

Copy link
Contributor

Choose a reason for hiding this comment

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

It will be closed by this try-with-resources block when initialize is called:

try (RESTClient initClient = clientBuilder.apply(props);

Copy link
Author

Choose a reason for hiding this comment

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

Sorry for being slow, but how do we usually deal with this kind of situation? Normally, clientBuilder will build a new HttpClient for us to use, but in this case we are returning a static client. We can't do Mokito.spy unless it's static.

Copy link
Contributor

Choose a reason for hiding this comment

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

Try this:

Mockito.doNothing().when(client).close();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants