-
Notifications
You must be signed in to change notification settings - Fork 184
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
Add initializer API for headers of HTTP proxy CONNECT request #2744
Changes from 2 commits
46ae4e4
6e157a5
9c5076d
a061338
c34a52e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
/* | ||
* Copyright © 2023 Apple Inc. and the ServiceTalk project authors | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package io.servicetalk.http.api; | ||
|
||
import io.servicetalk.transport.api.ClientSslConfig; | ||
|
||
import java.util.function.Consumer; | ||
|
||
/** | ||
* Configuration for a proxy. | ||
* | ||
* @param <A> the type of address | ||
*/ | ||
public interface ProxyConfig<A> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For others who review: this may look like an overkill to add a new "config" API while we need only one more config parameter, which could be a 2nd argument for original
Note that |
||
|
||
/** | ||
* Address of the proxy. | ||
* <p> | ||
* Usually, this is an unresolved proxy address and its type must match the type of address before resolution used | ||
* by {@link SingleAddressHttpClientBuilder}. However, if the client builder was created for a resolved address, | ||
* this address must also be already resolved. Otherwise, a runtime exceptions will occur. | ||
idelpivnitskiy marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* | ||
* @return address of the proxy | ||
*/ | ||
A address(); | ||
|
||
/** | ||
* An initializer for {@link HttpHeaders} related to | ||
* <a href="https://datatracker.ietf.org/doc/html/rfc9110#section-9.3.6">HTTP/1.1 CONNECT</a> request. | ||
* <p> | ||
* When this {@link ProxyConfig} is used for secure proxy tunnels (when | ||
* {@link SingleAddressHttpClientBuilder#sslConfig(ClientSslConfig) ClientSslConfig} is configured) the tunnel is | ||
* always initialized using {@code HTTP/1.1 CONNECT} request. This {@link Consumer} can be used to set custom | ||
* {@link HttpHeaders} for that request, like {@link HttpHeaderNames#PROXY_AUTHORIZATION proxy-authorization}, | ||
* tracing, or any other header. | ||
* | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If (are we?) doing any retries on the connect request, this comment I think should clarified if it is called N times or just once. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, there are no retries specifically for a |
||
* @return An initializer for {@link HttpHeaders} related to | ||
* <a href="https://datatracker.ietf.org/doc/html/rfc9110#section-9.3.6">HTTP/1.1 CONNECT</a> request | ||
*/ | ||
Consumer<HttpHeaders> connectRequestHeadersInitializer(); | ||
|
||
/** | ||
* Returns a {@link ProxyConfig} for the specified {@code address}. | ||
* <p> | ||
* All other {@link ProxyConfig} options will use their default values applied by {@link ProxyConfigBuilder}. | ||
* | ||
* @param address Address of the proxy | ||
* @param <A> the type of address | ||
* @return a {@link ProxyConfig} for the specified {@code address} | ||
* @see ProxyConfigBuilder | ||
*/ | ||
static <A> ProxyConfig<A> of(A address) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "forAddress" I think reads easier (since you are creating a proxy config for an address, not of an address), but as you mentioned using of aligns it with HostAndPort. I'm ok either way and it's not a big deal. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another option would be to not have this static method at all and let users go through the builder all the time? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had it originally and it looked quite verbose There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will rename to |
||
return new ProxyConfigBuilder<>(address).build(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
/* | ||
* Copyright © 2023 Apple Inc. and the ServiceTalk project authors | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package io.servicetalk.http.api; | ||
|
||
import java.util.function.Consumer; | ||
|
||
import static java.util.Objects.requireNonNull; | ||
|
||
/** | ||
* Builder for {@link ProxyConfig}. | ||
* | ||
* @param <A> the type of address | ||
*/ | ||
public final class ProxyConfigBuilder<A> { | ||
|
||
private static final Consumer<HttpHeaders> NOOP_HEADERS_CONSUMER = new Consumer<HttpHeaders>() { | ||
@Override | ||
public void accept(final HttpHeaders headers) { | ||
} | ||
|
||
@Override | ||
public String toString() { | ||
return "NOOP"; | ||
idelpivnitskiy marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
}; | ||
|
||
private final A address; | ||
private Consumer<HttpHeaders> connectRequestHeadersInitializer = NOOP_HEADERS_CONSUMER; | ||
|
||
/** | ||
* Creates a new instance. | ||
* | ||
* @param address Proxy address | ||
* @see ProxyConfig#address() | ||
*/ | ||
public ProxyConfigBuilder(final A address) { | ||
this.address = requireNonNull(address); | ||
} | ||
|
||
/** | ||
* Sets an initializer for {@link HttpHeaders} related to | ||
* <a href="https://datatracker.ietf.org/doc/html/rfc9110#section-9.3.6">HTTP/1.1 CONNECT</a> request. | ||
* | ||
* @param connectRequestHeadersInitializer {@link Consumer} that can be used to set custom {@link HttpHeaders} for | ||
* {@code HTTP/1.1 CONNECT} request (auth, tracing, etc.) | ||
* @return {@code this} | ||
* @see ProxyConfig#connectRequestHeadersInitializer() | ||
*/ | ||
public ProxyConfigBuilder<A> connectRequestHeadersInitializer( | ||
final Consumer<HttpHeaders> connectRequestHeadersInitializer) { | ||
this.connectRequestHeadersInitializer = requireNonNull(connectRequestHeadersInitializer); | ||
return this; | ||
} | ||
|
||
/** | ||
* Builds a new {@link ProxyConfig}. | ||
* | ||
* @return a new {@link ProxyConfig}. | ||
*/ | ||
public ProxyConfig<A> build() { | ||
return new DefaultProxyConfig<>(address, connectRequestHeadersInitializer); | ||
} | ||
|
||
private static final class DefaultProxyConfig<A> implements ProxyConfig<A> { | ||
|
||
private final A address; | ||
private final Consumer<HttpHeaders> connectRequestHeadersInitializer; | ||
|
||
private DefaultProxyConfig(final A address, final Consumer<HttpHeaders> connectRequestHeadersInitializer) { | ||
this.address = address; | ||
this.connectRequestHeadersInitializer = connectRequestHeadersInitializer; | ||
} | ||
|
||
@Override | ||
public A address() { | ||
return address; | ||
} | ||
|
||
@Override | ||
public Consumer<HttpHeaders> connectRequestHeadersInitializer() { | ||
return connectRequestHeadersInitializer; | ||
} | ||
|
||
@Override | ||
public boolean equals(final Object o) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because one of the fields is a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's fine to keep best effort and we can rely on users to care about equality for consumer. If they don't care, having different lambdas will make configs different. If they care, they must provide the same instance of Consumer or override equals/hashCode for consumer. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If that's an invariant which is important for the semantics of the executed code, I think we should point this out in a javadoc somewhere. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It feels kind of obvious that if users pass 2 different lambdas, the resulting "pojo" will be different. Note that there is no use-case for equals/hashCode right now to really worry about it. This is a pkg-private class, which makes it harder to document impl details. Not sure if we need to put this info on interface bcz it's not required right now. |
||
if (this == o) { | ||
return true; | ||
} | ||
if (!(o instanceof DefaultProxyConfig)) { | ||
return false; | ||
} | ||
|
||
final DefaultProxyConfig<?> that = (DefaultProxyConfig<?>) o; | ||
if (!address.equals(that.address)) { | ||
return false; | ||
} | ||
return connectRequestHeadersInitializer.equals(that.connectRequestHeadersInitializer); | ||
bryce-anderson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
@Override | ||
public int hashCode() { | ||
int result = address.hashCode(); | ||
result = 31 * result + connectRequestHeadersInitializer.hashCode(); | ||
return result; | ||
} | ||
|
||
@Override | ||
public String toString() { | ||
return getClass().getSimpleName() + | ||
"{address=" + address + | ||
", connectRequestHeadersInitializer=" + connectRequestHeadersInitializer + | ||
'}'; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -48,7 +48,7 @@ | |
*/ | ||
public interface SingleAddressHttpClientBuilder<U, R> extends HttpClientBuilder<U, R, ServiceDiscovererEvent<R>> { | ||
/** | ||
* Configure proxy to serve as an intermediary for requests. | ||
* Configures a proxy to serve as an intermediary for requests. | ||
* <p> | ||
* If the client talks to a proxy over http (not https, {@link #sslConfig(ClientSslConfig) ClientSslConfig} is NOT | ||
* configured), it will rewrite the request-target to | ||
|
@@ -65,10 +65,40 @@ public interface SingleAddressHttpClientBuilder<U, R> extends HttpClientBuilder< | |
* @param proxyAddress Unresolved address of the proxy. When used with a builder created for a resolved address, | ||
* {@code proxyAddress} should also be already resolved – otherwise runtime exceptions may occur. | ||
* @return {@code this}. | ||
* @deprecated Use {@link #proxyConfig(ProxyConfig)} with {@link ProxyConfig#of(Object)}. | ||
*/ | ||
default SingleAddressHttpClientBuilder<U, R> proxyAddress(U proxyAddress) { // FIXME: 0.43 - remove default impl | ||
throw new UnsupportedOperationException("Setting proxy address is not yet supported by " | ||
+ getClass().getName()); | ||
// FIXME: 0.43 - remove deprecated method | ||
@Deprecated | ||
@SuppressWarnings("DeprecatedIsStillUsed") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we keep this method for convenience? Related to my suggestions of the naming of the factory method: if we force them to go through the builder I would keep this convenience method, if we have the static factory I guess there is no harm in removing this since its going to be very short anyways. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Usually, users have more confusion when there are multiple overloads on the builder for the same functionality. Especially when they override each other. People don't really read javadoc and as a result, there is a high chance of confusing their understanding of runtime behavior when they use both of them: builder.proxyAddress(address)
.proxyConfig(config) Another use-case is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good point, makes sense. thanks! |
||
default SingleAddressHttpClientBuilder<U, R> proxyAddress(U proxyAddress) { | ||
proxyConfig(new ProxyConfigBuilder<>(proxyAddress).build()); | ||
return this; | ||
} | ||
|
||
/** | ||
* Configures a proxy to serve as an intermediary for requests. | ||
* <p> | ||
* If the client talks to a proxy over http (not https, {@link #sslConfig(ClientSslConfig) ClientSslConfig} is NOT | ||
* configured), it will rewrite the request-target to | ||
* <a href="https://tools.ietf.org/html/rfc7230#section-5.3.2">absolute-form</a>, as specified by the RFC. | ||
* <p> | ||
* For secure proxy tunnels (when {@link #sslConfig(ClientSslConfig) ClientSslConfig} is configured) the tunnel is | ||
* always initialized using | ||
* <a href="https://datatracker.ietf.org/doc/html/rfc9110#section-9.3.6">HTTP/1.1 CONNECT</a> request. The actual | ||
* protocol will be negotiated via <a href="https://tools.ietf.org/html/rfc7301">ALPN extension</a> of TLS protocol, | ||
* taking into account HTTP protocols configured via {@link #protocols(HttpProtocolConfig...)} method. In case of | ||
* any error during {@code CONNECT} process, {@link ProxyConnectException} or {@link ProxyConnectResponseException} | ||
* will be thrown when a request attempt is made through the constructed client instance. | ||
* | ||
* @param proxyConfig Configuration for a proxy. When used with a builder created for a resolved address, | ||
* {@link ProxyConfig#address()} must also be already resolved – otherwise runtime exceptions will occur. | ||
* @return {@code this}. | ||
* @see ProxyConfig#of(Object) | ||
* @see ProxyConfigBuilder | ||
*/ | ||
// FIXME: 0.43 - consider removing default impl | ||
default SingleAddressHttpClientBuilder<U, R> proxyConfig(ProxyConfig<U> proxyConfig) { | ||
throw new UnsupportedOperationException("Setting proxy config is not yet supported by " + getClass().getName()); | ||
} | ||
|
||
/** | ||
|
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 debated between
ProxyConfig.forAddress(proxyAddress)
andProxyConfig.of(proxyAddress)
. Went withof
only because wanted to be consistent with similar static method on other interfaces/classes, likeHostAndPort
,HttpRequestMethod
, etc. Ifof
doesn't read well for this use-case, happy to rename it by your recommendations.