-
Notifications
You must be signed in to change notification settings - Fork 2.3k
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
Auth Manager API part 1: HTTPRequest, HTTPHeader #11769
base: main
Are you sure you want to change the base?
Conversation
|
||
/** Returns the headers of this request. */ | ||
@Value.Default | ||
default HTTPHeaders headers() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@danielcweeks @nastra FYI, I wasn't satisfied with returning Map<String, List<String>>
from here, especially because of corner cases around case-sensitivity. I was planning to tackle that as a follow-up task, but since we are starting from square zero here, what you see here is the "enhanced" version.
* values. Header names are case-insensitive. | ||
*/ | ||
@Value.Lazy | ||
default Map<String, List<String>> asMap() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All these methods will be useful later.
@Value.Style(depluralize = true) | ||
@Value.Immutable | ||
@SuppressWarnings({"ImmutablesStyle", "SafeLoggingPropagation"}) | ||
public interface HTTPHeaders { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This class is loosely inspired by org.apache.hc.core5.http.message.HeaderGroup
.
*/ | ||
@Value.Lazy | ||
default URI requestUri() { | ||
return RESTUtil.buildRequestUri(this); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
to me it seems like we should rather embed the functionality of building an URI from a HTTPRequest
here (and also do the same for encoding the body). Additionally, building an URI from the path does some validation to make sure it doesn't start with a /
. I'd say this is something that we can move into a check method annotated with @Value.Check
. That way we don't need the util methods in RESTUtil
and can move testing into TestHTTPRequest
. I've added an example diff below:
--- a/core/src/main/java/org/apache/iceberg/rest/HTTPRequest.java
+++ b/core/src/main/java/org/apache/iceberg/rest/HTTPRequest.java
@@ -18,10 +18,14 @@
*/
package org.apache.iceberg.rest;
+import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
+import java.net.URISyntaxException;
import java.util.Map;
import javax.annotation.Nullable;
+import org.apache.hc.core5.net.URIBuilder;
+import org.apache.iceberg.exceptions.RESTException;
import org.immutables.value.Value;
/** Represents an HTTP request. */
@@ -49,7 +53,19 @@ public interface HTTPRequest {
*/
@Value.Lazy
default URI requestUri() {
- return RESTUtil.buildRequestUri(this);
+ // if full path is provided, use the input path as path
+ String fullPath =
+ (path().startsWith("https://") || path().startsWith("http://"))
+ ? path()
+ : String.format("%s/%s", baseUri(), path());
+ try {
+ URIBuilder builder = new URIBuilder(RESTUtil.stripTrailingSlash(fullPath));
+ queryParameters().forEach(builder::addParameter);
+ return builder.build();
+ } catch (URISyntaxException e) {
+ throw new RESTException(
+ "Failed to create request URI from base %s, params %s", fullPath, queryParameters());
+ }
}
/** Returns the HTTP method of this request. */
@@ -77,7 +93,17 @@ public interface HTTPRequest {
@Nullable
@Value.Redacted
default String encodedBody() {
- return RESTUtil.encodeRequestBody(this);
+ Object body = body();
+ if (body instanceof Map) {
+ return RESTUtil.encodeFormData((Map<?, ?>) body);
+ } else if (body != null) {
+ try {
+ return mapper().writeValueAsString(body);
+ } catch (JsonProcessingException e) {
+ throw new RESTException(e, "Failed to encode request body: %s", body);
+ }
+ }
+ return null;
}
/**
@@ -88,4 +114,13 @@ public interface HTTPRequest {
default ObjectMapper mapper() {
return RESTObjectMapper.mapper();
}
+
+ @Value.Check
+ default void check() {
+ if (path().startsWith("/")) {
+ throw new RESTException(
+ "Received a malformed path for a REST request: %s. Paths should not start with /",
+ path());
+ }
+ }
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes that's a good idea.
* case-insensitive. | ||
*/ | ||
@Value.Lazy | ||
default ListMultimap<String, String> asMultiMap() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we actually have a use case for this? I see the value in having asMap()
/ asSimpleMap()
but I feel like we don't need this method atm?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one will be very useful later to do things like this in HTTPClient
:
HTTPRequest req = ...
HttpUriRequestBase request = new HttpUriRequestBase(req.method().name(), req.requestUri());
req.headers().asMultiMap().forEach(request::addHeader);
return builder.build(); | ||
} | ||
|
||
static HTTPHeaders fromMultiMap(Multimap<String, String> headers) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same as I mentioned above. Do we actually have a use case for a bunch of these static methods? I would try to keep those to the amount we actually need
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the methods are tested, is that a big deal? This one indeed has no usage today, but more auth managers in the future could use it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Iceberg project typically tries to only add things to APIs that are needed for current use cases instead of planning for potential use cases that may or may not actually exist/happen (independent of whether there are tests for unused methods or not).
You also have to keep in mind that everything that's being added to iceberg-core
can't be easily changed without going through an API deprecation cycle, so we're trying to be mindful about what's really required in terms of APIs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough, let's remove all the static methods for now.
|
||
final HTTPHeaders headers = | ||
HTTPHeaders.of( | ||
HTTPHeader.of("header1", "value1a"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please also add a test where the key/value is null
|
||
class TestHTTPHeaders { | ||
|
||
final HTTPHeaders headers = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can be private
|
||
@Test | ||
void entries() { | ||
assertThat(headers.entries("header1")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please also add tests where null
is passed to all the methods that take nullable parameters, such as entries()
/contains()
/addIfAbsent()
@@ -95,6 +100,9 @@ void addIfAbsentHTTPHeader() { | |||
"header1", List.of("value1a", "value1b"), | |||
"header2", List.of("value2"), | |||
"header3", List.of("value3"))); | |||
|
|||
assertThatThrownBy(() -> headers.addIfAbsent((HTTPHeaders) null)) | |||
.isInstanceOf(NullPointerException.class); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
checks like this should always have a .hasMessage
check to make sure we get the right error msg back (unfortunately we can't easily enforce such a rule via checkstyle)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The message is generated by Immutables and will be rather cryptic. But OK.
* Returns a new instance with the added header, or the current instance if the header is already | ||
* present. | ||
*/ | ||
default HTTPHeaders addIfAbsent(HTTPHeader header) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we might want to consider naming this (and the other method) withHeaderIfAbsent()
to clearly indicate that (potentially) a new instance is returned because add
in the name suggests that the existing object is modified
@ParameterizedTest | ||
@MethodSource("validRequestBodies") | ||
public void encodedBody(HTTPRequest request, String expected) { | ||
assertThat(request.encodedBody()).isEqualTo(expected); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
personally I think the test code would be much more readable without having a parameterized test and rather have just two separate test methods that verify the body is properly encoded as form data/JSON, but I'm ok with what's currently here and I'll leave that up to you.
Also we probably might want to have some tests where the body can't be encoded due to:
- body is null
- body is a Map with null values
- body is an invalid JSON
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, thanks @adutra for all your work on this.
HTTPHeaders EMPTY = of(); | ||
|
||
/** Returns all the header entries in this group. */ | ||
List<HTTPHeader> entries(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this be Set<HTTPHeader>
? This would also take care of cases where there are actualy k/v duplicates (which we don't appear to handle)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like we should also annotate this as @NotNull
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see a @NotNull
annotation available in iceberg-core? Besides, the immutable generated class is annotated with @AllParametersAreNonNullByDefault
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@danielcweeks @NotNull
shouldn't be needed here, because everything not marked as @Nullable
or Optional is implicitly not null with Immutables
List<HTTPHeader> entries(); | ||
|
||
/** Returns all the entries in this group for the given name (case-insensitive). */ | ||
default List<HTTPHeader> entries(String name) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here, set
* Returns a new instance with the added headers, or the current instance if all headers are | ||
* already present. | ||
*/ | ||
default HTTPHeaders withHeaderIfAbsent(HTTPHeaders headers) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like putIfAbsent()
is more idiomatic here. Otherwise it just code would generally be redundant:
headers.putHeaderIfAbsent(header)
vs.
headers.putIfAbsent(header)
* Returns a new instance with the added header, or the current instance if the header is already | ||
* present. | ||
*/ | ||
default HTTPHeaders withHeaderIfAbsent(HTTPHeader header) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same idiomatic comment here. Generally we use with
prefix for builders, not mutators.
} | ||
|
||
/** Returns whether this group contains an entry with the given name (case-insensitive). */ | ||
default boolean contains(String name) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there are reason we're making this overridable? It seems like we should insulate the case behavior by making this a private static method.
|
||
String name(); | ||
|
||
@Value.Redacted |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this mean that we redact all header values? Maybe I don't fully understand the Immutable behavior here, but that's a bit problematic because we probably only want to redact sensitive headers (like AUTHORIZATION
).
import org.apache.iceberg.rest.HTTPHeaders.HTTPHeader; | ||
import org.junit.jupiter.api.Test; | ||
|
||
class TestHTTPHeaders { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be good to add a test for duplicate behavior as well.
As requested, I'm splitting #10753 in many PRs. This one is the first one. It introduces
HTTPRequest
which is a prerequisite for theAuthManager
API.