RFR: 8087112 HTTP API and HTTP/1.1 implementation

Anthony Vanelverdinghe anthony.vanelverdinghe at gmail.com
Mon Feb 22 00:47:00 UTC 2016


Hi Michael

Here's my feedback. If that'd be helpful, I'm willing to contribute 
patches to address these points. Either way, please let me know if you 
have any questions.

Some general proposals:

First, MultiExchange implements a retry mechanism. However, I see some 
issues with this:
- it's unconfigurable and undocumented in the API
- it seems that requests are retried irrespective of the HTTP method. 
Everything that is not a standard, idempotent method should never be 
retried.
- a given HttpRequest.BodyProcessor may not be able to handle retries
Given the above, I believe the retry mechanism should be disabled by 
default for now (it seems that setting DEFAULT_MAX_ATTEMPTS to 0 would 
do so).


Second, I'd like to change the Processor interfaces as below, to make 
use of the j.u.c.Flow API.

interface HttpRequest.BodyProcessor {

     void send(HttpRequest request, SubmissionPublisher<ByteBuffer> 
publisher);

}

and an example implementation:

class FromString implements HttpRequest.BodyProcessor {

     final String s;

     FromString(String s) {
         this.s = s;
     }

     void send(HttpRequest request, SubmissionPublisher<ByteBuffer> 
publisher) {
         Optional<Charset> charset = getCharsetParameterOfContentType();
         charset.ifPresentOrElse(
             cs -> { publisher.submit(ByteBuffer.wrap(s.getBytes(cs))); 
publisher.close(); },
             () -> publisher.closeExceptionally(new 
IllegalArgumentException("request doesn't specify charset"))
         );
     }

}


interface HttpResponse.BodyProcessor<T> extends 
Flow.Subscriber<ByteBuffer> {

     Optional<T> beforeReceipt(long contentLength, int statusCode, 
HttpHeaders responseHeaders) throws IOException;

     T afterReceipt() throws IOException;

}

and an example implementation:

class AsFile implements HttpResponse.BodyProcessor<Path> {

     final Path p;
     FileChannel fc;
     IOException e;
     Flow.Subscription subscription;

     AsFile(Path p) {
         this.p = p;
     }

     Optional<Path> beforeReceipt(long contentLength, int statusCode, 
HttpHeaders responseHeaders) throws IOException {
         this.fc = FileChannel.open(p);
         return Optional.empty();
     }

     Path afterReceipt() throws IOException {
         if(e == null) {
             return p;
         } else {
             throw e;
         }
     }

     void onSubscribe(Flow.Subscription subscription) {
         this.subscription = subscription;
         subscription.request(1);
     }

     void onNext(ByteBuffer item) {
         try {
             fc.write(item);
             subscription.request(1);
         } catch(IOException e) {
             subscription.cancel();
             try {
                 fc.close();
             } catch(IOException eSup) {
                 e.addSuppressed(eSup);
             }
             this.e = e;
         }
     }

     void onComplete() {
         try {
             fc.close();
         } catch(IOException e) {
             this.e = e;
         }
     }

     void onError(Throwable throwable) {
         try {
             fc.close();
         } catch(IOException e) {
             this.e = e;
         }
     }

}


Finally, HttpResponse.MultiProcessor would be eliminated altogether, by 
replacing HttpRequest::multiResponseAsync with the following method:
CompletionStage<Map.Entry<HttpResponse, List<HttpResponse>>> 
responseAsync(BiFunction<HttpRequest, HttpHeaders, Boolean> pushHandler) 
{ ... }

Moreover, response() and responseAsync() could then easily be 
implemented in terms of this method, as follows:
HttpResponse response() { return responseAsync().get(); }
CompletionStage<HttpResponse> responseAsync() { return 
responseAsync((request, headers) -> false).getKey(); }


On to more specific issues:

On the Javadoc:
java.net.http package-info
- "superceded" should be "superseded"


On the API:
java.net.http
- convert all classes to interfaces
--- none of the classes contains any implementation code
- specify NPEs wherever applicable
- specify synchronization requirements more rigorously
--- e.g. should the ProxySelector of a HttpClient be thread-safe, or 
will the HttpClient synchronize access externally (assuming the 
ProxySelector is confined to the HttpClient)?

HttpClient
- add "Optional<Duration> timeout()"
- authenticator(): specify that if no Authenticator was set, but a 
default was set with Authenticator.setDefault(), then that Authenticator 
will be used (though not returned from this method)
--- ease migration of java.net-based applications
- cookieManager(): change to "CookieHandler cookieHandler()", with 
default value CookieHandler.getDefault() if set, or else a default 
object is returned (e.g. new CookieManager())
--- allows to use CookieHandler.getDefault() and ease migration of 
java.net-based applications
- debugPrint(): remove
- followRedirects(): change to "Predicate<HttpResponse> redirectionPolicy()"
--- allows to implement custom redirection policies
- pipelining(): remove
--- unsupported
- proxySelector(): change to "ProxySelector proxySelector()", with 
default value ProxySelector.getDefault()
--- use a sensible default where possible
- sslParameters(): change to "SSLParameters sslParameters()", with 
default value sslContext().getDefaultSSLParameters()
--- use a sensible default where possible
- version(): change to "HttpVersion version()"

HttpClient.Builder
- add "HttpClient.Builder timeout(Duration timeout)"
- cookieManager(CookieManager manager): change to 
cookieHandler(CookieHandler handler)
- followRedirects(HttpClient.Redirect policy): change to 
redirectIf(Predicate<HttpResponse> policy)
- pipelining(boolean enable): remove
- priority(int priority): remove
--- unused, can't be set per-request, unable to express stream dependencies
- version(HttpClient.Version version): change to version(HttpVersion 
version)

HttpClient.Redirect
- replace with top-level "enum StandardRedirectionPolicies implements 
Predicate<HttpResponse>" with 4 constants: TRUE, FALSE, SECURE, 
SAME_PROTOCOL
--- analog to java.nio.file.StandardOpenOption, allows to implement 
custom redirection policies, while providing standard policies for the 
common use-cases

HttpClient.Version
- move to top-level class HttpVersion
--- not client-specific

HttpHeaders
- firstValueAsLong(String name): change to "OptionalLong 
firstValueAsLong(String name)"

HttpRequest
- add "HttpRequest.Builder create()"
--- for consistency with HttpClient.request()
- add "ProxySelector proxy()"
- add "Optional<Duration> timeout()"
- followRedirects(): change to "Predicate<HttpResponse> redirectionPolicy()"
- fromXxx(): move to HttpRequest.BodyProcessor
--- HttpRequest API becomes cleaner, and aids in discoverability to find 
implementations in the interface itself
- fromByteArray(byte[] buf, int offset, int length): remove
--- easily done with fromByteArray(new ByteArrayInputStream(buf, offset, 
length))
- fromByteArrays(Iterator<byte[]> iter): replace with 
"fromByteArrays(Stream<byte[]> stream)"
- fromString(String body): converted using the character set as 
specified in the "charset" parameter of the Content-Type HTTP header
--- from RFC 7231: "The default charset of ISO-8859-1 for text media 
types has been removed; the default is now whatever the media type 
definition says."
- fromString(String s, Charset charset): remove
--- to ensure consistency with the "charset" parameter of the 
Content-Type HTTP header, don't allow to pass a charset here
- multiResponseAsync(HttpResponse.MultiProcessor<U> rspproc): replace 
(cf. proposals)
- noBody(): move to HttpRequest.BodyProcessor
- responseAsync(): change return type to CompletionStage<HttpResponse>
--- the caller shouldn't be able to complete the CompletableFuture itself

HttpRequest.Builder
- add "HttpRequest.Builder DELETE()"
--- often used with "RESTful APIs". Moreover, PUT was already added
- body(HttpRequest.BodyProcessor reqproc): change to 
body(Supplier<HttpRequest.BodyProcessor> reqproc)
--- to allow copy: copying the BodyProcessor itself would cause 
concurrent calls to methods of the same BodyProcessor instance
- followRedirects(HttpClient.Redirect policy): change to 
redirectIf(Predicate<HttpResponse> policy)
- header(String name, String value): change to header(String name, 
UnaryOperator<List<String>> valueUpdater)
--- allows to replace setHeader, and is much more flexible, e.g. can be 
used to remove values
- headers(String... headers): change to headers(Map<String, String> headers)
- setHeader(String name, String value): remove
- timeout(TimeUnit unit, long timeval): change to timeout(Duration timeout)

HttpResponse
- asXxx(): move to HttpRequest.BodyProcessor
- asByteArrayConsumer(Consumer<byte[]> consumer): replace with "static 
HttpResponse.BodyProcessor<Stream<byte[]>> asByteArrays()"
- asString(): specify that if the Content-Type header does not have a 
"charset" parameter, or its value is unsupported, an exception is 
thrown; replace reference to Content-encoding with Content-type
- asString(Charset charset): replace with asString(Charset charset, 
boolean force); replace reference to Content-encoding with Content-type
--- if "force" is false, only use if Content-type's charset parameter is 
not available. If "force" is true, always use the given charset
- body(HttpResponse.BodyProcessor<T> processor): change to 
body(Supplier<HttpResponse.BodyProcessor<T>> processorSupplier)
--- such that the library has the ability to create BodyProcessors 
itself, e.g. in case of a retry. It also shows the intention that 
processors should typically not be shared
- bodyAsync(HttpResponse.BodyProcessor<T> processor): change to 
"CompletionStage<T> bodyAsync(Supplier<HttpResponse.BodyProcessor<T>> 
processorSupplier)"
--- prevent caller from completing the CompletableFuture itself + same 
as body()
- multiFile(Path destination): move to HttpResponse.MultiProcessor
- sslParameters(): remove
--- cannot be HttpRequest-specific, and thus can always be retrieved as 
request().client().sslParameters()

HttpRequest.BodyProcessor
HttpResponse.BodyProcessor
HttpResponse.MultiProcessor
- cf. proposals above

HttpTimeoutException
- HttpTimeoutException(String message): make package-private
--- in case you'd want to add a public method such as "HttpRequest 
request()" in a future release, you could then just add a new 
package-private constructor with a HttpRequest parameter, and guarantee 
request() would never return null


And on the implementation (I just skimmed through the code):
java.net.http
- running NetBeans' analyzers on java.net.http gives 143 
warnings/errors, so it would be good to fix those that need fixing
- declare HashMap and LinkedList variables as Map and Queue/List 
wherever possible

AuthenticationFilter
- use HttpUrlConnection.UNAUTHORIZED / HTTP_PROXY_AUTH instead of 
hard-coded 401 / 407

BufferHandler
- why not named ByteBufferPool, analog to ConnectionPool?

Exchange/ExchangeImpl
- it's confusing that ExchangeImpl isn't a subclass of Exchange

Http1Request
- collectHeaders contains: URI uri = request.uri(); [...] if (request == 
null) {

HttpClientImpl
- starting the selmgr Thread in the constructor is unsafe
- there's more synchronization than is required, since there are 2 
independent groups of methods: 1 for timeouts & 1 for freelist
- the default client's ExecutorService doesn't use daemon threads, while 
a client without explicit ExecutorService does

HttpConnection
- contains hard-coded Unix path: new FileOutputStream("/tmp/httplog.txt")

HttpRequestBuilderImpl
- copy: should clone userHeaders

HttpRequestImpl
- DISALLOWED_HEADERS: why is "user-agent" disallowed? and "date" (which 
is a response header)?

Pair
- use Map.Entry and the factory method Map.entry() instead

SSLTunnelConnection
- connected(): "&" should be "&&"

Utils
- BUFSIZE: why "Must never be higher than 16K."?
- validateToken: must handle token being null or empty, must accept 
single quotes, must reject parentheses
- copySSLParameters: move to SSLDelegate
- unflip/compact/copy: remove


Finally, here's some issues I noticed while trying it out:

Every request seems to include the "?" of the URI's query part.

The following is a simple server and client, where the server stops as 
soon as it has read a request.
If you start the server, then start the client, the client will throw:
     java.net.http.FatalIOException: Retry limit exceeded
however, there are neither suppressed exceptions, nor a cause. Also, the 
exception on the retries looks suspicious:
java.io.IOException: wrong content length
     at java.net.http.Http1Request.writeFixedContent(Http1Request.java:320)

import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;

public class SimpleServer {

     public static void main(String[] args) throws IOException {
         HttpServer server = HttpServer.create(new 
InetSocketAddress(8080), 0);
         server.setExecutor(null);
         server.createContext("/", e -> {
             e.getRequestBody().readAllBytes();
             server.stop(0);
         });
         server.start();
     }

}

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpRequest;

public class SimpleClient {

     public static void main(String[] args) throws IOException, 
InterruptedException {
         HttpRequest.create(URI.create("http://localhost:8080"))
             .body(HttpRequest.fromString("helloworld"))
             .POST()
             .response();
     }

}

Kind regards,
Anthony


On 4/02/2016 17:14, Michael McMahon wrote:
> Hi,
>
> The following webrevs are for the initial implementation of JEP 110.
> Most of it is in the jdk repository with some build configuration in 
> the top
> level repo for putting this code in its own module (java.httpclient).
>
> http://cr.openjdk.java.net/~michaelm/8087112/01/top/webrev/
>
> http://cr.openjdk.java.net/~michaelm/8087112/01/jdk/webrev/
>
> The HTTP/2 implementation will come later.
>
> Thanks,
> Michael.



More information about the net-dev mailing list