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.