Proposal - 8209137: Add ability to bind to specific local address to HTTP client
Michael McMahon
michael.x.mcmahon at oracle.com
Fri Dec 3 16:09:14 UTC 2021
Yes, I agree with this. It's a niche use case which should be made clear
in the docs.
The new method should have a default implementation.
I think I prefer also to use a SocketAddress in case there is some
future need to also
specify the local port. The InetSocketAddress constructors allow a
default value of zero
to mean any port.
Thanks,
Michael.
On 03/12/2021 12:29, Daniel Fuchs wrote:
> Hi Jaikiran,
>
> A couple of remarks:
>
> > However, in this specific case the Builder is expected
> > to be created using the static java.net.http.HTTPClient.newBuilder()
> > method and that implementation always returns an internal
> implementation
> > of the Builder interface. That effectively means that the only
> > implementation impacted by this new method addition would be the JDK
> > internal jdk.internal.net.http.HttpClientBuilderImpl. Because of
> this, I
> > decided not to create this above new method as a "default" one.
>
> I do not think this is right. Even though the only implementation
> is in the JDK, libraries that provides adds-on on top of the JDK API
> could very well provide their own builder implementation - obtained
> by other means than calling HttpClient::newBuilder().
>
> In this case I would recommend having a default body that
> throws UnsupportedOperationException - and adding the @throws
> to the API documentation of the new method.
>
> You will also need an @implSpec to document the default body,
> and possibly say that builders obtained from HttpClient::newBuilder
> provide an implementation of this method.
>
> There will also be some security checks that need to be performed
> probably in the builder implementation, either when passing the
> address or possibly when build() is called (or maybe both?).
> See SocketChannel::bind. @throws SecurityException with appropriate
> documentation will need to be added where needed.
> The HttpClient does these checks before hand - so I believe a
> security check needs to happen in build() at the latest.
>
> There's also the question on whether we should take a SocketAddress
> here, even though at this time we only accept InetAddress.
> There might be pro and cons.
>
> > In addition to the above method on the Builder interface, a new method
> > has also been introduced on the java.net.http.HttpClient class:
> >
> > /**
> > * Returns an {@link InetAddress} which will be used by this
> client
> > * to bind a socket when creating connections. If no
> > * {@link Builder#localAddress() local address} was set in the
> > * client's builder, then this method returns null, in which case
> > * the sockets created by this client will be bound to
> automatically
> > * assigned socket addresses.
> > *
> > * @return the local address that will be used by this client when
> > * creating connections
> > * @since 19
> > */
> > public InetAddress localAddress() {
> > return null;
> > }
>
> I am not sure we should add that.
> If we do add this method, then it should probably
> return Optional<InetAddress> like other methods in this
> class (e.g. proxy(), authenticator(), selector())
>
> WRT to SocketChannel::bind I believe we should treat that as a non
> blocking call - as it shouldn't involve any network operation.
>
> Finally, supplying a local address to the HttpClient builder is really
> for advanced users. Only hosts reachable through that interface will
> be connectable. This means that providing a "bad" address there
> can prevent accessing the servers targetted by the request URI.
> In pathological cases it could also prevent accessing the proxy
> returned by the proxy selector.
>
> The documentation of Builder::localAddress should probably have
> more information about the use cases for supplying a local address
> and some warning about the consequences of doing so.
>
> Michael may have more insight/comments to provide.
>
> best regards,
>
> -- daniel
>
> On 03/12/2021 06:21, Jaikiran Pai wrote:
>> I've been working on adding support for the enhancement noted in
>> https://bugs.openjdk.java.net/browse/JDK-8209137. As noted in that
>> issue, right now the java.net.http.HTTPClient doesn't have a way to
>> allow applications to specify which local address to use while
>> connecting to target hosts during requests >
>> The proposed enhancement will introduce an API to allow applications
>> to configure a particular local address that the HTTPClient will use
>> when creating Socket(s) during connection. I've an initial version of
>> this enhancement that I've created as a draft PR here
>> https://github.com/openjdk/jdk/pull/6690 for initial discussion. I
>> would like some inputs on these changes.
>>
>> User facing API:
>> ---------------
>> A new API has been introduced on the java.net.http.HTTPClient.Builder
>> interface:
>>
>>
>> /**
>> * Binds the socket to this local address when creating
>> * connections for sending requests.
>> *
>> * <p> If no local address is set or {@code null} is passed
>> * to this method then sockets created by the
>> * HTTP client will be bound to an automatically
>> * assigned socket address.
>> *
>> * @param localAddr The local socket address. Can be null.
>> * @return this builder
>> * @since 19
>> */
>> public Builder localAddress(InetAddress localAddr);
>>
>> The java.net.http.HTTPClient.Builder is a public API and typically
>> any new method additions to such interfaces would mean using a
>> "default" method so as to prevent and breakages of existing
>> implementations of that interface. However, in this specific case the
>> Builder is expected to be created using the static
>> java.net.http.HTTPClient.newBuilder() method and that implementation
>> always returns an internal implementation of the Builder interface.
>> That effectively means that the only implementation impacted by this
>> new method addition would be the JDK internal
>> jdk.internal.net.http.HttpClientBuilderImpl. Because of this, I
>> decided not to create this above new method as a "default" one.
>>
>> This new locaAddress method accepts an instance of
>> java.net.InetAddress. Calling applications are expected to create the
>> InetAddress using different available methods. I paid some thought to
>> see if it's worth allowing calling applications to pass a
>> "hostname/IP" String as a parameter to this new method instead of
>> accepting a InetAddress, but decided to expect a pre-constructed
>> InetAddress to keep it simple (for the HTTPClient) as well as to let
>> the application/callers deal with the InetAddress creation.
>>
>> Calling this method is optional and as noted in the javadoc of this
>> method, passing null or not calling this method will have the same
>> result - the connections created by the HTTPClient will end up using
>> an automatically assigned socket address, just like it does in the
>> current version.
>>
>> Furthermore, the Builder, neither in the implementation of this
>> method nor during the build() call will do any validations on the
>> passed InetAddress. In other words, the localAddress that's set on
>> the HTTPClient will just be used as-is during connection creation and
>> any failures to bind due to the passed InetAddress will be propagated
>> back as a IOException. I spent some thought on whether to do any kind
>> of validations, like whether the host of the InetAddress is
>> resolvable and such, during the Builder.build() but decided not to.
>> One reason for that is, the HTTPClient is a long lived object and, as
>> far as I remember, is encouraged to be used as one single instance
>> for the lifetime of an application. The address resolution result of
>> an InetAddress (as per its javadoc) are cachable values and are
>> refreshed from time to time (based on internal implementation details
>> of that class). As such, there's no guarantee that a "valid"
>> InetAddress during Builder.build() would still be "valid" when the
>> connection is being established by the HTTPClient at some later point
>> in time (maybe some hours later). So doing such validations at
>> Builder.build() time, I thought, would add no real value.
>>
>> In addition to the above method on the Builder interface, a new
>> method has also been introduced on the java.net.http.HttpClient class:
>>
>> /**
>> * Returns an {@link InetAddress} which will be used by this client
>> * to bind a socket when creating connections. If no
>> * {@link Builder#localAddress() local address} was set in the
>> * client's builder, then this method returns null, in which case
>> * the sockets created by this client will be bound to
>> automatically
>> * assigned socket addresses.
>> *
>> * @return the local address that will be used by this client when
>> * creating connections
>> * @since 19
>> */
>> public InetAddress localAddress() {
>> return null;
>> }
>>
>> This method just returns whatever was set as the localAddress on
>> the Builder, using which this instance of HTTPClient was built.
>>
>>
>> (Internal) Implementation:
>> --------------------------
>> Within the java.net.http.HTTPClient implementation,
>> jdk.internal.net.http.HttpConnection is where the underlying
>> SocketChannel connections are handled. There are multiple sub-classes
>> of the jdk.internal.net.http.HttpConnection abstract class. However
>> each of those sub-classes ultimately ends up calling the
>> jdk.internal.net.http.PlainHttpConnection class for doing the actual
>> connection on the SocketChannel. The implementation in this class has
>> now been enhanced to take into account any localAddress that might
>> have been set on the HTTPClient. In the presence of such a configured
>> InetAddress, this implementation now calls a SocketChannel.bind()
>> before calling the SocketChannel.connect().
>>
>> More precisely, the PlainHttpConnection.connectAsync(Exchange<?>
>> exchange) method has been enhanced to call the bind() before calling
>> the connect() on the channel. This
>> PlainHttpConnection.connectAsync(), as the name suggests, isn't
>> supposed to have any blocking calls. My knowledge in this area isn't
>> vast, so I was unsure whether a SocketChannel.bind() call is a
>> blocking call, even when the SocketChannel is configured to be
>> non-blocking. The SocketChannel.bind() doesn't state much about this
>> semantic (unlike the SocketChannel.connect()). Looking at the API and
>> the implementation (which ends up calling the native layer for the
>> socket bind), I think a call to bind() would be a blocking one. More
>> so in the case where it perhaps has to find a free/available port
>> (when the passed port to the socket address is 0, like in the
>> implementation here). As such, in this proposed implementation, I've
>> considered SocketChannel.bind() to be blocking and thus wrapped it
>> around in a async call that needs to complete successfully before an
>> async connect is issued. I look forward to inputs around this part on
>> whether or not this is the right way to go about it.
>>
>> Testing:
>> -------
>> A new jtreg test has been introduced to test this new method. The
>> test is configured to run with both IPv6 and IPv4. tier1 testing with
>> these changes have passed on a personal run of GitHub Actions and a
>> local test run of "test/jdk/java/net/httpclient/" has also passed
>> with these changes. I'll run tier2 tests locally once we have
>> finalized on the API part of this change.
>>
>> The PR is currently in draft and I will move it out of draft after
>> the API is finalized. I don't plan to rush this into JDK 18, so the
>> new APIs have all been marked for @since 19.
>>
>> -Jaikiran
>>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.java.net/pipermail/net-dev/attachments/20211203/f2c4cf98/attachment-0001.htm>
More information about the net-dev
mailing list