8204679: HTTP Client refresh
Reviewed-by: chegar, dfuchs, michaelm
Contributed-by: Chris Hegarty <chris.hegarty@oracle.com>, Daniel Fuchs <daniel.fuchs@oracle.com>, Michael McMahon <michael.x.mcmahon@oracle.com>, Pavel Rappo <pavel.rappo@oracle.com>
--- a/src/java.net.http/share/classes/java/net/http/HttpClient.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/java/net/http/HttpClient.java Wed Jun 20 09:05:57 2018 -0700
@@ -57,7 +57,7 @@
* and can be used to send multiple requests.
*
* <p> An {@code HttpClient} provides configuration information, and resource
- * sharing, for all requests send through it.
+ * sharing, for all requests sent through it.
*
* <p> A {@link BodyHandler BodyHandler} must be supplied for each {@link
* HttpRequest} sent. The {@code BodyHandler} determines how to handle the
@@ -232,11 +232,10 @@
*
* <p> If this method is not invoked prior to {@linkplain #build()
* building}, a default executor is created for each newly built {@code
- * HttpClient}. The default executor uses a {@linkplain
- * Executors#newCachedThreadPool(ThreadFactory) cached thread pool},
- * with a custom thread factory.
+ * HttpClient}.
*
- * @implNote If a security manager has been installed, the thread
+ * @implNote The default executor uses a thread pool, with a custom
+ * thread factory. If a security manager has been installed, the thread
* factory creates threads that run with an access control context that
* has no permissions.
*
@@ -451,7 +450,7 @@
* then the response, containing the {@code 3XX} response code, is returned,
* where it can be handled manually.
*
- * <p> {@code Redirect} policy is set via the {@linkplain
+ * <p> {@code Redirect} policy is set through the {@linkplain
* HttpClient.Builder#followRedirects(Redirect) Builder.followRedirects}
* method.
*
--- a/src/java.net.http/share/classes/java/net/http/HttpHeaders.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/java/net/http/HttpHeaders.java Wed Jun 20 09:05:57 2018 -0700
@@ -25,62 +25,68 @@
package java.net.http;
+import java.util.ArrayList;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalLong;
-import static java.util.Collections.emptyList;
-import static java.util.Collections.unmodifiableList;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.function.BiPredicate;
+import static java.lang.String.CASE_INSENSITIVE_ORDER;
+import static java.util.Collections.unmodifiableMap;
import static java.util.Objects.requireNonNull;
/**
* A read-only view of a set of HTTP headers.
*
- * <p> An {@code HttpHeaders} is not created directly, but rather returned from
- * an {@link HttpResponse HttpResponse}. Specific HTTP headers can be set for
- * {@linkplain HttpRequest requests} through the one of the request builder's
- * {@link HttpRequest.Builder#header(String, String) headers} methods.
+ * <p> An {@code HttpHeaders} is not typically created directly, but rather
+ * returned from an {@link HttpRequest#headers() HttpRequest} or an
+ * {@link HttpResponse#headers() HttpResponse}. Specific HTTP headers can be
+ * set for a {@linkplain HttpRequest request} through one of the request
+ * builder's {@link HttpRequest.Builder#header(String, String) headers} methods.
*
* <p> The methods of this class ( that accept a String header name ), and the
- * Map returned by the {@link #map() map} method, operate without regard to
- * case when retrieving the header value.
+ * {@code Map} returned by the {@link #map() map} method, operate without regard
+ * to case when retrieving the header value(s).
+ *
+ * <p> An HTTP header name may appear more than once in the HTTP protocol. As
+ * such, headers are represented as a name and a list of values. Each occurrence
+ * of a header value is added verbatim, to the appropriate header name list,
+ * without interpreting its value. In particular, {@code HttpHeaders} does not
+ * perform any splitting or joining of comma separated header value strings. The
+ * order of elements in a header value list is preserved when {@link
+ * HttpRequest.Builder#header(String, String) building} a request. For
+ * responses, the order of elements in a header value list is the order in which
+ * they were received. The {@code Map} returned by the {@code map} method,
+ * however, does not provide any guarantee with regard to the ordering of its
+ * entries.
*
* <p> {@code HttpHeaders} instances are immutable.
*
* @since 11
*/
-public abstract class HttpHeaders {
-
- /**
- * Creates an HttpHeaders.
- */
- protected HttpHeaders() {}
+public final class HttpHeaders {
/**
- * Returns an {@link Optional} containing the first value of the given named
- * (and possibly multi-valued) header. If the header is not present, then
- * the returned {@code Optional} is empty.
- *
- * @implSpec
- * The default implementation invokes
- * {@code allValues(name).stream().findFirst()}
+ * Returns an {@link Optional} containing the first header string value of
+ * the given named (and possibly multi-valued) header. If the header is not
+ * present, then the returned {@code Optional} is empty.
*
* @param name the header name
- * @return an {@code Optional<String>} for the first named value
+ * @return an {@code Optional<String>} containing the first named header
+ * string value, if present
*/
public Optional<String> firstValue(String name) {
return allValues(name).stream().findFirst();
}
/**
- * Returns an {@link OptionalLong} containing the first value of the
- * named header field. If the header is not present, then the Optional is
- * empty. If the header is present but contains a value that does not parse
- * as a {@code Long} value, then an exception is thrown.
- *
- * @implSpec
- * The default implementation invokes
- * {@code allValues(name).stream().mapToLong(Long::valueOf).findFirst()}
+ * Returns an {@link OptionalLong} containing the first header string value
+ * of the named header field. If the header is not present, then the
+ * Optional is empty. If the header is present but contains a value that
+ * does not parse as a {@code Long} value, then an exception is thrown.
*
* @param name the header name
* @return an {@code OptionalLong}
@@ -92,23 +98,19 @@
}
/**
- * Returns an unmodifiable List of all of the values of the given named
- * header. Always returns a List, which may be empty if the header is not
- * present.
- *
- * @implSpec
- * The default implementation invokes, among other things, the
- * {@code map().get(name)} to retrieve the list of header values.
+ * Returns an unmodifiable List of all of the header string values of the
+ * given named header. Always returns a List, which may be empty if the
+ * header is not present.
*
* @param name the header name
- * @return a List of String values
+ * @return a List of headers string values
*/
public List<String> allValues(String name) {
requireNonNull(name);
List<String> values = map().get(name);
// Making unmodifiable list out of empty in order to make a list which
// throws UOE unconditionally
- return values != null ? values : unmodifiableList(emptyList());
+ return values != null ? values : List.of();
}
/**
@@ -116,7 +118,9 @@
*
* @return the Map
*/
- public abstract Map<String, List<String>> map();
+ public Map<String,List<String>> map() {
+ return headers;
+ }
/**
* Tests this HTTP headers instance for equality with the given object.
@@ -149,7 +153,11 @@
* @return the hash-code value for this HTTP headers
*/
public final int hashCode() {
- return map().hashCode();
+ int h = 0;
+ for (Map.Entry<String, List<String>> e : map().entrySet()) {
+ h += entryHash(e);
+ }
+ return h;
}
/**
@@ -165,4 +173,93 @@
sb.append(" }");
return sb.toString();
}
+
+ /**
+ * Returns an HTTP headers from the given map. The given map's key
+ * represents the header name, and its value the list of string header
+ * values for that header name.
+ *
+ * <p> An HTTP header name may appear more than once in the HTTP protocol.
+ * Such, <i>multi-valued</i>, headers must be represented by a single entry
+ * in the given map, whose entry value is a list that represents the
+ * multiple header string values. Leading and trailing whitespaces are
+ * removed from all string values retrieved from the given map and its lists
+ * before processing. Only headers that, after filtering, contain at least
+ * one, possibly empty string, value will be added to the HTTP headers.
+ *
+ * @apiNote The primary purpose of this method is for testing frameworks.
+ * Per-request headers can be set through one of the {@code HttpRequest}
+ * {@link HttpRequest.Builder#header(String, String) headers} methods.
+ *
+ * @param headerMap the map containing the header names and values
+ * @param filter a filter that can be used to inspect each
+ * header-name-and-value pair in the given map to determine if
+ * it should, or should not, be added to the to the HTTP
+ * headers
+ * @return an HTTP headers instance containing the given headers
+ * @throws NullPointerException if any of: {@code headerMap}, a key or value
+ * in the given map, or an entry in the map's value list, or
+ * {@code filter}, is {@code null}
+ * @throws IllegalArgumentException if the given {@code headerMap} contains
+ * any two keys that are equal ( without regard to case ); or if the
+ * given map contains any key whose length, after trimming
+ * whitespaces, is {@code 0}
+ */
+ public static HttpHeaders of(Map<String,List<String>> headerMap,
+ BiPredicate<String,String> filter) {
+ requireNonNull(headerMap);
+ requireNonNull(filter);
+ return headersOf(headerMap, filter);
+ }
+
+ // --
+
+ private static final HttpHeaders NO_HEADERS = new HttpHeaders(Map.of());
+
+ private final Map<String,List<String>> headers;
+
+ private HttpHeaders(Map<String,List<String>> headers) {
+ this.headers = headers;
+ }
+
+ private static final int entryHash(Map.Entry<String, List<String>> e) {
+ String key = e.getKey();
+ List<String> value = e.getValue();
+ // we know that by construction key and values can't be null
+ int keyHash = key.toLowerCase(Locale.ROOT).hashCode();
+ int valueHash = value.hashCode();
+ return keyHash ^ valueHash;
+ }
+
+ // Returns a new HTTP headers after performing a structural copy and filtering.
+ private static HttpHeaders headersOf(Map<String,List<String>> map,
+ BiPredicate<String,String> filter) {
+ TreeMap<String,List<String>> other = new TreeMap<>(CASE_INSENSITIVE_ORDER);
+ TreeSet<String> notAdded = new TreeSet<>(CASE_INSENSITIVE_ORDER);
+ ArrayList<String> tempList = new ArrayList<>();
+ map.forEach((key, value) -> {
+ String headerName = requireNonNull(key).trim();
+ if (headerName.isEmpty()) {
+ throw new IllegalArgumentException("empty key");
+ }
+ List<String> headerValues = requireNonNull(value);
+ headerValues.forEach(headerValue -> {
+ headerValue = requireNonNull(headerValue).trim();
+ if (filter.test(headerName, headerValue)) {
+ tempList.add(headerValue);
+ }
+ });
+
+ if (tempList.isEmpty()) {
+ if (other.containsKey(headerName)
+ || notAdded.contains(headerName.toLowerCase(Locale.ROOT)))
+ throw new IllegalArgumentException("duplicate key: " + headerName);
+ notAdded.add(headerName.toLowerCase(Locale.ROOT));
+ } else if (other.put(headerName, List.copyOf(tempList)) != null) {
+ throw new IllegalArgumentException("duplicate key: " + headerName);
+ }
+ tempList.clear();
+ });
+ return other.isEmpty() ? NO_HEADERS : new HttpHeaders(unmodifiableMap(other));
+ }
}
--- a/src/java.net.http/share/classes/java/net/http/HttpRequest.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/java/net/http/HttpRequest.java Wed Jun 20 09:05:57 2018 -0700
@@ -89,10 +89,12 @@
* <p> Instances of {@code HttpRequest.Builder} are created by calling {@link
* HttpRequest#newBuilder(URI)} or {@link HttpRequest#newBuilder()}.
*
- * <p> Each of the setter methods modifies the state of the builder
- * and returns the same instance. The methods are not synchronized and
- * should not be called from multiple threads without external
- * synchronization. The {@link #build() build} method returns a new
+ * <p> The builder can be used to configure per-request state, such as: the
+ * request URI, the request method (default is GET unless explicitly set),
+ * specific request headers, etc. Each of the setter methods modifies the
+ * state of the builder and returns the same instance. The methods are not
+ * synchronized and should not be called from multiple threads without
+ * external synchronization. The {@link #build() build} method returns a new
* {@code HttpRequest} each time it is invoked. Once built an {@code
* HttpRequest} is immutable, and can be sent multiple times.
*
--- a/src/java.net.http/share/classes/java/net/http/HttpResponse.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/java/net/http/HttpResponse.java Wed Jun 20 09:05:57 2018 -0700
@@ -830,13 +830,13 @@
* BodySubscriber} provides implementations of many common body subscribers.
*
* <p> The object acts as a {@link Flow.Subscriber}<{@link List}<{@link
- * ByteBuffer}>> to the HTTP client implementation, which publishes
- * unmodifiable lists of read-only ByteBuffers containing the response body.
- * The Flow of data, as well as the order of ByteBuffers in the Flow lists,
- * is a strictly ordered representation of the response body. Both the Lists
- * and the ByteBuffers, once passed to the subscriber, are no longer used by
- * the HTTP client. The subscriber converts the incoming buffers of data to
- * some higher-level Java type {@code T}.
+ * ByteBuffer}>> to the HTTP Client implementation, which publishes
+ * lists of ByteBuffers containing the response body. The Flow of data, as
+ * well as the order of ByteBuffers in the Flow lists, is a strictly ordered
+ * representation of the response body. Both the Lists and the ByteBuffers,
+ * once passed to the subscriber, are no longer used by the HTTP Client. The
+ * subscriber converts the incoming buffers of data to some higher-level
+ * Java type {@code T}.
*
* <p> The {@link #getBody()} method returns a
* {@link CompletionStage}<{@code T}> that provides the response body
@@ -859,6 +859,9 @@
* may cause the underlying HTTP connection to be closed and prevent it
* from being reused for subsequent operations.
*
+ * @implNote The flow of data containing the response body is immutable.
+ * Specifically, it is a flow of unmodifiable lists of read-only ByteBuffers.
+ *
* @param <T> the response body type
* @see BodySubscribers
* @since 11
@@ -888,20 +891,20 @@
*
* <pre>{@code // Streams the response body to a File
* HttpResponse<byte[]> response = client
- * .send(request, (statusCode, responseHeaders) -> BodySubscribers.ofByteArray());
+ * .send(request, responseInfo -> BodySubscribers.ofByteArray());
*
* // Accumulates the response body and returns it as a byte[]
* HttpResponse<byte[]> response = client
- * .send(request, (statusCode, responseHeaders) -> BodySubscribers.ofByteArray());
+ * .send(request, responseInfo -> BodySubscribers.ofByteArray());
*
* // Discards the response body
* HttpResponse<Void> response = client
- * .send(request, (statusCode, responseHeaders) -> BodySubscribers.discarding());
+ * .send(request, responseInfo -> BodySubscribers.discarding());
*
* // Accumulates the response body as a String then maps it to its bytes
* HttpResponse<byte[]> response = client
- * .send(request, (sc, hdrs) ->
- * BodySubscribers.mapping(BodySubscribers.ofString(), String::getBytes));
+ * .send(request, responseInfo ->
+ * BodySubscribers.mapping(BodySubscribers.ofString(UTF_8), String::getBytes));
* }</pre>
*
* @since 11
--- a/src/java.net.http/share/classes/java/net/http/WebSocket.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/java/net/http/WebSocket.java Wed Jun 20 09:05:57 2018 -0700
@@ -35,9 +35,9 @@
/**
* A WebSocket Client.
*
- * <p> {@code WebSocket} instances can be created via {@link WebSocket.Builder}.
+ * <p> {@code WebSocket} instances are created through {@link WebSocket.Builder}.
*
- * <p> WebSocket has an input and an output sides. These sides are independent
+ * <p> WebSocket has an input and an output side. These sides are independent
* from each other. A side can either be open or closed. Once closed, the side
* remains closed. WebSocket messages are sent through a {@code WebSocket} and
* received through a {@code WebSocket.Listener} associated with it. Messages
@@ -55,21 +55,22 @@
*
* <p> A receive method is any of the {@code onText}, {@code onBinary},
* {@code onPing}, {@code onPong} and {@code onClose} methods of
- * {@code Listener}. A receive method initiates a receive operation and returns
- * a {@code CompletionStage} which completes once the operation has completed.
+ * {@code Listener}. WebSocket initiates a receive operation by invoking a
+ * receive method on the listener. The listener then must return a
+ * {@code CompletionStage} which completes once the operation has completed.
*
- * <p> A WebSocket maintains an <a id="counter">internal counter</a>.
- * This counter's value is a number of times the WebSocket has yet to invoke a
- * receive method. While this counter is zero the WebSocket does not invoke
- * receive methods. The counter is incremented by {@code n} when {@code
- * request(n)} is called. The counter is decremented by one when the WebSocket
- * invokes a receive method. {@code onOpen} and {@code onError} are not receive
- * methods. WebSocket invokes {@code onOpen} prior to any other methods on the
- * listener. WebSocket invokes {@code onOpen} at most once. WebSocket may invoke
- * {@code onError} at any given time. If the WebSocket invokes {@code onError}
- * or {@code onClose}, then no further listener's methods will be invoked, no
- * matter the value of the counter. For a newly built WebSocket the counter is
- * zero. A WebSocket invokes methods on the listener in a thread-safe manner.
+ * <p> To control receiving of messages, a WebSocket maintains an
+ * <a id="counter">internal counter</a>. This counter's value is a number of
+ * times the WebSocket has yet to invoke a receive method. While this counter is
+ * zero the WebSocket does not invoke receive methods. The counter is
+ * incremented by {@code n} when {@code request(n)} is called. The counter is
+ * decremented by one when the WebSocket invokes a receive method.
+ * {@code onOpen} and {@code onError} are not receive methods. WebSocket invokes
+ * {@code onOpen} prior to any other methods on the listener. WebSocket invokes
+ * {@code onOpen} at most once. WebSocket may invoke {@code onError} at any
+ * given time. If the WebSocket invokes {@code onError} or {@code onClose}, then
+ * no further listener's methods will be invoked, no matter the value of the
+ * counter. For a newly built WebSocket the counter is zero.
*
* <p> Unless otherwise stated, {@code null} arguments will cause methods
* of {@code WebSocket} to throw {@code NullPointerException}, similarly,
@@ -105,13 +106,13 @@
/**
* A builder of {@linkplain WebSocket WebSocket Clients}.
*
- * <p> A builder can be created by invoking the
- * {@link HttpClient#newWebSocketBuilder HttpClient.newWebSocketBuilder}
- * method. The intermediate (setter-like) methods change the state of the
- * builder and return the same builder they have been invoked on. If an
- * intermediate method is not invoked, an appropriate default value (or
- * behavior) will be assumed. A {@code Builder} is not safe for use by
- * multiple threads without external synchronization.
+ * <p> Builders are created by invoking
+ * {@link HttpClient#newWebSocketBuilder HttpClient.newWebSocketBuilder}.
+ * The intermediate (setter-like) methods change the state of the builder
+ * and return the same builder they have been invoked on. If an intermediate
+ * method is not invoked, an appropriate default value (or behavior) will be
+ * assumed. A {@code Builder} is not safe for use by multiple threads
+ * without external synchronization.
*
* @since 11
*/
@@ -155,7 +156,7 @@
* Sets a request for the given subprotocols.
*
* <p> After the {@code WebSocket} has been built, the actual
- * subprotocol can be queried via
+ * subprotocol can be queried through
* {@link WebSocket#getSubprotocol WebSocket.getSubprotocol()}.
*
* <p> Subprotocols are specified in the order of preference. The most
@@ -218,11 +219,17 @@
* The receiving interface of {@code WebSocket}.
*
* <p> A {@code WebSocket} invokes methods of the associated listener
- * passing itself as an argument. When data has been received, the
- * {@code WebSocket} invokes a receive method. Methods {@code onText},
- * {@code onBinary}, {@code onPing} and {@code onPong} must return a
- * {@code CompletionStage} that completes once the message has been received
- * by the listener.
+ * passing itself as an argument. These methods are invoked in a thread-safe
+ * manner, such that the next invocation may start only after the previous
+ * one has finished.
+ *
+ * <p> When data has been received, the {@code WebSocket} invokes a receive
+ * method. Methods {@code onText}, {@code onBinary}, {@code onPing} and
+ * {@code onPong} must return a {@code CompletionStage} that completes once
+ * the message has been received by the listener. If a listener's method
+ * returns {@code null} rather than a {@code CompletionStage},
+ * {@code WebSocket} will behave as if the listener returned a
+ * {@code CompletionStage} that is already completed normally.
*
* <p> An {@code IOException} raised in {@code WebSocket} will result in an
* invocation of {@code onError} with that exception (if the input is not
@@ -231,11 +238,14 @@
* exceptionally, the WebSocket will invoke {@code onError} with this
* exception.
*
- * <p> If a listener's method returns {@code null} rather than a
- * {@code CompletionStage}, {@code WebSocket} will behave as if the listener
- * returned a {@code CompletionStage} that is already completed normally.
+ * @apiNote The strict sequential order of invocations from
+ * {@code WebSocket} to {@code Listener} means, in particular, that the
+ * {@code Listener}'s methods are treated as non-reentrant. This means that
+ * {@code Listener} implementations do not need to be concerned with
+ * possible recursion or the order in which they invoke
+ * {@code WebSocket.request} in relation to their processing logic.
*
- * @apiNote Careful attention may be required if a listener is associated
+ * <p> Careful attention may be required if a listener is associated
* with more than a single {@code WebSocket}. In this case invocations
* related to different instances of {@code WebSocket} may not be ordered
* and may even happen concurrently.
--- a/src/java.net.http/share/classes/java/net/http/WebSocketHandshakeException.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/java/net/http/WebSocketHandshakeException.java Wed Jun 20 09:05:57 2018 -0700
@@ -28,7 +28,7 @@
import java.io.IOException;
/**
- * An exception used to signal the opening handshake failed.
+ * Thrown when the opening handshake has failed.
*
* @since 11
*/
@@ -55,6 +55,10 @@
* <p> The value may be unavailable ({@code null}) if this exception has
* been serialized and then deserialized.
*
+ * @apiNote The primary purpose of this method is to allow programmatic
+ * examination of the reasons behind the failure of the opening handshake.
+ * Some of these reasons might allow recovery.
+ *
* @return server response
*/
public HttpResponse<?> getResponse() {
--- a/src/java.net.http/share/classes/java/net/http/package-info.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/java/net/http/package-info.java Wed Jun 20 09:05:57 2018 -0700
@@ -42,20 +42,24 @@
* Hypertext Transfer Protocol (HTTP/1.1)</a>, and
* <a href="https://tools.ietf.org/html/rfc6455">The WebSocket Protocol</a>.
*
- * <p> Asynchronous tasks and dependent actions of returned {@link
- * java.util.concurrent.CompletableFuture} instances are executed on the threads
- * supplied by the client's {@link java.util.concurrent.Executor}, where
- * practical.
+ * <p> In general, asynchronous tasks execute in either the thread invoking
+ * the operation, e.g. {@linkplain HttpClient#send(HttpRequest, BodyHandler)
+ * sending} an HTTP request, or by the threads supplied by the client's {@link
+ * HttpClient#executor() executor}. Dependent tasks, those that are triggered by
+ * returned CompletionStages or CompletableFutures, that do not explicitly
+ * specify an executor, execute in the same {@link
+ * CompletableFuture#defaultExecutor() default executor} as that of {@code
+ * CompletableFuture}, or the invoking thread if the operation completes before
+ * the dependent task is registered.
*
* <p> {@code CompletableFuture}s returned by this API will throw {@link
- * java.lang.UnsupportedOperationException} for their {@link
- * java.util.concurrent.CompletableFuture#obtrudeValue(Object) obtrudeValue}
- * and {@link java.util.concurrent.CompletableFuture#obtrudeException(Throwable)
- * obtrudeException} methods. Invoking the {@link
- * java.util.concurrent.CompletableFuture#cancel cancel} method on a {@code
- * CompletableFuture} returned by this API will not interrupt the underlying
- * operation, but may be useful to complete, exceptionally, dependent stages
- * that have not already completed.
+ * UnsupportedOperationException} for their {@link
+ * CompletableFuture#obtrudeValue(Object) obtrudeValue}
+ * and {@link CompletableFuture#obtrudeException(Throwable)
+ * obtrudeException} methods. Invoking the {@link CompletableFuture#cancel
+ * cancel} method on a {@code CompletableFuture} returned by this API may not
+ * interrupt the underlying operation, but may be useful to complete,
+ * exceptionally, dependent stages that have not already completed.
*
* <p> Unless otherwise stated, {@code null} parameter values will cause methods
* of all classes in this package to throw {@code NullPointerException}.
@@ -63,3 +67,9 @@
* @since 11
*/
package java.net.http;
+
+import java.lang.UnsupportedOperationException;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse.BodyHandler;
+import java.util.concurrent.CompletableFuture;
--- a/src/java.net.http/share/classes/jdk/internal/net/http/AuthenticationFilter.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/AuthenticationFilter.java Wed Jun 20 09:05:57 2018 -0700
@@ -34,7 +34,6 @@
import java.net.URL;
import java.util.Base64;
import java.util.LinkedList;
-import java.util.List;
import java.util.Objects;
import java.util.WeakHashMap;
import java.net.http.HttpHeaders;
@@ -59,9 +58,9 @@
static final int UNAUTHORIZED = 401;
static final int PROXY_UNAUTHORIZED = 407;
- private static final List<String> BASIC_DUMMY =
- List.of("Basic " + Base64.getEncoder()
- .encodeToString("o:o".getBytes(ISO_8859_1)));
+ private static final String BASIC_DUMMY =
+ "Basic " + Base64.getEncoder()
+ .encodeToString("o:o".getBytes(ISO_8859_1));
// A public no-arg constructor is required by FilterFactory
public AuthenticationFilter() {}
@@ -182,14 +181,12 @@
String value = "Basic " + s;
if (proxy) {
if (r.isConnect()) {
- if (!Utils.PROXY_TUNNEL_FILTER
- .test(hdrname, List.of(value))) {
+ if (!Utils.PROXY_TUNNEL_FILTER.test(hdrname, value)) {
Log.logError("{0} disabled", hdrname);
return;
}
} else if (r.proxy() != null) {
- if (!Utils.PROXY_FILTER
- .test(hdrname, List.of(value))) {
+ if (!Utils.PROXY_FILTER.test(hdrname, value)) {
Log.logError("{0} disabled", hdrname);
return;
}
@@ -261,9 +258,16 @@
boolean proxy = status == PROXY_UNAUTHORIZED;
String authname = proxy ? "Proxy-Authenticate" : "WWW-Authenticate";
- String authval = hdrs.firstValue(authname).orElseThrow(() -> {
- return new IOException("Invalid auth header");
- });
+ String authval = hdrs.firstValue(authname).orElse(null);
+ if (authval == null) {
+ if (exchange.client().authenticator().isPresent()) {
+ throw new IOException(authname + " header missing for response code " + status);
+ } else {
+ // No authenticator? let the caller deal with this.
+ return null;
+ }
+ }
+
HeaderParser parser = new HeaderParser(authval);
String scheme = parser.findKey(0);
--- a/src/java.net.http/share/classes/jdk/internal/net/http/ConnectionPool.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/ConnectionPool.java Wed Jun 20 09:05:57 2018 -0700
@@ -53,6 +53,8 @@
static final long KEEP_ALIVE = Utils.getIntegerNetProperty(
"jdk.httpclient.keepalive.timeout", 1200); // seconds
+ static final long MAX_POOL_SIZE = Utils.getIntegerNetProperty(
+ "jdk.httpclient.connectionPoolSize", 0); // unbounded
final Logger debug = Utils.getDebugLogger(this::dbgString, Utils.DEBUG);
// Pools of idle connections
@@ -160,6 +162,7 @@
CleanupTrigger cleanup = registerCleanupTrigger(conn);
// it's possible that cleanup may have been called.
+ HttpConnection toClose = null;
synchronized(this) {
if (cleanup.isDone()) {
return;
@@ -167,6 +170,10 @@
conn.close();
return;
}
+ if (MAX_POOL_SIZE > 0 && expiryList.size() >= MAX_POOL_SIZE) {
+ toClose = expiryList.removeOldest();
+ if (toClose != null) removeFromPool(toClose);
+ }
if (conn instanceof PlainHttpConnection) {
putConnection(conn, plainPool);
} else {
@@ -175,6 +182,13 @@
}
expiryList.add(conn, now, keepAlive);
}
+ if (toClose != null) {
+ if (debug.on()) {
+ debug.log("Maximum pool size reached: removing oldest connection %s",
+ toClose.dbgString());
+ }
+ close(toClose);
+ }
//System.out.println("Return to pool: " + conn);
}
@@ -314,6 +328,8 @@
private final LinkedList<ExpiryEntry> list = new LinkedList<>();
private volatile boolean mayContainEntries;
+ int size() { return list.size(); }
+
// A loosely accurate boolean whose value is computed
// at the end of each operation performed on ExpiryList;
// Does not require synchronizing on the ConnectionPool.
@@ -331,6 +347,13 @@
// should only be called while holding a synchronization
// lock on the ConnectionPool
+ HttpConnection removeOldest() {
+ ExpiryEntry entry = list.pollLast();
+ return entry == null ? null : entry.connection;
+ }
+
+ // should only be called while holding a synchronization
+ // lock on the ConnectionPool
void add(HttpConnection conn) {
add(conn, Instant.now(), KEEP_ALIVE);
}
@@ -419,17 +442,38 @@
}
}
+ // Remove a connection from the pool.
+ // should only be called while holding a synchronization
+ // lock on the ConnectionPool
+ private void removeFromPool(HttpConnection c) {
+ assert Thread.holdsLock(this);
+ if (c instanceof PlainHttpConnection) {
+ removeFromPool(c, plainPool);
+ } else {
+ assert c.isSecure();
+ removeFromPool(c, sslPool);
+ }
+ }
+
+ // Used by tests
+ synchronized boolean contains(HttpConnection c) {
+ final CacheKey key = c.cacheKey();
+ List<HttpConnection> list;
+ if ((list = plainPool.get(key)) != null) {
+ if (list.contains(c)) return true;
+ }
+ if ((list = sslPool.get(key)) != null) {
+ if (list.contains(c)) return true;
+ }
+ return false;
+ }
+
void cleanup(HttpConnection c, Throwable error) {
if (debug.on())
debug.log("%s : ConnectionPool.cleanup(%s)",
String.valueOf(c.getConnectionFlow()), error);
synchronized(this) {
- if (c instanceof PlainHttpConnection) {
- removeFromPool(c, plainPool);
- } else {
- assert c.isSecure();
- removeFromPool(c, sslPool);
- }
+ removeFromPool(c);
expiryList.remove(c);
}
c.close();
--- a/src/java.net.http/share/classes/jdk/internal/net/http/CookieFilter.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/CookieFilter.java Wed Jun 20 09:05:57 2018 -0700
@@ -31,7 +31,7 @@
import java.util.Map;
import java.util.Optional;
import java.net.http.HttpHeaders;
-import jdk.internal.net.http.common.HttpHeadersImpl;
+import jdk.internal.net.http.common.HttpHeadersBuilder;
import jdk.internal.net.http.common.Log;
import jdk.internal.net.http.common.Utils;
@@ -50,7 +50,7 @@
Map<String,List<String>> cookies = cookieHandler.get(r.uri(), userheaders);
// add the returned cookies
- HttpHeadersImpl systemHeaders = r.getSystemHeaders();
+ HttpHeadersBuilder systemHeadersBuilder = r.getSystemHeadersBuilder();
if (cookies.isEmpty()) {
Log.logTrace("Request: no cookie to add for {0}", r.uri());
} else {
@@ -65,7 +65,7 @@
if (values == null || values.isEmpty()) continue;
for (String val : values) {
if (Utils.isValidValue(val)) {
- systemHeaders.addHeader(hdrname, val);
+ systemHeadersBuilder.addHeader(hdrname, val);
}
}
}
--- a/src/java.net.http/share/classes/jdk/internal/net/http/Exchange.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/Exchange.java Wed Jun 20 09:05:57 2018 -0700
@@ -307,6 +307,7 @@
Function<ExchangeImpl<T>,CompletableFuture<Response>> andThen) {
t = Utils.getCompletionCause(t);
if (t instanceof ProxyAuthenticationRequired) {
+ if (debug.on()) debug.log("checkFor407: ProxyAuthenticationRequired: building synthetic response");
bodyIgnored = MinimalFuture.completedFuture(null);
Response proxyResponse = ((ProxyAuthenticationRequired)t).proxyResponse;
HttpConnection c = ex == null ? null : ex.connection();
@@ -315,8 +316,10 @@
proxyResponse.version, true);
return MinimalFuture.completedFuture(syntheticResponse);
} else if (t != null) {
+ if (debug.on()) debug.log("checkFor407: no response - %s", t);
return MinimalFuture.failedFuture(t);
} else {
+ if (debug.on()) debug.log("checkFor407: all clear");
return andThen.apply(ex);
}
}
@@ -332,6 +335,7 @@
int rcode = r1.statusCode();
if (rcode == 100) {
Log.logTrace("Received 100-Continue: sending body");
+ if (debug.on()) debug.log("Received 100-Continue for %s", r1);
CompletableFuture<Response> cf =
exchImpl.sendBodyAsync()
.thenCompose(exIm -> exIm.getResponseAsync(parentExecutor));
@@ -341,6 +345,7 @@
} else {
Log.logTrace("Expectation failed: Received {0}",
rcode);
+ if (debug.on()) debug.log("Expect-Continue failed (%d) for: %s", rcode, r1);
if (upgrading && rcode == 101) {
IOException failed = new IOException(
"Unable to handle 101 while waiting for 100");
@@ -357,6 +362,7 @@
// send the request body and proceed.
private CompletableFuture<Response> sendRequestBody(ExchangeImpl<T> ex) {
assert !request.expectContinue();
+ if (debug.on()) debug.log("sendRequestBody");
CompletableFuture<Response> cf = ex.sendBodyAsync()
.thenCompose(exIm -> exIm.getResponseAsync(parentExecutor));
cf = wrapForUpgrade(cf);
--- a/src/java.net.http/share/classes/jdk/internal/net/http/ExchangeImpl.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/ExchangeImpl.java Wed Jun 20 09:05:57 2018 -0700
@@ -32,8 +32,8 @@
import java.net.http.HttpResponse;
import jdk.internal.net.http.common.Logger;
import jdk.internal.net.http.common.MinimalFuture;
+import jdk.internal.net.http.common.Utils;
import static java.net.http.HttpClient.Version.HTTP_1_1;
-import jdk.internal.net.http.common.Utils;
/**
* Splits request so that headers and body can be sent separately with optional
--- a/src/java.net.http/share/classes/jdk/internal/net/http/Http1AsyncReceiver.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http1AsyncReceiver.java Wed Jun 20 09:05:57 2018 -0700
@@ -27,7 +27,6 @@
import java.io.EOFException;
import java.io.IOException;
-import java.lang.System.Logger.Level;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.HashSet;
@@ -42,7 +41,9 @@
import java.util.function.Consumer;
import jdk.internal.net.http.common.Demand;
import jdk.internal.net.http.common.FlowTube.TubeSubscriber;
+import jdk.internal.net.http.common.Log;
import jdk.internal.net.http.common.Logger;
+import jdk.internal.net.http.common.MinimalFuture;
import jdk.internal.net.http.common.SequentialScheduler;
import jdk.internal.net.http.common.ConnectionExpiredException;
import jdk.internal.net.http.common.Utils;
@@ -166,6 +167,7 @@
= new ConcurrentLinkedDeque<>();
private final SequentialScheduler scheduler =
SequentialScheduler.synchronizedScheduler(this::flush);
+ final MinimalFuture<Void> whenFinished;
private final Executor executor;
private final Http1TubeSubscriber subscriber = new Http1TubeSubscriber();
private final AtomicReference<Http1AsyncDelegate> pendingDelegateRef;
@@ -184,6 +186,7 @@
public Http1AsyncReceiver(Executor executor, Http1Exchange<?> owner) {
this.pendingDelegateRef = new AtomicReference<>();
this.executor = executor;
+ this.whenFinished = new MinimalFuture<>();
this.owner = owner;
this.client = owner.client;
}
@@ -261,6 +264,14 @@
}
}
+ private String describe() {
+ Http1Exchange<?> exchange = owner;
+ if (exchange != null) {
+ return String.valueOf(exchange.request());
+ }
+ return "<uri unavailable>";
+ }
+
/**
* Must be called from within the scheduler main loop.
* Handles any pending errors by calling delegate.onReadError().
@@ -284,6 +295,10 @@
+ "\t\t queue.isEmpty: " + queue.isEmpty());
scheduler.stop();
delegate.onReadError(x);
+ whenFinished.completeExceptionally(x);
+ if (Log.channel()) {
+ Log.logChannel("HTTP/1 read subscriber stopped for: {0}", describe());
+ }
if (stopRequested) {
// This is the special case where the subscriber
// has requested an illegal number of items.
@@ -464,27 +479,35 @@
// throw ConnectionExpiredException
// to try & force a retry of the request.
retry = false;
- ex = new ConnectionExpiredException(
- "subscription is finished", ex);
+ ex = new ConnectionExpiredException(ex);
}
}
error = ex;
}
+ }
final Throwable t = (recorded == null ? ex : recorded);
if (debug.on())
debug.log("recorded " + t + "\n\t delegate: " + delegate
+ "\t\t queue.isEmpty: " + queue.isEmpty(), ex);
+ if (Log.errors()) {
+ Log.logError("HTTP/1 read subscriber recorded error: {0} - {1}", describe(), t);
}
if (queue.isEmpty() || pendingDelegateRef.get() != null || stopRequested) {
// This callback is called from within the selector thread.
// Use an executor here to avoid doing the heavy lifting in the
// selector.
+ if (Log.errors()) {
+ Log.logError("HTTP/1 propagating recorded error: {0} - {1}", describe(), t);
+ }
scheduler.runOrSchedule(executor);
}
}
void stop() {
if (debug.on()) debug.log("stopping");
+ if (Log.channel() && !scheduler.isStopped()) {
+ Log.logChannel("HTTP/1 read subscriber stopped for {0}", describe());
+ }
scheduler.stop();
// make sure ref count is handled properly by
// closing the delegate.
@@ -492,6 +515,7 @@
if (previous != null) previous.close(error);
delegate = null;
owner = null;
+ whenFinished.complete(null);
}
/**
@@ -514,12 +538,18 @@
// supports being called multiple time.
// doesn't cancel the previous subscription, since that is
// most probably the same as the new subscription.
+ if (debug.on()) debug.log("Received onSubscribed from upstream");
+ if (Log.channel()) {
+ Log.logChannel("HTTP/1 read subscriber got subscription from {0}", describe());
+ }
assert this.subscription == null || dropped == false;
this.subscription = subscription;
dropped = false;
canRequestMore.set(true);
if (delegate != null) {
scheduler.runOrSchedule(executor);
+ } else {
+ if (debug.on()) debug.log("onSubscribe: read delegate not present yet");
}
}
--- a/src/java.net.http/share/classes/jdk/internal/net/http/Http1Exchange.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http1Exchange.java Wed Jun 20 09:05:57 2018 -0700
@@ -26,7 +26,6 @@
package jdk.internal.net.http;
import java.io.IOException;
-import java.lang.System.Logger.Level;
import java.net.InetSocketAddress;
import java.net.http.HttpResponse.BodyHandler;
import java.net.http.HttpResponse.BodySubscriber;
@@ -46,6 +45,7 @@
import jdk.internal.net.http.common.MinimalFuture;
import jdk.internal.net.http.common.Utils;
import static java.net.http.HttpClient.Version.HTTP_1_1;
+import static jdk.internal.net.http.common.Utils.wrapWithExtraDetail;
/**
* Encapsulates one HTTP/1.1 request/response exchange.
@@ -134,6 +134,9 @@
subscription.request(n);
}
+ /** A current-state message suitable for inclusion in an exception detail message. */
+ abstract String currentStateMessage();
+
final boolean isSubscribed() {
return subscription != null;
}
@@ -159,6 +162,7 @@
@Override public void onNext(ByteBuffer item) { error(); }
@Override public void onError(Throwable throwable) { error(); }
@Override public void onComplete() { error(); }
+ @Override String currentStateMessage() { return null; }
private void error() {
throw new InternalError("should not reach here");
}
@@ -193,35 +197,6 @@
}
this.requestAction = new Http1Request(request, this);
this.asyncReceiver = new Http1AsyncReceiver(executor, this);
- asyncReceiver.subscribe(new InitialErrorReceiver());
- }
-
- /** An initial receiver that handles no data, but cancels the request if
- * it receives an error. Will be replaced when reading response body. */
- final class InitialErrorReceiver implements Http1AsyncReceiver.Http1AsyncDelegate {
- volatile AbstractSubscription s;
- @Override
- public boolean tryAsyncReceive(ByteBuffer ref) {
- return false; // no data has been processed, leave it in the queue
- }
-
- @Override
- public void onReadError(Throwable ex) {
- cancelImpl(ex);
- }
-
- @Override
- public void onSubscribe(AbstractSubscription s) {
- this.s = s;
- }
-
- @Override
- public AbstractSubscription subscription() {
- return s;
- }
-
- @Override
- public void close(Throwable error) {}
}
@Override
@@ -244,16 +219,16 @@
// create the response before sending the request headers, so that
// the response can set the appropriate receivers.
if (debug.on()) debug.log("Sending headers only");
+ // If the first attempt to read something triggers EOF, or
+ // IOException("channel reset by peer"), we're going to retry.
+ // Instruct the asyncReceiver to throw ConnectionExpiredException
+ // to force a retry.
+ asyncReceiver.setRetryOnError(true);
if (response == null) {
response = new Http1Response<>(connection, this, asyncReceiver);
}
if (debug.on()) debug.log("response created in advance");
- // If the first attempt to read something triggers EOF, or
- // IOException("channel reset by peer"), we're going to retry.
- // Instruct the asyncReceiver to throw ConnectionExpiredException
- // to force a retry.
- asyncReceiver.setRetryOnError(true);
CompletableFuture<Void> connectCF;
if (!connection.connected()) {
@@ -296,6 +271,8 @@
return cf;
} catch (Throwable t) {
if (debug.on()) debug.log("Failed to send headers: %s", t);
+ headersSentCF.completeExceptionally(t);
+ bodySentCF.completeExceptionally(t);
connection.close();
cf.completeExceptionally(t);
return cf;
@@ -303,28 +280,52 @@
.thenCompose(unused -> headersSentCF);
}
+ private void cancelIfFailed(Flow.Subscription s) {
+ asyncReceiver.whenFinished.whenCompleteAsync((r,t) -> {
+ if (debug.on()) debug.log("asyncReceiver finished (failed=%s)", t);
+ if (t != null) {
+ s.cancel();
+ // Don't complete exceptionally here as 't'
+ // might not be the right exception: it will
+ // not have been decorated yet.
+ // t is an exception raised by the read side,
+ // an EOFException or Broken Pipe...
+ // We are cancelling the BodyPublisher subscription
+ // and completing bodySentCF to allow the next step
+ // to flow and call readHeaderAsync, which will
+ // get the right exception from the asyncReceiver.
+ bodySentCF.complete(this);
+ }
+ }, executor);
+ }
+
@Override
CompletableFuture<ExchangeImpl<T>> sendBodyAsync() {
assert headersSentCF.isDone();
+ if (debug.on()) debug.log("sendBodyAsync");
try {
bodySubscriber = requestAction.continueRequest();
+ if (debug.on()) debug.log("bodySubscriber is %s",
+ bodySubscriber == null ? null : bodySubscriber.getClass());
if (bodySubscriber == null) {
bodySubscriber = Http1BodySubscriber.completeSubscriber(debug);
appendToOutgoing(Http1BodySubscriber.COMPLETED);
} else {
// start
bodySubscriber.whenSubscribed
+ .thenAccept((s) -> cancelIfFailed(s))
.thenAccept((s) -> requestMoreBody());
}
} catch (Throwable t) {
cancelImpl(t);
bodySentCF.completeExceptionally(t);
}
- return bodySentCF;
+ return Utils.wrapForDebug(debug, "sendBodyAsync", bodySentCF);
}
@Override
CompletableFuture<Response> getResponseAsync(Executor executor) {
+ if (debug.on()) debug.log("reading headers");
CompletableFuture<Response> cf = response.readHeadersAsync(executor);
Throwable cause;
synchronized (lock) {
@@ -348,7 +349,7 @@
debug.log(acknowledged ? ("completed response with " + cause)
: ("response already completed, ignoring " + cause));
}
- return cf;
+ return Utils.wrapForDebug(debug, "getResponseAsync", cf);
}
@Override
@@ -421,7 +422,6 @@
&& response != null && response.finished()) {
return;
}
- connection.close(); // TODO: ensure non-blocking if holding the lock
writePublisher.writeScheduler.stop();
if (operations.isEmpty()) {
Log.logTrace("Http1Exchange: request [{0}/timeout={1}ms] no pending operation."
@@ -444,27 +444,31 @@
operations.clear();
}
}
- Log.logError("Http1Exchange.cancel: count=" + count);
- if (toComplete != null) {
- // We might be in the selector thread in case of timeout, when
- // the SelectorManager calls purgeTimeoutsAndReturnNextDeadline()
- // There may or may not be other places that reach here
- // from the SelectorManager thread, so just make sure we
- // don't complete any CF from within the selector manager
- // thread.
- Executor exec = client.isSelectorThread()
- ? executor
- : this::runInline;
- Throwable x = error;
- while (!toComplete.isEmpty()) {
- CompletableFuture<?> cf = toComplete.poll();
- exec.execute(() -> {
- if (cf.completeExceptionally(x)) {
- if (debug.on())
- debug.log("%s: completed cf with %s", request.uri(), x);
- }
- });
+ try {
+ Log.logError("Http1Exchange.cancel: count=" + count);
+ if (toComplete != null) {
+ // We might be in the selector thread in case of timeout, when
+ // the SelectorManager calls purgeTimeoutsAndReturnNextDeadline()
+ // There may or may not be other places that reach here
+ // from the SelectorManager thread, so just make sure we
+ // don't complete any CF from within the selector manager
+ // thread.
+ Executor exec = client.isSelectorThread()
+ ? executor
+ : this::runInline;
+ Throwable x = error;
+ while (!toComplete.isEmpty()) {
+ CompletableFuture<?> cf = toComplete.poll();
+ exec.execute(() -> {
+ if (cf.completeExceptionally(x)) {
+ if (debug.on())
+ debug.log("%s: completed cf with %s", request.uri(), x);
+ }
+ });
+ }
}
+ } finally {
+ connection.close();
}
}
@@ -511,7 +515,7 @@
private void requestMoreBody() {
try {
- if (debug.on()) debug.log("requesting more body from the subscriber");
+ if (debug.on()) debug.log("requesting more request body from the subscriber");
bodySubscriber.request(1);
} catch (Throwable t) {
if (debug.on()) debug.log("Subscription::request failed", t);
@@ -527,46 +531,62 @@
final Executor exec = client.theExecutor();
final DataPair dp = outgoing.pollFirst();
+ if (writePublisher.cancelled) {
+ if (debug.on()) debug.log("cancelling upstream publisher");
+ if (bodySubscriber != null) {
+ exec.execute(bodySubscriber::cancelSubscription);
+ } else if (debug.on()) {
+ debug.log("bodySubscriber is null");
+ }
+ headersSentCF.completeAsync(() -> this, exec);
+ bodySentCF.completeAsync(() -> this, exec);
+ return null;
+ }
+
if (dp == null) // publisher has not published anything yet
return null;
- synchronized (lock) {
- if (dp.throwable != null) {
+ if (dp.throwable != null) {
+ synchronized (lock) {
state = State.ERROR;
- exec.execute(() -> {
- headersSentCF.completeExceptionally(dp.throwable);
- bodySentCF.completeExceptionally(dp.throwable);
- connection.close();
- });
- return dp;
}
-
- switch (state) {
- case HEADERS:
- state = State.BODY;
- // completeAsync, since dependent tasks should run in another thread
- if (debug.on()) debug.log("initiating completion of headersSentCF");
- headersSentCF.completeAsync(() -> this, exec);
- break;
- case BODY:
- if (dp.data == Http1BodySubscriber.COMPLETED) {
- state = State.COMPLETING;
- if (debug.on()) debug.log("initiating completion of bodySentCF");
- bodySentCF.completeAsync(() -> this, exec);
- } else {
- exec.execute(this::requestMoreBody);
- }
- break;
- case INITIAL:
- case ERROR:
- case COMPLETING:
- case COMPLETED:
- default:
- assert false : "Unexpected state:" + state;
- }
-
+ exec.execute(() -> {
+ headersSentCF.completeExceptionally(dp.throwable);
+ bodySentCF.completeExceptionally(dp.throwable);
+ connection.close();
+ });
return dp;
}
+
+ switch (state) {
+ case HEADERS:
+ synchronized (lock) {
+ state = State.BODY;
+ }
+ // completeAsync, since dependent tasks should run in another thread
+ if (debug.on()) debug.log("initiating completion of headersSentCF");
+ headersSentCF.completeAsync(() -> this, exec);
+ break;
+ case BODY:
+ if (dp.data == Http1BodySubscriber.COMPLETED) {
+ synchronized (lock) {
+ state = State.COMPLETING;
+ }
+ if (debug.on()) debug.log("initiating completion of bodySentCF");
+ bodySentCF.completeAsync(() -> this, exec);
+ } else {
+ exec.execute(this::requestMoreBody);
+ }
+ break;
+ case INITIAL:
+ case ERROR:
+ case COMPLETING:
+ case COMPLETED:
+ default:
+ assert false : "Unexpected state:" + state;
+ }
+
+ return dp;
}
/** A Publisher of HTTP/1.1 headers and request body. */
@@ -608,6 +628,14 @@
public void run() {
assert state != State.COMPLETED : "Unexpected state:" + state;
if (debug.on()) debug.log("WriteTask");
+
+ if (cancelled) {
+ if (debug.on()) debug.log("handling cancellation");
+ writeScheduler.stop();
+ getOutgoing();
+ return;
+ }
+
if (subscriber == null) {
if (debug.on()) debug.log("no subscriber yet");
return;
@@ -615,6 +643,8 @@
if (debug.on()) debug.log(() -> "hasOutgoing = " + hasOutgoing());
while (hasOutgoing() && demand.tryDecrement()) {
DataPair dp = getOutgoing();
+ if (dp == null)
+ break;
if (dp.throwable != null) {
if (debug.on()) debug.log("onError");
@@ -661,7 +691,7 @@
if (cancelled)
return; //no-op
cancelled = true;
- writeScheduler.stop();
+ writeScheduler.runOrSchedule(client.theExecutor());
}
}
}
--- a/src/java.net.http/share/classes/jdk/internal/net/http/Http1HeaderParser.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http1HeaderParser.java Wed Jun 20 09:05:57 2018 -0700
@@ -35,6 +35,7 @@
import java.net.http.HttpHeaders;
import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
+import static jdk.internal.net.http.common.Utils.ACCEPT_ALL;
class Http1HeaderParser {
@@ -49,10 +50,13 @@
private HttpHeaders headers;
private Map<String,List<String>> privateMap = new HashMap<>();
- enum State { STATUS_LINE,
+ enum State { INITIAL,
+ STATUS_LINE,
STATUS_LINE_FOUND_CR,
+ STATUS_LINE_FOUND_LF,
STATUS_LINE_END,
STATUS_LINE_END_CR,
+ STATUS_LINE_END_LF,
HEADER,
HEADER_FOUND_CR,
HEADER_FOUND_LF,
@@ -60,7 +64,7 @@
HEADER_FOUND_CR_LF_CR,
FINISHED }
- private State state = State.STATUS_LINE;
+ private State state = State.INITIAL;
/** Returns the status-line. */
String statusLine() { return statusLine; }
@@ -69,7 +73,29 @@
int responseCode() { return responseCode; }
/** Returns the headers, possibly empty. */
- HttpHeaders headers() { assert state == State.FINISHED; return headers; }
+ HttpHeaders headers() {
+ assert state == State.FINISHED : "Unexpected state " + state;
+ return headers;
+ }
+
+ /** A current-state message suitable for inclusion in an exception detail message. */
+ public String currentStateMessage() {
+ String stateName = state.name();
+ String msg;
+ if (stateName.contains("INITIAL")) {
+ return format("HTTP/1.1 header parser received no bytes");
+ } else if (stateName.contains("STATUS")) {
+ msg = format("parsing HTTP/1.1 status line, receiving [%s]", sb.toString());
+ } else if (stateName.contains("HEADER")) {
+ String headerName = sb.toString();
+ if (headerName.indexOf(':') != -1)
+ headerName = headerName.substring(0, headerName.indexOf(':')+1) + "...";
+ msg = format("parsing HTTP/1.1 header, receiving [%s]", headerName);
+ } else {
+ msg =format("HTTP/1.1 parser receiving [%s]", state, sb.toString());
+ }
+ return format("%s, parser state [%s]", msg , state);
+ }
/**
* Parses HTTP/1.X status-line and headers from the given bytes. Must be
@@ -84,18 +110,25 @@
boolean parse(ByteBuffer input) throws ProtocolException {
requireNonNull(input, "null input");
- while (input.hasRemaining() && state != State.FINISHED) {
+ while (canContinueParsing(input)) {
switch (state) {
+ case INITIAL:
+ state = State.STATUS_LINE;
+ break;
case STATUS_LINE:
readResumeStatusLine(input);
break;
+ // fallthrough
case STATUS_LINE_FOUND_CR:
+ case STATUS_LINE_FOUND_LF:
readStatusLineFeed(input);
break;
case STATUS_LINE_END:
maybeStartHeaders(input);
break;
+ // fallthrough
case STATUS_LINE_END_CR:
+ case STATUS_LINE_END_LF:
maybeEndHeaders(input);
break;
case HEADER:
@@ -121,21 +154,35 @@
return state == State.FINISHED;
}
+ private boolean canContinueParsing(ByteBuffer buffer) {
+ // some states don't require any input to transition
+ // to the next state.
+ switch (state) {
+ case FINISHED: return false;
+ case STATUS_LINE_FOUND_LF: return true;
+ case STATUS_LINE_END_LF: return true;
+ case HEADER_FOUND_LF: return true;
+ default: return buffer.hasRemaining();
+ }
+ }
+
private void readResumeStatusLine(ByteBuffer input) {
char c = 0;
while (input.hasRemaining() && (c =(char)input.get()) != CR) {
+ if (c == LF) break;
sb.append(c);
}
-
if (c == CR) {
state = State.STATUS_LINE_FOUND_CR;
+ } else if (c == LF) {
+ state = State.STATUS_LINE_FOUND_LF;
}
}
private void readStatusLineFeed(ByteBuffer input) throws ProtocolException {
- char c = (char)input.get();
+ char c = state == State.STATUS_LINE_FOUND_LF ? LF : (char)input.get();
if (c != LF) {
- throw protocolException("Bad trailing char, \"%s\", when parsing status-line, \"%s\"",
+ throw protocolException("Bad trailing char, \"%s\", when parsing status line, \"%s\"",
c, sb.toString());
}
@@ -158,6 +205,8 @@
char c = (char)input.get();
if (c == CR) {
state = State.STATUS_LINE_END_CR;
+ } else if (c == LF) {
+ state = State.STATUS_LINE_END_LF;
} else {
sb.append(c);
state = State.HEADER;
@@ -165,15 +214,15 @@
}
private void maybeEndHeaders(ByteBuffer input) throws ProtocolException {
- assert state == State.STATUS_LINE_END_CR;
+ assert state == State.STATUS_LINE_END_CR || state == State.STATUS_LINE_END_LF;
assert sb.length() == 0;
- char c = (char)input.get();
+ char c = state == State.STATUS_LINE_END_LF ? LF : (char)input.get();
if (c == LF) {
- headers = ImmutableHeaders.of(privateMap);
+ headers = HttpHeaders.of(privateMap, ACCEPT_ALL);
privateMap = null;
state = State.FINISHED; // no headers
} else {
- throw protocolException("Unexpected \"%s\", after status-line CR", c);
+ throw protocolException("Unexpected \"%s\", after status line CR", c);
}
}
@@ -212,8 +261,8 @@
private void resumeOrLF(ByteBuffer input) {
assert state == State.HEADER_FOUND_CR || state == State.HEADER_FOUND_LF;
- char c = (char)input.get();
- if (c == LF && state == State.HEADER_FOUND_CR) {
+ char c = state == State.HEADER_FOUND_LF ? LF : (char)input.get();
+ if (c == LF) {
// header value will be flushed by
// resumeOrSecondCR if next line does not
// begin by SP or HT
@@ -231,7 +280,7 @@
private void resumeOrSecondCR(ByteBuffer input) {
assert state == State.HEADER_FOUND_CR_LF;
char c = (char)input.get();
- if (c == CR) {
+ if (c == CR || c == LF) {
if (sb.length() > 0) {
// no continuation line - flush
// previous header value.
@@ -239,7 +288,13 @@
sb = new StringBuilder();
addHeaderFromString(headerString);
}
- state = State.HEADER_FOUND_CR_LF_CR;
+ if (c == CR) {
+ state = State.HEADER_FOUND_CR_LF_CR;
+ } else {
+ state = State.FINISHED;
+ headers = HttpHeaders.of(privateMap, ACCEPT_ALL);
+ privateMap = null;
+ }
} else if (c == SP || c == HT) {
assert sb.length() != 0;
sb.append(SP); // continuation line
@@ -262,7 +317,7 @@
char c = (char)input.get();
if (c == LF) {
state = State.FINISHED;
- headers = ImmutableHeaders.of(privateMap);
+ headers = HttpHeaders.of(privateMap, ACCEPT_ALL);
privateMap = null;
} else {
throw protocolException("Unexpected \"%s\", after CR LF CR", c);
--- a/src/java.net.http/share/classes/jdk/internal/net/http/Http1Request.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http1Request.java Wed Jun 20 09:05:57 2018 -0700
@@ -26,7 +26,6 @@
package jdk.internal.net.http;
import java.io.IOException;
-import java.lang.System.Logger.Level;
import java.net.URI;
import java.nio.ByteBuffer;
import java.util.ArrayList;
@@ -39,11 +38,12 @@
import java.net.http.HttpHeaders;
import java.net.http.HttpRequest;
import jdk.internal.net.http.Http1Exchange.Http1BodySubscriber;
-import jdk.internal.net.http.common.HttpHeadersImpl;
+import jdk.internal.net.http.common.HttpHeadersBuilder;
import jdk.internal.net.http.common.Log;
import jdk.internal.net.http.common.Logger;
import jdk.internal.net.http.common.Utils;
+import static java.lang.String.format;
import static java.nio.charset.StandardCharsets.US_ASCII;
/**
@@ -52,7 +52,7 @@
class Http1Request {
private static final String COOKIE_HEADER = "Cookie";
- private static final BiPredicate<String,List<String>> NOCOOKIES =
+ private static final BiPredicate<String,String> NOCOOKIES =
(k,v) -> !COOKIE_HEADER.equalsIgnoreCase(k);
private final HttpRequestImpl request;
@@ -60,7 +60,7 @@
private final HttpConnection connection;
private final HttpRequest.BodyPublisher requestPublisher;
private final HttpHeaders userHeaders;
- private final HttpHeadersImpl systemHeaders;
+ private final HttpHeadersBuilder systemHeadersBuilder;
private volatile boolean streaming;
private volatile long contentLength;
@@ -73,7 +73,7 @@
this.connection = http1Exchange.connection();
this.requestPublisher = request.requestPublisher; // may be null
this.userHeaders = request.getUserHeaders();
- this.systemHeaders = request.getSystemHeaders();
+ this.systemHeadersBuilder = request.getSystemHeadersBuilder();
}
private void logHeaders(String completeHeaders) {
@@ -92,12 +92,13 @@
private void collectHeaders0(StringBuilder sb) {
- BiPredicate<String,List<String>> filter =
+ BiPredicate<String,String> filter =
connection.headerFilter(request);
// Filter out 'Cookie:' headers, we will collect them at the end.
- BiPredicate<String,List<String>> nocookies =
- NOCOOKIES.and(filter);
+ BiPredicate<String,String> nocookies = NOCOOKIES.and(filter);
+
+ HttpHeaders systemHeaders = systemHeadersBuilder.build();
// If we're sending this request through a tunnel,
// then don't send any preemptive proxy-* headers that
@@ -112,8 +113,7 @@
// Gather all 'Cookie:' headers and concatenate their
// values in a single line.
- collectCookies(sb, COOKIE_HEADER,
- systemHeaders, userHeaders, filter);
+ collectCookies(sb, systemHeaders, userHeaders);
// terminate headers
sb.append('\r').append('\n');
@@ -142,20 +142,16 @@
// any illegal character for header field values.
//
private void collectCookies(StringBuilder sb,
- String key,
HttpHeaders system,
- HttpHeaders user,
- BiPredicate<String, List<String>> filter) {
- List<String> systemList = system.allValues(key);
- if (systemList != null && !filter.test(key, systemList)) systemList = null;
- List<String> userList = user.allValues(key);
- if (userList != null && !filter.test(key, userList)) userList = null;
+ HttpHeaders user) {
+ List<String> systemList = system.allValues(COOKIE_HEADER);
+ List<String> userList = user.allValues(COOKIE_HEADER);
boolean found = false;
if (systemList != null) {
for (String cookie : systemList) {
if (!found) {
found = true;
- sb.append(key).append(':').append(' ');
+ sb.append(COOKIE_HEADER).append(':').append(' ');
} else {
sb.append(';').append(' ');
}
@@ -166,7 +162,7 @@
for (String cookie : userList) {
if (!found) {
found = true;
- sb.append(key).append(':').append(' ');
+ sb.append(COOKIE_HEADER).append(':').append(' ');
} else {
sb.append(';').append(' ');
}
@@ -176,13 +172,15 @@
if (found) sb.append('\r').append('\n');
}
- private void collectHeaders1(StringBuilder sb, HttpHeaders headers,
- BiPredicate<String, List<String>> filter) {
+ private void collectHeaders1(StringBuilder sb,
+ HttpHeaders headers,
+ BiPredicate<String,String> filter) {
for (Map.Entry<String,List<String>> entry : headers.map().entrySet()) {
String key = entry.getKey();
List<String> values = entry.getValue();
- if (!filter.test(key, values)) continue;
for (String value : values) {
+ if (!filter.test(key, value))
+ continue;
sb.append(key).append(':').append(' ')
.append(value)
.append('\r').append('\n');
@@ -279,7 +277,7 @@
URI uri = request.uri();
if (uri != null) {
- systemHeaders.setHeader("Host", hostString());
+ systemHeadersBuilder.setHeader("Host", hostString());
}
if (requestPublisher == null) {
// Not a user request, or maybe a method, e.g. GET, with no body.
@@ -289,13 +287,13 @@
}
if (contentLength == 0) {
- systemHeaders.setHeader("Content-Length", "0");
+ systemHeadersBuilder.setHeader("Content-Length", "0");
} else if (contentLength > 0) {
- systemHeaders.setHeader("Content-Length", Long.toString(contentLength));
+ systemHeadersBuilder.setHeader("Content-Length", Long.toString(contentLength));
streaming = false;
} else {
streaming = true;
- systemHeaders.setHeader("Transfer-encoding", "chunked");
+ systemHeadersBuilder.setHeader("Transfer-encoding", "chunked");
}
collectHeaders0(sb);
String hs = sb.toString();
@@ -350,6 +348,11 @@
}
@Override
+ public String currentStateMessage() {
+ return "streaming request body " + (complete ? "complete" : "incomplete");
+ }
+
+ @Override
public void onError(Throwable throwable) {
if (complete)
return;
@@ -417,6 +420,12 @@
}
@Override
+ public String currentStateMessage() {
+ return format("fixed content-length: %d, bytes sent: %d",
+ contentLength, contentWritten);
+ }
+
+ @Override
public void onError(Throwable throwable) {
if (debug.on()) debug.log("onError");
if (complete) // TODO: error?
--- a/src/java.net.http/share/classes/jdk/internal/net/http/Http1Response.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http1Response.java Wed Jun 20 09:05:57 2018 -0700
@@ -44,9 +44,10 @@
import jdk.internal.net.http.common.Logger;
import jdk.internal.net.http.common.MinimalFuture;
import jdk.internal.net.http.common.Utils;
-
import static java.net.http.HttpClient.Version.HTTP_1_1;
import static java.net.http.HttpResponse.BodySubscribers.discarding;
+import static jdk.internal.net.http.common.Utils.wrapWithExtraDetail;
+import static jdk.internal.net.http.RedirectFilter.HTTP_NOT_MODIFIED;
/**
* Handles a HTTP/1.1 response (headers + body).
@@ -76,6 +77,7 @@
final Logger debug = Utils.getDebugLogger(this::dbgString, Utils.DEBUG);
final static AtomicLong responseCount = new AtomicLong();
final long id = responseCount.incrementAndGet();
+ private Http1HeaderParser hd;
Http1Response(HttpConnection conn,
Http1Exchange<T> exchange,
@@ -87,6 +89,11 @@
this.asyncReceiver = asyncReceiver;
headersReader = new HeadersReader(this::advance);
bodyReader = new BodyReader(this::advance);
+
+ hd = new Http1HeaderParser();
+ readProgress = State.READING_HEADERS;
+ headersReader.start(hd);
+ asyncReceiver.subscribe(headersReader);
}
String dbgTag;
@@ -150,19 +157,36 @@
}
}
+ private volatile boolean firstTimeAround = true;
+
public CompletableFuture<Response> readHeadersAsync(Executor executor) {
if (debug.on())
debug.log("Reading Headers: (remaining: "
+ asyncReceiver.remaining() +") " + readProgress);
- // with expect continue we will resume reading headers + body.
- asyncReceiver.unsubscribe(bodyReader);
- bodyReader.reset();
- Http1HeaderParser hd = new Http1HeaderParser();
- readProgress = State.READING_HEADERS;
- headersReader.start(hd);
- asyncReceiver.subscribe(headersReader);
+
+ if (firstTimeAround) {
+ if (debug.on()) debug.log("First time around");
+ firstTimeAround = false;
+ } else {
+ // with expect continue we will resume reading headers + body.
+ asyncReceiver.unsubscribe(bodyReader);
+ bodyReader.reset();
+
+ hd = new Http1HeaderParser();
+ readProgress = State.READING_HEADERS;
+ headersReader.reset();
+ headersReader.start(hd);
+ asyncReceiver.subscribe(headersReader);
+ }
+
CompletableFuture<State> cf = headersReader.completion();
assert cf != null : "parsing not started";
+ if (debug.on()) {
+ debug.log("headersReader is %s",
+ cf == null ? "not yet started"
+ : cf.isDone() ? "already completed"
+ : "not yet completed");
+ }
Function<State, Response> lambda = (State completed) -> {
assert completed == State.READING_HEADERS;
@@ -207,7 +231,7 @@
}
int fixupContentLen(int clen) {
- if (request.method().equalsIgnoreCase("HEAD")) {
+ if (request.method().equalsIgnoreCase("HEAD") || responseCode == HTTP_NOT_MODIFIED) {
return 0;
}
if (clen == -1) {
@@ -289,13 +313,19 @@
try {
userSubscriber.onComplete();
} catch (Throwable x) {
- propagateError(t = withError = Utils.getCompletionCause(x));
- // rethrow and let the caller deal with it.
+ // Simply propagate the error by calling
+ // onError on the user subscriber, and let the
+ // connection be reused since we should have received
+ // and parsed all the bytes when we reach here.
+ // If onError throws in turn, then we will simply
+ // let that new exception flow up to the caller
+ // and let it deal with it.
// (i.e: log and close the connection)
- // arguably we could decide to not throw and let the
- // connection be reused since we should have received and
- // parsed all the bytes when we reach here.
- throw x;
+ // Note that rethrowing here could introduce a
+ // race that might cause the next send() operation to
+ // fail as the connection has already been put back
+ // into the cache when we reach here.
+ propagateError(t = withError = Utils.getCompletionCause(x));
}
} else {
propagateError(t);
@@ -613,6 +643,7 @@
@Override
public final void onReadError(Throwable t) {
+ t = wrapWithExtraDetail(t, parser::currentStateMessage);
Http1Response.this.onReadError(t);
}
@@ -692,6 +723,7 @@
@Override
public final void onReadError(Throwable t) {
+ t = wrapWithExtraDetail(t, parser::currentStateMessage);
Http1Response.this.onReadError(t);
}
--- a/src/java.net.http/share/classes/jdk/internal/net/http/Http2ClientImpl.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http2ClientImpl.java Wed Jun 20 09:05:57 2018 -0700
@@ -25,6 +25,9 @@
package jdk.internal.net.http;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.util.Base64;
@@ -95,15 +98,20 @@
synchronized (this) {
Http2Connection connection = connections.get(key);
if (connection != null) {
- if (connection.closed || !connection.reserveStream(true)) {
- if (debug.on())
- debug.log("removing found closed or closing connection: %s", connection);
- deleteConnection(connection);
- } else {
- // fast path if connection already exists
- if (debug.on())
- debug.log("found connection in the pool: %s", connection);
- return MinimalFuture.completedFuture(connection);
+ try {
+ if (connection.closed || !connection.reserveStream(true)) {
+ if (debug.on())
+ debug.log("removing found closed or closing connection: %s", connection);
+ deleteConnection(connection);
+ } else {
+ // fast path if connection already exists
+ if (debug.on())
+ debug.log("found connection in the pool: %s", connection);
+ return MinimalFuture.completedFuture(connection);
+ }
+ } catch (IOException e) {
+ // thrown by connection.reserveStream()
+ return MinimalFuture.failedFuture(e);
}
}
@@ -119,6 +127,11 @@
.whenComplete((conn, t) -> {
synchronized (Http2ClientImpl.this) {
if (conn != null) {
+ try {
+ conn.reserveStream(true);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e); // shouldn't happen
+ }
offerConnection(conn);
} else {
Throwable cause = Utils.getCompletionCause(t);
--- a/src/java.net.http/share/classes/jdk/internal/net/http/Http2Connection.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http2Connection.java Wed Jun 20 09:05:57 2018 -0700
@@ -28,7 +28,6 @@
import java.io.EOFException;
import java.io.IOException;
import java.io.UncheckedIOException;
-import java.lang.System.Logger.Level;
import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.ByteBuffer;
@@ -54,7 +53,7 @@
import jdk.internal.net.http.HttpConnection.HttpPublisher;
import jdk.internal.net.http.common.FlowTube;
import jdk.internal.net.http.common.FlowTube.TubeSubscriber;
-import jdk.internal.net.http.common.HttpHeadersImpl;
+import jdk.internal.net.http.common.HttpHeadersBuilder;
import jdk.internal.net.http.common.Log;
import jdk.internal.net.http.common.Logger;
import jdk.internal.net.http.common.MinimalFuture;
@@ -82,7 +81,6 @@
import static java.nio.charset.StandardCharsets.UTF_8;
import static jdk.internal.net.http.frame.SettingsFrame.*;
-
/**
* An Http2Connection. Encapsulates the socket(channel) and any SSLEngine used
* over it. Contains an HttpConnection which hides the SocketChannel SSL stuff.
@@ -259,6 +257,8 @@
// assigning a stream to a connection.
private int lastReservedClientStreamid = 1;
private int lastReservedServerStreamid = 0;
+ private int numReservedClientStreams = 0; // count of current streams
+ private int numReservedServerStreams = 0; // count of current streams
private final Encoder hpackOut;
private final Decoder hpackIn;
final SettingsFrame clientSettings;
@@ -273,7 +273,7 @@
*/
private final WindowController windowController = new WindowController();
private final FramesController framesController = new FramesController();
- private final Http2TubeSubscriber subscriber = new Http2TubeSubscriber();
+ private final Http2TubeSubscriber subscriber;
final ConnectionWindowUpdateSender windowUpdater;
private volatile Throwable cause;
private volatile Supplier<ByteBuffer> initial;
@@ -290,6 +290,7 @@
String key) {
this.connection = connection;
this.client2 = client2;
+ this.subscriber = new Http2TubeSubscriber(client2.client());
this.nextstreamid = nextstreamid;
this.key = key;
this.clientSettings = this.client2.getClientSettings();
@@ -310,7 +311,7 @@
/**
* Case 1) Create from upgraded HTTP/1.1 connection.
- * Is ready to use. Can be SSL. exchange is the Exchange
+ * Is ready to use. Can't be SSL. exchange is the Exchange
* that initiated the connection, whose response will be delivered
* on a Stream.
*/
@@ -324,6 +325,7 @@
client2,
3, // stream 1 is registered during the upgrade
keyFor(connection));
+ reserveStream(true);
Log.logTrace("Connection send window size {0} ", windowController.connectionWindowSize());
Stream<?> initialStream = createStream(exchange);
@@ -407,7 +409,8 @@
// call these before assigning a request/stream to a connection
// if false returned then a new Http2Connection is required
// if true, the the stream may be assigned to this connection
- synchronized boolean reserveStream(boolean clientInitiated) {
+ // for server push, if false returned, then the stream should be cancelled
+ synchronized boolean reserveStream(boolean clientInitiated) throws IOException {
if (finalStream) {
return false;
}
@@ -424,6 +427,19 @@
lastReservedClientStreamid+=2;
else
lastReservedServerStreamid+=2;
+
+ assert numReservedClientStreams >= 0;
+ assert numReservedServerStreams >= 0;
+ if (clientInitiated && numReservedClientStreams >= getMaxConcurrentClientStreams()) {
+ throw new IOException("too many concurrent streams");
+ } else if (clientInitiated) {
+ numReservedClientStreams++;
+ }
+ if (!clientInitiated && numReservedServerStreams >= getMaxConcurrentServerStreams()) {
+ return false;
+ } else if (!clientInitiated) {
+ numReservedServerStreams++;
+ }
return true;
}
@@ -564,6 +580,14 @@
return serverSettings.getParameter(INITIAL_WINDOW_SIZE);
}
+ final int getMaxConcurrentClientStreams() {
+ return serverSettings.getParameter(MAX_CONCURRENT_STREAMS);
+ }
+
+ final int getMaxConcurrentServerStreams() {
+ return clientSettings.getParameter(MAX_CONCURRENT_STREAMS);
+ }
+
void close() {
Log.logTrace("Closing HTTP/2 connection: to {0}", connection.address());
GoAwayFrame f = new GoAwayFrame(0,
@@ -637,13 +661,19 @@
if (closed == true) return;
closed = true;
}
- Log.logError(t);
+ if (Log.errors()) {
+ if (!(t instanceof EOFException) || isActive()) {
+ Log.logError(t);
+ } else if (t != null) {
+ Log.logError("Shutting down connection: {0}", t.getMessage());
+ }
+ }
Throwable initialCause = this.cause;
if (initialCause == null) this.cause = t;
client2.deleteConnection(this);
List<Stream<?>> c = new LinkedList<>(streams.values());
for (Stream<?> s : c) {
- s.cancelImpl(t);
+ s.connectionClosing(t);
}
connection.close();
}
@@ -766,7 +796,7 @@
nextPushStream += 2;
}
- HttpHeadersImpl headers = decoder.headers();
+ HttpHeaders headers = decoder.headers();
HttpRequestImpl pushReq = HttpRequestImpl.createPushRequest(parentReq, headers);
Exchange<T> pushExch = new Exchange<>(pushReq, parent.exchange.multi);
Stream.PushedStream<T> pushStream = createPushStream(parent, pushExch);
@@ -797,16 +827,44 @@
}
void resetStream(int streamid, int code) throws IOException {
- Log.logError(
- "Resetting stream {0,number,integer} with error code {1,number,integer}",
- streamid, code);
- ResetFrame frame = new ResetFrame(streamid, code);
- sendFrame(frame);
- closeStream(streamid);
+ try {
+ if (connection.channel().isOpen()) {
+ // no need to try & send a reset frame if the
+ // connection channel is already closed.
+ Log.logError(
+ "Resetting stream {0,number,integer} with error code {1,number,integer}",
+ streamid, code);
+ ResetFrame frame = new ResetFrame(streamid, code);
+ sendFrame(frame);
+ } else if (debug.on()) {
+ debug.log("Channel already closed, no need to reset stream %d",
+ streamid);
+ }
+ } finally {
+ decrementStreamsCount(streamid);
+ closeStream(streamid);
+ }
+ }
+
+ // reduce count of streams by 1 if stream still exists
+ synchronized void decrementStreamsCount(int streamid) {
+ Stream<?> s = streams.get(streamid);
+ if (s == null || !s.deRegister())
+ return;
+ if (streamid % 2 == 1) {
+ numReservedClientStreams--;
+ assert numReservedClientStreams >= 0 :
+ "negative client stream count for stream=" + streamid;
+ } else {
+ numReservedServerStreams--;
+ assert numReservedServerStreams >= 0 :
+ "negative server stream count for stream=" + streamid;
+ }
}
void closeStream(int streamid) {
if (debug.on()) debug.log("Closed stream %d", streamid);
+ boolean isClient = (streamid % 2) == 1;
Stream<?> s = streams.remove(streamid);
if (s != null) {
// decrement the reference count on the HttpClientImpl
@@ -1148,14 +1206,19 @@
* A simple tube subscriber for reading from the connection flow.
*/
final class Http2TubeSubscriber implements TubeSubscriber {
- volatile Flow.Subscription subscription;
- volatile boolean completed;
- volatile boolean dropped;
- volatile Throwable error;
- final ConcurrentLinkedQueue<ByteBuffer> queue
+ private volatile Flow.Subscription subscription;
+ private volatile boolean completed;
+ private volatile boolean dropped;
+ private volatile Throwable error;
+ private final ConcurrentLinkedQueue<ByteBuffer> queue
= new ConcurrentLinkedQueue<>();
- final SequentialScheduler scheduler =
+ private final SequentialScheduler scheduler =
SequentialScheduler.synchronizedScheduler(this::processQueue);
+ private final HttpClientImpl client;
+
+ Http2TubeSubscriber(HttpClientImpl client) {
+ this.client = Objects.requireNonNull(client);
+ }
final void processQueue() {
try {
@@ -1179,6 +1242,12 @@
}
}
+ private final void runOrSchedule() {
+ if (client.isSelectorThread()) {
+ scheduler.runOrSchedule(client.theExecutor());
+ } else scheduler.runOrSchedule();
+ }
+
@Override
public void onSubscribe(Flow.Subscription subscription) {
// supports being called multiple time.
@@ -1202,7 +1271,7 @@
if (debug.on()) debug.log(() -> "onNext: got " + Utils.remaining(item)
+ " bytes in " + item.size() + " buffers");
queue.addAll(item);
- scheduler.runOrSchedule(client().theExecutor());
+ runOrSchedule();
}
@Override
@@ -1210,15 +1279,18 @@
if (debug.on()) debug.log(() -> "onError: " + throwable);
error = throwable;
completed = true;
- scheduler.runOrSchedule(client().theExecutor());
+ runOrSchedule();
}
@Override
public void onComplete() {
- if (debug.on()) debug.log("EOF");
- error = new EOFException("EOF reached while reading");
+ String msg = isActive()
+ ? "EOF reached while reading"
+ : "Idle connection closed by HTTP/2 peer";
+ if (debug.on()) debug.log(msg);
+ error = new EOFException(msg);
completed = true;
- scheduler.runOrSchedule(client().theExecutor());
+ runOrSchedule();
}
@Override
@@ -1230,6 +1302,10 @@
}
}
+ synchronized boolean isActive() {
+ return numReservedClientStreams > 0 || numReservedServerStreams > 0;
+ }
+
@Override
public final String toString() {
return dbgString();
@@ -1242,10 +1318,10 @@
static class HeaderDecoder extends ValidatingHeadersConsumer {
- HttpHeadersImpl headers;
+ HttpHeadersBuilder headersBuilder;
HeaderDecoder() {
- this.headers = new HttpHeadersImpl();
+ this.headersBuilder = new HttpHeadersBuilder();
}
@Override
@@ -1253,11 +1329,11 @@
String n = name.toString();
String v = value.toString();
super.onDecoded(n, v);
- headers.addHeader(n, v);
+ headersBuilder.addHeader(n, v);
}
- HttpHeadersImpl headers() {
- return headers;
+ HttpHeaders headers() {
+ return headersBuilder.build();
}
}
--- a/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java Wed Jun 20 09:05:57 2018 -0700
@@ -28,11 +28,14 @@
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import java.io.IOException;
+import java.io.UncheckedIOException;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.net.Authenticator;
+import java.net.ConnectException;
import java.net.CookieHandler;
import java.net.ProxySelector;
+import java.net.http.HttpTimeoutException;
import java.nio.ByteBuffer;
import java.nio.channels.CancelledKeyException;
import java.nio.channels.ClosedChannelException;
@@ -56,13 +59,14 @@
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.BooleanSupplier;
import java.util.stream.Stream;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
@@ -121,6 +125,34 @@
}
}
+ /**
+ * A DelegatingExecutor is an executor that delegates tasks to
+ * a wrapped executor when it detects that the current thread
+ * is the SelectorManager thread. If the current thread is not
+ * the selector manager thread the given task is executed inline.
+ */
+ final static class DelegatingExecutor implements Executor {
+ private final BooleanSupplier isInSelectorThread;
+ private final Executor delegate;
+ DelegatingExecutor(BooleanSupplier isInSelectorThread, Executor delegate) {
+ this.isInSelectorThread = isInSelectorThread;
+ this.delegate = delegate;
+ }
+
+ Executor delegate() {
+ return delegate;
+ }
+
+ @Override
+ public void execute(Runnable command) {
+ if (isInSelectorThread.getAsBoolean()) {
+ delegate.execute(command);
+ } else {
+ command.run();
+ }
+ }
+ }
+
private final CookieHandler cookieHandler;
private final Redirect followRedirects;
private final Optional<ProxySelector> userProxySelector;
@@ -128,7 +160,7 @@
private final Authenticator authenticator;
private final Version version;
private final ConnectionPool connections;
- private final Executor executor;
+ private final DelegatingExecutor delegatingExecutor;
private final boolean isDefaultExecutor;
// Security parameters
private final SSLContext sslContext;
@@ -240,12 +272,11 @@
ex = Executors.newCachedThreadPool(new DefaultThreadFactory(id));
isDefaultExecutor = true;
} else {
- ex = builder.executor;
isDefaultExecutor = false;
}
+ delegatingExecutor = new DelegatingExecutor(this::isSelectorThread, ex);
facadeRef = new WeakReference<>(facadeFactory.createFacade(this));
client2 = new Http2ClientImpl(this);
- executor = ex;
cookieHandler = builder.cookieHandler;
followRedirects = builder.followRedirects == null ?
Redirect.NEVER : builder.followRedirects;
@@ -489,21 +520,38 @@
send(HttpRequest req, BodyHandler<T> responseHandler)
throws IOException, InterruptedException
{
+ CompletableFuture<HttpResponse<T>> cf = null;
try {
- return sendAsync(req, responseHandler, null).get();
+ cf = sendAsync(req, responseHandler, null, null);
+ return cf.get();
+ } catch (InterruptedException ie) {
+ if (cf != null )
+ cf.cancel(true);
+ throw ie;
} catch (ExecutionException e) {
- Throwable t = e.getCause();
- if (t instanceof Error)
- throw (Error)t;
- if (t instanceof RuntimeException)
- throw (RuntimeException)t;
- else if (t instanceof IOException)
- throw Utils.getIOException(t);
- else
- throw new InternalError("Unexpected exception", t);
+ final Throwable throwable = e.getCause();
+ final String msg = throwable.getMessage();
+
+ if (throwable instanceof IllegalArgumentException) {
+ throw new IllegalArgumentException(msg, throwable);
+ } else if (throwable instanceof SecurityException) {
+ throw new SecurityException(msg, throwable);
+ } else if (throwable instanceof HttpTimeoutException) {
+ throw new HttpTimeoutException(msg);
+ } else if (throwable instanceof ConnectException) {
+ ConnectException ce = new ConnectException(msg);
+ ce.initCause(throwable);
+ throw ce;
+ } else if (throwable instanceof IOException) {
+ throw new IOException(msg, throwable);
+ } else {
+ throw new IOException(msg, throwable);
+ }
}
}
+ private static final Executor ASYNC_POOL = new CompletableFuture<Void>().defaultExecutor();
+
@Override
public <T> CompletableFuture<HttpResponse<T>>
sendAsync(HttpRequest userRequest, BodyHandler<T> responseHandler)
@@ -511,13 +559,20 @@
return sendAsync(userRequest, responseHandler, null);
}
-
@Override
public <T> CompletableFuture<HttpResponse<T>>
sendAsync(HttpRequest userRequest,
BodyHandler<T> responseHandler,
- PushPromiseHandler<T> pushPromiseHandler)
- {
+ PushPromiseHandler<T> pushPromiseHandler) {
+ return sendAsync(userRequest, responseHandler, pushPromiseHandler, delegatingExecutor.delegate);
+ }
+
+ private <T> CompletableFuture<HttpResponse<T>>
+ sendAsync(HttpRequest userRequest,
+ BodyHandler<T> responseHandler,
+ PushPromiseHandler<T> pushPromiseHandler,
+ Executor exchangeExecutor) {
+
Objects.requireNonNull(userRequest);
Objects.requireNonNull(responseHandler);
@@ -536,9 +591,17 @@
if (debugelapsed.on())
debugelapsed.log("ClientImpl (async) send %s", userRequest);
- Executor executor = acc == null
- ? this.executor
- : new PrivilegedExecutor(this.executor, acc);
+ // When using sendAsync(...) we explicitly pass the
+ // executor's delegate as exchange executor to force
+ // asynchronous scheduling of the exchange.
+ // When using send(...) we don't specify any executor
+ // and default to using the client's delegating executor
+ // which only spawns asynchronous tasks if it detects
+ // that the current thread is the selector manager
+ // thread. This will cause everything to execute inline
+ // until we need to schedule some event with the selector.
+ Executor executor = exchangeExecutor == null
+ ? this.delegatingExecutor : exchangeExecutor;
MultiExchange<T> mex = new MultiExchange<>(userRequest,
requestImpl,
@@ -547,15 +610,18 @@
pushPromiseHandler,
acc);
CompletableFuture<HttpResponse<T>> res =
- mex.responseAsync().whenComplete((b,t) -> unreference());
+ mex.responseAsync(executor).whenComplete((b,t) -> unreference());
if (DEBUGELAPSED) {
res = res.whenComplete(
(b,t) -> debugCompleted("ClientImpl (async)", start, userRequest));
}
- // makes sure that any dependent actions happen in the executor
- res = res.whenCompleteAsync((r, t) -> { /* do nothing */}, executor);
-
+ // makes sure that any dependent actions happen in the CF default
+ // executor. This is only needed for sendAsync(...), when
+ // exchangeExecutor is non-null.
+ if (exchangeExecutor != null) {
+ res = res.whenCompleteAsync((r, t) -> { /* do nothing */}, ASYNC_POOL);
+ }
return res;
} catch(Throwable t) {
unreference();
@@ -654,6 +720,7 @@
}
synchronized void shutdown() {
+ Log.logTrace("{0}: shutting down", getName());
if (debug.on()) debug.log("SelectorManager shutting down");
closed = true;
try {
@@ -670,6 +737,7 @@
List<AsyncEvent> readyList = new ArrayList<>();
List<Runnable> resetList = new ArrayList<>();
try {
+ if (Log.channel()) Log.logChannel(getName() + ": starting");
while (!Thread.currentThread().isInterrupted()) {
synchronized (this) {
assert errorList.isEmpty();
@@ -706,7 +774,7 @@
throw new IOException("Channel closed");
}
} catch (IOException e) {
- Log.logTrace("HttpClientImpl: " + e);
+ Log.logTrace("{0}: {1}", getName(), e);
if (debug.on())
debug.log("Got " + e.getClass().getName()
+ " while handling registration events");
@@ -738,7 +806,9 @@
// Check whether client is still alive, and if not,
// gracefully stop this thread
if (!owner.isReferenced()) {
- Log.logTrace("HttpClient no longer referenced. Exiting...");
+ Log.logTrace("{0}: {1}",
+ getName(),
+ "HttpClient no longer referenced. Exiting...");
return;
}
@@ -780,7 +850,9 @@
// Check whether client is still alive, and if not,
// gracefully stop this thread
if (!owner.isReferenced()) {
- Log.logTrace("HttpClient no longer referenced. Exiting...");
+ Log.logTrace("{0}: {1}",
+ getName(),
+ "HttpClient no longer referenced. Exiting...");
return;
}
owner.purgeTimeoutsAndReturnNextDeadline();
@@ -831,17 +903,18 @@
}
} catch (Throwable e) {
- //e.printStackTrace();
if (!closed) {
// This terminates thread. So, better just print stack trace
String err = Utils.stackTrace(e);
- Log.logError("HttpClientImpl: fatal error: " + err);
+ Log.logError("{0}: {1}: {2}", getName(),
+ "HttpClientImpl shutting down due to fatal error", err);
}
if (debug.on()) debug.log("shutting down", e);
if (Utils.ASSERTIONSENABLED && !debug.on()) {
e.printStackTrace(System.err); // always print the stack
}
} finally {
+ if (Log.channel()) Log.logChannel(getName() + ": stopping");
shutdown();
}
}
@@ -1013,13 +1086,15 @@
return Optional.ofNullable(authenticator);
}
- /*package-private*/ final Executor theExecutor() {
- return executor;
+ /*package-private*/ final DelegatingExecutor theExecutor() {
+ return delegatingExecutor;
}
@Override
public final Optional<Executor> executor() {
- return isDefaultExecutor ? Optional.empty() : Optional.of(executor);
+ return isDefaultExecutor
+ ? Optional.empty()
+ : Optional.of(delegatingExecutor.delegate());
}
ConnectionPool connectionPool() {
--- a/src/java.net.http/share/classes/jdk/internal/net/http/HttpConnection.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpConnection.java Wed Jun 20 09:05:57 2018 -0700
@@ -27,7 +27,6 @@
import java.io.Closeable;
import java.io.IOException;
-import java.lang.System.Logger.Level;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
@@ -250,7 +249,7 @@
* @param request
* @return
*/
- BiPredicate<String,List<String>> headerFilter(HttpRequestImpl request) {
+ BiPredicate<String,String> headerFilter(HttpRequestImpl request) {
if (isTunnel()) {
// talking to a server through a proxy tunnel
// don't send proxy-* headers to a plain server
@@ -280,12 +279,12 @@
// start with "proxy-"
private static HttpHeaders proxyTunnelHeaders(HttpRequestImpl request) {
Map<String, List<String>> combined = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
- combined.putAll(request.getSystemHeaders().map());
+ combined.putAll(request.getSystemHeadersBuilder().map());
combined.putAll(request.headers().map()); // let user override system
// keep only proxy-* - and also strip authorization headers
// for disabled schemes
- return ImmutableHeaders.of(combined, Utils.PROXY_TUNNEL_FILTER);
+ return HttpHeaders.of(combined, Utils.PROXY_TUNNEL_FILTER);
}
/* Returns either a plain HTTP connection or a plain tunnelling connection
--- a/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestBuilderImpl.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestBuilderImpl.java Wed Jun 20 09:05:57 2018 -0700
@@ -32,9 +32,9 @@
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublisher;
-import jdk.internal.net.http.common.HttpHeadersImpl;
+
+import jdk.internal.net.http.common.HttpHeadersBuilder;
import jdk.internal.net.http.common.Utils;
-import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
import static jdk.internal.net.http.common.Utils.isValidName;
import static jdk.internal.net.http.common.Utils.isValidValue;
@@ -42,7 +42,7 @@
public class HttpRequestBuilderImpl implements HttpRequest.Builder {
- private HttpHeadersImpl userHeaders;
+ private HttpHeadersBuilder headersBuilder;
private URI uri;
private String method;
private boolean expectContinue;
@@ -54,13 +54,13 @@
requireNonNull(uri, "uri must be non-null");
checkURI(uri);
this.uri = uri;
- this.userHeaders = new HttpHeadersImpl();
+ this.headersBuilder = new HttpHeadersBuilder();
this.method = "GET"; // default, as per spec
this.version = Optional.empty();
}
public HttpRequestBuilderImpl() {
- this.userHeaders = new HttpHeadersImpl();
+ this.headersBuilder = new HttpHeadersBuilder();
this.method = "GET"; // default, as per spec
this.version = Optional.empty();
}
@@ -90,7 +90,7 @@
public HttpRequestBuilderImpl copy() {
HttpRequestBuilderImpl b = new HttpRequestBuilderImpl();
b.uri = this.uri;
- b.userHeaders = this.userHeaders.deepCopy();
+ b.headersBuilder = this.headersBuilder.structuralCopy();
b.method = this.method;
b.expectContinue = this.expectContinue;
b.bodyPublisher = bodyPublisher;
@@ -106,7 +106,7 @@
if (!isValidName(name)) {
throw newIAE("invalid header name: \"%s\"", name);
}
- if (!Utils.ALLOWED_HEADERS.test(name)) {
+ if (!Utils.ALLOWED_HEADERS.test(name, null)) {
throw newIAE("restricted header name: \"%s\"", name);
}
if (!isValidValue(value)) {
@@ -117,14 +117,14 @@
@Override
public HttpRequestBuilderImpl setHeader(String name, String value) {
checkNameAndValue(name, value);
- userHeaders.setHeader(name, value);
+ headersBuilder.setHeader(name, value);
return this;
}
@Override
public HttpRequestBuilderImpl header(String name, String value) {
checkNameAndValue(name, value);
- userHeaders.addHeader(name, value);
+ headersBuilder.addHeader(name, value);
return this;
}
@@ -155,7 +155,7 @@
return this;
}
- HttpHeadersImpl headers() { return userHeaders; }
+ HttpHeadersBuilder headersBuilder() { return headersBuilder; }
URI uri() { return uri; }
@@ -216,6 +216,13 @@
if (uri == null)
throw new IllegalStateException("uri is null");
assert method != null;
+ return new ImmutableHttpRequest(this);
+ }
+
+ public HttpRequestImpl buildForWebSocket() {
+ if (uri == null)
+ throw new IllegalStateException("uri is null");
+ assert method != null;
return new HttpRequestImpl(this);
}
--- a/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestImpl.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestImpl.java Wed Jun 20 09:05:57 2018 -0700
@@ -41,16 +41,16 @@
import java.net.http.HttpClient;
import java.net.http.HttpHeaders;
import java.net.http.HttpRequest;
-import jdk.internal.net.http.common.HttpHeadersImpl;
+import jdk.internal.net.http.common.HttpHeadersBuilder;
import jdk.internal.net.http.common.Utils;
import jdk.internal.net.http.websocket.WebSocketRequest;
import static jdk.internal.net.http.common.Utils.ALLOWED_HEADERS;
-class HttpRequestImpl extends HttpRequest implements WebSocketRequest {
+public class HttpRequestImpl extends HttpRequest implements WebSocketRequest {
private final HttpHeaders userHeaders;
- private final HttpHeadersImpl systemHeaders;
+ private final HttpHeadersBuilder systemHeadersBuilder;
private final URI uri;
private volatile Proxy proxy; // ensure safe publishing
private final InetSocketAddress authority; // only used when URI not specified
@@ -78,8 +78,8 @@
public HttpRequestImpl(HttpRequestBuilderImpl builder) {
String method = builder.method();
this.method = method == null ? "GET" : method;
- this.userHeaders = ImmutableHeaders.of(builder.headers().map(), ALLOWED_HEADERS);
- this.systemHeaders = new HttpHeadersImpl();
+ this.userHeaders = HttpHeaders.of(builder.headersBuilder().map(), ALLOWED_HEADERS);
+ this.systemHeadersBuilder = new HttpHeadersBuilder();
this.uri = builder.uri();
assert uri != null;
this.proxy = null;
@@ -106,21 +106,23 @@
"uri must be non null");
Duration timeout = request.timeout().orElse(null);
this.method = method == null ? "GET" : method;
- this.userHeaders = ImmutableHeaders.validate(request.headers());
+ this.userHeaders = HttpHeaders.of(request.headers().map(), Utils.VALIDATE_USER_HEADER);
if (request instanceof HttpRequestImpl) {
// all cases exception WebSocket should have a new system headers
this.isWebSocket = ((HttpRequestImpl) request).isWebSocket;
if (isWebSocket) {
- this.systemHeaders = ((HttpRequestImpl) request).systemHeaders;
+ this.systemHeadersBuilder = ((HttpRequestImpl)request).systemHeadersBuilder;
} else {
- this.systemHeaders = new HttpHeadersImpl();
+ this.systemHeadersBuilder = new HttpHeadersBuilder();
}
} else {
HttpRequestBuilderImpl.checkURI(requestURI);
checkTimeout(timeout);
- this.systemHeaders = new HttpHeadersImpl();
+ this.systemHeadersBuilder = new HttpHeadersBuilder();
}
- this.systemHeaders.setHeader("User-Agent", USER_AGENT);
+ if (!userHeaders.firstValue("User-Agent").isPresent()) {
+ this.systemHeadersBuilder.setHeader("User-Agent", USER_AGENT);
+ }
this.uri = requestURI;
if (isWebSocket) {
// WebSocket determines and sets the proxy itself
@@ -169,8 +171,10 @@
this.method = method == null? "GET" : method;
this.userHeaders = other.userHeaders;
this.isWebSocket = other.isWebSocket;
- this.systemHeaders = new HttpHeadersImpl();
- this.systemHeaders.setHeader("User-Agent", USER_AGENT);
+ this.systemHeadersBuilder = new HttpHeadersBuilder();
+ if (!userHeaders.firstValue("User-Agent").isPresent()) {
+ this.systemHeadersBuilder.setHeader("User-Agent", USER_AGENT);
+ }
this.uri = uri;
this.proxy = other.proxy;
this.expectContinue = other.expectContinue;
@@ -189,8 +193,8 @@
// to the connection pool (we might need to revisit this constructor later)
assert "CONNECT".equalsIgnoreCase(method);
this.method = method;
- this.systemHeaders = new HttpHeadersImpl();
- this.userHeaders = ImmutableHeaders.of(headers);
+ this.systemHeadersBuilder = new HttpHeadersBuilder();
+ this.userHeaders = headers;
this.uri = URI.create("socket://" + authority.getHostString() + ":"
+ Integer.toString(authority.getPort()) + "/");
this.proxy = null;
@@ -218,14 +222,14 @@
* parent.
*/
static HttpRequestImpl createPushRequest(HttpRequestImpl parent,
- HttpHeadersImpl headers)
+ HttpHeaders headers)
throws IOException
{
return new HttpRequestImpl(parent, headers);
}
// only used for push requests
- private HttpRequestImpl(HttpRequestImpl parent, HttpHeadersImpl headers)
+ private HttpRequestImpl(HttpRequestImpl parent, HttpHeaders headers)
throws IOException
{
this.method = headers.firstValue(":method")
@@ -240,8 +244,8 @@
sb.append(scheme).append("://").append(authority).append(path);
this.uri = URI.create(sb.toString());
this.proxy = null;
- this.userHeaders = ImmutableHeaders.of(headers.map(), ALLOWED_HEADERS);
- this.systemHeaders = parent.systemHeaders;
+ this.userHeaders = HttpHeaders.of(headers.map(), ALLOWED_HEADERS);
+ this.systemHeadersBuilder = parent.systemHeadersBuilder;
this.expectContinue = parent.expectContinue;
this.secure = parent.secure;
this.requestPublisher = parent.requestPublisher;
@@ -264,9 +268,9 @@
InetSocketAddress authority() { return authority; }
void setH2Upgrade(Http2ClientImpl h2client) {
- systemHeaders.setHeader("Connection", "Upgrade, HTTP2-Settings");
- systemHeaders.setHeader("Upgrade", "h2c");
- systemHeaders.setHeader("HTTP2-Settings", h2client.getSettingsString());
+ systemHeadersBuilder.setHeader("Connection", "Upgrade, HTTP2-Settings");
+ systemHeadersBuilder.setHeader("Upgrade", "h2c");
+ systemHeadersBuilder.setHeader("HTTP2-Settings", h2client.getSettingsString());
}
@Override
@@ -332,18 +336,18 @@
HttpHeaders getUserHeaders() { return userHeaders; }
- HttpHeadersImpl getSystemHeaders() { return systemHeaders; }
+ HttpHeadersBuilder getSystemHeadersBuilder() { return systemHeadersBuilder; }
@Override
public Optional<HttpClient.Version> version() { return version; }
void addSystemHeader(String name, String value) {
- systemHeaders.addHeader(name, value);
+ systemHeadersBuilder.addHeader(name, value);
}
@Override
public void setSystemHeader(String name, String value) {
- systemHeaders.setHeader(name, value);
+ systemHeadersBuilder.setHeader(name, value);
}
InetSocketAddress getAddress() {
--- a/src/java.net.http/share/classes/jdk/internal/net/http/ImmutableHeaders.java Wed Jun 20 17:15:16 2018 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,116 +0,0 @@
-/*
- * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation. Oracle designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
-package jdk.internal.net.http;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.TreeMap;
-import java.util.function.BiPredicate;
-import java.util.function.Predicate;
-import java.net.http.HttpHeaders;
-import jdk.internal.net.http.common.HttpHeadersImpl;
-import jdk.internal.net.http.common.Utils;
-import static java.util.Collections.emptyMap;
-import static java.util.Collections.unmodifiableList;
-import static java.util.Collections.unmodifiableMap;
-import static java.util.Objects.requireNonNull;
-
-final class ImmutableHeaders extends HttpHeaders {
-
- private final Map<String, List<String>> map;
-
- public static ImmutableHeaders empty() {
- return of(emptyMap());
- }
-
- public static ImmutableHeaders of(Map<String, List<String>> src) {
- return of(src, x -> true);
- }
-
- public static ImmutableHeaders of(HttpHeaders headers) {
- return (headers instanceof ImmutableHeaders)
- ? (ImmutableHeaders)headers
- : of(headers.map());
- }
-
- static ImmutableHeaders validate(HttpHeaders headers) {
- if (headers instanceof ImmutableHeaders) {
- return of(headers);
- }
- if (headers instanceof HttpHeadersImpl) {
- return of(headers);
- }
- Map<String, List<String>> map = headers.map();
- return new ImmutableHeaders(map, Utils.VALIDATE_USER_HEADER);
- }
-
- public static ImmutableHeaders of(Map<String, List<String>> src,
- Predicate<? super String> keyAllowed) {
- requireNonNull(src, "src");
- requireNonNull(keyAllowed, "keyAllowed");
- return new ImmutableHeaders(src, headerAllowed(keyAllowed));
- }
-
- public static ImmutableHeaders of(Map<String, List<String>> src,
- BiPredicate<? super String, ? super List<String>> headerAllowed) {
- requireNonNull(src, "src");
- requireNonNull(headerAllowed, "headerAllowed");
- return new ImmutableHeaders(src, headerAllowed);
- }
-
- private ImmutableHeaders(Map<String, List<String>> src,
- BiPredicate<? super String, ? super List<String>> headerAllowed) {
- Map<String, List<String>> m = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
- src.entrySet().stream()
- .forEach(e -> addIfAllowed(e, headerAllowed, m));
- this.map = unmodifiableMap(m);
- }
-
- private static void addIfAllowed(Map.Entry<String, List<String>> e,
- BiPredicate<? super String, ? super List<String>> headerAllowed,
- Map<String, List<String>> map) {
- String key = e.getKey();
- List<String> values = unmodifiableValues(e.getValue());
- if (headerAllowed.test(key, values)) {
- map.put(key, values);
- }
- }
-
- private static List<String> unmodifiableValues(List<String> values) {
- return unmodifiableList(new ArrayList<>(Objects.requireNonNull(values)));
- }
-
- private static BiPredicate<String, List<String>> headerAllowed(Predicate<? super String> keyAllowed) {
- return (n,v) -> keyAllowed.test(n);
- }
-
- @Override
- public Map<String, List<String>> map() {
- return map;
- }
-}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/ImmutableHttpRequest.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,85 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.internal.net.http;
+
+import java.net.URI;
+import java.net.http.HttpHeaders;
+import java.net.http.HttpRequest;
+import java.time.Duration;
+import java.util.Objects;
+import java.util.Optional;
+import java.net.http.HttpClient.Version;
+import static jdk.internal.net.http.common.Utils.ALLOWED_HEADERS;
+
+final class ImmutableHttpRequest extends HttpRequest {
+
+ private final String method;
+ private final URI uri;
+ private final HttpHeaders headers;
+ private final Optional<BodyPublisher> requestPublisher;
+ private final boolean expectContinue;
+ private final Optional<Duration> timeout;
+ private final Optional<Version> version;
+
+ /** Creates an ImmutableHttpRequest from the given builder. */
+ ImmutableHttpRequest(HttpRequestBuilderImpl builder) {
+ this.method = Objects.requireNonNull(builder.method());
+ this.uri = Objects.requireNonNull(builder.uri());
+ this.headers = HttpHeaders.of(builder.headersBuilder().map(), ALLOWED_HEADERS);
+ this.requestPublisher = Optional.ofNullable(builder.bodyPublisher());
+ this.expectContinue = builder.expectContinue();
+ this.timeout = Optional.ofNullable(builder.timeout());
+ this.version = Objects.requireNonNull(builder.version());
+ }
+
+ @Override
+ public String method() { return method; }
+
+ @Override
+ public URI uri() { return uri; }
+
+ @Override
+ public HttpHeaders headers() {
+ return headers;
+ }
+
+ @Override
+ public Optional<BodyPublisher> bodyPublisher() { return requestPublisher; }
+
+ @Override
+ public boolean expectContinue() { return expectContinue; }
+
+ @Override
+ public Optional<Duration> timeout() { return timeout; }
+
+ @Override
+ public Optional<Version> version() { return version; }
+
+ @Override
+ public String toString() {
+ return uri.toString() + " " + method;
+ }
+}
--- a/src/java.net.http/share/classes/jdk/internal/net/http/MultiExchange.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/MultiExchange.java Wed Jun 20 09:05:57 2018 -0700
@@ -26,7 +26,7 @@
package jdk.internal.net.http;
import java.io.IOException;
-import java.lang.System.Logger.Level;
+import java.net.ConnectException;
import java.time.Duration;
import java.util.Iterator;
import java.util.LinkedList;
@@ -69,7 +69,7 @@
final AccessControlContext acc;
final HttpClientImpl client;
final HttpResponse.BodyHandler<T> responseHandler;
- final Executor executor;
+ final HttpClientImpl.DelegatingExecutor executor;
final AtomicInteger attempts = new AtomicInteger();
HttpRequestImpl currentreq; // used for retries & redirect
HttpRequestImpl previousreq; // used for retries & redirect
@@ -124,8 +124,8 @@
if (pushPromiseHandler != null) {
Executor executor = acc == null
- ? this.executor
- : new PrivilegedExecutor(this.executor, acc);
+ ? this.executor.delegate()
+ : new PrivilegedExecutor(this.executor.delegate(), acc);
this.pushGroup = new PushGroup<>(pushPromiseHandler, request, executor);
} else {
pushGroup = null;
@@ -193,7 +193,7 @@
getExchange().cancel(cause);
}
- public CompletableFuture<HttpResponse<T>> responseAsync() {
+ public CompletableFuture<HttpResponse<T>> responseAsync(Executor executor) {
CompletableFuture<Void> start = new MinimalFuture<>();
CompletableFuture<HttpResponse<T>> cf = responseAsync0(start);
start.completeAsync( () -> null, executor); // trigger execution
@@ -285,13 +285,22 @@
private static boolean retryPostValue() {
String s = Utils.getNetProperty("jdk.httpclient.enableAllMethodRetry");
- if (s == "" || "true".equals(s))
- return true;
- return false;
+ if (s == null)
+ return false;
+ return s.isEmpty() ? true : Boolean.parseBoolean(s);
+ }
+
+ private static boolean retryConnect() {
+ String s = Utils.getNetProperty("jdk.httpclient.disableRetryConnect");
+ if (s == null)
+ return false;
+ return s.isEmpty() ? true : Boolean.parseBoolean(s);
}
/** True if ALL ( even non-idempotent ) requests can be automatic retried. */
private static final boolean RETRY_ALWAYS = retryPostValue();
+ /** True if ConnectException should cause a retry. Enabled by default */
+ private static final boolean RETRY_CONNECT = retryConnect();
/** Returns true is given request has an idempotent method. */
private static boolean isIdempotentRequest(HttpRequest request) {
@@ -307,13 +316,23 @@
/** Returns true if the given request can be automatically retried. */
private static boolean canRetryRequest(HttpRequest request) {
+ if (RETRY_ALWAYS)
+ return true;
if (isIdempotentRequest(request))
return true;
- if (RETRY_ALWAYS)
- return true;
return false;
}
+ private boolean retryOnFailure(Throwable t) {
+ return t instanceof ConnectionExpiredException
+ || (RETRY_CONNECT && (t instanceof ConnectException));
+ }
+
+ private Throwable retryCause(Throwable t) {
+ Throwable cause = t instanceof ConnectionExpiredException ? t.getCause() : t;
+ return cause == null ? t : cause;
+ }
+
/**
* Takes a Throwable and returns a suitable CompletableFuture that is
* completed exceptionally, or null.
@@ -326,21 +345,20 @@
}
if (cancelled && t instanceof IOException) {
t = new HttpTimeoutException("request timed out");
- } else if (t instanceof ConnectionExpiredException) {
- Throwable cause = t;
- if (t.getCause() != null) {
- cause = t.getCause(); // unwrap the ConnectionExpiredException
- }
+ } else if (retryOnFailure(t)) {
+ Throwable cause = retryCause(t);
- if (!canRetryRequest(currentreq)) {
- return failedFuture(cause); // fails with original cause
+ if (!(t instanceof ConnectException)) {
+ if (!canRetryRequest(currentreq)) {
+ return failedFuture(cause); // fails with original cause
+ }
}
// allow the retry mechanism to do its work
retryCause = cause;
if (!expiredOnce) {
if (debug.on())
- debug.log("ConnectionExpiredException (async): retrying...", t);
+ debug.log(t.getClass().getSimpleName() + " (async): retrying...", t);
expiredOnce = true;
// The connection was abruptly closed.
// We return null to retry the same request a second time.
@@ -350,9 +368,11 @@
previousreq = currentreq;
return null;
} else {
- if (debug.on())
- debug.log("ConnectionExpiredException (async): already retried once.", t);
- if (t.getCause() != null) t = t.getCause();
+ if (debug.on()) {
+ debug.log(t.getClass().getSimpleName()
+ + " (async): already retried once.", t);
+ }
+ t = cause;
}
}
return failedFuture(t);
@@ -364,9 +384,10 @@
}
@Override
public void handle() {
- if (debug.on())
+ if (debug.on()) {
debug.log("Cancelling MultiExchange due to timeout for request %s",
- request);
+ request);
+ }
cancel(new HttpTimeoutException("request timed out"));
}
}
--- a/src/java.net.http/share/classes/jdk/internal/net/http/PlainHttpConnection.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/PlainHttpConnection.java Wed Jun 20 09:05:57 2018 -0700
@@ -26,10 +26,8 @@
package jdk.internal.net.http;
import java.io.IOException;
-import java.lang.System.Logger.Level;
import java.net.InetSocketAddress;
import java.net.StandardSocketOptions;
-import java.nio.ByteBuffer;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
@@ -91,14 +89,16 @@
// complete async since the event runs on the SelectorManager thread
cf.completeAsync(() -> null, client().theExecutor());
} catch (Throwable e) {
- client().theExecutor().execute( () -> cf.completeExceptionally(e));
+ Throwable t = Utils.toConnectException(e);
+ client().theExecutor().execute( () -> cf.completeExceptionally(t));
+ close();
}
}
@Override
public void abort(IOException ioe) {
+ client().theExecutor().execute( () -> cf.completeExceptionally(ioe));
close();
- client().theExecutor().execute( () -> cf.completeExceptionally(ioe));
}
}
@@ -114,7 +114,7 @@
try {
finished = AccessController.doPrivileged(pa);
} catch (PrivilegedActionException e) {
- cf.completeExceptionally(e.getCause());
+ throw e.getCause();
}
if (finished) {
if (debug.on()) debug.log("connect finished without blocking");
@@ -125,7 +125,13 @@
client().registerEvent(new ConnectEvent(cf));
}
} catch (Throwable throwable) {
- cf.completeExceptionally(throwable);
+ cf.completeExceptionally(Utils.toConnectException(throwable));
+ try {
+ close();
+ } catch (Exception x) {
+ if (debug.on())
+ debug.log("Failed to close channel after unsuccessful connect");
+ }
}
return cf;
}
--- a/src/java.net.http/share/classes/jdk/internal/net/http/RedirectFilter.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/RedirectFilter.java Wed Jun 20 09:05:57 2018 -0700
@@ -43,6 +43,13 @@
static final int DEFAULT_MAX_REDIRECTS = 5;
URI uri;
+ /*
+ * NOT_MODIFIED status code results from a conditional GET where
+ * the server does not (must not) return a response body because
+ * the condition specified in the request disallows it
+ */
+ static final int HTTP_NOT_MODIFIED = 304;
+
static final int max_redirects = Utils.getIntegerNetProperty(
"jdk.httpclient.redirects.retrylimit", DEFAULT_MAX_REDIRECTS
);
@@ -91,6 +98,10 @@
if (rcode == 200 || policy == HttpClient.Redirect.NEVER) {
return null;
}
+
+ if (rcode == HTTP_NOT_MODIFIED)
+ return null;
+
if (rcode >= 300 && rcode <= 399) {
URI redir = getRedirectedURI(r.headers());
String newMethod = redirectedMethod(rcode, method);
--- a/src/java.net.http/share/classes/jdk/internal/net/http/Response.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/Response.java Wed Jun 20 09:05:57 2018 -0700
@@ -64,7 +64,7 @@
int statusCode,
HttpClient.Version version,
boolean isConnectResponse) {
- this.headers = ImmutableHeaders.of(headers);
+ this.headers = headers;
this.request = req;
this.version = version;
this.exchange = exchange;
--- a/src/java.net.http/share/classes/jdk/internal/net/http/ResponseContent.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/ResponseContent.java Wed Jun 20 09:05:57 2018 -0700
@@ -26,7 +26,6 @@
package jdk.internal.net.http;
import java.io.IOException;
-import java.lang.System.Logger.Level;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
@@ -34,9 +33,9 @@
import java.util.function.Consumer;
import java.net.http.HttpHeaders;
import java.net.http.HttpResponse;
-
import jdk.internal.net.http.common.Logger;
import jdk.internal.net.http.common.Utils;
+import static java.lang.String.format;
/**
* Implements chunked/fixed transfer encodings of HTTP/1.1 responses.
@@ -95,6 +94,9 @@
interface BodyParser extends Consumer<ByteBuffer> {
void onSubscribe(AbstractSubscription sub);
+ // A current-state message suitable for inclusion in an exception
+ // detail message.
+ String currentStateMessage();
}
// Returns a parser that will take care of parsing the received byte
@@ -145,6 +147,11 @@
}
@Override
+ public String currentStateMessage() {
+ return format("chunked transfer encoding, state: %s", state);
+ }
+
+ @Override
public void accept(ByteBuffer b) {
if (closedExceptionally != null) {
if (debug.on())
@@ -425,6 +432,12 @@
}
@Override
+ public String currentStateMessage() {
+ return format("fixed content-length: %d, bytes received: %d",
+ contentLength, contentLength - remaining);
+ }
+
+ @Override
public void accept(ByteBuffer b) {
if (closedExceptionally != null) {
if (debug.on())
--- a/src/java.net.http/share/classes/jdk/internal/net/http/SocketTube.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/SocketTube.java Wed Jun 20 09:05:57 2018 -0700
@@ -41,6 +41,7 @@
import jdk.internal.net.http.common.BufferSupplier;
import jdk.internal.net.http.common.Demand;
import jdk.internal.net.http.common.FlowTube;
+import jdk.internal.net.http.common.Log;
import jdk.internal.net.http.common.Logger;
import jdk.internal.net.http.common.SequentialScheduler;
import jdk.internal.net.http.common.SequentialScheduler.DeferredCompleter;
@@ -149,6 +150,10 @@
void signalClosed() {
// Ensures that the subscriber will be terminated and that future
// subscribers will be notified when the connection is closed.
+ if (Log.channel()) {
+ Log.logChannel("Connection close signalled: connection closed locally ({0})",
+ channelDescr());
+ }
readPublisher.subscriptionImpl.signalError(
new IOException("connection closed locally"));
}
@@ -364,6 +369,10 @@
void startSubscription() {
try {
if (debug.on()) debug.log("write: starting subscription");
+ if (Log.channel()) {
+ Log.logChannel("Start requesting bytes for writing to channel: {0}",
+ channelDescr());
+ }
assert client.isSelectorThread();
// make sure read registrations are handled before;
readPublisher.subscriptionImpl.handlePending();
@@ -409,6 +418,10 @@
void signalError(Throwable error) {
debug.log(() -> "write error: " + error);
+ if (Log.channel()) {
+ Log.logChannel("Failed to write to channel ({0}: {1})",
+ channelDescr(), error);
+ }
completed = true;
readPublisher.signalError(error);
}
@@ -455,6 +468,7 @@
@Override
public void cancel() {
+ if (debug.on()) debug.log("write: cancel");
dropSubscription();
upstreamSubscription.cancel();
}
@@ -558,6 +572,10 @@
if (!errorRef.compareAndSet(null, error)) {
return;
}
+ if (Log.channel()) {
+ Log.logChannel("Error signalled on channel {0}: {1}",
+ channelDescr(), error);
+ }
subscriptionImpl.handleError();
}
@@ -665,6 +683,7 @@
final void handleSubscribeEvent() {
assert client.isSelectorThread();
debug.log("subscribe event raised");
+ if (Log.channel()) Log.logChannel("Start reading from {0}", channelDescr());
readScheduler.runOrSchedule();
if (readScheduler.isStopped() || completed) {
// if already completed or stopped we can handle any
@@ -702,6 +721,10 @@
@Override
public final void cancel() {
pauseReadEvent();
+ if (Log.channel()) {
+ Log.logChannel("Read subscription cancelled for channel {0}",
+ channelDescr());
+ }
readScheduler.stop();
}
@@ -726,6 +749,10 @@
return;
}
if (debug.on()) debug.log("got read error: " + error);
+ if (Log.channel()) {
+ Log.logChannel("Read error signalled on channel {0}: {1}",
+ channelDescr(), error);
+ }
readScheduler.runOrSchedule();
}
@@ -772,6 +799,10 @@
if (debug.on())
debug.log("Sending error " + error
+ " to subscriber " + subscriber);
+ if (Log.channel()) {
+ Log.logChannel("Raising error with subscriber for {0}: {1}",
+ channelDescr(), error);
+ }
current.errorRef.compareAndSet(null, error);
current.signalCompletion();
readScheduler.stop();
@@ -788,6 +819,10 @@
if (bytes == EOF) {
if (!completed) {
if (debug.on()) debug.log("got read EOF");
+ if (Log.channel()) {
+ Log.logChannel("EOF read from channel: {0}",
+ channelDescr());
+ }
completed = true;
// safe to pause here because we're finished
// anyway.
@@ -849,6 +884,12 @@
if (debug.on()) debug.log("Unexpected exception in read loop", t);
signalError(t);
} finally {
+ if (readScheduler.isStopped()) {
+ if (debug.on()) debug.log("Read scheduler stopped");
+ if (Log.channel()) {
+ Log.logChannel("Stopped reading from channel {0}", channelDescr());
+ }
+ }
handlePending();
}
}
@@ -1238,4 +1279,8 @@
final String dbgString() {
return "SocketTube("+id+")";
}
+
+ final String channelDescr() {
+ return String.valueOf(channel);
+ }
}
--- a/src/java.net.http/share/classes/jdk/internal/net/http/Stream.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/Stream.java Wed Jun 20 09:05:57 2018 -0700
@@ -25,9 +25,9 @@
package jdk.internal.net.http;
+import java.io.EOFException;
import java.io.IOException;
import java.io.UncheckedIOException;
-import java.lang.System.Logger.Level;
import java.net.URI;
import java.nio.ByteBuffer;
import java.util.ArrayList;
@@ -39,6 +39,7 @@
import java.util.concurrent.Executor;
import java.util.concurrent.Flow;
import java.util.concurrent.Flow.Subscription;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiPredicate;
import java.net.http.HttpClient;
@@ -114,8 +115,8 @@
final Http2Connection connection;
final HttpRequestImpl request;
final HeadersConsumer rspHeadersConsumer;
- final HttpHeadersImpl responseHeaders;
- final HttpHeadersImpl requestPseudoHeaders;
+ final HttpHeadersBuilder responseHeadersBuilder;
+ final HttpHeaders requestPseudoHeaders;
volatile HttpResponse.BodySubscriber<T> responseSubscriber;
final HttpRequest.BodyPublisher requestPublisher;
volatile RequestSubscriber requestSubscriber;
@@ -133,6 +134,8 @@
private volatile boolean closed;
private volatile boolean endStreamSent;
+ final AtomicBoolean deRegistered = new AtomicBoolean(false);
+
// state flags
private boolean requestSent, responseReceived;
@@ -171,7 +174,7 @@
Http2Frame frame = inputQ.peek();
if (frame instanceof ResetFrame) {
inputQ.remove();
- handleReset((ResetFrame)frame);
+ handleReset((ResetFrame)frame, subscriber);
return;
}
DataFrame df = (DataFrame)frame;
@@ -185,6 +188,7 @@
Log.logTrace("responseSubscriber.onComplete");
if (debug.on()) debug.log("incoming: onComplete");
sched.stop();
+ connection.decrementStreamsCount(streamid);
subscriber.onComplete();
onCompleteCalled = true;
setEndStreamReceived();
@@ -198,6 +202,7 @@
Log.logTrace("responseSubscriber.onComplete");
if (debug.on()) debug.log("incoming: onComplete");
sched.stop();
+ connection.decrementStreamsCount(streamid);
subscriber.onComplete();
onCompleteCalled = true;
setEndStreamReceived();
@@ -251,6 +256,10 @@
return true; // end of stream
}
+ boolean deRegister() {
+ return deRegistered.compareAndSet(false, true);
+ }
+
@Override
CompletableFuture<T> readBodyAsync(HttpResponse.BodyHandler<T> handler,
boolean returnConnectionToPool,
@@ -258,6 +267,7 @@
{
try {
Log.logTrace("Reading body on stream {0}", streamid);
+ debug.log("Getting BodySubscriber for: " + response);
BodySubscriber<T> bodySubscriber = handler.apply(new ResponseInfoImpl(response));
CompletableFuture<T> cf = receiveData(bodySubscriber, executor);
@@ -337,10 +347,9 @@
this.windowController = windowController;
this.request = e.request();
this.requestPublisher = request.requestPublisher; // may be null
- responseHeaders = new HttpHeadersImpl();
- rspHeadersConsumer = new HeadersConsumer();
- this.requestPseudoHeaders = new HttpHeadersImpl();
- // NEW
+ this.responseHeadersBuilder = new HttpHeadersBuilder();
+ this.rspHeadersConsumer = new HeadersConsumer();
+ this.requestPseudoHeaders = createPseudoHeaders(request);
this.windowUpdater = new StreamWindowUpdateSender(connection);
}
@@ -391,6 +400,7 @@
}
protected void handleResponse() throws IOException {
+ HttpHeaders responseHeaders = responseHeadersBuilder.build();
responseCode = (int)responseHeaders
.firstValueAsLong(":status")
.orElseThrow(() -> new IOException("no statuscode in response"));
@@ -424,25 +434,57 @@
} else if (closed) {
Log.logTrace("Ignoring RST_STREAM frame received on closed stream {0}", streamid);
} else {
- // put it in the input queue in order to read all
- // pending data frames first. Indeed, a server may send
- // RST_STREAM after sending END_STREAM, in which case we should
- // ignore it. However, we won't know if we have received END_STREAM
- // or not until all pending data frames are read.
- receiveResetFrame(frame);
- // RST_STREAM was pushed to the queue. It will be handled by
- // asyncReceive after all pending data frames have been
- // processed.
- Log.logTrace("RST_STREAM pushed in queue for stream {0}", streamid);
+ Flow.Subscriber<?> subscriber =
+ responseSubscriber == null ? pendingResponseSubscriber : responseSubscriber;
+ if (response == null && subscriber == null) {
+ // we haven't receive the headers yet, and won't receive any!
+ // handle reset now.
+ handleReset(frame, subscriber);
+ } else {
+ // put it in the input queue in order to read all
+ // pending data frames first. Indeed, a server may send
+ // RST_STREAM after sending END_STREAM, in which case we should
+ // ignore it. However, we won't know if we have received END_STREAM
+ // or not until all pending data frames are read.
+ receiveResetFrame(frame);
+ // RST_STREAM was pushed to the queue. It will be handled by
+ // asyncReceive after all pending data frames have been
+ // processed.
+ Log.logTrace("RST_STREAM pushed in queue for stream {0}", streamid);
+ }
}
}
- void handleReset(ResetFrame frame) {
+ void handleReset(ResetFrame frame, Flow.Subscriber<?> subscriber) {
Log.logTrace("Handling RST_STREAM on stream {0}", streamid);
if (!closed) {
- close();
- int error = frame.getErrorCode();
- completeResponseExceptionally(new IOException(ErrorFrame.stringForCode(error)));
+ synchronized (this) {
+ if (closed) {
+ if (debug.on()) debug.log("Stream already closed: ignoring RESET");
+ return;
+ }
+ closed = true;
+ }
+ try {
+ int error = frame.getErrorCode();
+ IOException e = new IOException("Received RST_STREAM: "
+ + ErrorFrame.stringForCode(error));
+ if (errorRef.compareAndSet(null, e)) {
+ if (subscriber != null) {
+ subscriber.onError(e);
+ }
+ }
+ completeResponseExceptionally(e);
+ if (!requestBodyCF.isDone()) {
+ requestBodyCF.completeExceptionally(errorRef.get()); // we may be sending the body..
+ }
+ if (responseBodyCF != null) {
+ responseBodyCF.completeExceptionally(errorRef.get());
+ }
+ } finally {
+ connection.decrementStreamsCount(streamid);
+ connection.closeStream(streamid);
+ }
} else {
Log.logTrace("Ignoring RST_STREAM frame received on closed stream {0}", streamid);
}
@@ -535,13 +577,12 @@
}
private OutgoingHeaders<Stream<T>> headerFrame(long contentLength) {
- HttpHeadersImpl h = request.getSystemHeaders();
+ HttpHeadersBuilder h = request.getSystemHeadersBuilder();
if (contentLength > 0) {
h.setHeader("content-length", Long.toString(contentLength));
}
- setPseudoHeaderFields();
- HttpHeaders sysh = filter(h);
- HttpHeaders userh = filter(request.getUserHeaders());
+ HttpHeaders sysh = filterHeaders(h.build());
+ HttpHeaders userh = filterHeaders(request.getUserHeaders());
OutgoingHeaders<Stream<T>> f = new OutgoingHeaders<>(sysh, userh, this);
if (contentLength == 0) {
f.setFlag(HeadersFrame.END_STREAM);
@@ -565,7 +606,7 @@
// If nothing needs filtering then we can just use the
// original headers.
private boolean needsFiltering(HttpHeaders headers,
- BiPredicate<String, List<String>> filter) {
+ BiPredicate<String, String> filter) {
if (filter == Utils.PROXY_TUNNEL_FILTER || filter == Utils.PROXY_FILTER) {
// we're either connecting or proxying
// slight optimization: we only need to filter out
@@ -583,18 +624,17 @@
}
}
- private HttpHeaders filter(HttpHeaders headers) {
+ private HttpHeaders filterHeaders(HttpHeaders headers) {
HttpConnection conn = connection();
- BiPredicate<String, List<String>> filter =
- conn.headerFilter(request);
+ BiPredicate<String, String> filter = conn.headerFilter(request);
if (needsFiltering(headers, filter)) {
- return ImmutableHeaders.of(headers.map(), filter);
+ return HttpHeaders.of(headers.map(), filter);
}
return headers;
}
- private void setPseudoHeaderFields() {
- HttpHeadersImpl hdrs = requestPseudoHeaders;
+ private static HttpHeaders createPseudoHeaders(HttpRequest request) {
+ HttpHeadersBuilder hdrs = new HttpHeadersBuilder();
String method = request.method();
hdrs.setHeader(":method", method);
URI uri = request.uri();
@@ -615,9 +655,10 @@
path += "?" + query;
}
hdrs.setHeader(":path", Utils.encode(path));
+ return hdrs.build();
}
- HttpHeadersImpl getRequestPseudoHeaders() {
+ HttpHeaders getRequestPseudoHeaders() {
return requestPseudoHeaders;
}
@@ -657,6 +698,7 @@
if (streamid > 0) {
if (debug.on()) debug.log("Released stream %d", streamid);
// remove this stream from the Http2Connection map.
+ connection.decrementStreamsCount(streamid);
connection.closeStream(streamid);
} else {
if (debug.on()) debug.log("Can't release stream %d", streamid);
@@ -945,14 +987,18 @@
if (!cf.isDone()) {
Log.logTrace("Completing response (streamid={0}): {1}",
streamid, cf);
+ if (debug.on())
+ debug.log("Completing responseCF(%d) with response headers", i);
+ response_cfs.remove(cf);
cf.complete(resp);
- response_cfs.remove(cf);
return;
} // else we found the previous response: just leave it alone.
}
cf = MinimalFuture.completedFuture(resp);
Log.logTrace("Created completed future (streamid={0}): {1}",
streamid, cf);
+ if (debug.on())
+ debug.log("Adding completed responseCF(0) with response headers");
response_cfs.add(cf);
}
}
@@ -983,8 +1029,8 @@
for (int i = 0; i < response_cfs.size(); i++) {
CompletableFuture<Response> cf = response_cfs.get(i);
if (!cf.isDone()) {
+ response_cfs.remove(i);
cf.completeExceptionally(t);
- response_cfs.remove(i);
return;
}
}
@@ -1033,6 +1079,15 @@
cancelImpl(cause);
}
+ void connectionClosing(Throwable cause) {
+ Flow.Subscriber<?> subscriber =
+ responseSubscriber == null ? pendingResponseSubscriber : responseSubscriber;
+ errorRef.compareAndSet(null, cause);
+ if (subscriber != null && !sched.isStopped() && !inputQ.isEmpty()) {
+ sched.runOrSchedule();
+ } else cancelImpl(cause);
+ }
+
// This method sends a RST_STREAM frame
void cancelImpl(Throwable e) {
errorRef.compareAndSet(null, e);
@@ -1062,7 +1117,14 @@
try {
// will send a RST_STREAM frame
if (streamid != 0) {
- connection.resetStream(streamid, ResetFrame.CANCEL);
+ connection.decrementStreamsCount(streamid);
+ e = Utils.getCompletionCause(e);
+ if (e instanceof EOFException) {
+ // read EOF: no need to try & send reset
+ connection.closeStream(streamid);
+ } else {
+ connection.resetStream(streamid, ResetFrame.CANCEL);
+ }
}
} catch (IOException ex) {
Log.logError(ex);
@@ -1184,6 +1246,7 @@
// create and return the PushResponseImpl
@Override
protected void handleResponse() {
+ HttpHeaders responseHeaders = responseHeadersBuilder.build();
responseCode = (int)responseHeaders
.firstValueAsLong(":status")
.orElse(-1);
@@ -1252,17 +1315,18 @@
void reset() {
super.reset();
- responseHeaders.clear();
+ responseHeadersBuilder.clear();
+ debug.log("Response builder cleared, ready to receive new headers.");
}
@Override
public void onDecoded(CharSequence name, CharSequence value)
- throws UncheckedIOException
+ throws UncheckedIOException
{
String n = name.toString();
String v = value.toString();
super.onDecoded(n, v);
- responseHeaders.addHeader(n, v);
+ responseHeadersBuilder.addHeader(n, v);
if (Log.headers() && Log.trace()) {
Log.logTrace("RECEIVED HEADER (streamid={0}): {1}: {2}",
streamid, n, v);
--- a/src/java.net.http/share/classes/jdk/internal/net/http/common/ConnectionExpiredException.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/ConnectionExpiredException.java Wed Jun 20 09:05:57 2018 -0700
@@ -35,13 +35,12 @@
private static final long serialVersionUID = 0;
/**
- * Constructs a {@code ConnectionExpiredException} with the specified detail
- * message and cause.
+ * Constructs a {@code ConnectionExpiredException} with a detail message of
+ * "subscription is finished" and the given cause.
*
- * @param s the detail message
* @param cause the throwable cause
*/
- public ConnectionExpiredException(String s, Throwable cause) {
- super(s, cause);
+ public ConnectionExpiredException(Throwable cause) {
+ super("subscription is finished", cause);
}
}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/HttpHeadersBuilder.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,85 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.internal.net.http.common;
+
+import java.net.http.HttpHeaders;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import static jdk.internal.net.http.common.Utils.ACCEPT_ALL;
+
+/** A mutable builder for collecting and building HTTP headers. */
+public class HttpHeadersBuilder {
+
+ private final TreeMap<String, List<String>> headersMap;
+
+ public HttpHeadersBuilder() {
+ headersMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ }
+
+ public HttpHeadersBuilder structuralCopy() {
+ HttpHeadersBuilder builder = new HttpHeadersBuilder();
+ for (Map.Entry<String, List<String>> entry : headersMap.entrySet()) {
+ List<String> valuesCopy = new ArrayList<>(entry.getValue());
+ builder.headersMap.put(entry.getKey(), valuesCopy);
+ }
+ return builder;
+ }
+
+ public void addHeader(String name, String value) {
+ headersMap.computeIfAbsent(name, k -> new ArrayList<>(1))
+ .add(value);
+ }
+
+ public void setHeader(String name, String value) {
+ // headers typically have one value
+ List<String> values = new ArrayList<>(1);
+ values.add(value);
+ headersMap.put(name, values);
+ }
+
+ public void clear() {
+ headersMap.clear();
+ }
+
+ public Map<String, List<String>> map() {
+ return headersMap;
+ }
+
+ public HttpHeaders build() {
+ return HttpHeaders.of(headersMap, ACCEPT_ALL);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(super.toString()).append(" { ");
+ sb.append(map());
+ sb.append(" }");
+ return sb.toString();
+ }
+}
--- a/src/java.net.http/share/classes/jdk/internal/net/http/common/HttpHeadersImpl.java Wed Jun 20 17:15:16 2018 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,89 +0,0 @@
-/*
- * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation. Oracle designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
-package jdk.internal.net.http.common;
-
-import java.net.http.HttpHeaders;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.TreeMap;
-
-/**
- * Implementation of HttpHeaders.
- *
- * The public HttpHeaders API provides a read-only view, while the
- * non-HttpHeaders members allow for implementation specific mutation, e.g.
- * during creation, etc.
- */
-public class HttpHeadersImpl extends HttpHeaders {
-
- private final TreeMap<String, List<String>> headers;
-
- public HttpHeadersImpl() {
- headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
- }
-
- @Override
- public Map<String, List<String>> map() {
- return Collections.unmodifiableMap(headersMap());
- }
-
- // non-HttpHeaders private mutators
-
- public HttpHeadersImpl deepCopy() {
- HttpHeadersImpl h1 = newDeepCopy();
- for (Map.Entry<String, List<String>> entry : headersMap().entrySet()) {
- List<String> valuesCopy = new ArrayList<>(entry.getValue());
- h1.headersMap().put(entry.getKey(), valuesCopy);
- }
- return h1;
- }
-
- public void addHeader(String name, String value) {
- headersMap().computeIfAbsent(name, k -> new ArrayList<>(1))
- .add(value);
- }
-
- public void setHeader(String name, String value) {
- // headers typically have one value
- List<String> values = new ArrayList<>(1);
- values.add(value);
- headersMap().put(name, values);
- }
-
- public void clear() {
- headersMap().clear();
- }
-
- protected HttpHeadersImpl newDeepCopy() {
- return new HttpHeadersImpl();
- }
-
- protected Map<String, List<String>> headersMap() {
- return headers;
- }
-}
--- a/src/java.net.http/share/classes/jdk/internal/net/http/common/Log.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/Log.java Wed Jun 20 09:05:57 2018 -0700
@@ -43,7 +43,7 @@
/**
* -Djava.net.HttpClient.log=
* errors,requests,headers,
- * frames[:control:data:window:all..],content,ssl,trace
+ * frames[:control:data:window:all..],content,ssl,trace,channel
*
* Any of errors, requests, headers or content are optional.
*
@@ -65,6 +65,7 @@
public static final int FRAMES = 0x10;
public static final int SSL = 0x20;
public static final int TRACE = 0x40;
+ public static final int CHANNEL = 0x80;
static int logging;
// Frame types: "control", "data", "window", "all"
@@ -99,11 +100,15 @@
case "ssl":
logging |= SSL;
break;
+ case "channel":
+ logging |= CHANNEL;
+ break;
case "trace":
logging |= TRACE;
break;
case "all":
- logging |= CONTENT|HEADERS|REQUESTS|FRAMES|ERRORS|TRACE|SSL;
+ logging |= CONTENT|HEADERS|REQUESTS|FRAMES|ERRORS|TRACE|SSL| CHANNEL;
+ frametypes |= ALL;
break;
default:
// ignore bad values
@@ -166,6 +171,10 @@
return (logging & FRAMES) != 0;
}
+ public static boolean channel() {
+ return (logging & CHANNEL) != 0;
+ }
+
public static void logError(String s, Object... s1) {
if (errors()) {
logger.log(Level.INFO, "ERROR: " + s, s1);
@@ -191,9 +200,21 @@
}
}
+ public static void logChannel(String s, Object... s1) {
+ if (channel()) {
+ logger.log(Level.INFO, "CHANNEL: " + s, s1);
+ }
+ }
+
+ public static void logChannel(Supplier<String> msgSupplier) {
+ if (channel()) {
+ logger.log(Level.INFO, "CHANNEL: " + msgSupplier.get());
+ }
+ }
+
public static void logTrace(String s, Object... s1) {
if (trace()) {
- String format = "TRACE: " + s;
+ String format = "MISC: " + s;
logger.log(Level.INFO, format, s1);
}
}
--- a/src/java.net.http/share/classes/jdk/internal/net/http/common/SSLFlowDelegate.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/SSLFlowDelegate.java Wed Jun 20 09:05:57 2018 -0700
@@ -46,6 +46,7 @@
import java.util.concurrent.Flow.Subscriber;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
+import java.util.function.IntBinaryOperator;
/**
* Implements SSL using two SubscriberWrappers.
@@ -87,13 +88,18 @@
final Logger debug =
Utils.getDebugLogger(this::dbgString, Utils.DEBUG);
+ private static final ByteBuffer SENTINEL = Utils.EMPTY_BYTEBUFFER;
+ private static final ByteBuffer HS_TRIGGER = ByteBuffer.allocate(0);
+ // When handshake is in progress trying to wrap may produce no bytes.
+ private static final ByteBuffer NOTHING = ByteBuffer.allocate(0);
+ private static final String monProp = Utils.getProperty("jdk.internal.httpclient.monitorFlowDelegate");
+
final Executor exec;
final Reader reader;
final Writer writer;
final SSLEngine engine;
final String tubeName; // hack
final CompletableFuture<String> alpnCF; // completes on initial handshake
- final static ByteBuffer SENTINEL = Utils.EMPTY_BYTEBUFFER;
volatile boolean close_notify_received;
final CompletableFuture<Void> readerCF;
final CompletableFuture<Void> writerCF;
@@ -146,7 +152,8 @@
// Writer to the downWriter.
connect(downReader, downWriter);
- //Monitor.add(this::monitor);
+ if (monProp != null && (monProp.equals("") || monProp.equalsIgnoreCase("true")))
+ Monitor.add(this::monitor);
}
/**
@@ -245,13 +252,16 @@
final Logger debugr = Utils.getDebugLogger(this::dbgString, Utils.DEBUG);
private final class ReaderDownstreamPusher implements Runnable {
- @Override public void run() { processData(); }
+ @Override
+ public void run() {
+ processData();
+ }
}
Reader() {
super();
scheduler = SequentialScheduler.synchronizedScheduler(
- new ReaderDownstreamPusher());
+ new ReaderDownstreamPusher());
this.readBuf = ByteBuffer.allocate(1024);
readBuf.limit(0); // keep in read mode
}
@@ -276,7 +286,7 @@
public void incoming(List<ByteBuffer> buffers, boolean complete) {
if (debugr.on())
debugr.log("Adding %d bytes to read buffer",
- Utils.remaining(buffers));
+ Utils.remaining(buffers));
addToReadBuf(buffers, complete);
scheduler.runOrSchedule(exec);
}
@@ -289,7 +299,7 @@
private void reallocReadBuf() {
int sz = readBuf.capacity();
- ByteBuffer newb = ByteBuffer.allocate(sz*2);
+ ByteBuffer newb = ByteBuffer.allocate(sz * 2);
readBuf.flip();
Utils.copy(readBuf, newb);
readBuf = newb;
@@ -300,7 +310,7 @@
if (readBuf.remaining() > TARGET_BUFSIZE) {
if (debugr.on())
debugr.log("readBuf has more than TARGET_BUFSIZE: %d",
- readBuf.remaining());
+ readBuf.remaining());
return 0;
} else {
return super.upstreamWindowUpdate(currentWindow, downstreamQsize);
@@ -309,6 +319,7 @@
// readBuf is kept ready for reading outside of this method
private void addToReadBuf(List<ByteBuffer> buffers, boolean complete) {
+ assert Utils.remaining(buffers) > 0 || buffers.isEmpty();
synchronized (readBufferLock) {
for (ByteBuffer buf : buffers) {
readBuf.compact();
@@ -344,14 +355,15 @@
// In this case we need to wait for more bytes than what
// we had before calling unwrap() again.
volatile int minBytesRequired;
+
// work function where it all happens
final void processData() {
try {
if (debugr.on())
debugr.log("processData:"
- + " readBuf remaining:" + readBuf.remaining()
- + ", state:" + states(handshakeState)
- + ", engine handshake status:" + engine.getHandshakeStatus());
+ + " readBuf remaining:" + readBuf.remaining()
+ + ", state:" + states(handshakeState)
+ + ", engine handshake status:" + engine.getHandshakeStatus());
int len;
boolean complete = false;
while (readBuf.remaining() > (len = minBytesRequired)) {
@@ -400,14 +412,13 @@
outgoing(Utils.EMPTY_BB_LIST, true);
return;
}
- if (result.handshaking() && !complete) {
+ if (result.handshaking()) {
+ handshaking = true;
if (debugr.on()) debugr.log("handshaking");
- if (doHandshake(result, READER)) {
- resumeActivity();
- }
- handshaking = true;
+ if (doHandshake(result, READER)) continue; // need unwrap
+ else break; // doHandshake will have triggered the write scheduler if necessary
} else {
- if ((handshakeState.getAndSet(NOT_HANDSHAKING)& ~DOING_TASKS) == HANDSHAKING) {
+ if ((handshakeState.getAndSet(NOT_HANDSHAKING) & ~DOING_TASKS) == HANDSHAKING) {
handshaking = false;
applicationBufferSize = engine.getSession().getApplicationBufferSize();
packetBufferSize = engine.getSession().getPacketBufferSize();
@@ -443,12 +454,19 @@
EngineResult unwrapBuffer(ByteBuffer src) throws IOException {
ByteBuffer dst = getAppBuffer();
+ int len = src.remaining();
while (true) {
SSLEngineResult sslResult = engine.unwrap(src, dst);
switch (sslResult.getStatus()) {
case BUFFER_OVERFLOW:
- // may happen only if app size buffer was changed.
- // get it again if app buffer size changed
+ // may happen if app size buffer was changed, or if
+ // our 'adaptiveBufferSize' guess was too small for
+ // the current payload. In that case, update the
+ // value of applicationBufferSize, and allocate a
+ // buffer of that size, which we are sure will be
+ // big enough to decode whatever needs to be
+ // decoded. We will later update adaptiveBufferSize
+ // in OK: below.
int appSize = applicationBufferSize =
engine.getSession().getApplicationBufferSize();
ByteBuffer b = ByteBuffer.allocate(appSize + dst.position());
@@ -457,11 +475,26 @@
dst = b;
break;
case CLOSED:
+ assert dst.position() == 0;
return doClosure(new EngineResult(sslResult));
case BUFFER_UNDERFLOW:
// handled implicitly by compaction/reallocation of readBuf
+ assert dst.position() == 0;
return new EngineResult(sslResult);
case OK:
+ int size = dst.position();
+ if (debug.on()) {
+ debugr.log("Decoded " + size + " bytes out of " + len
+ + " into buffer of " + dst.capacity()
+ + " remaining to decode: " + src.remaining());
+ }
+ // if the record payload was bigger than what was originally
+ // allocated, then sets the adaptiveAppBufferSize to size
+ // and we will use that new size as a guess for the next app
+ // buffer.
+ if (size > adaptiveAppBufferSize) {
+ adaptiveAppBufferSize = ((size + 7) >>> 3) << 3;
+ }
dst.flip();
return new EngineResult(sslResult, dst);
}
@@ -662,8 +695,8 @@
}
cleanList(writeList); // tidy up the source list
sendResultBytes(result);
- if (handshaking && !completing) {
- if (needWrap()) {
+ if (handshaking) {
+ if (!completing && needWrap()) {
continue;
} else {
return;
@@ -687,11 +720,30 @@
}
}
+ // The SSLEngine insists on being given a buffer that is at least
+ // SSLSession.getPacketBufferSize() long (usually 16K). If given
+ // a smaller buffer it will go in BUFFER_OVERFLOW, even if it only
+ // has 6 bytes to wrap. Typical usage shows that for GET we
+ // usually produce an average of ~ 100 bytes.
+ // To avoid wasting space, and because allocating and zeroing
+ // 16K buffers for encoding 6 bytes is costly, we are reusing the
+ // same writeBuffer to interact with SSLEngine.wrap().
+ // If the SSLEngine produces less than writeBuffer.capacity() / 2,
+ // then we copy off the bytes to a smaller buffer that we send
+ // downstream. Otherwise, we send the writeBuffer downstream
+ // and will allocate a new one next time.
+ volatile ByteBuffer writeBuffer;
@SuppressWarnings("fallthrough")
EngineResult wrapBuffers(ByteBuffer[] src) throws SSLException {
+ long len = Utils.remaining(src);
if (debugw.on())
- debugw.log("wrapping " + Utils.remaining(src) + " bytes");
- ByteBuffer dst = getNetBuffer();
+ debugw.log("wrapping " + len + " bytes");
+
+ ByteBuffer dst = writeBuffer;
+ if (dst == null) dst = writeBuffer = getNetBuffer();
+ assert dst.position() == 0 : "buffer position is " + dst.position();
+ assert dst.hasRemaining() : "buffer has no remaining space: capacity=" + dst.capacity();
+
while (true) {
SSLEngineResult sslResult = engine.wrap(src, dst);
if (debugw.on()) debugw.log("SSLResult: " + sslResult);
@@ -702,7 +754,7 @@
if (debugw.on()) debugw.log("BUFFER_OVERFLOW");
int netSize = packetBufferSize
= engine.getSession().getPacketBufferSize();
- ByteBuffer b = ByteBuffer.allocate(netSize + dst.position());
+ ByteBuffer b = writeBuffer = ByteBuffer.allocate(netSize + dst.position());
dst.flip();
b.put(dst);
dst = b;
@@ -712,11 +764,27 @@
// fallthrough. There could be some remaining data in dst.
// CLOSED will be handled by the caller.
case OK:
- dst.flip();
- final ByteBuffer dest = dst;
+ final ByteBuffer dest;
+ if (dst.position() == 0) {
+ dest = NOTHING; // can happen if handshake is in progress
+ } else if (dst.position() < dst.capacity() / 2) {
+ // less than half the buffer was used.
+ // copy off the bytes to a smaller buffer, and keep
+ // the writeBuffer for next time.
+ dst.flip();
+ dest = Utils.copyAligned(dst);
+ dst.clear();
+ } else {
+ // more than half the buffer was used.
+ // just send that buffer downstream, and we will
+ // get a new writeBuffer next time it is needed.
+ dst.flip();
+ dest = dst;
+ writeBuffer = null;
+ }
if (debugw.on())
- debugw.log("OK => produced: %d, not wrapped: %d",
- dest.remaining(), Utils.remaining(src));
+ debugw.log("OK => produced: %d bytes into %d, not wrapped: %d",
+ dest.remaining(), dest.capacity(), Utils.remaining(src));
return new EngineResult(sslResult, dest);
case BUFFER_UNDERFLOW:
// Shouldn't happen. Doesn't returns when wrap()
@@ -799,8 +867,12 @@
private static final int NOT_HANDSHAKING = 0;
private static final int HANDSHAKING = 1;
- private static final int DOING_TASKS = 4; // bit added to above state
- private static final ByteBuffer HS_TRIGGER = ByteBuffer.allocate(0);
+ // Bit flags
+ // a thread is currently executing tasks
+ private static final int DOING_TASKS = 4;
+ // a thread wants to execute tasks, while another thread is executing
+ private static final int REQUESTING_TASKS = 8;
+ private static final int TASK_BITS = 12; // Both bits
private static final int READER = 1;
private static final int WRITER = 2;
@@ -808,7 +880,7 @@
private static String states(AtomicInteger state) {
int s = state.get();
StringBuilder sb = new StringBuilder();
- int x = s & ~DOING_TASKS;
+ int x = s & ~TASK_BITS;
switch (x) {
case NOT_HANDSHAKING:
sb.append(" NOT_HANDSHAKING ");
@@ -821,6 +893,8 @@
}
if ((s & DOING_TASKS) > 0)
sb.append("|DOING_TASKS");
+ if ((s & REQUESTING_TASKS) > 0)
+ sb.append("|REQUESTING_TASKS");
return sb.toString();
}
@@ -833,18 +907,37 @@
final ConcurrentLinkedQueue<String> stateList =
debug.on() ? new ConcurrentLinkedQueue<>() : null;
+ // Atomically executed to update task bits. Sets either DOING_TASKS or REQUESTING_TASKS
+ // depending on previous value
+ private static final IntBinaryOperator REQUEST_OR_DO_TASKS = (current, ignored) -> {
+ if ((current & DOING_TASKS) == 0)
+ return DOING_TASKS | (current & HANDSHAKING);
+ else
+ return DOING_TASKS | REQUESTING_TASKS | (current & HANDSHAKING);
+ };
+
+ // Atomically executed to update task bits. Sets DOING_TASKS if REQUESTING was set
+ // clears bits if not.
+ private static final IntBinaryOperator FINISH_OR_DO_TASKS = (current, ignored) -> {
+ if ((current & REQUESTING_TASKS) != 0)
+ return DOING_TASKS | (current & HANDSHAKING);
+ // clear both bits
+ return (current & HANDSHAKING);
+ };
+
private boolean doHandshake(EngineResult r, int caller) {
- // unconditionally sets the HANDSHAKING bit, while preserving DOING_TASKS
- handshakeState.getAndAccumulate(HANDSHAKING, (current, update) -> update | (current & DOING_TASKS));
+ // unconditionally sets the HANDSHAKING bit, while preserving task bits
+ handshakeState.getAndAccumulate(0, (current, unused) -> HANDSHAKING | (current & TASK_BITS));
if (stateList != null && debug.on()) {
stateList.add(r.handshakeStatus().toString());
stateList.add(Integer.toString(caller));
}
switch (r.handshakeStatus()) {
case NEED_TASK:
- int s = handshakeState.getAndUpdate((current) -> current | DOING_TASKS);
- if ((s & DOING_TASKS) > 0) // someone else was doing tasks
+ int s = handshakeState.accumulateAndGet(0, REQUEST_OR_DO_TASKS);
+ if ((s & REQUESTING_TASKS) > 0) { // someone else is or will do tasks
return false;
+ }
if (debug.on()) debug.log("obtaining and initiating task execution");
List<Runnable> tasks = obtainTasks();
@@ -878,20 +971,25 @@
}
private void executeTasks(List<Runnable> tasks) {
- if (tasks.isEmpty())
- return;
exec.execute(() -> {
try {
List<Runnable> nextTasks = tasks;
+ if (debug.on()) debug.log("#tasks to execute: " + Integer.toString(nextTasks.size()));
do {
nextTasks.forEach(Runnable::run);
if (engine.getHandshakeStatus() == HandshakeStatus.NEED_TASK) {
nextTasks = obtainTasks();
} else {
+ int s = handshakeState.accumulateAndGet(0, FINISH_OR_DO_TASKS);
+ if ((s & DOING_TASKS) != 0) {
+ if (debug.on()) debug.log("re-running tasks (B)");
+ nextTasks = obtainTasks();
+ continue;
+ }
break;
}
} while (true);
- handshakeState.getAndUpdate((current) -> current & ~DOING_TASKS);
+ if (debug.on()) debug.log("finished task execution");
resumeActivity();
} catch (Throwable t) {
handleError(t);
@@ -997,6 +1095,8 @@
}
}
+ // The maximum network buffer size negotiated during
+ // the handshake. Usually 16K.
volatile int packetBufferSize;
final ByteBuffer getNetBuffer() {
int netSize = packetBufferSize;
@@ -1006,13 +1106,32 @@
return ByteBuffer.allocate(netSize);
}
+ // The maximum application buffer size negotiated during
+ // the handshake. Usually close to 16K.
volatile int applicationBufferSize;
+ // Despite of the maximum applicationBufferSize negotiated
+ // above, TLS records usually have a much smaller payload.
+ // The adaptativeAppBufferSize records the max payload
+ // ever decoded, and we use that as a guess for how big
+ // a buffer we will need for the next payload.
+ // This avoids allocating and zeroing a 16K buffer for
+ // nothing...
+ volatile int adaptiveAppBufferSize;
final ByteBuffer getAppBuffer() {
int appSize = applicationBufferSize;
if (appSize <= 0) {
- applicationBufferSize = appSize = engine.getSession().getApplicationBufferSize();
+ applicationBufferSize = appSize
+ = engine.getSession().getApplicationBufferSize();
}
- return ByteBuffer.allocate(appSize);
+ int size = adaptiveAppBufferSize;
+ if (size <= 0) {
+ size = 512; // start with 512 this is usually enough for handshaking / headers
+ } else if (size > appSize) {
+ size = appSize;
+ }
+ // will cause a BUFFER_OVERFLOW if not big enough, but
+ // that's OK.
+ return ByteBuffer.allocate(size);
}
final String dbgString() {
--- a/src/java.net.http/share/classes/jdk/internal/net/http/common/SSLTube.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/SSLTube.java Wed Jun 20 09:05:57 2018 -0700
@@ -309,7 +309,7 @@
synchronized (this) {
previous = pendingDelegate.getAndSet(delegateWrapper);
subscription = readSubscription;
- handleNow = this.errorRef.get() != null || finished;
+ handleNow = this.errorRef.get() != null || onCompleteReceived;
}
if (previous != null) {
previous.dropSubscription();
@@ -424,12 +424,20 @@
// if onError is invoked concurrently with setDelegate.
synchronized (this) {
failed = this.errorRef.get();
- completed = finished;
+ completed = onCompleteReceived;
subscribed = subscriberImpl;
}
+
if (failed != null) {
+ if (debug.on())
+ debug.log("onNewSubscription: subscriberImpl:%s, invoking onError:%s",
+ subscriberImpl, failed);
subscriberImpl.onError(failed);
} else if (completed) {
+ if (debug.on())
+ debug.log("onNewSubscription: subscriberImpl:%s, invoking onCompleted",
+ subscriberImpl);
+ finished = true;
subscriberImpl.onComplete();
}
}
@@ -490,7 +498,6 @@
@Override
public void onComplete() {
assert !finished && !onCompleteReceived;
- onCompleteReceived = true;
DelegateWrapper subscriberImpl;
synchronized(this) {
subscriberImpl = subscribed;
@@ -505,8 +512,10 @@
onErrorImpl(new SSLHandshakeException(
"Remote host terminated the handshake"));
} else if (subscriberImpl != null) {
- finished = true;
+ onCompleteReceived = finished = true;
subscriberImpl.onComplete();
+ } else {
+ onCompleteReceived = true;
}
// now if we have any pending subscriber, we should complete
// them immediately as the read scheduler will already be stopped.
@@ -528,12 +537,17 @@
final class SSLSubscriptionWrapper implements Flow.Subscription {
volatile Flow.Subscription delegate;
+ private volatile boolean cancelled;
void setSubscription(Flow.Subscription sub) {
long demand = writeDemand.get(); // FIXME: isn't it a racy way of passing the demand?
delegate = sub;
- if (debug.on()) debug.log("setSubscription: demand=%d", demand);
- if (demand > 0)
+ if (debug.on())
+ debug.log("setSubscription: demand=%d, cancelled:%s", demand, cancelled);
+
+ if (cancelled)
+ delegate.cancel();
+ else if (demand > 0)
sub.request(demand);
}
@@ -549,7 +563,9 @@
@Override
public void cancel() {
- // TODO: no-op or error?
+ cancelled = true;
+ if (delegate != null)
+ delegate.cancel();
}
}
--- a/src/java.net.http/share/classes/jdk/internal/net/http/common/SubscriberWrapper.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/SubscriberWrapper.java Wed Jun 20 09:05:57 2018 -0700
@@ -260,6 +260,8 @@
try {
run1();
} catch (Throwable t) {
+ if (debug.on())
+ debug.log("DownstreamPusher threw: " + t);
errorCommon(t);
}
}
@@ -292,6 +294,7 @@
pushScheduler.stop();
outputQ.clear();
downstreamSubscriber.onError(error);
+ cf.completeExceptionally(error);
return;
}
@@ -383,9 +386,8 @@
(throwable = new AssertionError("null throwable")) != null;
if (errorRef.compareAndSet(null, throwable)) {
if (debug.on()) debug.log("error", throwable);
+ upstreamCompleted = true;
pushScheduler.runOrSchedule();
- upstreamCompleted = true;
- cf.completeExceptionally(throwable);
return true;
}
return false;
--- a/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java Wed Jun 20 09:05:57 2018 -0700
@@ -30,19 +30,23 @@
import sun.net.www.HeaderParser;
import javax.net.ssl.ExtendedSSLSession;
+import javax.net.ssl.SSLException;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSession;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
+import java.io.EOFException;
import java.io.IOException;
import java.io.PrintStream;
import java.io.UncheckedIOException;
import java.io.UnsupportedEncodingException;
import java.lang.System.Logger.Level;
+import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URLPermission;
import java.net.http.HttpHeaders;
+import java.net.http.HttpTimeoutException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
@@ -58,9 +62,11 @@
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.function.BiPredicate;
+import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@@ -119,6 +125,8 @@
"jdk.httpclient.bufsize", DEFAULT_BUFSIZE
);
+ public static final BiPredicate<String,String> ACCEPT_ALL = (x,y) -> true;
+
private static final Set<String> DISALLOWED_HEADERS_SET;
static {
@@ -131,24 +139,21 @@
DISALLOWED_HEADERS_SET = Collections.unmodifiableSet(treeSet);
}
- public static final Predicate<String>
- ALLOWED_HEADERS = header -> !DISALLOWED_HEADERS_SET.contains(header);
+ public static final BiPredicate<String, String>
+ ALLOWED_HEADERS = (header, unused) -> !DISALLOWED_HEADERS_SET.contains(header);
- public static final BiPredicate<String, List<String>> VALIDATE_USER_HEADER =
- (name, lv) -> {
- requireNonNull(name, "header name");
- requireNonNull(lv, "header values");
+ public static final BiPredicate<String, String> VALIDATE_USER_HEADER =
+ (name, value) -> {
+ assert name != null : "null header name";
+ assert value != null : "null header value";
if (!isValidName(name)) {
throw newIAE("invalid header name: \"%s\"", name);
}
- if (!Utils.ALLOWED_HEADERS.test(name)) {
+ if (!Utils.ALLOWED_HEADERS.test(name, null)) {
throw newIAE("restricted header name: \"%s\"", name);
}
- for (String value : lv) {
- requireNonNull(value, "header value");
- if (!isValidValue(value)) {
- throw newIAE("invalid header value for %s: \"%s\"", name, value);
- }
+ if (!isValidValue(value)) {
+ throw newIAE("invalid header value for %s: \"%s\"", name, value);
}
return true;
};
@@ -180,9 +185,20 @@
.collect(Collectors.toUnmodifiableSet());
}
+ public static <T> CompletableFuture<T> wrapForDebug(Logger logger, String name, CompletableFuture<T> cf) {
+ if (logger.on()) {
+ return cf.handle((r,t) -> {
+ logger.log("%s completed %s", name, t == null ? "successfully" : t );
+ return cf;
+ }).thenCompose(Function.identity());
+ } else {
+ return cf;
+ }
+ }
+
private static final String WSPACES = " \t\r\n";
private static final boolean isAllowedForProxy(String name,
- List<String> value,
+ String value,
Set<String> disabledSchemes,
Predicate<String> allowedKeys) {
if (!allowedKeys.test(name)) return false;
@@ -191,21 +207,19 @@
if (value.isEmpty()) return false;
for (String scheme : disabledSchemes) {
int slen = scheme.length();
- for (String v : value) {
- int vlen = v.length();
- if (vlen == slen) {
- if (v.equalsIgnoreCase(scheme)) {
+ int vlen = value.length();
+ if (vlen == slen) {
+ if (value.equalsIgnoreCase(scheme)) {
+ return false;
+ }
+ } else if (vlen > slen) {
+ if (value.substring(0,slen).equalsIgnoreCase(scheme)) {
+ int c = value.codePointAt(slen);
+ if (WSPACES.indexOf(c) > -1
+ || Character.isSpaceChar(c)
+ || Character.isWhitespace(c)) {
return false;
}
- } else if (vlen > slen) {
- if (v.substring(0,slen).equalsIgnoreCase(scheme)) {
- int c = v.codePointAt(slen);
- if (WSPACES.indexOf(c) > -1
- || Character.isSpaceChar(c)
- || Character.isWhitespace(c)) {
- return false;
- }
- }
}
}
}
@@ -213,13 +227,13 @@
return true;
}
- public static final BiPredicate<String, List<String>> PROXY_TUNNEL_FILTER =
+ public static final BiPredicate<String, String> PROXY_TUNNEL_FILTER =
(s,v) -> isAllowedForProxy(s, v, PROXY_AUTH_TUNNEL_DISABLED_SCHEMES,
IS_PROXY_HEADER);
- public static final BiPredicate<String, List<String>> PROXY_FILTER =
+ public static final BiPredicate<String, String> PROXY_FILTER =
(s,v) -> isAllowedForProxy(s, v, PROXY_AUTH_DISABLED_SCHEMES,
ALL_HEADERS);
- public static final BiPredicate<String, List<String>> NO_PROXY_HEADERS_FILTER =
+ public static final BiPredicate<String, String> NO_PROXY_HEADERS_FILTER =
(n,v) -> Utils.NO_PROXY_HEADER.test(n);
@@ -256,6 +270,35 @@
return new IOException(t);
}
+ /**
+ * Adds a more specific exception detail message, based on the given
+ * exception type and the message supplier. This is primarily to present
+ * more descriptive messages in IOExceptions that may be visible to calling
+ * code.
+ *
+ * @return a possibly new exception that has as its detail message, the
+ * message from the messageSupplier, and the given throwable as its
+ * cause. Otherwise returns the given throwable
+ */
+ public static Throwable wrapWithExtraDetail(Throwable t,
+ Supplier<String> messageSupplier) {
+ if (!(t instanceof IOException))
+ return t;
+
+ String msg = messageSupplier.get();
+ if (msg == null)
+ return t;
+
+ if (t instanceof ConnectionExpiredException) {
+ IOException ioe = new IOException(msg, t.getCause());
+ t = new ConnectionExpiredException(ioe);
+ } else {
+ IOException ioe = new IOException(msg, t);
+ t = ioe;
+ }
+ return t;
+ }
+
private Utils() { }
/**
@@ -534,6 +577,16 @@
return dst;
}
+ public static ByteBuffer copyAligned(ByteBuffer src) {
+ int len = src.remaining();
+ int size = ((len + 7) >> 3) << 3;
+ assert size >= len;
+ ByteBuffer dst = ByteBuffer.allocate(size);
+ dst.put(src);
+ dst.flip();
+ return dst;
+ }
+
public static String dump(Object... objects) {
return Arrays.toString(objects);
}
@@ -901,6 +954,20 @@
return address;
}
+ public static Throwable toConnectException(Throwable e) {
+ if (e == null) return null;
+ e = getCompletionCause(e);
+ if (e instanceof ConnectException) return e;
+ if (e instanceof SecurityException) return e;
+ if (e instanceof SSLException) return e;
+ if (e instanceof Error) return e;
+ if (e instanceof HttpTimeoutException) return e;
+ Throwable cause = e;
+ e = new ConnectException(e.getMessage());
+ e.initCause(cause);
+ return e;
+ }
+
/**
* Returns the smallest (closest to zero) positive number {@code m} (which
* is also a power of 2) such that {@code n <= m}.
--- a/src/java.net.http/share/classes/jdk/internal/net/http/frame/SettingsFrame.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/frame/SettingsFrame.java Wed Jun 20 09:05:57 2018 -0700
@@ -110,14 +110,14 @@
return TYPE;
}
- public int getParameter(int paramID) {
+ public synchronized int getParameter(int paramID) {
if (paramID > MAX_PARAM) {
throw new IllegalArgumentException("illegal parameter");
}
return parameters[paramID - 1];
}
- public SettingsFrame setParameter(int paramID, int value) {
+ public synchronized SettingsFrame setParameter(int paramID, int value) {
if (paramID > MAX_PARAM) {
throw new IllegalArgumentException("illegal parameter");
}
@@ -166,7 +166,7 @@
// TODO: check these values
f.setParameter(ENABLE_PUSH, 1);
f.setParameter(HEADER_TABLE_SIZE, 4 * K);
- f.setParameter(MAX_CONCURRENT_STREAMS, 35);
+ f.setParameter(MAX_CONCURRENT_STREAMS, 100);
f.setParameter(INITIAL_WINDOW_SIZE, 64 * K - 1);
f.setParameter(MAX_FRAME_SIZE, 16 * K);
return f;
--- a/src/java.net.http/share/classes/jdk/internal/net/http/hpack/Decoder.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/Decoder.java Wed Jun 20 09:05:57 2018 -0700
@@ -28,6 +28,7 @@
import java.io.IOException;
import java.nio.ByteBuffer;
+import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import static jdk.internal.net.http.hpack.HPACK.Logger.Level.EXTRA;
@@ -66,7 +67,8 @@
private final Logger logger;
private static final AtomicLong DECODERS_IDS = new AtomicLong();
- private static final State[] states = new State[256];
+ /* An immutable list of states */
+ private static final List<State> states;
static {
// To be able to do a quick lookup, each of 256 possibilities are mapped
@@ -78,21 +80,23 @@
// I do it mainly for better debugging (to not go each time step by step
// through if...else tree). As for performance win for the decoding, I
// believe is negligible.
- for (int i = 0; i < states.length; i++) {
+ State[] s = new State[256];
+ for (int i = 0; i < s.length; i++) {
if ((i & 0b1000_0000) == 0b1000_0000) {
- states[i] = State.INDEXED;
+ s[i] = State.INDEXED;
} else if ((i & 0b1100_0000) == 0b0100_0000) {
- states[i] = State.LITERAL_WITH_INDEXING;
+ s[i] = State.LITERAL_WITH_INDEXING;
} else if ((i & 0b1110_0000) == 0b0010_0000) {
- states[i] = State.SIZE_UPDATE;
+ s[i] = State.SIZE_UPDATE;
} else if ((i & 0b1111_0000) == 0b0001_0000) {
- states[i] = State.LITERAL_NEVER_INDEXED;
+ s[i] = State.LITERAL_NEVER_INDEXED;
} else if ((i & 0b1111_0000) == 0b0000_0000) {
- states[i] = State.LITERAL;
+ s[i] = State.LITERAL;
} else {
throw new InternalError(String.valueOf(i));
}
}
+ states = List.of(s);
}
private final long id;
@@ -278,7 +282,7 @@
private void resumeReady(ByteBuffer input) {
int b = input.get(input.position()) & 0xff; // absolute read
- State s = states[b];
+ State s = states.get(b);
if (logger.isLoggable(EXTRA)) {
logger.log(EXTRA, () -> format("next binary representation %s (first byte 0x%02x)",
s, b));
@@ -388,15 +392,17 @@
try {
if (firstValueIndex) {
if (logger.isLoggable(NORMAL)) {
- logger.log(NORMAL, () -> format("literal without indexing ('%s', '%s')",
- intValue, value));
+ logger.log(NORMAL, () -> format(
+ "literal without indexing (%s, '%s', huffman=%b)",
+ intValue, value, valueHuffmanEncoded));
}
SimpleHeaderTable.HeaderField f = getHeaderFieldAt(intValue);
action.onLiteral(intValue, f.name, value, valueHuffmanEncoded);
} else {
if (logger.isLoggable(NORMAL)) {
- logger.log(NORMAL, () -> format("literal without indexing ('%s', '%s')",
- name, value));
+ logger.log(NORMAL, () -> format(
+ "literal without indexing ('%s', huffman=%b, '%s', huffman=%b)",
+ name, nameHuffmanEncoded, value, valueHuffmanEncoded));
}
action.onLiteral(name, nameHuffmanEncoded, value, valueHuffmanEncoded);
}
@@ -445,8 +451,9 @@
String v = value.toString();
if (firstValueIndex) {
if (logger.isLoggable(NORMAL)) {
- logger.log(NORMAL, () -> format("literal with incremental indexing ('%s', '%s')",
- intValue, value));
+ logger.log(NORMAL, () -> format(
+ "literal with incremental indexing (%s, '%s', huffman=%b)",
+ intValue, value, valueHuffmanEncoded));
}
SimpleHeaderTable.HeaderField f = getHeaderFieldAt(intValue);
n = f.name;
@@ -454,8 +461,9 @@
} else {
n = name.toString();
if (logger.isLoggable(NORMAL)) {
- logger.log(NORMAL, () -> format("literal with incremental indexing ('%s', '%s')",
- n, value));
+ logger.log(NORMAL, () -> format(
+ "literal with incremental indexing ('%s', huffman=%b, '%s', huffman=%b)",
+ n, nameHuffmanEncoded, value, valueHuffmanEncoded));
}
action.onLiteralWithIndexing(n, nameHuffmanEncoded, v, valueHuffmanEncoded);
}
@@ -496,15 +504,17 @@
try {
if (firstValueIndex) {
if (logger.isLoggable(NORMAL)) {
- logger.log(NORMAL, () -> format("literal never indexed ('%s', '%s')",
- intValue, value));
+ logger.log(NORMAL, () -> format(
+ "literal never indexed (%s, '%s', huffman=%b)",
+ intValue, value, valueHuffmanEncoded));
}
SimpleHeaderTable.HeaderField f = getHeaderFieldAt(intValue);
action.onLiteralNeverIndexed(intValue, f.name, value, valueHuffmanEncoded);
} else {
if (logger.isLoggable(NORMAL)) {
- logger.log(NORMAL, () -> format("literal never indexed ('%s', '%s')",
- name, value));
+ logger.log(NORMAL, () -> format(
+ "literal never indexed ('%s', huffman=%b, '%s', huffman=%b)",
+ name, nameHuffmanEncoded, value, valueHuffmanEncoded));
}
action.onLiteralNeverIndexed(name, nameHuffmanEncoded, value, valueHuffmanEncoded);
}
--- a/src/java.net.http/share/classes/jdk/internal/net/http/hpack/Encoder.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/Encoder.java Wed Jun 20 09:05:57 2018 -0700
@@ -105,8 +105,8 @@
private static final AtomicLong ENCODERS_IDS = new AtomicLong();
- // TODO: enum: no huffman/smart huffman/always huffman
- private static final boolean DEFAULT_HUFFMAN = true;
+ /* Used to calculate the number of bytes required for Huffman encoding */
+ private final QuickHuffman.Writer huffmanWriter = new QuickHuffman.Writer();
private final Logger logger;
private final long id;
@@ -241,21 +241,30 @@
int index = t.indexOf(name, value);
if (index > 0) {
indexed(index);
- } else if (index < 0) {
- if (sensitive) {
- literalNeverIndexed(-index, value, DEFAULT_HUFFMAN);
+ } else {
+ boolean huffmanValue = isHuffmanBetterFor(value);
+ if (index < 0) {
+ if (sensitive) {
+ literalNeverIndexed(-index, value, huffmanValue);
+ } else {
+ literal(-index, value, huffmanValue);
+ }
} else {
- literal(-index, value, DEFAULT_HUFFMAN);
- }
- } else {
- if (sensitive) {
- literalNeverIndexed(name, DEFAULT_HUFFMAN, value, DEFAULT_HUFFMAN);
- } else {
- literal(name, DEFAULT_HUFFMAN, value, DEFAULT_HUFFMAN);
+ boolean huffmanName = isHuffmanBetterFor(name);
+ if (sensitive) {
+ literalNeverIndexed(name, huffmanName, value, huffmanValue);
+ } else {
+ literal(name, huffmanName, value, huffmanValue);
+ }
}
}
}
+ private boolean isHuffmanBetterFor(CharSequence value) {
+ // prefer Huffman encoding only if it is strictly smaller than Latin-1
+ return huffmanWriter.lengthOf(value) < value.length();
+ }
+
/**
* Sets a maximum capacity of the header table.
*
@@ -299,7 +308,7 @@
if (calculated < 0 || calculated > capacity) {
throw new IllegalArgumentException(
format("0 <= calculated <= capacity: calculated=%s, capacity=%s",
- calculated, capacity));
+ calculated, capacity));
}
capacityUpdate = true;
// maxCapacity needs to be updated unconditionally, so the encoder
@@ -416,8 +425,9 @@
boolean useHuffman)
throws IndexOutOfBoundsException {
if (logger.isLoggable(EXTRA)) {
- logger.log(EXTRA, () -> format("literal without indexing ('%s', '%s')",
- index, value));
+ logger.log(EXTRA, () -> format(
+ "literal without indexing (%s, '%s', huffman=%b)",
+ index, value, useHuffman));
}
checkEncoding();
encoding = true;
@@ -428,10 +438,12 @@
protected final void literal(CharSequence name,
boolean nameHuffman,
CharSequence value,
- boolean valueHuffman) {
+ boolean valueHuffman)
+ {
if (logger.isLoggable(EXTRA)) {
- logger.log(EXTRA, () -> format("literal without indexing ('%s', '%s')",
- name, value));
+ logger.log(EXTRA, () -> format(
+ "literal without indexing ('%s', huffman=%b, '%s', huffman=%b)",
+ name, nameHuffman, value, valueHuffman));
}
checkEncoding();
encoding = true;
@@ -442,10 +454,12 @@
protected final void literalNeverIndexed(int index,
CharSequence value,
boolean valueHuffman)
- throws IndexOutOfBoundsException {
+ throws IndexOutOfBoundsException
+ {
if (logger.isLoggable(EXTRA)) {
- logger.log(EXTRA, () -> format("literal never indexed ('%s', '%s')",
- index, value));
+ logger.log(EXTRA, () -> format(
+ "literal never indexed (%s, '%s', huffman=%b)",
+ index, value, valueHuffman));
}
checkEncoding();
encoding = true;
@@ -458,8 +472,9 @@
CharSequence value,
boolean valueHuffman) {
if (logger.isLoggable(EXTRA)) {
- logger.log(EXTRA, () -> format("literal never indexed ('%s', '%s')",
- name, value));
+ logger.log(EXTRA, () -> format(
+ "literal never indexed ('%s', huffman=%b, '%s', huffman=%b)",
+ name, nameHuffman, value, valueHuffman));
}
checkEncoding();
encoding = true;
@@ -472,8 +487,9 @@
boolean valueHuffman)
throws IndexOutOfBoundsException {
if (logger.isLoggable(EXTRA)) {
- logger.log(EXTRA, () -> format("literal with incremental indexing ('%s', '%s')",
- index, value));
+ logger.log(EXTRA, () -> format(
+ "literal with incremental indexing (%s, '%s', huffman=%b)",
+ index, value, valueHuffman));
}
checkEncoding();
encoding = true;
@@ -485,9 +501,10 @@
boolean nameHuffman,
CharSequence value,
boolean valueHuffman) {
- if (logger.isLoggable(EXTRA)) { // TODO: include huffman info?
- logger.log(EXTRA, () -> format("literal with incremental indexing ('%s', '%s')",
- name, value));
+ if (logger.isLoggable(EXTRA)) {
+ logger.log(EXTRA, () -> format(
+ "literal with incremental indexing ('%s', huffman=%b, '%s', huffman=%b)",
+ name, nameHuffman, value, valueHuffman));
}
checkEncoding();
encoding = true;
@@ -506,7 +523,7 @@
if (capacity > this.maxCapacity) {
throw new IllegalArgumentException(
format("capacity <= maxCapacity: capacity=%s, maxCapacity=%s",
- capacity, maxCapacity));
+ capacity, maxCapacity));
}
writer = sizeUpdateWriter.maxHeaderTableSize(capacity);
}
--- a/src/java.net.http/share/classes/jdk/internal/net/http/hpack/HPACK.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/HPACK.java Wed Jun 20 09:05:57 2018 -0700
@@ -27,6 +27,7 @@
import jdk.internal.net.http.common.Utils;
import jdk.internal.net.http.hpack.HPACK.Logger.Level;
+import java.nio.ByteBuffer;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Map;
@@ -171,4 +172,124 @@
}
}
+
+ // -- low-level utilities --
+
+ @FunctionalInterface
+ interface BufferUpdateConsumer {
+ void accept(long data, int len);
+ }
+
+ @SuppressWarnings("fallthrough")
+ public static int read(ByteBuffer source,
+ long buffer,
+ int bufferLen,
+ BufferUpdateConsumer consumer)
+ {
+ // read as much as possible (up to 8 bytes)
+ int nBytes = Math.min((64 - bufferLen) >> 3, source.remaining());
+ switch (nBytes) {
+ case 0:
+ break;
+ case 3:
+ buffer |= ((source.get() & 0x00000000000000ffL) << (56 - bufferLen));
+ bufferLen += 8;
+ case 2:
+ buffer |= ((source.get() & 0x00000000000000ffL) << (56 - bufferLen));
+ bufferLen += 8;
+ case 1:
+ buffer |= ((source.get() & 0x00000000000000ffL) << (56 - bufferLen));
+ bufferLen += 8;
+ consumer.accept(buffer, bufferLen);
+ break;
+ case 7:
+ buffer |= ((source.get() & 0x00000000000000ffL) << (56 - bufferLen));
+ bufferLen += 8;
+ case 6:
+ buffer |= ((source.get() & 0x00000000000000ffL) << (56 - bufferLen));
+ bufferLen += 8;
+ case 5:
+ buffer |= ((source.get() & 0x00000000000000ffL) << (56 - bufferLen));
+ bufferLen += 8;
+ case 4:
+ buffer |= ((source.getInt() & 0x00000000ffffffffL) << (32 - bufferLen));
+ bufferLen += 32;
+ consumer.accept(buffer, bufferLen);
+ break;
+ case 8:
+ buffer = source.getLong();
+ bufferLen = 64;
+ consumer.accept(buffer, bufferLen);
+ break;
+ default:
+ throw new InternalError(String.valueOf(nBytes));
+ }
+ return nBytes;
+ }
+
+ // The number of bytes that can be written at once
+ // (calculating in bytes, not bits, since
+ // destination.remaining() * 8 might overflow)
+ @SuppressWarnings("fallthrough")
+ public static int write(long buffer,
+ int bufferLen,
+ BufferUpdateConsumer consumer,
+ ByteBuffer destination)
+ {
+ int nBytes = Math.min(bufferLen >> 3, destination.remaining());
+ switch (nBytes) {
+ case 0:
+ break;
+ case 3:
+ destination.put((byte) (buffer >>> 56));
+ buffer <<= 8;
+ bufferLen -= 8;
+ case 2:
+ destination.put((byte) (buffer >>> 56));
+ buffer <<= 8;
+ bufferLen -= 8;
+ case 1:
+ destination.put((byte) (buffer >>> 56));
+ buffer <<= 8;
+ bufferLen -= 8;
+ consumer.accept(buffer, bufferLen);
+ break;
+ case 7:
+ destination.put((byte) (buffer >>> 56));
+ buffer <<= 8;
+ bufferLen -= 8;
+ case 6:
+ destination.put((byte) (buffer >>> 56));
+ buffer <<= 8;
+ bufferLen -= 8;
+ case 5:
+ destination.put((byte) (buffer >>> 56));
+ buffer <<= 8;
+ bufferLen -= 8;
+ case 4:
+ destination.putInt((int) (buffer >>> 32));
+ buffer <<= 32;
+ bufferLen -= 32;
+ consumer.accept(buffer, bufferLen);
+ break;
+ case 8:
+ destination.putLong(buffer);
+ buffer = 0;
+ bufferLen = 0;
+ consumer.accept(buffer, bufferLen);
+ break;
+ default:
+ throw new InternalError(String.valueOf(nBytes));
+ }
+ return nBytes;
+ }
+
+ /*
+ * Returns the number of bytes the given number of bits constitute.
+ */
+ static int bytesForBits(int n) {
+ assert (n / 8 + (n % 8 != 0 ? 1 : 0)) == (n + 7) / 8
+ && (n + 7) / 8 == ((n + 7) >> 3) : n;
+ return (n + 7) >> 3;
+ }
}
--- a/src/java.net.http/share/classes/jdk/internal/net/http/hpack/HeaderTable.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/HeaderTable.java Wed Jun 20 09:05:57 2018 -0700
@@ -28,7 +28,6 @@
import java.util.Deque;
import java.util.HashMap;
-import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.Map;
@@ -104,16 +103,24 @@
// Long.MAX_VALUE :-)
//
- private static final Map<String, LinkedHashMap<String, Integer>> staticIndexes;
+ /* An immutable map of static header fields' indexes */
+ private static final Map<String, Map<String, Integer>> staticIndexes;
static {
- staticIndexes = new HashMap<>(STATIC_TABLE_LENGTH); // TODO: Map.of
+ Map<String, Map<String, Integer>> map
+ = new HashMap<>(STATIC_TABLE_LENGTH);
for (int i = 1; i <= STATIC_TABLE_LENGTH; i++) {
- HeaderField f = staticTable[i];
- Map<String, Integer> values = staticIndexes
- .computeIfAbsent(f.name, k -> new LinkedHashMap<>());
+ HeaderField f = staticTable.get(i);
+ Map<String, Integer> values
+ = map.computeIfAbsent(f.name, k -> new HashMap<>());
values.put(f.value, i);
}
+ // create an immutable deep copy
+ Map<String, Map<String, Integer>> copy = new HashMap<>(map.size());
+ for (Map.Entry<String, Map<String, Integer>> e : map.entrySet()) {
+ copy.put(e.getKey(), Map.copyOf(e.getValue()));
+ }
+ staticIndexes = Map.copyOf(copy);
}
// name -> (value -> [index])
--- a/src/java.net.http/share/classes/jdk/internal/net/http/hpack/ISO_8859_1.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/ISO_8859_1.java Wed Jun 20 09:05:57 2018 -0700
@@ -46,29 +46,58 @@
public static final class Reader {
+ private final HPACK.BufferUpdateConsumer UPDATER =
+ (buf, bufLen) -> {
+ buffer = buf;
+ bufferLen = bufLen;
+ };
+
+ private long buffer;
+ private int bufferLen;
+
public void read(ByteBuffer source, Appendable destination)
- throws IOException {
- for (int i = 0, len = source.remaining(); i < len; i++) {
- char c = (char) (source.get() & 0xff);
- try {
- destination.append(c);
- } catch (IOException e) {
- throw new IOException(
- "Error appending to the destination", e);
+ throws IOException
+ {
+ while (true) {
+ int nBytes = HPACK.read(source, buffer, bufferLen, UPDATER);
+ if (nBytes == 0) {
+ return;
+ }
+ assert bufferLen % 8 == 0 : bufferLen;
+ while (bufferLen > 0) {
+ char c = (char) (buffer >>> 56);
+ try {
+ destination.append(c);
+ } catch (IOException e) {
+ throw new IOException(
+ "Error appending to the destination", e);
+ }
+ buffer <<= 8;
+ bufferLen -= 8;
}
}
}
public Reader reset() {
+ buffer = 0;
+ bufferLen = 0;
return this;
}
}
public static final class Writer {
+ private final HPACK.BufferUpdateConsumer UPDATER =
+ (buf, bufLen) -> {
+ buffer = buf;
+ bufferLen = bufLen;
+ };
+
private CharSequence source;
private int pos;
private int end;
+ private long buffer;
+ private int bufferLen;
public Writer configure(CharSequence source, int start, int end) {
this.source = source;
@@ -78,25 +107,39 @@
}
public boolean write(ByteBuffer destination) {
- for (; pos < end; pos++) {
- char c = source.charAt(pos);
- if (c > '\u00FF') {
- throw new IllegalArgumentException(
- "Illegal ISO-8859-1 char: " + (int) c);
+ while (true) {
+ while (true) { // stuff codes into long
+ if (pos >= end) {
+ break;
+ }
+ char c = source.charAt(pos);
+ if (c > 255) {
+ throw new IllegalArgumentException(Integer.toString((int) c));
+ }
+ if (bufferLen <= 56) {
+ buffer |= (((long) c) << (56 - bufferLen)); // append
+ bufferLen += 8;
+ pos++;
+ } else {
+ break;
+ }
}
- if (destination.hasRemaining()) {
- destination.put((byte) c);
- } else {
+ if (bufferLen == 0) {
+ return true;
+ }
+ int nBytes = HPACK.write(buffer, bufferLen, UPDATER, destination);
+ if (nBytes == 0) {
return false;
}
}
- return true;
}
public Writer reset() {
source = null;
pos = -1;
end = -1;
+ buffer = 0;
+ bufferLen = 0;
return this;
}
}
--- a/src/java.net.http/share/classes/jdk/internal/net/http/hpack/QuickHuffman.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/QuickHuffman.java Wed Jun 20 09:05:57 2018 -0700
@@ -25,19 +25,21 @@
package jdk.internal.net.http.hpack;
+import jdk.internal.net.http.hpack.HPACK.BufferUpdateConsumer;
+
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.Objects;
+import static jdk.internal.net.http.hpack.HPACK.bytesForBits;
+
public final class QuickHuffman {
/*
- * Huffman codes for encoding.
- *
- * EOS will never be matched, since there is no symbol for it, thus no need
- * to store it in the array. Thus, the length of the array is 256, not 257.
- * Code information for each character is encoded as follows:
+ * Mapping of characters to their code information. Code information
+ * consists of a code value and a code length. Both components are packed in
+ * a single long as follows:
*
* MSB LSB
* +----------------+----------------+
@@ -46,9 +48,14 @@
* |<----- 32 ----->|<----- 32 ----->|
* |<------------- 64 -------------->|
*
- * The leftmost 32 bits hold the code value. This value is aligned left
- * (or MSB). The rightmost 32 bits hold the length of the code value.
- * This length is aligned right (or LSB).
+ * The leftmost 32 bits hold the code value. This value is aligned left (or
+ * MSB). The rightmost 32 bits hold the code length. This length is aligned
+ * right (or LSB). Such structure is possible thanks to codes being up to 30
+ * bits long and their lengths being up to 5 bits long (length = 5..30).
+ * This allows both components to fit into long and, thus, better locality.
+ *
+ * Input strings never contain EOS. Thus there's no need to provide EOS
+ * mapping. Hence, the length of the array is 256, not 257.
*/
private static final long[] codes = new long[256];
@@ -62,16 +69,19 @@
private static final int EOS_LENGTH = 30;
private static final int EOS_LSB = 0x3fffffff;
- private static final long EOS_MSB = EOS_LSB << (64 - EOS_LENGTH);
+ // EOS_LSB is casted to long before the shift, to allow long shift (6 bits)
+ // instead of int shift (5 bits):
+ private static final long EOS_MSB = ((long) EOS_LSB) << (64 - EOS_LENGTH);
/*
- * Huffman codes for decoding.
+ * Huffman trie.
*
- * Root node contains 257 descendant nodes, including EOS.
+ * The root node leads to 257 descendant leaf nodes, of which 256 nodes
+ * correspond to characters and 1 node correspond to EOS.
*/
- private static final Node root;
+ private static final Node root = buildTrie();
- static {
+ private static Node buildTrie() {
TemporaryNode tmpRoot = new TemporaryNode();
addChar(tmpRoot, 0, 0x1ff8, 13);
addChar(tmpRoot, 1, 0x7fffd8, 23);
@@ -333,8 +343,8 @@
// The difference in performance can always be checked by not using
// the immutable trie:
- // root = tmpRoot;
- root = ImmutableNode.copyOf(tmpRoot);
+ // return tmpRoot;
+ return ImmutableNode.copyOf(tmpRoot);
}
private QuickHuffman() { }
@@ -605,6 +615,12 @@
static final class Reader implements Huffman.Reader {
+ private final BufferUpdateConsumer UPDATER =
+ (buf, bufLen) -> {
+ buffer = buf;
+ bufferLen = bufLen;
+ };
+
private Node curr = root; // current position in the trie
private long buffer; // bits left from the previous match (aligned to the left, or MSB)
private int bufferLen; // number of bits in the buffer
@@ -616,53 +632,9 @@
Appendable destination,
boolean isLast) throws IOException
{
- read(source, destination, true, isLast);
- }
-
- @Override
- public void reset() {
- curr = root;
- len = 0;
- buffer = 0;
- bufferLen = 0;
- done = false;
- }
-
- @SuppressWarnings("fallthrough")
- void read(ByteBuffer source,
- Appendable destination,
- boolean reportEOS, /* reportEOS is exposed for tests */
- boolean isLast) throws IOException
- {
while (!done) {
- // read as much as possible (up to 8 bytes)
int remaining = source.remaining();
- int nBytes = Math.min((64 - bufferLen) >> 3, remaining);
- switch (nBytes) {
- case 0:
- break;
- case 3:
- readByte(source);
- case 2:
- readByte(source);
- case 1:
- readByte(source);
- break;
- case 7:
- readByte(source);
- case 6:
- readByte(source);
- case 5:
- readByte(source);
- case 4:
- readInt(source);
- break;
- case 8:
- readLong(source);
- break;
- default:
- throw new InternalError(String.valueOf(nBytes));
- }
+ int nBytes = HPACK.read(source, buffer, bufferLen, UPDATER);
// write as much as possible
while (true) {
if (bufferLen < 8) {
@@ -671,7 +643,7 @@
} else if (!isLast) { // exit the method to accept more input
return;
} else if (bufferLen > 0) { // no more data is expected, pad
- // (this padding may be done more than once)
+ // (this padding may be done more than once)
buffer |= ((0xff00000000000000L >>> bufferLen)
& 0xff00000000000000L);
// do not update bufferLen, since all those ones are
@@ -692,7 +664,7 @@
throw new IOException(
"Not a EOS prefix padding or unexpected end of data");
}
- if (reportEOS && node.isEOSPath()) {
+ if (node.isEOSPath()) {
throw new IOException("Encountered EOS");
}
destination.append(node.getSymbol());
@@ -715,27 +687,24 @@
}
}
- private void readLong(ByteBuffer source) {
- buffer = source.getLong();
- bufferLen = 64;
- }
-
- private void readInt(ByteBuffer source) {
- long b;
- b = source.getInt() & 0x00000000ffffffffL;
- buffer |= (b << (32 - bufferLen));
- bufferLen += 32;
- }
-
- private void readByte(ByteBuffer source) {
- long b = source.get() & 0x00000000000000ffL;
- buffer |= (b << (56 - bufferLen));
- bufferLen += 8;
+ @Override
+ public void reset() {
+ curr = root;
+ len = 0;
+ buffer = 0;
+ bufferLen = 0;
+ done = false;
}
}
static final class Writer implements Huffman.Writer {
+ private final BufferUpdateConsumer UPDATER =
+ (buf, bufLen) -> {
+ buffer = buf;
+ bufferLen = bufLen;
+ };
+
private CharSequence source;
private boolean padded;
private int pos;
@@ -752,43 +721,44 @@
return this;
}
- @SuppressWarnings("fallthrough")
@Override
public boolean write(ByteBuffer destination) {
while (true) {
- while (bufferLen < 32 && pos < end) {
- char c = source.charAt(pos++);
- buffer |= (codeValueOf(c) >>> bufferLen); // append
- bufferLen += codeLengthOf(c);
+ while (true) { // stuff codes into long
+ if (pos >= end) {
+ break;
+ }
+ char c = source.charAt(pos);
+ if (c > 255) {
+ throw new IllegalArgumentException("char=" + ((int) c));
+ }
+ long len = codeLengthOf(c);
+ if (bufferLen + len <= 64) {
+ buffer |= (codeValueOf(c) >>> bufferLen); // append
+ bufferLen += len;
+ pos++;
+ } else {
+ break;
+ }
}
if (bufferLen == 0) {
return true;
}
- if (pos >= end && !padded) { // no more chars, pad
+ if (pos >= end && !padded) { // no more input chars are expected, pad
padded = true;
- buffer |= (EOS_MSB >>> bufferLen);
- bufferLen = bytesForBits(bufferLen) << 3;
+ // A long shift to 64 will result in the same long, not
+ // necessarily 0L. In which case padding will be performed
+ // incorrectly. If bufferLen is equal to 64, the shift (and
+ // padding) in not performed.
+ // (see https://docs.oracle.com/javase/specs/jls/se10/html/jls-15.html#jls-15.19)
+ if (bufferLen != 64) {
+ buffer |= (EOS_MSB >>> bufferLen);
+ bufferLen = bytesForBits(bufferLen) << 3;
+ }
}
- // The number of bytes that can be written at once
- // (calculating in bytes, not bits, since
- // destination.remaining() * 8 might overflow)
-
- int nBytes = Math.min(bytesForBits(bufferLen), destination.remaining()); // ceil?
- switch (nBytes) {
- case 0:
- return false;
- case 1:
- case 2:
- case 3:
- destination.put((byte) (buffer >>> 56));
- buffer <<= 8;
- bufferLen -= 8;
- break;
- default:
- destination.putInt((int) (buffer >>> 32));
- buffer <<= 32;
- bufferLen -= 32;
- break;
+ int nBytes = HPACK.write(buffer, bufferLen, UPDATER, destination);
+ if (nBytes == 0) {
+ return false;
}
}
}
@@ -814,13 +784,4 @@
return bytesForBits(len);
}
}
-
- /*
- * Returns the number of bytes the given number of bits constitute.
- */
- private static int bytesForBits(int n) {
- assert (n / 8 + (n % 8 != 0 ? 1 : 0)) == (n + 7) / 8
- && (n + 7) / 8 == ((n + 7) >> 3) : n;
- return (n + 7) >> 3;
- }
}
--- a/src/java.net.http/share/classes/jdk/internal/net/http/hpack/SimpleHeaderTable.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/SimpleHeaderTable.java Wed Jun 20 09:05:57 2018 -0700
@@ -26,6 +26,7 @@
import jdk.internal.net.http.hpack.HPACK.Logger;
+import java.util.List;
import java.util.NoSuchElementException;
import static jdk.internal.net.http.common.Utils.pow2Size;
@@ -41,8 +42,9 @@
*/
class SimpleHeaderTable {
- protected static final HeaderField[] staticTable = {
- null, // To make index 1-based, instead of 0-based
+ /* An immutable list of static header fields */
+ protected static final List<HeaderField> staticTable = List.of(
+ new HeaderField(""), // A dummy to make the list index 1-based, instead of 0-based
new HeaderField(":authority"),
new HeaderField(":method", "GET"),
new HeaderField(":method", "POST"),
@@ -103,10 +105,9 @@
new HeaderField("user-agent"),
new HeaderField("vary"),
new HeaderField("via"),
- new HeaderField("www-authenticate")
- };
+ new HeaderField("www-authenticate"));
- protected static final int STATIC_TABLE_LENGTH = staticTable.length - 1;
+ protected static final int STATIC_TABLE_LENGTH = staticTable.size() - 1;
protected static final int ENTRY_SIZE = 32;
private final Logger logger;
@@ -134,7 +135,7 @@
HeaderField get(int index) {
checkIndex(index);
if (index <= STATIC_TABLE_LENGTH) {
- return staticTable[index];
+ return staticTable.get(index);
} else {
return buffer.get(index - STATIC_TABLE_LENGTH - 1);
}
@@ -263,23 +264,6 @@
public String toString() {
return value.isEmpty() ? name : name + ": " + value;
}
-
- @Override // TODO: remove since used only for testing
- public boolean equals(Object o) {
- if (this == o) {
- return true;
- }
- if (o == null || getClass() != o.getClass()) {
- return false;
- }
- HeaderField that = (HeaderField) o;
- return name.equals(that.name) && value.equals(that.value);
- }
-
- @Override // TODO: remove since used only for testing
- public int hashCode() {
- return 31 * name.hashCode() + value.hashCode();
- }
}
private final CircularBuffer<HeaderField> buffer = new CircularBuffer<>(0);
--- a/src/java.net.http/share/classes/jdk/internal/net/http/websocket/OpeningHandshake.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/websocket/OpeningHandshake.java Wed Jun 20 09:05:57 2018 -0700
@@ -32,6 +32,9 @@
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.net.http.WebSocketHandshakeException;
+
+import jdk.internal.net.http.HttpRequestBuilderImpl;
+import jdk.internal.net.http.HttpRequestImpl;
import jdk.internal.net.http.common.MinimalFuture;
import jdk.internal.net.http.common.Pair;
import jdk.internal.net.http.common.Utils;
@@ -104,7 +107,7 @@
}
}
- private final HttpRequest request;
+ private final HttpRequestImpl request;
private final Collection<String> subprotocols;
private final String nonce;
@@ -114,7 +117,7 @@
checkPermissions(b, proxy);
this.client = b.getClient();
URI httpURI = createRequestURI(b.getUri());
- HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(httpURI);
+ HttpRequestBuilderImpl requestBuilder = new HttpRequestBuilderImpl(httpURI);
Duration connectTimeout = b.getConnectTimeout();
if (connectTimeout != null) {
requestBuilder.timeout(connectTimeout);
@@ -127,7 +130,7 @@
}
this.subprotocols = createRequestSubprotocols(b.getSubprotocols());
if (!this.subprotocols.isEmpty()) {
- String p = this.subprotocols.stream().collect(Collectors.joining(", "));
+ String p = String.join(", ", this.subprotocols);
requestBuilder.header(HEADER_PROTOCOL, p);
}
requestBuilder.header(HEADER_VERSION, VERSION);
@@ -137,12 +140,12 @@
// to upgrade from HTTP/2 to WebSocket (as of August 2016):
//
// https://tools.ietf.org/html/draft-hirano-httpbis-websocket-over-http2-00
- this.request = requestBuilder.version(Version.HTTP_1_1).GET().build();
- WebSocketRequest r = (WebSocketRequest) this.request;
- r.isWebSocket(true);
- r.setSystemHeader(HEADER_UPGRADE, "websocket");
- r.setSystemHeader(HEADER_CONNECTION, "Upgrade");
- r.setProxy(proxy);
+ requestBuilder.version(Version.HTTP_1_1).GET();
+ request = requestBuilder.buildForWebSocket();
+ request.isWebSocket(true);
+ request.setSystemHeader(HEADER_UPGRADE, "websocket");
+ request.setSystemHeader(HEADER_CONNECTION, "Upgrade");
+ request.setProxy(proxy);
}
private static Collection<String> createRequestSubprotocols(
--- a/src/java.net.http/share/classes/jdk/internal/net/http/websocket/WebSocketImpl.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/websocket/WebSocketImpl.java Wed Jun 20 09:05:57 2018 -0700
@@ -34,7 +34,6 @@
import jdk.internal.net.http.websocket.OpeningHandshake.Result;
import java.io.IOException;
-import java.lang.System.Logger.Level;
import java.lang.ref.Reference;
import java.net.ProtocolException;
import java.net.URI;
@@ -88,7 +87,7 @@
PING,
PONG,
CLOSE,
- ERROR;
+ ERROR
}
private final AtomicReference<ByteBuffer> lastAutomaticPong = new AtomicReference<>();
--- a/src/jdk.httpserver/share/classes/sun/net/httpserver/ExchangeImpl.java Wed Jun 20 17:15:16 2018 +0200
+++ b/src/jdk.httpserver/share/classes/sun/net/httpserver/ExchangeImpl.java Wed Jun 20 09:05:57 2018 -0700
@@ -227,8 +227,8 @@
contentLen = -1;
}
- if (isHeadRequest()) {
- /* HEAD requests should not set a content length by passing it
+ if (isHeadRequest() || rCode == 304) {
+ /* HEAD requests or 304 responses should not set a content length by passing it
* through this API, but should instead manually set the required
* headers.*/
if (contentLen >= 0) {
@@ -239,7 +239,7 @@
}
noContentToSend = true;
contentLen = 0;
- } else { /* not a HEAD request */
+ } else { /* not a HEAD request or 304 response */
if (contentLen == 0) {
if (http10) {
o.setWrappedStream (new UndefLengthOutputStream (this, ros));
--- a/test/jdk/java/net/httpclient/AbstractNoBody.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/AbstractNoBody.java Wed Jun 20 09:05:57 2018 -0700
@@ -57,7 +57,7 @@
String https2URI_chunk;
static final String SIMPLE_STRING = "Hello world. Goodbye world";
- static final int ITERATION_COUNT = 10;
+ static final int ITERATION_COUNT = 3;
// a shared executor helps reduce the amount of threads created by the test
static final Executor executor = Executors.newFixedThreadPool(ITERATION_COUNT * 2);
static final ExecutorService serverExecutor = Executors.newFixedThreadPool(ITERATION_COUNT * 4);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/AbstractThrowingPublishers.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,724 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import com.sun.net.httpserver.HttpServer;
+import com.sun.net.httpserver.HttpsConfigurator;
+import com.sun.net.httpserver.HttpsServer;
+import jdk.testlibrary.SimpleSSLContext;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.AfterTest;
+import org.testng.annotations.BeforeTest;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import javax.net.ssl.SSLContext;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UncheckedIOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.BodyPublisher;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandler;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Flow;
+import java.util.concurrent.SubmissionPublisher;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.BiPredicate;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static java.lang.String.format;
+import static java.lang.System.out;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+public abstract class AbstractThrowingPublishers implements HttpServerAdapters {
+
+ SSLContext sslContext;
+ HttpTestServer httpTestServer; // HTTP/1.1 [ 4 servers ]
+ HttpTestServer httpsTestServer; // HTTPS/1.1
+ HttpTestServer http2TestServer; // HTTP/2 ( h2c )
+ HttpTestServer https2TestServer; // HTTP/2 ( h2 )
+ String httpURI_fixed;
+ String httpURI_chunk;
+ String httpsURI_fixed;
+ String httpsURI_chunk;
+ String http2URI_fixed;
+ String http2URI_chunk;
+ String https2URI_fixed;
+ String https2URI_chunk;
+
+ static final int ITERATION_COUNT = 1;
+ // a shared executor helps reduce the amount of threads created by the test
+ static final Executor executor = new TestExecutor(Executors.newCachedThreadPool());
+ static final ConcurrentMap<String, Throwable> FAILURES = new ConcurrentHashMap<>();
+ static volatile boolean tasksFailed;
+ static final AtomicLong serverCount = new AtomicLong();
+ static final AtomicLong clientCount = new AtomicLong();
+ static final long start = System.nanoTime();
+ public static String now() {
+ long now = System.nanoTime() - start;
+ long secs = now / 1000_000_000;
+ long mill = (now % 1000_000_000) / 1000_000;
+ long nan = now % 1000_000;
+ return String.format("[%d s, %d ms, %d ns] ", secs, mill, nan);
+ }
+
+ final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE;
+ private volatile HttpClient sharedClient;
+
+ static class TestExecutor implements Executor {
+ final AtomicLong tasks = new AtomicLong();
+ Executor executor;
+ TestExecutor(Executor executor) {
+ this.executor = executor;
+ }
+
+ @Override
+ public void execute(Runnable command) {
+ long id = tasks.incrementAndGet();
+ executor.execute(() -> {
+ try {
+ command.run();
+ } catch (Throwable t) {
+ tasksFailed = true;
+ System.out.printf(now() + "Task %s failed: %s%n", id, t);
+ System.err.printf(now() + "Task %s failed: %s%n", id, t);
+ FAILURES.putIfAbsent("Task " + id, t);
+ throw t;
+ }
+ });
+ }
+ }
+
+ @AfterClass
+ static final void printFailedTests() {
+ out.println("\n=========================");
+ try {
+ out.printf("%n%sCreated %d servers and %d clients%n",
+ now(), serverCount.get(), clientCount.get());
+ if (FAILURES.isEmpty()) return;
+ out.println("Failed tests: ");
+ FAILURES.entrySet().forEach((e) -> {
+ out.printf("\t%s: %s%n", e.getKey(), e.getValue());
+ e.getValue().printStackTrace(out);
+ });
+ if (tasksFailed) {
+ System.out.println("WARNING: Some tasks failed");
+ }
+ } finally {
+ out.println("\n=========================\n");
+ }
+ }
+
+ private String[] uris() {
+ return new String[] {
+ httpURI_fixed,
+ httpURI_chunk,
+ httpsURI_fixed,
+ httpsURI_chunk,
+ http2URI_fixed,
+ http2URI_chunk,
+ https2URI_fixed,
+ https2URI_chunk,
+ };
+ }
+
+ @DataProvider(name = "sanity")
+ public Object[][] sanity() {
+ String[] uris = uris();
+ Object[][] result = new Object[uris.length * 2][];
+ //Object[][] result = new Object[uris.length][];
+ int i = 0;
+ for (boolean sameClient : List.of(false, true)) {
+ //if (!sameClient) continue;
+ for (String uri: uris()) {
+ result[i++] = new Object[] {uri + "/sanity", sameClient};
+ }
+ }
+ assert i == uris.length * 2;
+ // assert i == uris.length ;
+ return result;
+ }
+
+ enum Where {
+ BEFORE_SUBSCRIBE, BEFORE_REQUEST, BEFORE_NEXT_REQUEST, BEFORE_CANCEL,
+ AFTER_SUBSCRIBE, AFTER_REQUEST, AFTER_NEXT_REQUEST, AFTER_CANCEL;
+ public Consumer<Where> select(Consumer<Where> consumer) {
+ return new Consumer<Where>() {
+ @Override
+ public void accept(Where where) {
+ if (Where.this == where) {
+ consumer.accept(where);
+ }
+ }
+ };
+ }
+ }
+
+ private Object[][] variants(List<Thrower> throwers, Set<Where> whereValues) {
+ String[] uris = uris();
+ Object[][] result = new Object[uris.length * 2 * throwers.size()][];
+ //Object[][] result = new Object[(uris.length/2) * 2 * 2][];
+ int i = 0;
+ for (Thrower thrower : throwers) {
+ for (boolean sameClient : List.of(false, true)) {
+ for (String uri : uris()) {
+ // if (uri.contains("http2") || uri.contains("https2")) continue;
+ // if (!sameClient) continue;
+ result[i++] = new Object[]{uri, sameClient, thrower, whereValues};
+ }
+ }
+ }
+ assert i == uris.length * 2 * throwers.size();
+ //assert Stream.of(result).filter(o -> o != null).count() == result.length;
+ return result;
+ }
+
+ @DataProvider(name = "subscribeProvider")
+ public Object[][] subscribeProvider() {
+ return variants(List.of(
+ new UncheckedCustomExceptionThrower(),
+ new UncheckedIOExceptionThrower()),
+ EnumSet.of(Where.BEFORE_SUBSCRIBE, Where.AFTER_SUBSCRIBE));
+ }
+
+ @DataProvider(name = "requestProvider")
+ public Object[][] requestProvider() {
+ return variants(List.of(
+ new UncheckedCustomExceptionThrower(),
+ new UncheckedIOExceptionThrower()),
+ EnumSet.of(Where.BEFORE_REQUEST, Where.AFTER_REQUEST));
+ }
+
+ @DataProvider(name = "nextRequestProvider")
+ public Object[][] nextRequestProvider() {
+ return variants(List.of(
+ new UncheckedCustomExceptionThrower(),
+ new UncheckedIOExceptionThrower()),
+ EnumSet.of(Where.BEFORE_NEXT_REQUEST, Where.AFTER_NEXT_REQUEST));
+ }
+
+ @DataProvider(name = "beforeCancelProviderIO")
+ public Object[][] beforeCancelProviderIO() {
+ return variants(List.of(
+ new UncheckedIOExceptionThrower()),
+ EnumSet.of(Where.BEFORE_CANCEL));
+ }
+
+ @DataProvider(name = "afterCancelProviderIO")
+ public Object[][] afterCancelProviderIO() {
+ return variants(List.of(
+ new UncheckedIOExceptionThrower()),
+ EnumSet.of(Where.AFTER_CANCEL));
+ }
+
+ @DataProvider(name = "beforeCancelProviderCustom")
+ public Object[][] beforeCancelProviderCustom() {
+ return variants(List.of(
+ new UncheckedCustomExceptionThrower()),
+ EnumSet.of(Where.BEFORE_CANCEL));
+ }
+
+ @DataProvider(name = "afterCancelProviderCustom")
+ public Object[][] afterCancelProvider() {
+ return variants(List.of(
+ new UncheckedCustomExceptionThrower()),
+ EnumSet.of(Where.AFTER_CANCEL));
+ }
+
+ private HttpClient makeNewClient() {
+ clientCount.incrementAndGet();
+ return TRACKER.track(HttpClient.newBuilder()
+ .proxy(HttpClient.Builder.NO_PROXY)
+ .executor(executor)
+ .sslContext(sslContext)
+ .build());
+ }
+
+ HttpClient newHttpClient(boolean share) {
+ if (!share) return makeNewClient();
+ HttpClient shared = sharedClient;
+ if (shared != null) return shared;
+ synchronized (this) {
+ shared = sharedClient;
+ if (shared == null) {
+ shared = sharedClient = makeNewClient();
+ }
+ return shared;
+ }
+ }
+
+ final String BODY = "Some string | that ? can | be split ? several | ways.";
+
+ //@Test(dataProvider = "sanity")
+ protected void testSanityImpl(String uri, boolean sameClient)
+ throws Exception {
+ HttpClient client = null;
+ out.printf("%n%s testSanity(%s, %b)%n", now(), uri, sameClient);
+ for (int i=0; i< ITERATION_COUNT; i++) {
+ if (!sameClient || client == null)
+ client = newHttpClient(sameClient);
+
+ SubmissionPublisher<ByteBuffer> publisher
+ = new SubmissionPublisher<>(executor,10);
+ ThrowingBodyPublisher bodyPublisher = new ThrowingBodyPublisher((w) -> {},
+ BodyPublishers.fromPublisher(publisher));
+ CompletableFuture<Void> subscribedCF = bodyPublisher.subscribedCF();
+ subscribedCF.whenComplete((r,t) -> System.out.println(now() + " subscribe completed " + t))
+ .thenAcceptAsync((v) -> {
+ Stream.of(BODY.split("\\|"))
+ .forEachOrdered(s -> {
+ System.out.println("submitting \"" + s +"\"");
+ publisher.submit(ByteBuffer.wrap(s.getBytes(StandardCharsets.UTF_8)));
+ });
+ System.out.println("publishing done");
+ publisher.close();
+ },
+ executor);
+
+ HttpRequest req = HttpRequest.newBuilder(URI.create(uri))
+ .POST(bodyPublisher)
+ .build();
+ BodyHandler<String> handler = BodyHandlers.ofString();
+ CompletableFuture<HttpResponse<String>> response = client.sendAsync(req, handler);
+
+ String body = response.join().body();
+ assertEquals(body, Stream.of(BODY.split("\\|")).collect(Collectors.joining()));
+ }
+ }
+
+ // @Test(dataProvider = "variants")
+ protected void testThrowingAsStringImpl(String uri,
+ boolean sameClient,
+ Thrower thrower,
+ Set<Where> whereValues)
+ throws Exception
+ {
+ String test = format("testThrowingAsString(%s, %b, %s, %s)",
+ uri, sameClient, thrower, whereValues);
+ List<byte[]> bytes = Stream.of(BODY.split("|"))
+ .map(s -> s.getBytes(UTF_8))
+ .collect(Collectors.toList());
+ testThrowing(test, uri, sameClient, () -> BodyPublishers.ofByteArrays(bytes),
+ this::shouldNotThrowInCancel, thrower,false, whereValues);
+ }
+
+ private <T,U> void testThrowing(String name, String uri, boolean sameClient,
+ Supplier<BodyPublisher> publishers,
+ Finisher finisher, Thrower thrower,
+ boolean async, Set<Where> whereValues)
+ throws Exception
+ {
+ out.printf("%n%s%s%n", now(), name);
+ try {
+ testThrowing(uri, sameClient, publishers, finisher, thrower, async, whereValues);
+ } catch (Error | Exception x) {
+ FAILURES.putIfAbsent(name, x);
+ throw x;
+ }
+ }
+
+ private void testThrowing(String uri, boolean sameClient,
+ Supplier<BodyPublisher> publishers,
+ Finisher finisher, Thrower thrower,
+ boolean async, Set<Where> whereValues)
+ throws Exception
+ {
+ HttpClient client = null;
+ for (Where where : whereValues) {
+ //if (where == Where.ON_SUBSCRIBE) continue;
+ //if (where == Where.ON_ERROR) continue;
+ if (!sameClient || client == null)
+ client = newHttpClient(sameClient);
+
+ ThrowingBodyPublisher bodyPublisher =
+ new ThrowingBodyPublisher(where.select(thrower), publishers.get());
+ HttpRequest req = HttpRequest.
+ newBuilder(URI.create(uri))
+ .header("X-expect-exception", "true")
+ .POST(bodyPublisher)
+ .build();
+ BodyHandler<String> handler = BodyHandlers.ofString();
+ System.out.println("try throwing in " + where);
+ HttpResponse<String> response = null;
+ if (async) {
+ try {
+ response = client.sendAsync(req, handler).join();
+ } catch (Error | Exception x) {
+ Throwable cause = findCause(where, x, thrower);
+ if (cause == null) throw causeNotFound(where, x);
+ System.out.println(now() + "Got expected exception: " + cause);
+ }
+ } else {
+ try {
+ response = client.send(req, handler);
+ } catch (Error | Exception t) {
+ // synchronous send will rethrow exceptions
+ Throwable throwable = t.getCause();
+ assert throwable != null;
+
+ if (thrower.test(where, throwable)) {
+ System.out.println(now() + "Got expected exception: " + throwable);
+ } else throw causeNotFound(where, t);
+ }
+ }
+ if (response != null) {
+ finisher.finish(where, response, thrower);
+ }
+ }
+ }
+
+ // can be used to reduce the surface of the test when diagnosing
+ // some failure
+ Set<Where> whereValues() {
+ //return EnumSet.of(Where.BEFORE_CANCEL, Where.AFTER_CANCEL);
+ return EnumSet.allOf(Where.class);
+ }
+
+ interface Thrower extends Consumer<Where>, BiPredicate<Where,Throwable> {
+
+ }
+
+ interface Finisher<T,U> {
+ U finish(Where w, HttpResponse<T> resp, Thrower thrower) throws IOException;
+ }
+
+ final <T,U> U shouldNotThrowInCancel(Where w, HttpResponse<T> resp, Thrower thrower) {
+ switch (w) {
+ case BEFORE_CANCEL: return null;
+ case AFTER_CANCEL: return null;
+ default: break;
+ }
+ return shouldHaveThrown(w, resp, thrower);
+ }
+
+
+ final <T,U> U shouldHaveThrown(Where w, HttpResponse<T> resp, Thrower thrower) {
+ String msg = "Expected exception not thrown in " + w
+ + "\n\tReceived: " + resp
+ + "\n\tWith body: " + resp.body();
+ System.out.println(msg);
+ throw new RuntimeException(msg);
+ }
+
+
+ private static Throwable findCause(Where w,
+ Throwable x,
+ BiPredicate<Where, Throwable> filter) {
+ while (x != null && !filter.test(w,x)) x = x.getCause();
+ return x;
+ }
+
+ static AssertionError causeNotFound(Where w, Throwable t) {
+ return new AssertionError("Expected exception not found in " + w, t);
+ }
+
+ static boolean isConnectionClosedLocally(Throwable t) {
+ if (t instanceof CompletionException) t = t.getCause();
+ if (t instanceof ExecutionException) t = t.getCause();
+ if (t instanceof IOException) {
+ String msg = t.getMessage();
+ return msg == null ? false
+ : msg.contains("connection closed locally");
+ }
+ return false;
+ }
+
+ static final class UncheckedCustomExceptionThrower implements Thrower {
+ @Override
+ public void accept(Where where) {
+ out.println(now() + "Throwing in " + where);
+ throw new UncheckedCustomException(where.name());
+ }
+
+ @Override
+ public boolean test(Where w, Throwable throwable) {
+ switch (w) {
+ case AFTER_REQUEST:
+ case BEFORE_NEXT_REQUEST:
+ case AFTER_NEXT_REQUEST:
+ if (isConnectionClosedLocally(throwable)) return true;
+ break;
+ default:
+ break;
+ }
+ return UncheckedCustomException.class.isInstance(throwable);
+ }
+
+ @Override
+ public String toString() {
+ return "UncheckedCustomExceptionThrower";
+ }
+ }
+
+ static final class UncheckedIOExceptionThrower implements Thrower {
+ @Override
+ public void accept(Where where) {
+ out.println(now() + "Throwing in " + where);
+ throw new UncheckedIOException(new CustomIOException(where.name()));
+ }
+
+ @Override
+ public boolean test(Where w, Throwable throwable) {
+ switch (w) {
+ case AFTER_REQUEST:
+ case BEFORE_NEXT_REQUEST:
+ case AFTER_NEXT_REQUEST:
+ if (isConnectionClosedLocally(throwable)) return true;
+ break;
+ default:
+ break;
+ }
+ return UncheckedIOException.class.isInstance(throwable)
+ && CustomIOException.class.isInstance(throwable.getCause());
+ }
+
+ @Override
+ public String toString() {
+ return "UncheckedIOExceptionThrower";
+ }
+ }
+
+ static final class UncheckedCustomException extends RuntimeException {
+ UncheckedCustomException(String message) {
+ super(message);
+ }
+ UncheckedCustomException(String message, Throwable cause) {
+ super(message, cause);
+ }
+ }
+
+ static final class CustomIOException extends IOException {
+ CustomIOException(String message) {
+ super(message);
+ }
+ CustomIOException(String message, Throwable cause) {
+ super(message, cause);
+ }
+ }
+
+
+ static final class ThrowingBodyPublisher implements BodyPublisher {
+ private final BodyPublisher publisher;
+ private final CompletableFuture<Void> subscribedCF = new CompletableFuture<>();
+ final Consumer<Where> throwing;
+ ThrowingBodyPublisher(Consumer<Where> throwing, BodyPublisher publisher) {
+ this.throwing = throwing;
+ this.publisher = publisher;
+ }
+
+ @Override
+ public long contentLength() {
+ return publisher.contentLength();
+ }
+
+ @Override
+ public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
+ try {
+ throwing.accept(Where.BEFORE_SUBSCRIBE);
+ publisher.subscribe(new SubscriberWrapper(subscriber));
+ subscribedCF.complete(null);
+ throwing.accept(Where.AFTER_SUBSCRIBE);
+ } catch (Throwable t) {
+ subscribedCF.completeExceptionally(t);
+ throw t;
+ }
+ }
+
+ CompletableFuture<Void> subscribedCF() {
+ return subscribedCF;
+ }
+
+ class SubscriptionWrapper implements Flow.Subscription {
+ final Flow.Subscription subscription;
+ final AtomicLong requestCount = new AtomicLong();
+ SubscriptionWrapper(Flow.Subscription subscription) {
+ this.subscription = subscription;
+ }
+ @Override
+ public void request(long n) {
+ long count = requestCount.incrementAndGet();
+ System.out.printf("%s request-%d(%d)%n", now(), count, n);
+ if (count > 1) throwing.accept(Where.BEFORE_NEXT_REQUEST);
+ throwing.accept(Where.BEFORE_REQUEST);
+ subscription.request(n);
+ throwing.accept(Where.AFTER_REQUEST);
+ if (count > 1) throwing.accept(Where.AFTER_NEXT_REQUEST);
+ }
+
+ @Override
+ public void cancel() {
+ throwing.accept(Where.BEFORE_CANCEL);
+ subscription.cancel();
+ throwing.accept(Where.AFTER_CANCEL);
+ }
+ }
+
+ class SubscriberWrapper implements Flow.Subscriber<ByteBuffer> {
+ final Flow.Subscriber<? super ByteBuffer> subscriber;
+ SubscriberWrapper(Flow.Subscriber<? super ByteBuffer> subscriber) {
+ this.subscriber = subscriber;
+ }
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ subscriber.onSubscribe(new SubscriptionWrapper(subscription));
+ }
+ @Override
+ public void onNext(ByteBuffer item) {
+ subscriber.onNext(item);
+ }
+ @Override
+ public void onComplete() {
+ subscriber.onComplete();
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ subscriber.onError(throwable);
+ }
+ }
+ }
+
+
+ @BeforeTest
+ public void setup() throws Exception {
+ sslContext = new SimpleSSLContext().get();
+ if (sslContext == null)
+ throw new AssertionError("Unexpected null sslContext");
+
+ // HTTP/1.1
+ HttpTestHandler h1_fixedLengthHandler = new HTTP_FixedLengthHandler();
+ HttpTestHandler h1_chunkHandler = new HTTP_ChunkedHandler();
+ InetSocketAddress sa = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0);
+ httpTestServer = HttpTestServer.of(HttpServer.create(sa, 0));
+ httpTestServer.addHandler(h1_fixedLengthHandler, "/http1/fixed");
+ httpTestServer.addHandler(h1_chunkHandler, "/http1/chunk");
+ httpURI_fixed = "http://" + httpTestServer.serverAuthority() + "/http1/fixed/x";
+ httpURI_chunk = "http://" + httpTestServer.serverAuthority() + "/http1/chunk/x";
+
+ HttpsServer httpsServer = HttpsServer.create(sa, 0);
+ httpsServer.setHttpsConfigurator(new HttpsConfigurator(sslContext));
+ httpsTestServer = HttpTestServer.of(httpsServer);
+ httpsTestServer.addHandler(h1_fixedLengthHandler, "/https1/fixed");
+ httpsTestServer.addHandler(h1_chunkHandler, "/https1/chunk");
+ httpsURI_fixed = "https://" + httpsTestServer.serverAuthority() + "/https1/fixed/x";
+ httpsURI_chunk = "https://" + httpsTestServer.serverAuthority() + "/https1/chunk/x";
+
+ // HTTP/2
+ HttpTestHandler h2_fixedLengthHandler = new HTTP_FixedLengthHandler();
+ HttpTestHandler h2_chunkedHandler = new HTTP_ChunkedHandler();
+
+ http2TestServer = HttpTestServer.of(new Http2TestServer("localhost", false, 0));
+ http2TestServer.addHandler(h2_fixedLengthHandler, "/http2/fixed");
+ http2TestServer.addHandler(h2_chunkedHandler, "/http2/chunk");
+ http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed/x";
+ http2URI_chunk = "http://" + http2TestServer.serverAuthority() + "/http2/chunk/x";
+
+ https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, sslContext));
+ https2TestServer.addHandler(h2_fixedLengthHandler, "/https2/fixed");
+ https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk");
+ https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed/x";
+ https2URI_chunk = "https://" + https2TestServer.serverAuthority() + "/https2/chunk/x";
+
+ serverCount.addAndGet(4);
+ httpTestServer.start();
+ httpsTestServer.start();
+ http2TestServer.start();
+ https2TestServer.start();
+ }
+
+ @AfterTest
+ public void teardown() throws Exception {
+ String sharedClientName =
+ sharedClient == null ? null : sharedClient.toString();
+ sharedClient = null;
+ Thread.sleep(100);
+ AssertionError fail = TRACKER.check(500);
+ try {
+ httpTestServer.stop();
+ httpsTestServer.stop();
+ http2TestServer.stop();
+ https2TestServer.stop();
+ } finally {
+ if (fail != null) {
+ if (sharedClientName != null) {
+ System.err.println("Shared client name is: " + sharedClientName);
+ }
+ throw fail;
+ }
+ }
+ }
+
+ static class HTTP_FixedLengthHandler implements HttpTestHandler {
+ @Override
+ public void handle(HttpTestExchange t) throws IOException {
+ out.println("HTTP_FixedLengthHandler received request to " + t.getRequestURI());
+ byte[] resp;
+ try (InputStream is = t.getRequestBody()) {
+ resp = is.readAllBytes();
+ }
+ t.sendResponseHeaders(200, resp.length); //fixed content length
+ try (OutputStream os = t.getResponseBody()) {
+ os.write(resp);
+ }
+ }
+ }
+
+ static class HTTP_ChunkedHandler implements HttpTestHandler {
+ @Override
+ public void handle(HttpTestExchange t) throws IOException {
+ out.println("HTTP_ChunkedHandler received request to " + t.getRequestURI());
+ byte[] resp;
+ try (InputStream is = t.getRequestBody()) {
+ resp = is.readAllBytes();
+ }
+ t.sendResponseHeaders(200, -1); // chunked/variable
+ try (OutputStream os = t.getResponseBody()) {
+ os.write(resp);
+ }
+ }
+ }
+
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/AbstractThrowingPushPromises.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,776 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Tests what happens when push promise handlers and their
+ * response body handlers and subscribers throw unexpected exceptions.
+ * @library /lib/testlibrary http2/server
+ * @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters
+ * ReferenceTracker AbstractThrowingPushPromises
+ * @modules java.base/sun.net.www.http
+ * java.net.http/jdk.internal.net.http.common
+ * java.net.http/jdk.internal.net.http.frame
+ * java.net.http/jdk.internal.net.http.hpack
+ * @run testng/othervm -Djdk.internal.httpclient.debug=true AbstractThrowingPushPromises
+ */
+
+import jdk.testlibrary.SimpleSSLContext;
+import org.testng.annotations.AfterTest;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeTest;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import javax.net.ssl.SSLContext;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.UncheckedIOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.http.HttpClient;
+import java.net.http.HttpHeaders;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandler;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.net.http.HttpResponse.BodySubscriber;
+import java.net.http.HttpResponse.PushPromiseHandler;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Flow;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.BiPredicate;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static java.lang.System.out;
+import static java.lang.System.err;
+import static java.lang.String.format;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+public abstract class AbstractThrowingPushPromises implements HttpServerAdapters {
+
+ SSLContext sslContext;
+ HttpTestServer http2TestServer; // HTTP/2 ( h2c )
+ HttpTestServer https2TestServer; // HTTP/2 ( h2 )
+ String http2URI_fixed;
+ String http2URI_chunk;
+ String https2URI_fixed;
+ String https2URI_chunk;
+
+ static final int ITERATION_COUNT = 1;
+ // a shared executor helps reduce the amount of threads created by the test
+ static final Executor executor = new TestExecutor(Executors.newCachedThreadPool());
+ static final ConcurrentMap<String, Throwable> FAILURES = new ConcurrentHashMap<>();
+ static volatile boolean tasksFailed;
+ static final AtomicLong serverCount = new AtomicLong();
+ static final AtomicLong clientCount = new AtomicLong();
+ static final long start = System.nanoTime();
+ public static String now() {
+ long now = System.nanoTime() - start;
+ long secs = now / 1000_000_000;
+ long mill = (now % 1000_000_000) / 1000_000;
+ long nan = now % 1000_000;
+ return String.format("[%d s, %d ms, %d ns] ", secs, mill, nan);
+ }
+
+ final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE;
+ private volatile HttpClient sharedClient;
+
+ static class TestExecutor implements Executor {
+ final AtomicLong tasks = new AtomicLong();
+ Executor executor;
+ TestExecutor(Executor executor) {
+ this.executor = executor;
+ }
+
+ @Override
+ public void execute(Runnable command) {
+ long id = tasks.incrementAndGet();
+ executor.execute(() -> {
+ try {
+ command.run();
+ } catch (Throwable t) {
+ tasksFailed = true;
+ out.printf(now() + "Task %s failed: %s%n", id, t);
+ err.printf(now() + "Task %s failed: %s%n", id, t);
+ FAILURES.putIfAbsent("Task " + id, t);
+ throw t;
+ }
+ });
+ }
+ }
+
+ @AfterClass
+ static final void printFailedTests() {
+ out.println("\n=========================");
+ try {
+ out.printf("%n%sCreated %d servers and %d clients%n",
+ now(), serverCount.get(), clientCount.get());
+ if (FAILURES.isEmpty()) return;
+ out.println("Failed tests: ");
+ FAILURES.entrySet().forEach((e) -> {
+ out.printf("\t%s: %s%n", e.getKey(), e.getValue());
+ e.getValue().printStackTrace(out);
+ e.getValue().printStackTrace();
+ });
+ if (tasksFailed) {
+ out.println("WARNING: Some tasks failed");
+ }
+ } finally {
+ out.println("\n=========================\n");
+ }
+ }
+
+ private String[] uris() {
+ return new String[] {
+ http2URI_fixed,
+ http2URI_chunk,
+ https2URI_fixed,
+ https2URI_chunk,
+ };
+ }
+
+ @DataProvider(name = "sanity")
+ public Object[][] sanity() {
+ String[] uris = uris();
+ Object[][] result = new Object[uris.length * 2][];
+
+ int i = 0;
+ for (boolean sameClient : List.of(false, true)) {
+ for (String uri: uris()) {
+ result[i++] = new Object[] {uri, sameClient};
+ }
+ }
+ assert i == uris.length * 2;
+ return result;
+ }
+
+ enum Where {
+ BODY_HANDLER, ON_SUBSCRIBE, ON_NEXT, ON_COMPLETE, ON_ERROR, GET_BODY, BODY_CF,
+ BEFORE_ACCEPTING, AFTER_ACCEPTING;
+ public Consumer<Where> select(Consumer<Where> consumer) {
+ return new Consumer<Where>() {
+ @Override
+ public void accept(Where where) {
+ if (Where.this == where) {
+ consumer.accept(where);
+ }
+ }
+ };
+ }
+ }
+
+ private Object[][] variants(List<Thrower> throwers) {
+ String[] uris = uris();
+ Object[][] result = new Object[uris.length * 2 * throwers.size()][];
+ int i = 0;
+ for (Thrower thrower : throwers) {
+ for (boolean sameClient : List.of(false, true)) {
+ for (String uri : uris()) {
+ result[i++] = new Object[]{uri, sameClient, thrower};
+ }
+ }
+ }
+ assert i == uris.length * 2 * throwers.size();
+ return result;
+ }
+
+ @DataProvider(name = "ioVariants")
+ public Object[][] ioVariants() {
+ return variants(List.of(
+ new UncheckedIOExceptionThrower()));
+ }
+
+ @DataProvider(name = "customVariants")
+ public Object[][] customVariants() {
+ return variants(List.of(
+ new UncheckedCustomExceptionThrower()));
+ }
+
+ private HttpClient makeNewClient() {
+ clientCount.incrementAndGet();
+ return TRACKER.track(HttpClient.newBuilder()
+ .proxy(HttpClient.Builder.NO_PROXY)
+ .executor(executor)
+ .sslContext(sslContext)
+ .build());
+ }
+
+ HttpClient newHttpClient(boolean share) {
+ if (!share) return makeNewClient();
+ HttpClient shared = sharedClient;
+ if (shared != null) return shared;
+ synchronized (this) {
+ shared = sharedClient;
+ if (shared == null) {
+ shared = sharedClient = makeNewClient();
+ }
+ return shared;
+ }
+ }
+
+ // @Test(dataProvider = "sanity")
+ protected void testSanityImpl(String uri, boolean sameClient)
+ throws Exception {
+ HttpClient client = null;
+ out.printf("%ntestNoThrows(%s, %b)%n", uri, sameClient);
+ for (int i=0; i< ITERATION_COUNT; i++) {
+ if (!sameClient || client == null)
+ client = newHttpClient(sameClient);
+
+ HttpRequest req = HttpRequest.newBuilder(URI.create(uri))
+ .build();
+ BodyHandler<Stream<String>> handler =
+ new ThrowingBodyHandler((w) -> {},
+ BodyHandlers.ofLines());
+ Map<HttpRequest, CompletableFuture<HttpResponse<Stream<String>>>> pushPromises =
+ new ConcurrentHashMap<>();
+ PushPromiseHandler<Stream<String>> pushHandler = new PushPromiseHandler<>() {
+ @Override
+ public void applyPushPromise(HttpRequest initiatingRequest,
+ HttpRequest pushPromiseRequest,
+ Function<BodyHandler<Stream<String>>,
+ CompletableFuture<HttpResponse<Stream<String>>>>
+ acceptor) {
+ pushPromises.putIfAbsent(pushPromiseRequest, acceptor.apply(handler));
+ }
+ };
+ HttpResponse<Stream<String>> response =
+ client.sendAsync(req, BodyHandlers.ofLines(), pushHandler).get();
+ String body = response.body().collect(Collectors.joining("|"));
+ assertEquals(URI.create(body).getPath(), URI.create(uri).getPath());
+ for (HttpRequest promised : pushPromises.keySet()) {
+ out.printf("%s Received promise: %s%n\tresponse: %s%n",
+ now(), promised, pushPromises.get(promised).get());
+ String promisedBody = pushPromises.get(promised).get().body()
+ .collect(Collectors.joining("|"));
+ assertEquals(promisedBody, promised.uri().toASCIIString());
+ }
+ assertEquals(3, pushPromises.size());
+ }
+ }
+
+ // @Test(dataProvider = "variants")
+ protected void testThrowingAsStringImpl(String uri,
+ boolean sameClient,
+ Thrower thrower)
+ throws Exception
+ {
+ String test = format("testThrowingAsString(%s, %b, %s)",
+ uri, sameClient, thrower);
+ testThrowing(test, uri, sameClient, BodyHandlers::ofString,
+ this::checkAsString, thrower);
+ }
+
+ //@Test(dataProvider = "variants")
+ protected void testThrowingAsLinesImpl(String uri,
+ boolean sameClient,
+ Thrower thrower)
+ throws Exception
+ {
+ String test = format("testThrowingAsLines(%s, %b, %s)",
+ uri, sameClient, thrower);
+ testThrowing(test, uri, sameClient, BodyHandlers::ofLines,
+ this::checkAsLines, thrower);
+ }
+
+ //@Test(dataProvider = "variants")
+ protected void testThrowingAsInputStreamImpl(String uri,
+ boolean sameClient,
+ Thrower thrower)
+ throws Exception
+ {
+ String test = format("testThrowingAsInputStream(%s, %b, %s)",
+ uri, sameClient, thrower);
+ testThrowing(test, uri, sameClient, BodyHandlers::ofInputStream,
+ this::checkAsInputStream, thrower);
+ }
+
+ private <T,U> void testThrowing(String name, String uri, boolean sameClient,
+ Supplier<BodyHandler<T>> handlers,
+ Finisher finisher, Thrower thrower)
+ throws Exception
+ {
+ out.printf("%n%s%s%n", now(), name);
+ try {
+ testThrowing(uri, sameClient, handlers, finisher, thrower);
+ } catch (Error | Exception x) {
+ FAILURES.putIfAbsent(name, x);
+ throw x;
+ }
+ }
+
+ private <T,U> void testThrowing(String uri, boolean sameClient,
+ Supplier<BodyHandler<T>> handlers,
+ Finisher finisher, Thrower thrower)
+ throws Exception
+ {
+ HttpClient client = null;
+ for (Where where : Where.values()) {
+ if (where == Where.ON_ERROR) continue;
+ if (!sameClient || client == null)
+ client = newHttpClient(sameClient);
+
+ HttpRequest req = HttpRequest.
+ newBuilder(URI.create(uri))
+ .build();
+ ConcurrentMap<HttpRequest, CompletableFuture<HttpResponse<T>>> promiseMap =
+ new ConcurrentHashMap<>();
+ Supplier<BodyHandler<T>> throwing = () ->
+ new ThrowingBodyHandler(where.select(thrower), handlers.get());
+ PushPromiseHandler<T> pushHandler = new ThrowingPromiseHandler<>(
+ where.select(thrower),
+ PushPromiseHandler.of((r) -> throwing.get(), promiseMap));
+ out.println("try throwing in " + where);
+ HttpResponse<T> response = null;
+ try {
+ response = client.sendAsync(req, handlers.get(), pushHandler).join();
+ } catch (Error | Exception x) {
+ throw x;
+ }
+ if (response != null) {
+ finisher.finish(where, req.uri(), response, thrower, promiseMap);
+ }
+ }
+ }
+
+ interface Thrower extends Consumer<Where>, Predicate<Throwable> {
+
+ }
+
+ interface Finisher<T,U> {
+ U finish(Where w, URI requestURI, HttpResponse<T> resp, Thrower thrower,
+ Map<HttpRequest, CompletableFuture<HttpResponse<T>>> promises);
+ }
+
+ final <T,U> U shouldHaveThrown(Where w, HttpResponse<T> resp, Thrower thrower) {
+ String msg = "Expected exception not thrown in " + w
+ + "\n\tReceived: " + resp
+ + "\n\tWith body: " + resp.body();
+ System.out.println(msg);
+ throw new RuntimeException(msg);
+ }
+
+ final List<String> checkAsString(Where w, URI reqURI,
+ HttpResponse<String> resp,
+ Thrower thrower,
+ Map<HttpRequest, CompletableFuture<HttpResponse<String>>> promises) {
+ Function<HttpResponse<String>, List<String>> extractor =
+ (r) -> List.of(r.body());
+ return check(w, reqURI, resp, thrower, promises, extractor);
+ }
+
+ final List<String> checkAsLines(Where w, URI reqURI,
+ HttpResponse<Stream<String>> resp,
+ Thrower thrower,
+ Map<HttpRequest, CompletableFuture<HttpResponse<Stream<String>>>> promises) {
+ Function<HttpResponse<Stream<String>>, List<String>> extractor =
+ (r) -> r.body().collect(Collectors.toList());
+ return check(w, reqURI, resp, thrower, promises, extractor);
+ }
+
+ final List<String> checkAsInputStream(Where w, URI reqURI,
+ HttpResponse<InputStream> resp,
+ Thrower thrower,
+ Map<HttpRequest, CompletableFuture<HttpResponse<InputStream>>> promises)
+ {
+ Function<HttpResponse<InputStream>, List<String>> extractor = (r) -> {
+ List<String> result;
+ try (InputStream is = r.body()) {
+ result = new BufferedReader(new InputStreamReader(is))
+ .lines().collect(Collectors.toList());
+ } catch (Throwable t) {
+ throw new CompletionException(t);
+ }
+ return result;
+ };
+ return check(w, reqURI, resp, thrower, promises, extractor);
+ }
+
+ private final <T> List<String> check(Where w, URI reqURI,
+ HttpResponse<T> resp,
+ Thrower thrower,
+ Map<HttpRequest, CompletableFuture<HttpResponse<T>>> promises,
+ Function<HttpResponse<T>, List<String>> extractor)
+ {
+ List<String> result = extractor.apply(resp);
+ for (HttpRequest req : promises.keySet()) {
+ switch (w) {
+ case BEFORE_ACCEPTING:
+ throw new RuntimeException("No push promise should have been received" +
+ " for " + reqURI + " in " + w + ": got " + promises.keySet());
+ default:
+ break;
+ }
+ HttpResponse<T> presp;
+ try {
+ presp = promises.get(req).join();
+ } catch (Error | Exception x) {
+ Throwable cause = findCause(x, thrower);
+ if (cause != null) {
+ out.println(now() + "Got expected exception in "
+ + w + ": " + cause);
+ continue;
+ }
+ throw x;
+ }
+ switch (w) {
+ case BEFORE_ACCEPTING:
+ case AFTER_ACCEPTING:
+ case BODY_HANDLER:
+ case GET_BODY:
+ case BODY_CF:
+ return shouldHaveThrown(w, presp, thrower);
+ default:
+ break;
+ }
+ List<String> presult = null;
+ try {
+ presult = extractor.apply(presp);
+ } catch (Error | Exception x) {
+ Throwable cause = findCause(x, thrower);
+ if (cause != null) {
+ out.println(now() + "Got expected exception for "
+ + req + " in " + w + ": " + cause);
+ continue;
+ }
+ throw x;
+ }
+ throw new RuntimeException("Expected exception not thrown for "
+ + req + " in " + w);
+ }
+ final int expectedCount;
+ switch (w) {
+ case BEFORE_ACCEPTING:
+ expectedCount = 0;
+ break;
+ default:
+ expectedCount = 3;
+ }
+ assertEquals(promises.size(), expectedCount,
+ "bad promise count for " + reqURI + " with " + w);
+ assertEquals(result, List.of(reqURI.toASCIIString()));
+ return result;
+ }
+
+ private static Throwable findCause(Throwable x,
+ Predicate<Throwable> filter) {
+ while (x != null && !filter.test(x)) x = x.getCause();
+ return x;
+ }
+
+ static final class UncheckedCustomExceptionThrower implements Thrower {
+ @Override
+ public void accept(Where where) {
+ out.println(now() + "Throwing in " + where);
+ throw new UncheckedCustomException(where.name());
+ }
+
+ @Override
+ public boolean test(Throwable throwable) {
+ return UncheckedCustomException.class.isInstance(throwable);
+ }
+
+ @Override
+ public String toString() {
+ return "UncheckedCustomExceptionThrower";
+ }
+ }
+
+ static final class UncheckedIOExceptionThrower implements Thrower {
+ @Override
+ public void accept(Where where) {
+ out.println(now() + "Throwing in " + where);
+ throw new UncheckedIOException(new CustomIOException(where.name()));
+ }
+
+ @Override
+ public boolean test(Throwable throwable) {
+ return UncheckedIOException.class.isInstance(throwable)
+ && CustomIOException.class.isInstance(throwable.getCause());
+ }
+
+ @Override
+ public String toString() {
+ return "UncheckedIOExceptionThrower";
+ }
+ }
+
+ static final class UncheckedCustomException extends RuntimeException {
+ UncheckedCustomException(String message) {
+ super(message);
+ }
+ UncheckedCustomException(String message, Throwable cause) {
+ super(message, cause);
+ }
+ }
+
+ static final class CustomIOException extends IOException {
+ CustomIOException(String message) {
+ super(message);
+ }
+ CustomIOException(String message, Throwable cause) {
+ super(message, cause);
+ }
+ }
+
+ static final class ThrowingPromiseHandler<T> implements PushPromiseHandler<T> {
+ final Consumer<Where> throwing;
+ final PushPromiseHandler<T> pushHandler;
+ ThrowingPromiseHandler(Consumer<Where> throwing, PushPromiseHandler<T> pushHandler) {
+ this.throwing = throwing;
+ this.pushHandler = pushHandler;
+ }
+
+ @Override
+ public void applyPushPromise(HttpRequest initiatingRequest,
+ HttpRequest pushPromiseRequest,
+ Function<BodyHandler<T>,
+ CompletableFuture<HttpResponse<T>>> acceptor) {
+ throwing.accept(Where.BEFORE_ACCEPTING);
+ pushHandler.applyPushPromise(initiatingRequest, pushPromiseRequest, acceptor);
+ throwing.accept(Where.AFTER_ACCEPTING);
+ }
+ }
+
+ static final class ThrowingBodyHandler<T> implements BodyHandler<T> {
+ final Consumer<Where> throwing;
+ final BodyHandler<T> bodyHandler;
+ ThrowingBodyHandler(Consumer<Where> throwing, BodyHandler<T> bodyHandler) {
+ this.throwing = throwing;
+ this.bodyHandler = bodyHandler;
+ }
+ @Override
+ public BodySubscriber<T> apply(HttpResponse.ResponseInfo rinfo) {
+ throwing.accept(Where.BODY_HANDLER);
+ BodySubscriber<T> subscriber = bodyHandler.apply(rinfo);
+ return new ThrowingBodySubscriber(throwing, subscriber);
+ }
+ }
+
+ static final class ThrowingBodySubscriber<T> implements BodySubscriber<T> {
+ private final BodySubscriber<T> subscriber;
+ volatile boolean onSubscribeCalled;
+ final Consumer<Where> throwing;
+ ThrowingBodySubscriber(Consumer<Where> throwing, BodySubscriber<T> subscriber) {
+ this.throwing = throwing;
+ this.subscriber = subscriber;
+ }
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ //out.println("onSubscribe ");
+ onSubscribeCalled = true;
+ throwing.accept(Where.ON_SUBSCRIBE);
+ subscriber.onSubscribe(subscription);
+ }
+
+ @Override
+ public void onNext(List<ByteBuffer> item) {
+ // out.println("onNext " + item);
+ assertTrue(onSubscribeCalled);
+ throwing.accept(Where.ON_NEXT);
+ subscriber.onNext(item);
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ //out.println("onError");
+ assertTrue(onSubscribeCalled);
+ throwing.accept(Where.ON_ERROR);
+ subscriber.onError(throwable);
+ }
+
+ @Override
+ public void onComplete() {
+ //out.println("onComplete");
+ assertTrue(onSubscribeCalled, "onComplete called before onSubscribe");
+ throwing.accept(Where.ON_COMPLETE);
+ subscriber.onComplete();
+ }
+
+ @Override
+ public CompletionStage<T> getBody() {
+ throwing.accept(Where.GET_BODY);
+ try {
+ throwing.accept(Where.BODY_CF);
+ } catch (Throwable t) {
+ return CompletableFuture.failedFuture(t);
+ }
+ return subscriber.getBody();
+ }
+ }
+
+
+ @BeforeTest
+ public void setup() throws Exception {
+ sslContext = new SimpleSSLContext().get();
+ if (sslContext == null)
+ throw new AssertionError("Unexpected null sslContext");
+
+ // HTTP/2
+ HttpTestHandler h2_fixedLengthHandler = new HTTP_FixedLengthHandler();
+ HttpTestHandler h2_chunkedHandler = new HTTP_ChunkedHandler();
+
+ http2TestServer = HttpTestServer.of(new Http2TestServer("localhost", false, 0));
+ http2TestServer.addHandler(h2_fixedLengthHandler, "/http2/fixed");
+ http2TestServer.addHandler(h2_chunkedHandler, "/http2/chunk");
+ http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed/x";
+ http2URI_chunk = "http://" + http2TestServer.serverAuthority() + "/http2/chunk/x";
+
+ https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, sslContext));
+ https2TestServer.addHandler(h2_fixedLengthHandler, "/https2/fixed");
+ https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk");
+ https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed/x";
+ https2URI_chunk = "https://" + https2TestServer.serverAuthority() + "/https2/chunk/x";
+
+ serverCount.addAndGet(2);
+ http2TestServer.start();
+ https2TestServer.start();
+ }
+
+ @AfterTest
+ public void teardown() throws Exception {
+ String sharedClientName =
+ sharedClient == null ? null : sharedClient.toString();
+ sharedClient = null;
+ Thread.sleep(100);
+ AssertionError fail = TRACKER.check(500);
+ try {
+ http2TestServer.stop();
+ https2TestServer.stop();
+ } finally {
+ if (fail != null) {
+ if (sharedClientName != null) {
+ System.err.println("Shared client name is: " + sharedClientName);
+ }
+ throw fail;
+ }
+ }
+ }
+
+ static final BiPredicate<String,String> ACCEPT_ALL = (x, y) -> true;
+
+ private static void pushPromiseFor(HttpTestExchange t,
+ URI requestURI,
+ String pushPath,
+ boolean fixed)
+ throws IOException
+ {
+ try {
+ URI promise = new URI(requestURI.getScheme(),
+ requestURI.getAuthority(),
+ pushPath, null, null);
+ byte[] promiseBytes = promise.toASCIIString().getBytes(UTF_8);
+ out.printf("TestServer: %s Pushing promise: %s%n", now(), promise);
+ err.printf("TestServer: %s Pushing promise: %s%n", now(), promise);
+ HttpHeaders headers;
+ if (fixed) {
+ String length = String.valueOf(promiseBytes.length);
+ headers = HttpHeaders.of(Map.of("Content-Length", List.of(length)),
+ ACCEPT_ALL);
+ } else {
+ headers = HttpHeaders.of(Map.of(), ACCEPT_ALL); // empty
+ }
+ t.serverPush(promise, headers, promiseBytes);
+ } catch (URISyntaxException x) {
+ throw new IOException(x.getMessage(), x);
+ }
+ }
+
+ static class HTTP_FixedLengthHandler implements HttpTestHandler {
+ @Override
+ public void handle(HttpTestExchange t) throws IOException {
+ out.println("HTTP_FixedLengthHandler received request to " + t.getRequestURI());
+ try (InputStream is = t.getRequestBody()) {
+ is.readAllBytes();
+ }
+ URI requestURI = t.getRequestURI();
+ for (int i = 1; i<2; i++) {
+ String path = requestURI.getPath() + "/before/promise-" + i;
+ pushPromiseFor(t, requestURI, path, true);
+ }
+ byte[] resp = t.getRequestURI().toString().getBytes(StandardCharsets.UTF_8);
+ t.sendResponseHeaders(200, resp.length); //fixed content length
+ try (OutputStream os = t.getResponseBody()) {
+ int bytes = resp.length/3;
+ for (int i = 0; i<2; i++) {
+ String path = requestURI.getPath() + "/after/promise-" + (i + 2);
+ os.write(resp, i * bytes, bytes);
+ os.flush();
+ pushPromiseFor(t, requestURI, path, true);
+ }
+ os.write(resp, 2*bytes, resp.length - 2*bytes);
+ }
+ }
+
+ }
+
+ static class HTTP_ChunkedHandler implements HttpTestHandler {
+ @Override
+ public void handle(HttpTestExchange t) throws IOException {
+ out.println("HTTP_ChunkedHandler received request to " + t.getRequestURI());
+ byte[] resp = t.getRequestURI().toString().getBytes(StandardCharsets.UTF_8);
+ try (InputStream is = t.getRequestBody()) {
+ is.readAllBytes();
+ }
+ URI requestURI = t.getRequestURI();
+ for (int i = 1; i<2; i++) {
+ String path = requestURI.getPath() + "/before/promise-" + i;
+ pushPromiseFor(t, requestURI, path, false);
+ }
+ t.sendResponseHeaders(200, -1); // chunked/variable
+ try (OutputStream os = t.getResponseBody()) {
+ int bytes = resp.length/3;
+ for (int i = 0; i<2; i++) {
+ String path = requestURI.getPath() + "/after/promise-" + (i + 2);
+ os.write(resp, i * bytes, bytes);
+ os.flush();
+ pushPromiseFor(t, requestURI, path, false);
+ }
+ os.write(resp, 2*bytes, resp.length - 2*bytes);
+ }
+ }
+ }
+
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/AbstractThrowingSubscribers.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,733 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import com.sun.net.httpserver.HttpServer;
+import com.sun.net.httpserver.HttpsConfigurator;
+import com.sun.net.httpserver.HttpsServer;
+import jdk.testlibrary.SimpleSSLContext;
+import org.testng.annotations.AfterTest;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeTest;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import javax.net.ssl.SSLContext;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.UncheckedIOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpHeaders;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandler;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.net.http.HttpResponse.BodySubscriber;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Flow;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static java.lang.System.out;
+import static java.lang.String.format;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+public abstract class AbstractThrowingSubscribers implements HttpServerAdapters {
+
+ SSLContext sslContext;
+ HttpTestServer httpTestServer; // HTTP/1.1 [ 4 servers ]
+ HttpTestServer httpsTestServer; // HTTPS/1.1
+ HttpTestServer http2TestServer; // HTTP/2 ( h2c )
+ HttpTestServer https2TestServer; // HTTP/2 ( h2 )
+ String httpURI_fixed;
+ String httpURI_chunk;
+ String httpsURI_fixed;
+ String httpsURI_chunk;
+ String http2URI_fixed;
+ String http2URI_chunk;
+ String https2URI_fixed;
+ String https2URI_chunk;
+
+ static final int ITERATION_COUNT = 1;
+ // a shared executor helps reduce the amount of threads created by the test
+ static final Executor executor = new TestExecutor(Executors.newCachedThreadPool());
+ static final ConcurrentMap<String, Throwable> FAILURES = new ConcurrentHashMap<>();
+ static volatile boolean tasksFailed;
+ static final AtomicLong serverCount = new AtomicLong();
+ static final AtomicLong clientCount = new AtomicLong();
+ static final long start = System.nanoTime();
+ public static String now() {
+ long now = System.nanoTime() - start;
+ long secs = now / 1000_000_000;
+ long mill = (now % 1000_000_000) / 1000_000;
+ long nan = now % 1000_000;
+ return String.format("[%d s, %d ms, %d ns] ", secs, mill, nan);
+ }
+
+ final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE;
+ private volatile HttpClient sharedClient;
+
+ static class TestExecutor implements Executor {
+ final AtomicLong tasks = new AtomicLong();
+ Executor executor;
+ TestExecutor(Executor executor) {
+ this.executor = executor;
+ }
+
+ @Override
+ public void execute(Runnable command) {
+ long id = tasks.incrementAndGet();
+ executor.execute(() -> {
+ try {
+ command.run();
+ } catch (Throwable t) {
+ tasksFailed = true;
+ System.out.printf(now() + "Task %s failed: %s%n", id, t);
+ System.err.printf(now() + "Task %s failed: %s%n", id, t);
+ FAILURES.putIfAbsent("Task " + id, t);
+ throw t;
+ }
+ });
+ }
+ }
+
+ @AfterClass
+ static final void printFailedTests() {
+ out.println("\n=========================");
+ try {
+ out.printf("%n%sCreated %d servers and %d clients%n",
+ now(), serverCount.get(), clientCount.get());
+ if (FAILURES.isEmpty()) return;
+ out.println("Failed tests: ");
+ FAILURES.entrySet().forEach((e) -> {
+ out.printf("\t%s: %s%n", e.getKey(), e.getValue());
+ e.getValue().printStackTrace(out);
+ e.getValue().printStackTrace();
+ });
+ if (tasksFailed) {
+ System.out.println("WARNING: Some tasks failed");
+ }
+ } finally {
+ out.println("\n=========================\n");
+ }
+ }
+
+ private String[] uris() {
+ return new String[] {
+ httpURI_fixed,
+ httpURI_chunk,
+ httpsURI_fixed,
+ httpsURI_chunk,
+ http2URI_fixed,
+ http2URI_chunk,
+ https2URI_fixed,
+ https2URI_chunk,
+ };
+ }
+
+ static AtomicLong URICOUNT = new AtomicLong();
+
+ @DataProvider(name = "sanity")
+ public Object[][] sanity() {
+ String[] uris = uris();
+ Object[][] result = new Object[uris.length * 2][];
+ int i = 0;
+ for (boolean sameClient : List.of(false, true)) {
+ for (String uri: uris()) {
+ result[i++] = new Object[] {uri, sameClient};
+ }
+ }
+ assert i == uris.length * 2;
+ return result;
+ }
+
+ @DataProvider(name = "variants")
+ public Object[][] variants() {
+ String[] uris = uris();
+ Object[][] result = new Object[uris.length * 2 * 2][];
+ int i = 0;
+ for (Thrower thrower : List.of(
+ new UncheckedIOExceptionThrower(),
+ new UncheckedCustomExceptionThrower())) {
+ for (boolean sameClient : List.of(false, true)) {
+ for (String uri : uris()) {
+ result[i++] = new Object[]{uri, sameClient, thrower};
+ }
+ }
+ }
+ assert i == uris.length * 2 * 2;
+ return result;
+ }
+
+ private HttpClient makeNewClient() {
+ clientCount.incrementAndGet();
+ HttpClient client = HttpClient.newBuilder()
+ .proxy(HttpClient.Builder.NO_PROXY)
+ .executor(executor)
+ .sslContext(sslContext)
+ .build();
+ return TRACKER.track(client);
+ }
+
+ HttpClient newHttpClient(boolean share) {
+ if (!share) return makeNewClient();
+ HttpClient shared = sharedClient;
+ if (shared != null) return shared;
+ synchronized (this) {
+ shared = sharedClient;
+ if (shared == null) {
+ shared = sharedClient = makeNewClient();
+ }
+ return shared;
+ }
+ }
+
+ enum SubscriberType {
+ INLINE, // In line subscribers complete their CF on ON_COMPLETE
+ // e.g. BodySubscribers::ofString
+ OFFLINE; // Off line subscribers complete their CF immediately
+ // but require the client to pull the data after the
+ // CF completes (e.g. BodySubscribers::ofInputStream)
+ }
+
+ static EnumSet<Where> excludes(SubscriberType type) {
+ EnumSet<Where> set = EnumSet.noneOf(Where.class);
+
+ if (type == SubscriberType.OFFLINE) {
+ // Throwing on onSubscribe needs some more work
+ // for the case of InputStream, where the body has already
+ // completed by the time the subscriber is subscribed.
+ // The only way we have at that point to relay the exception
+ // is to call onError on the subscriber, but should we if
+ // Subscriber::onSubscribed has thrown an exception and
+ // not completed normally?
+ set.add(Where.ON_SUBSCRIBE);
+ }
+
+ // Don't know how to make the stack reliably cause onError
+ // to be called without closing the connection.
+ // And how do we get the exception if onError throws anyway?
+ set.add(Where.ON_ERROR);
+
+ return set;
+ }
+
+ //@Test(dataProvider = "sanity")
+ protected void testSanityImpl(String uri, boolean sameClient)
+ throws Exception {
+ HttpClient client = null;
+ String uri2 = uri + "-" + URICOUNT.incrementAndGet() + "/sanity";
+ out.printf("%ntestSanity(%s, %b)%n", uri2, sameClient);
+ for (int i=0; i< ITERATION_COUNT; i++) {
+ if (!sameClient || client == null)
+ client = newHttpClient(sameClient);
+
+ HttpRequest req = HttpRequest.newBuilder(URI.create(uri2))
+ .build();
+ BodyHandler<String> handler =
+ new ThrowingBodyHandler((w) -> {},
+ BodyHandlers.ofString());
+ HttpResponse<String> response = client.send(req, handler);
+ String body = response.body();
+ assertEquals(URI.create(body).getPath(), URI.create(uri2).getPath());
+ }
+ }
+
+ //@Test(dataProvider = "variants")
+ protected void testThrowingAsStringImpl(String uri,
+ boolean sameClient,
+ Thrower thrower)
+ throws Exception
+ {
+ uri = uri + "-" + URICOUNT.incrementAndGet();
+ String test = format("testThrowingAsString(%s, %b, %s)",
+ uri, sameClient, thrower);
+ testThrowing(test, uri, sameClient, BodyHandlers::ofString,
+ this::shouldHaveThrown, thrower,false,
+ excludes(SubscriberType.INLINE));
+ }
+
+ //@Test(dataProvider = "variants")
+ protected void testThrowingAsLinesImpl(String uri,
+ boolean sameClient,
+ Thrower thrower)
+ throws Exception
+ {
+ uri = uri + "-" + URICOUNT.incrementAndGet();
+ String test = format("testThrowingAsLines(%s, %b, %s)",
+ uri, sameClient, thrower);
+ testThrowing(test, uri, sameClient, BodyHandlers::ofLines,
+ this::checkAsLines, thrower,false,
+ excludes(SubscriberType.OFFLINE));
+ }
+
+ //@Test(dataProvider = "variants")
+ protected void testThrowingAsInputStreamImpl(String uri,
+ boolean sameClient,
+ Thrower thrower)
+ throws Exception
+ {
+ uri = uri + "-" + URICOUNT.incrementAndGet();
+ String test = format("testThrowingAsInputStream(%s, %b, %s)",
+ uri, sameClient, thrower);
+ testThrowing(test, uri, sameClient, BodyHandlers::ofInputStream,
+ this::checkAsInputStream, thrower,false,
+ excludes(SubscriberType.OFFLINE));
+ }
+
+ //@Test(dataProvider = "variants")
+ protected void testThrowingAsStringAsyncImpl(String uri,
+ boolean sameClient,
+ Thrower thrower)
+ throws Exception
+ {
+ uri = uri + "-" + URICOUNT.incrementAndGet();
+ String test = format("testThrowingAsStringAsync(%s, %b, %s)",
+ uri, sameClient, thrower);
+ testThrowing(test, uri, sameClient, BodyHandlers::ofString,
+ this::shouldHaveThrown, thrower, true,
+ excludes(SubscriberType.INLINE));
+ }
+
+ //@Test(dataProvider = "variants")
+ protected void testThrowingAsLinesAsyncImpl(String uri,
+ boolean sameClient,
+ Thrower thrower)
+ throws Exception
+ {
+ uri = uri + "-" + URICOUNT.incrementAndGet();
+ String test = format("testThrowingAsLinesAsync(%s, %b, %s)",
+ uri, sameClient, thrower);
+ testThrowing(test, uri, sameClient, BodyHandlers::ofLines,
+ this::checkAsLines, thrower,true,
+ excludes(SubscriberType.OFFLINE));
+ }
+
+ //@Test(dataProvider = "variants")
+ protected void testThrowingAsInputStreamAsyncImpl(String uri,
+ boolean sameClient,
+ Thrower thrower)
+ throws Exception
+ {
+ uri = uri + "-" + URICOUNT.incrementAndGet();
+ String test = format("testThrowingAsInputStreamAsync(%s, %b, %s)",
+ uri, sameClient, thrower);
+ testThrowing(test, uri, sameClient, BodyHandlers::ofInputStream,
+ this::checkAsInputStream, thrower,true,
+ excludes(SubscriberType.OFFLINE));
+ }
+
+ private <T,U> void testThrowing(String name, String uri, boolean sameClient,
+ Supplier<BodyHandler<T>> handlers,
+ Finisher finisher, Thrower thrower,
+ boolean async, EnumSet<Where> excludes)
+ throws Exception
+ {
+ out.printf("%n%s%s%n", now(), name);
+ try {
+ testThrowing(uri, sameClient, handlers, finisher, thrower, async, excludes);
+ } catch (Error | Exception x) {
+ FAILURES.putIfAbsent(name, x);
+ throw x;
+ }
+ }
+
+ private <T,U> void testThrowing(String uri, boolean sameClient,
+ Supplier<BodyHandler<T>> handlers,
+ Finisher finisher, Thrower thrower,
+ boolean async,
+ EnumSet<Where> excludes)
+ throws Exception
+ {
+ HttpClient client = null;
+ for (Where where : EnumSet.complementOf(excludes)) {
+
+ if (!sameClient || client == null)
+ client = newHttpClient(sameClient);
+ String uri2 = uri + "-" + where;
+ HttpRequest req = HttpRequest.
+ newBuilder(URI.create(uri2))
+ .build();
+ BodyHandler<T> handler =
+ new ThrowingBodyHandler(where.select(thrower), handlers.get());
+ System.out.println("try throwing in " + where);
+ HttpResponse<T> response = null;
+ if (async) {
+ try {
+ response = client.sendAsync(req, handler).join();
+ } catch (Error | Exception x) {
+ Throwable cause = findCause(x, thrower);
+ if (cause == null) throw causeNotFound(where, x);
+ System.out.println(now() + "Got expected exception: " + cause);
+ }
+ } else {
+ try {
+ response = client.send(req, handler);
+ } catch (Error | Exception t) {
+ // synchronous send will rethrow exceptions
+ Throwable throwable = t.getCause();
+ assert throwable != null;
+
+ if (thrower.test(throwable)) {
+ System.out.println(now() + "Got expected exception: " + throwable);
+ } else throw causeNotFound(where, t);
+ }
+ }
+ if (response != null) {
+ finisher.finish(where, response, thrower);
+ }
+ }
+ }
+
+ enum Where {
+ BODY_HANDLER, ON_SUBSCRIBE, ON_NEXT, ON_COMPLETE, ON_ERROR, GET_BODY, BODY_CF;
+ public Consumer<Where> select(Consumer<Where> consumer) {
+ return new Consumer<Where>() {
+ @Override
+ public void accept(Where where) {
+ if (Where.this == where) {
+ consumer.accept(where);
+ }
+ }
+ };
+ }
+ }
+
+ static AssertionError causeNotFound(Where w, Throwable t) {
+ return new AssertionError("Expected exception not found in " + w, t);
+ }
+
+ interface Thrower extends Consumer<Where>, Predicate<Throwable> {
+
+ }
+
+ interface Finisher<T,U> {
+ U finish(Where w, HttpResponse<T> resp, Thrower thrower) throws IOException;
+ }
+
+ final <T,U> U shouldHaveThrown(Where w, HttpResponse<T> resp, Thrower thrower) {
+ String msg = "Expected exception not thrown in " + w
+ + "\n\tReceived: " + resp
+ + "\n\tWith body: " + resp.body();
+ System.out.println(msg);
+ throw new RuntimeException(msg);
+ }
+
+ final List<String> checkAsLines(Where w, HttpResponse<Stream<String>> resp, Thrower thrower) {
+ switch(w) {
+ case BODY_HANDLER: return shouldHaveThrown(w, resp, thrower);
+ case GET_BODY: return shouldHaveThrown(w, resp, thrower);
+ case BODY_CF: return shouldHaveThrown(w, resp, thrower);
+ default: break;
+ }
+ List<String> result = null;
+ try {
+ result = resp.body().collect(Collectors.toList());
+ } catch (Error | Exception x) {
+ Throwable cause = findCause(x, thrower);
+ if (cause != null) {
+ out.println(now() + "Got expected exception in " + w + ": " + cause);
+ return result;
+ }
+ throw causeNotFound(w, x);
+ }
+ return shouldHaveThrown(w, resp, thrower);
+ }
+
+ final List<String> checkAsInputStream(Where w, HttpResponse<InputStream> resp,
+ Thrower thrower)
+ throws IOException
+ {
+ switch(w) {
+ case BODY_HANDLER: return shouldHaveThrown(w, resp, thrower);
+ case GET_BODY: return shouldHaveThrown(w, resp, thrower);
+ case BODY_CF: return shouldHaveThrown(w, resp, thrower);
+ default: break;
+ }
+ List<String> result = null;
+ try (InputStreamReader r1 = new InputStreamReader(resp.body(), UTF_8);
+ BufferedReader r = new BufferedReader(r1)) {
+ try {
+ result = r.lines().collect(Collectors.toList());
+ } catch (Error | Exception x) {
+ Throwable cause = findCause(x, thrower);
+ if (cause != null) {
+ out.println(now() + "Got expected exception in " + w + ": " + cause);
+ return result;
+ }
+ throw causeNotFound(w, x);
+ }
+ }
+ return shouldHaveThrown(w, resp, thrower);
+ }
+
+ private static Throwable findCause(Throwable x,
+ Predicate<Throwable> filter) {
+ while (x != null && !filter.test(x)) x = x.getCause();
+ return x;
+ }
+
+ static final class UncheckedCustomExceptionThrower implements Thrower {
+ @Override
+ public void accept(Where where) {
+ out.println(now() + "Throwing in " + where);
+ throw new UncheckedCustomException(where.name());
+ }
+
+ @Override
+ public boolean test(Throwable throwable) {
+ return UncheckedCustomException.class.isInstance(throwable);
+ }
+
+ @Override
+ public String toString() {
+ return "UncheckedCustomExceptionThrower";
+ }
+ }
+
+ static final class UncheckedIOExceptionThrower implements Thrower {
+ @Override
+ public void accept(Where where) {
+ out.println(now() + "Throwing in " + where);
+ throw new UncheckedIOException(new CustomIOException(where.name()));
+ }
+
+ @Override
+ public boolean test(Throwable throwable) {
+ return UncheckedIOException.class.isInstance(throwable)
+ && CustomIOException.class.isInstance(throwable.getCause());
+ }
+
+ @Override
+ public String toString() {
+ return "UncheckedIOExceptionThrower";
+ }
+ }
+
+ static final class UncheckedCustomException extends RuntimeException {
+ UncheckedCustomException(String message) {
+ super(message);
+ }
+ UncheckedCustomException(String message, Throwable cause) {
+ super(message, cause);
+ }
+ }
+
+ static final class CustomIOException extends IOException {
+ CustomIOException(String message) {
+ super(message);
+ }
+ CustomIOException(String message, Throwable cause) {
+ super(message, cause);
+ }
+ }
+
+ static final class ThrowingBodyHandler<T> implements BodyHandler<T> {
+ final Consumer<Where> throwing;
+ final BodyHandler<T> bodyHandler;
+ ThrowingBodyHandler(Consumer<Where> throwing, BodyHandler<T> bodyHandler) {
+ this.throwing = throwing;
+ this.bodyHandler = bodyHandler;
+ }
+ @Override
+ public BodySubscriber<T> apply(HttpResponse.ResponseInfo rinfo) {
+ throwing.accept(Where.BODY_HANDLER);
+ BodySubscriber<T> subscriber = bodyHandler.apply(rinfo);
+ return new ThrowingBodySubscriber(throwing, subscriber);
+ }
+ }
+
+ static final class ThrowingBodySubscriber<T> implements BodySubscriber<T> {
+ private final BodySubscriber<T> subscriber;
+ volatile boolean onSubscribeCalled;
+ final Consumer<Where> throwing;
+ ThrowingBodySubscriber(Consumer<Where> throwing, BodySubscriber<T> subscriber) {
+ this.throwing = throwing;
+ this.subscriber = subscriber;
+ }
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ //out.println("onSubscribe ");
+ onSubscribeCalled = true;
+ throwing.accept(Where.ON_SUBSCRIBE);
+ subscriber.onSubscribe(subscription);
+ }
+
+ @Override
+ public void onNext(List<ByteBuffer> item) {
+ // out.println("onNext " + item);
+ assertTrue(onSubscribeCalled);
+ throwing.accept(Where.ON_NEXT);
+ subscriber.onNext(item);
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ //out.println("onError");
+ assertTrue(onSubscribeCalled);
+ throwing.accept(Where.ON_ERROR);
+ subscriber.onError(throwable);
+ }
+
+ @Override
+ public void onComplete() {
+ //out.println("onComplete");
+ assertTrue(onSubscribeCalled, "onComplete called before onSubscribe");
+ throwing.accept(Where.ON_COMPLETE);
+ subscriber.onComplete();
+ }
+
+ @Override
+ public CompletionStage<T> getBody() {
+ throwing.accept(Where.GET_BODY);
+ try {
+ throwing.accept(Where.BODY_CF);
+ } catch (Throwable t) {
+ return CompletableFuture.failedFuture(t);
+ }
+ return subscriber.getBody();
+ }
+ }
+
+
+ @BeforeTest
+ public void setup() throws Exception {
+ sslContext = new SimpleSSLContext().get();
+ if (sslContext == null)
+ throw new AssertionError("Unexpected null sslContext");
+
+ // HTTP/1.1
+ HttpTestHandler h1_fixedLengthHandler = new HTTP_FixedLengthHandler();
+ HttpTestHandler h1_chunkHandler = new HTTP_ChunkedHandler();
+ InetSocketAddress sa = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0);
+ httpTestServer = HttpTestServer.of(HttpServer.create(sa, 0));
+ httpTestServer.addHandler(h1_fixedLengthHandler, "/http1/fixed");
+ httpTestServer.addHandler(h1_chunkHandler, "/http1/chunk");
+ httpURI_fixed = "http://" + httpTestServer.serverAuthority() + "/http1/fixed/x";
+ httpURI_chunk = "http://" + httpTestServer.serverAuthority() + "/http1/chunk/x";
+
+ HttpsServer httpsServer = HttpsServer.create(sa, 0);
+ httpsServer.setHttpsConfigurator(new HttpsConfigurator(sslContext));
+ httpsTestServer = HttpTestServer.of(httpsServer);
+ httpsTestServer.addHandler(h1_fixedLengthHandler, "/https1/fixed");
+ httpsTestServer.addHandler(h1_chunkHandler, "/https1/chunk");
+ httpsURI_fixed = "https://" + httpsTestServer.serverAuthority() + "/https1/fixed/x";
+ httpsURI_chunk = "https://" + httpsTestServer.serverAuthority() + "/https1/chunk/x";
+
+ // HTTP/2
+ HttpTestHandler h2_fixedLengthHandler = new HTTP_FixedLengthHandler();
+ HttpTestHandler h2_chunkedHandler = new HTTP_ChunkedHandler();
+
+ http2TestServer = HttpTestServer.of(new Http2TestServer("localhost", false, 0));
+ http2TestServer.addHandler(h2_fixedLengthHandler, "/http2/fixed");
+ http2TestServer.addHandler(h2_chunkedHandler, "/http2/chunk");
+ http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed/x";
+ http2URI_chunk = "http://" + http2TestServer.serverAuthority() + "/http2/chunk/x";
+
+ https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, sslContext));
+ https2TestServer.addHandler(h2_fixedLengthHandler, "/https2/fixed");
+ https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk");
+ https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed/x";
+ https2URI_chunk = "https://" + https2TestServer.serverAuthority() + "/https2/chunk/x";
+
+ serverCount.addAndGet(4);
+ httpTestServer.start();
+ httpsTestServer.start();
+ http2TestServer.start();
+ https2TestServer.start();
+ }
+
+ @AfterTest
+ public void teardown() throws Exception {
+ String sharedClientName =
+ sharedClient == null ? null : sharedClient.toString();
+ sharedClient = null;
+ Thread.sleep(100);
+ AssertionError fail = TRACKER.check(500);
+ try {
+ httpTestServer.stop();
+ httpsTestServer.stop();
+ http2TestServer.stop();
+ https2TestServer.stop();
+ } finally {
+ if (fail != null) {
+ if (sharedClientName != null) {
+ System.err.println("Shared client name is: " + sharedClientName);
+ }
+ throw fail;
+ }
+ }
+ }
+
+ static class HTTP_FixedLengthHandler implements HttpTestHandler {
+ @Override
+ public void handle(HttpTestExchange t) throws IOException {
+ out.println("HTTP_FixedLengthHandler received request to " + t.getRequestURI());
+ try (InputStream is = t.getRequestBody()) {
+ is.readAllBytes();
+ }
+ byte[] resp = t.getRequestURI().toString().getBytes(StandardCharsets.UTF_8);
+ t.sendResponseHeaders(200, resp.length); //fixed content length
+ try (OutputStream os = t.getResponseBody()) {
+ os.write(resp);
+ }
+ }
+ }
+
+ static class HTTP_ChunkedHandler implements HttpTestHandler {
+ @Override
+ public void handle(HttpTestExchange t) throws IOException {
+ out.println("HTTP_ChunkedHandler received request to " + t.getRequestURI());
+ byte[] resp = t.getRequestURI().toString().getBytes(StandardCharsets.UTF_8);
+ try (InputStream is = t.getRequestBody()) {
+ is.readAllBytes();
+ }
+ t.sendResponseHeaders(200, -1); // chunked/variable
+ try (OutputStream os = t.getResponseBody()) {
+ os.write(resp);
+ }
+ }
+ }
+
+}
--- a/test/jdk/java/net/httpclient/AsFileDownloadTest.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/AsFileDownloadTest.java Wed Jun 20 09:05:57 2018 -0700
@@ -53,6 +53,7 @@
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
+import java.net.http.HttpHeaders;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse;
@@ -63,9 +64,8 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
-import java.util.logging.ConsoleHandler;
-import java.util.logging.Level;
-import java.util.logging.Logger;
+import java.util.Locale;
+import java.util.Map;
import javax.net.ssl.SSLContext;
import jdk.testlibrary.SimpleSSLContext;
import jdk.test.lib.util.FileUtils;
@@ -188,6 +188,10 @@
assertEquals(response.headers().firstValue("Content-Disposition").get(),
contentDispositionValue);
assertEquals(fileContents, "May the luck of the Irish be with you!");
+
+ // additional checks unrelated to file download
+ caseInsensitivityOfHeaders(request.headers());
+ caseInsensitivityOfHeaders(response.headers());
}
// --- Negative
@@ -299,7 +303,7 @@
http2TestServer.addHandler(new Http2FileDispoHandler(), "/http2/afdt");
http2URI = "http://" + http2TestServer.serverAuthority() + "/http2/afdt";
- https2TestServer = new Http2TestServer("localhost", true, 0);
+ https2TestServer = new Http2TestServer("localhost", true, sslContext);
https2TestServer.addHandler(new Http2FileDispoHandler(), "/https2/afdt");
https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/afdt";
@@ -372,4 +376,30 @@
}
}
}
+
+ // ---
+
+ // Asserts case-insensitivity of headers (nothing to do with file
+ // download, just convenient as we have a couple of header instances. )
+ static void caseInsensitivityOfHeaders(HttpHeaders headers) {
+ try {
+ for (Map.Entry<String, List<String>> entry : headers.map().entrySet()) {
+ String headerName = entry.getKey();
+ List<String> headerValue = entry.getValue();
+
+ for (String name : List.of(headerName.toUpperCase(Locale.ROOT),
+ headerName.toLowerCase(Locale.ROOT))) {
+ assertTrue(headers.firstValue(name).isPresent());
+ assertEquals(headers.firstValue(name).get(), headerValue.get(0));
+ assertEquals(headers.allValues(name).size(), headerValue.size());
+ assertEquals(headers.allValues(name), headerValue);
+ assertEquals(headers.map().get(name).size(), headerValue.size());
+ assertEquals(headers.map().get(name), headerValue);
+ }
+ }
+ } catch (Throwable t) {
+ System.out.println("failure in caseInsensitivityOfHeaders with:" + headers);
+ throw t;
+ }
+ }
}
--- a/test/jdk/java/net/httpclient/BasicRedirectTest.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/BasicRedirectTest.java Wed Jun 20 09:05:57 2018 -0700
@@ -228,7 +228,7 @@
http2TestServer = HttpTestServer.of(new Http2TestServer("localhost", false, 0));
http2TestServer.addHandler(new BasicHttpRedirectHandler(), "/http2/same/");
http2URI = "http://" + http2TestServer.serverAuthority() + "/http2/same/redirect";
- https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, 0));
+ https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, sslContext));
https2TestServer.addHandler(new BasicHttpRedirectHandler(), "/https2/same/");
https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/same/redirect";
--- a/test/jdk/java/net/httpclient/BodyProcessorInputStreamTest.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/BodyProcessorInputStreamTest.java Wed Jun 20 09:05:57 2018 -0700
@@ -41,7 +41,7 @@
* @test
* @bug 8187503
* @summary An example on how to read a response body with InputStream.
- * @run main/manual -Dtest.debug=true BodyProcessorInputStreamTest
+ * @run main/othervm/manual -Dtest.debug=true BodyProcessorInputStreamTest
* @author daniel fuchs
*/
public class BodyProcessorInputStreamTest {
--- a/test/jdk/java/net/httpclient/ConcurrentResponses.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/ConcurrentResponses.java Wed Jun 20 09:05:57 2018 -0700
@@ -35,7 +35,9 @@
* @library /lib/testlibrary http2/server
* @build Http2TestServer
* @build jdk.testlibrary.SimpleSSLContext
- * @run testng/othervm -Djdk.internal.httpclient.debug=true ConcurrentResponses
+ * @run testng/othervm
+ * -Djdk.httpclient.HttpClient.log=headers,errors,channel
+ * ConcurrentResponses
*/
import java.io.IOException;
@@ -280,7 +282,7 @@
http2TestServer.addHandler(new Http2VariableHandler(), "/http2/variable");
http2VariableURI = "http://" + http2TestServer.serverAuthority() + "/http2/variable";
- https2TestServer = new Http2TestServer("localhost", true, 0);
+ https2TestServer = new Http2TestServer("localhost", true, sslContext);
https2TestServer.addHandler(new Http2FixedHandler(), "/https2/fixed");
https2FixedURI = "https://" + https2TestServer.serverAuthority() + "/https2/fixed";
https2TestServer.addHandler(new Http2VariableHandler(), "/https2/variable");
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/ConnectExceptionTest.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,167 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Expect ConnectException for all non-security related connect errors
+ * @bug 8204864
+ * @run testng/othervm ConnectExceptionTest
+ * @run testng/othervm/java.security.policy=noPermissions.policy ConnectExceptionTest
+ */
+
+import java.io.IOException;
+import java.net.ConnectException;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.ProxySelector;
+import java.net.SocketAddress;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+import static java.lang.System.out;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+public class ConnectExceptionTest {
+
+ static final ProxySelector INVALID_PROXY = new ProxySelector() {
+ final List<Proxy> proxy = List.of(new Proxy(Proxy.Type.HTTP,
+ InetSocketAddress.createUnresolved("proxy.invalid", 8080)));
+ @Override public List<Proxy> select(URI uri) { return proxy; }
+ @Override public void connectFailed(URI uri, SocketAddress sa, IOException ioe) { }
+ @Override public String toString() { return "INVALID_PROXY"; }
+ };
+
+ static final ProxySelector NO_PROXY = new ProxySelector() {
+ @Override public List<Proxy> select(URI uri) { return List.of(Proxy.NO_PROXY); }
+ @Override public void connectFailed(URI uri, SocketAddress sa, IOException ioe) { }
+ @Override public String toString() { return "NO_PROXY"; }
+ };
+
+ @DataProvider(name = "uris")
+ public Object[][] uris() {
+ return new Object[][]{
+ { "http://test.invalid/", NO_PROXY },
+ { "https://test.invalid/", NO_PROXY },
+ { "http://test.invalid/", INVALID_PROXY },
+ { "https://test.invalid/", INVALID_PROXY },
+ };
+ }
+
+ @Test(dataProvider = "uris")
+ void testSynchronousGET(String uriString, ProxySelector proxy) throws Exception {
+ out.printf("%n---%ntestSynchronousGET starting uri:%s, proxy:%s%n", uriString, proxy);
+ HttpClient client = HttpClient.newBuilder().proxy(proxy).build();
+
+ URI uri = URI.create(uriString);
+ HttpRequest request = HttpRequest.newBuilder(uri).build();
+ try {
+ HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
+ fail("UNEXPECTED response: " + response + ", body:" + response.body());
+ } catch (ConnectException ioe) {
+ out.println("Caught expected: " + ioe);
+ //ioe.printStackTrace(out);
+ } catch (SecurityException expectedIfSMIsSet) {
+ out.println("Caught expected: " + expectedIfSMIsSet);
+ assertTrue(System.getSecurityManager() != null);
+ }
+ }
+
+ @Test(dataProvider = "uris")
+ void testSynchronousPOST(String uriString, ProxySelector proxy) throws Exception {
+ out.printf("%n---%ntestSynchronousPOST starting uri:%s, proxy:%s%n", uriString, proxy);
+ HttpClient client = HttpClient.newBuilder().proxy(proxy).build();
+
+ URI uri = URI.create(uriString);
+ HttpRequest request = HttpRequest.newBuilder(uri)
+ .POST(BodyPublishers.ofString("Does not matter"))
+ .build();
+ try {
+ HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
+ fail("UNEXPECTED response: " + response + ", body:" + response.body());
+ } catch (ConnectException ioe) {
+ out.println("Caught expected: " + ioe);
+ //ioe.printStackTrace(out);
+ } catch (SecurityException expectedIfSMIsSet) {
+ out.println("Caught expected: " + expectedIfSMIsSet);
+ assertTrue(System.getSecurityManager() != null);
+ }
+ }
+
+ @Test(dataProvider = "uris")
+ void testAsynchronousGET(String uriString, ProxySelector proxy) throws Exception {
+ out.printf("%n---%ntestAsynchronousGET starting uri:%s, proxy:%s%n", uriString, proxy);
+ HttpClient client = HttpClient.newBuilder().proxy(proxy).build();
+
+ URI uri = URI.create(uriString);
+ HttpRequest request = HttpRequest.newBuilder(uri).build();
+ try {
+ HttpResponse<String> response = client.sendAsync(request, BodyHandlers.ofString()).get();
+ fail("UNEXPECTED response: " + response + ", body:" + response.body());
+ } catch (ExecutionException ee) {
+ Throwable t = ee.getCause();
+ if (t instanceof ConnectException) {
+ out.println("Caught expected: " + t);
+ } else if (t instanceof SecurityException) {
+ out.println("Caught expected: " + t);
+ assertTrue(System.getSecurityManager() != null);
+ } else {
+ t.printStackTrace(out);
+ fail("Unexpected exception: " + t);
+ }
+ }
+ }
+
+ @Test(dataProvider = "uris")
+ void testAsynchronousPOST(String uriString, ProxySelector proxy) throws Exception {
+ out.printf("%n---%ntestAsynchronousPOST starting uri:%s, proxy:%s%n", uriString, proxy);
+ HttpClient client = HttpClient.newBuilder().proxy(proxy).build();
+
+ URI uri = URI.create(uriString);
+ HttpRequest request = HttpRequest.newBuilder(uri)
+ .POST(BodyPublishers.ofString("Does not matter"))
+ .build();
+ try {
+ HttpResponse<String> response = client.sendAsync(request, BodyHandlers.ofString()).get();
+ fail("UNEXPECTED response: " + response + ", body:" + response.body());
+ } catch (ExecutionException ee) {
+ Throwable t = ee.getCause();
+ if (t instanceof ConnectException) {
+ out.println("Caught expected: " + t);
+ } else if (t instanceof SecurityException) {
+ out.println("Caught expected: " + t);
+ assertTrue(System.getSecurityManager() != null);
+ } else {
+ t.printStackTrace(out);
+ fail("Unexpected exception: " + t);
+ }
+ }
+ }
+}
--- a/test/jdk/java/net/httpclient/CookieHeaderTest.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/CookieHeaderTest.java Wed Jun 20 09:05:57 2018 -0700
@@ -205,7 +205,7 @@
http2TestServer = HttpTestServer.of(new Http2TestServer("localhost", false, 0));
http2TestServer.addHandler(new CookieValidationHandler(), "/http2/cookie/");
http2URI = "http://" + http2TestServer.serverAuthority() + "/http2/cookie/retry";
- https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, 0));
+ https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, sslContext));
https2TestServer.addHandler(new CookieValidationHandler(), "/https2/cookie/");
https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/cookie/retry";
--- a/test/jdk/java/net/httpclient/CustomRequestPublisher.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/CustomRequestPublisher.java Wed Jun 20 09:05:57 2018 -0700
@@ -340,7 +340,7 @@
http2TestServer.addHandler(new Http2EchoHandler(), "/http2/echo");
http2URI = "http://" + http2TestServer.serverAuthority() + "/http2/echo";
- https2TestServer = new Http2TestServer("localhost", true, 0);
+ https2TestServer = new Http2TestServer("localhost", true, sslContext);
https2TestServer.addHandler(new Http2EchoHandler(), "/https2/echo");
https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/echo";
--- a/test/jdk/java/net/httpclient/CustomResponseSubscriber.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/CustomResponseSubscriber.java Wed Jun 20 09:05:57 2018 -0700
@@ -217,7 +217,7 @@
http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed";
http2URI_chunk = "http://" + http2TestServer.serverAuthority() + "/http2/chunk";
- https2TestServer = new Http2TestServer("localhost", true, 0);
+ https2TestServer = new Http2TestServer("localhost", true, sslContext);
https2TestServer.addHandler(h2_fixedLengthHandler, "/https2/fixed");
https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk");
https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed";
--- a/test/jdk/java/net/httpclient/DependentActionsTest.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/DependentActionsTest.java Wed Jun 20 09:05:57 2018 -0700
@@ -27,7 +27,7 @@
* completes are executed either asynchronously in an executor when the
* CF later completes, or in the user thread that joins.
* @library /lib/testlibrary http2/server
- * @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters ThrowingPublishers
+ * @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters DependentActionsTest
* @modules java.base/sun.net.www.http
* java.net.http/jdk.internal.net.http.common
* java.net.http/jdk.internal.net.http.frame
@@ -82,12 +82,14 @@
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
+import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.lang.System.out;
import static java.lang.String.format;
+import static java.util.stream.Collectors.toList;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
@@ -374,13 +376,13 @@
}
final List<String> extractStream(HttpResponse<Stream<String>> resp) {
- return resp.body().collect(Collectors.toList());
+ return resp.body().collect(toList());
}
final List<String> extractInputStream(HttpResponse<InputStream> resp) {
try (InputStream is = resp.body()) {
return new BufferedReader(new InputStreamReader(is))
- .lines().collect(Collectors.toList());
+ .lines().collect(toList());
} catch (IOException x) {
throw new CompletionException(x);
}
@@ -399,43 +401,27 @@
.findFirst();
}
+ static final Predicate<StackFrame> DAT = sfe ->
+ sfe.getClassName().startsWith("DependentActionsTest");
+ static final Predicate<StackFrame> JUC = sfe ->
+ sfe.getClassName().startsWith("java.util.concurrent");
+ static final Predicate<StackFrame> JLT = sfe ->
+ sfe.getClassName().startsWith("java.lang.Thread");
+ static final Predicate<StackFrame> NotDATorJUCorJLT = Predicate.not(DAT.or(JUC).or(JLT));
+
+
<T> void checkThreadAndStack(Thread thread,
AtomicReference<RuntimeException> failed,
T result,
Throwable error) {
- if (Thread.currentThread() == thread) {
- //failed.set(new RuntimeException("Dependant action was executed in " + thread));
- List<StackFrame> httpStack = WALKER.walk(s -> s.filter(f -> f.getDeclaringClass()
- .getModule().equals(HttpClient.class.getModule()))
- .collect(Collectors.toList()));
- if (!httpStack.isEmpty()) {
- System.out.println("Found unexpected trace: ");
- httpStack.forEach(f -> System.out.printf("\t%s%n", f));
- failed.set(new RuntimeException("Dependant action has unexpected frame in " +
- Thread.currentThread() + ": " + httpStack.get(0)));
+ //failed.set(new RuntimeException("Dependant action was executed in " + thread));
+ List<StackFrame> otherFrames = WALKER.walk(s -> s.filter(NotDATorJUCorJLT).collect(toList()));
+ if (!otherFrames.isEmpty()) {
+ System.out.println("Found unexpected trace: ");
+ otherFrames.forEach(f -> System.out.printf("\t%s%n", f));
+ failed.set(new RuntimeException("Dependant action has unexpected frame in " +
+ Thread.currentThread() + ": " + otherFrames.get(0)));
- }
- return;
- } else if (System.getSecurityManager() != null) {
- Optional<StackFrame> sf = WALKER.walk(s -> findFrame(s, "PrivilegedRunnable"));
- if (!sf.isPresent()) {
- failed.set(new RuntimeException("Dependant action does not have expected frame in "
- + Thread.currentThread()));
- return;
- } else {
- System.out.println("Found expected frame: " + sf.get());
- }
- } else {
- List<StackFrame> httpStack = WALKER.walk(s -> s.filter(f -> f.getDeclaringClass()
- .getModule().equals(HttpClient.class.getModule()))
- .collect(Collectors.toList()));
- if (!httpStack.isEmpty()) {
- System.out.println("Found unexpected trace: ");
- httpStack.forEach(f -> System.out.printf("\t%s%n", f));
- failed.set(new RuntimeException("Dependant action has unexpected frame in " +
- Thread.currentThread() + ": " + httpStack.get(0)));
-
- }
}
}
@@ -620,7 +606,7 @@
http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed/x";
http2URI_chunk = "http://" + http2TestServer.serverAuthority() + "/http2/chunk/x";
- https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, 0));
+ https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, sslContext));
https2TestServer.addHandler(h2_fixedLengthHandler, "/https2/fixed");
https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk");
https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed/x";
--- a/test/jdk/java/net/httpclient/DependentPromiseActionsTest.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/DependentPromiseActionsTest.java Wed Jun 20 09:05:57 2018 -0700
@@ -27,7 +27,7 @@
* completes are executed either asynchronously in an executor when the
* CF later completes, or in the user thread that joins.
* @library /lib/testlibrary http2/server
- * @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters ThrowingPublishers
+ * @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters DependentPromiseActionsTest
* @modules java.base/sun.net.www.http
* java.net.http/jdk.internal.net.http.common
* java.net.http/jdk.internal.net.http.frame
@@ -40,7 +40,6 @@
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.lang.StackWalker.StackFrame;
-import jdk.internal.net.http.common.HttpHeadersImpl;
import jdk.testlibrary.SimpleSSLContext;
import org.testng.annotations.AfterTest;
import org.testng.annotations.AfterClass;
@@ -79,6 +78,7 @@
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BiPredicate;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
@@ -672,7 +672,7 @@
http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed/y";
http2URI_chunk = "http://" + http2TestServer.serverAuthority() + "/http2/chunk/y";
- https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, 0));
+ https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, sslContext));
https2TestServer.addHandler(h2_fixedLengthHandler, "/https2/fixed");
https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk");
https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed/y";
@@ -690,8 +690,13 @@
https2TestServer.stop();
}
- private static void pushPromiseFor(HttpTestExchange t, URI requestURI, String pushPath, boolean fixed)
- throws IOException
+ static final BiPredicate<String,String> ACCEPT_ALL = (x, y) -> true;
+
+ private static void pushPromiseFor(HttpTestExchange t,
+ URI requestURI,
+ String pushPath,
+ boolean fixed)
+ throws IOException
{
try {
URI promise = new URI(requestURI.getScheme(),
@@ -700,9 +705,13 @@
byte[] promiseBytes = promise.toASCIIString().getBytes(UTF_8);
out.printf("TestServer: %s Pushing promise: %s%n", now(), promise);
err.printf("TestServer: %s Pushing promise: %s%n", now(), promise);
- HttpTestHeaders headers = HttpTestHeaders.of(new HttpHeadersImpl());
+ HttpHeaders headers;
if (fixed) {
- headers.addHeader("Content-length", String.valueOf(promiseBytes.length));
+ String length = String.valueOf(promiseBytes.length);
+ headers = HttpHeaders.of(Map.of("Content-Length", List.of(length)),
+ ACCEPT_ALL);
+ } else {
+ headers = HttpHeaders.of(Map.of(), ACCEPT_ALL); // empty
}
t.serverPush(promise, headers, promiseBytes);
} catch (URISyntaxException x) {
--- a/test/jdk/java/net/httpclient/DigestEchoServer.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/DigestEchoServer.java Wed Jun 20 09:05:57 2018 -0700
@@ -170,7 +170,7 @@
}
}
- private static String toString(HttpTestHeaders headers) {
+ private static String toString(HttpTestRequestHeaders headers) {
return headers.entrySet().stream()
.map((e) -> e.getKey() + ": " + e.getValue())
.collect(Collectors.joining("\n"));
@@ -847,9 +847,9 @@
}
public static String computeDigest(boolean isRequest,
- String reqMethod,
- char[] password,
- DigestResponse params)
+ String reqMethod,
+ char[] password,
+ DigestResponse params)
throws NoSuchAlgorithmException
{
@@ -970,11 +970,13 @@
@Override
protected void requestAuthentication(HttpTestExchange he)
- throws IOException {
- he.getResponseHeaders().addHeader(getAuthenticate(),
- "Basic realm=\"" + auth.getRealm() + "\"");
- System.out.println(type + ": Requesting Basic Authentication "
- + he.getResponseHeaders().firstValue(getAuthenticate()));
+ throws IOException
+ {
+ String headerName = getAuthenticate();
+ String headerValue = "Basic realm=\"" + auth.getRealm() + "\"";
+ he.getResponseHeaders().addHeader(headerName, headerValue);
+ System.out.println(type + ": Requesting Basic Authentication, "
+ + headerName + " : "+ headerValue);
}
@Override
@@ -1061,14 +1063,13 @@
} else {
throw new InternalError(String.valueOf(v));
}
- he.getResponseHeaders().addHeader(getAuthenticate(),
- "Digest realm=\"" + auth.getRealm() + "\","
- + separator + "qop=\"auth\","
- + separator + "nonce=\"" + ns +"\"");
- System.out.println(type + ": Requesting Digest Authentication "
- + he.getResponseHeaders()
- .firstValue(getAuthenticate())
- .orElse("null"));
+ String headerName = getAuthenticate();
+ String headerValue = "Digest realm=\"" + auth.getRealm() + "\","
+ + separator + "qop=\"auth\","
+ + separator + "nonce=\"" + ns +"\"";
+ he.getResponseHeaders().addHeader(headerName, headerValue);
+ System.out.println(type + ": Requesting Digest Authentication, "
+ + headerName + " : " + headerValue);
}
@Override
--- a/test/jdk/java/net/httpclient/EncodedCharsInURI.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/EncodedCharsInURI.java Wed Jun 20 09:05:57 2018 -0700
@@ -33,7 +33,8 @@
* java.net.http/jdk.internal.net.http.frame
* java.net.http/jdk.internal.net.http.hpack
* @run testng/othervm
- * -Djdk.httpclient.HttpClient.log=headers EncodedCharsInURI
+ * -Djdk.internal.httpclient.debug=true
+ * -Djdk.httpclient.HttpClient.log=headers,errors EncodedCharsInURI
*/
//* -Djdk.internal.httpclient.debug=true
@@ -48,21 +49,14 @@
import org.testng.annotations.Test;
import javax.net.ServerSocketFactory;
-import javax.net.SocketFactory;
import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLServerSocket;
-import javax.net.ssl.SSLServerSocketFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.io.PrintWriter;
-import java.io.Writer;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
-import java.net.SocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
@@ -71,8 +65,6 @@
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandler;
import java.net.http.HttpResponse.BodyHandlers;
-import java.nio.file.Path;
-import java.nio.file.Paths;
import java.util.List;
import java.util.Locale;
import java.util.StringTokenizer;
@@ -87,6 +79,7 @@
import static java.lang.String.format;
import static java.lang.System.in;
import static java.lang.System.out;
+import static java.nio.charset.StandardCharsets.US_ASCII;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.net.http.HttpClient.Builder.NO_PROXY;
import static org.testng.Assert.assertEquals;
@@ -292,7 +285,7 @@
http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed/x";
http2URI_chunk = "http://" + http2TestServer.serverAuthority() + "/http2/chunk/x";
- https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, 0));
+ https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, sslContext));
https2TestServer.addHandler(h2_fixedLengthHandler, "/https2/fixed");
https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk");
https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed/x";
@@ -402,9 +395,6 @@
Socket targetConnection = null;
InputStream ccis = clientConnection.getInputStream();
OutputStream ccos = clientConnection.getOutputStream();
- Writer w = new OutputStreamWriter(
- clientConnection.getOutputStream(), "UTF-8");
- PrintWriter pw = new PrintWriter(w);
System.out.println(now() + getName() + ": Reading request line");
String requestLine = readLine(ccis);
System.out.println(now() + getName() + ": Request line: " + requestLine);
@@ -459,11 +449,13 @@
// Then send the 200 OK response to the client
System.out.println(now() + getName() + ": Sending "
+ response);
- pw.print(response);
- pw.flush();
+ ccos.write(response.toString().getBytes(UTF_8));
+ ccos.flush();
+ System.out.println(now() + getName() + ": sent response headers");
ccos.write(b);
ccos.flush();
ccos.close();
+ System.out.println(now() + getName() + ": sent " + b.length + " body bytes");
connections.remove(clientConnection);
clientConnection.close();
}
--- a/test/jdk/java/net/httpclient/EscapedOctetsInURI.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/EscapedOctetsInURI.java Wed Jun 20 09:05:57 2018 -0700
@@ -201,7 +201,7 @@
http2TestServer.addHandler(new HttpASCIIUriStringHandler(), "/http2");
http2URI = "http://" + http2TestServer.serverAuthority() + "/http2";
- https2TestServer = new Http2TestServer("localhost", true, 0);
+ https2TestServer = new Http2TestServer("localhost", true, sslContext);
https2TestServer.addHandler(new HttpASCIIUriStringHandler(), "/https2");
https2URI = "https://" + https2TestServer.serverAuthority() + "/https2";
--- a/test/jdk/java/net/httpclient/FlowAdapterPublisherTest.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/FlowAdapterPublisherTest.java Wed Jun 20 09:05:57 2018 -0700
@@ -359,7 +359,7 @@
http2TestServer.addHandler(new Http2EchoHandler(), "/http2/echo");
http2URI = "http://" + http2TestServer.serverAuthority() + "/http2/echo";
- https2TestServer = new Http2TestServer("localhost", true, 0);
+ https2TestServer = new Http2TestServer("localhost", true, sslContext);
https2TestServer.addHandler(new Http2EchoHandler(), "/https2/echo");
https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/echo";
--- a/test/jdk/java/net/httpclient/FlowAdapterSubscriberTest.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/FlowAdapterSubscriberTest.java Wed Jun 20 09:05:57 2018 -0700
@@ -534,7 +534,7 @@
http2TestServer.addHandler(new Http2EchoHandler(), "/http2/echo");
http2URI = "http://" + http2TestServer.serverAuthority() + "/http2/echo";
- https2TestServer = new Http2TestServer("localhost", true, 0);
+ https2TestServer = new Http2TestServer("localhost", true, sslContext);
https2TestServer.addHandler(new Http2EchoHandler(), "/https2/echo");
https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/echo";
--- a/test/jdk/java/net/httpclient/HandshakeFailureTest.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/HandshakeFailureTest.java Wed Jun 20 09:05:57 2018 -0700
@@ -23,6 +23,7 @@
import javax.net.ServerSocketFactory;
import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLSocket;
import java.io.DataInputStream;
@@ -45,7 +46,7 @@
/**
* @test
- * @run main/othervm HandshakeFailureTest
+ * @run main/othervm -Djdk.internal.httpclient.debug=true HandshakeFailureTest
* @summary Verify SSLHandshakeException is received when the handshake fails,
* either because the server closes ( EOF ) the connection during handshaking
* or no cipher suite ( or similar ) can be negotiated.
@@ -80,9 +81,17 @@
}
}
+ static HttpClient getClient() {
+ SSLParameters params = new SSLParameters();
+ params.setProtocols(new String[] {"TLSv1.2"});
+ return HttpClient.newBuilder()
+ .sslParameters(params)
+ .build();
+ }
+
void testSyncSameClient(URI uri, Version version) throws Exception {
out.printf("%n--- testSyncSameClient %s ---%n", version);
- HttpClient client = HttpClient.newHttpClient();
+ HttpClient client = getClient();
for (int i = 0; i < TIMES; i++) {
out.printf("iteration %d%n", i);
HttpRequest request = HttpRequest.newBuilder(uri)
@@ -92,8 +101,9 @@
HttpResponse<Void> response = client.send(request, discarding());
String msg = String.format("UNEXPECTED response=%s%n", response);
throw new RuntimeException(msg);
- } catch (SSLHandshakeException expected) {
+ } catch (IOException expected) {
out.printf("Client: caught expected exception: %s%n", expected);
+ checkExceptionOrCause(SSLHandshakeException.class, expected);
}
}
}
@@ -103,7 +113,7 @@
for (int i = 0; i < TIMES; i++) {
out.printf("iteration %d%n", i);
// a new client each time
- HttpClient client = HttpClient.newHttpClient();
+ HttpClient client = getClient();
HttpRequest request = HttpRequest.newBuilder(uri)
.version(version)
.build();
@@ -111,15 +121,16 @@
HttpResponse<Void> response = client.send(request, discarding());
String msg = String.format("UNEXPECTED response=%s%n", response);
throw new RuntimeException(msg);
- } catch (SSLHandshakeException expected) {
+ } catch (IOException expected) {
out.printf("Client: caught expected exception: %s%n", expected);
+ checkExceptionOrCause(SSLHandshakeException.class, expected);
}
}
}
void testAsyncSameClient(URI uri, Version version) throws Exception {
out.printf("%n--- testAsyncSameClient %s ---%n", version);
- HttpClient client = HttpClient.newHttpClient();
+ HttpClient client = getClient();
for (int i = 0; i < TIMES; i++) {
out.printf("iteration %d%n", i);
HttpRequest request = HttpRequest.newBuilder(uri)
@@ -132,12 +143,9 @@
String msg = String.format("UNEXPECTED response=%s%n", response);
throw new RuntimeException(msg);
} catch (CompletionException ce) {
- if (ce.getCause() instanceof SSLHandshakeException) {
- out.printf("Client: caught expected exception: %s%n", ce.getCause());
- } else {
- out.printf("Client: caught UNEXPECTED exception: %s%n", ce.getCause());
- throw ce;
- }
+ Throwable expected = ce.getCause();
+ out.printf("Client: caught expected exception: %s%n", expected);
+ checkExceptionOrCause(SSLHandshakeException.class, expected);
}
}
}
@@ -147,7 +155,7 @@
for (int i = 0; i < TIMES; i++) {
out.printf("iteration %d%n", i);
// a new client each time
- HttpClient client = HttpClient.newHttpClient();
+ HttpClient client = getClient();
HttpRequest request = HttpRequest.newBuilder(uri)
.version(version)
.build();
@@ -158,16 +166,26 @@
String msg = String.format("UNEXPECTED response=%s%n", response);
throw new RuntimeException(msg);
} catch (CompletionException ce) {
- if (ce.getCause() instanceof SSLHandshakeException) {
- out.printf("Client: caught expected exception: %s%n", ce.getCause());
- } else {
- out.printf("Client: caught UNEXPECTED exception: %s%n", ce.getCause());
- throw ce;
- }
+ ce.printStackTrace(out);
+ Throwable expected = ce.getCause();
+ out.printf("Client: caught expected exception: %s%n", expected);
+ checkExceptionOrCause(SSLHandshakeException.class, expected);
}
}
}
+ static void checkExceptionOrCause(Class<? extends Throwable> clazz, Throwable t) {
+ final Throwable original = t;
+ do {
+ if (clazz.isInstance(t)) {
+ System.out.println("Found expected exception/cause: " + t);
+ return; // found
+ }
+ } while ((t = t.getCause()) != null);
+ original.printStackTrace(System.out);
+ throw new RuntimeException("Expected " + clazz + "in " + original);
+ }
+
/** Common supertype for PlainServer and SSLServer. */
static abstract class AbstractServer extends Thread implements AutoCloseable {
protected final ServerSocket ss;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/HeadTest.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,234 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @bug 8203433
+ * @summary (httpclient) Add tests for HEAD and 304 responses.
+ * @modules java.base/sun.net.www.http
+ * java.net.http/jdk.internal.net.http.common
+ * java.net.http/jdk.internal.net.http.frame
+ * java.net.http/jdk.internal.net.http.hpack
+ * java.logging
+ * jdk.httpserver
+ * @library /lib/testlibrary /test/lib http2/server
+ * @build Http2TestServer
+ * @build jdk.testlibrary.SimpleSSLContext
+ * @run testng/othervm
+ * -Djdk.httpclient.HttpClient.log=trace,headers,requests
+ * HeadTest
+ */
+
+import com.sun.net.httpserver.HttpServer;
+import com.sun.net.httpserver.HttpsConfigurator;
+import com.sun.net.httpserver.HttpsServer;
+import jdk.testlibrary.SimpleSSLContext;
+import org.testng.annotations.AfterTest;
+import org.testng.annotations.BeforeTest;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import javax.net.ServerSocketFactory;
+import javax.net.ssl.SSLContext;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.Writer;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpClient.Redirect;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.StringTokenizer;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static java.lang.System.out;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.net.HttpURLConnection.HTTP_OK;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+public class HeadTest implements HttpServerAdapters {
+
+ SSLContext sslContext;
+ HttpTestServer httpTestServer; // HTTP/1.1
+ HttpTestServer httpsTestServer; // HTTPS/1.1
+ HttpTestServer http2TestServer; // HTTP/2 ( h2c )
+ HttpTestServer https2TestServer; // HTTP/2 ( h2 )
+ String httpURI;
+ String httpsURI;
+ String http2URI;
+ String https2URI;
+
+ static final String MESSAGE = "Basic HeadTest message body";
+ static final int ITERATIONS = 3;
+ static final String CONTENT_LEN = "300";
+
+ /*
+ * NOT_MODIFIED status code results from a conditional GET where
+ * the server does not (must not) return a response body because
+ * the condition specified in the request disallows it
+ */
+ static final int HTTP_NOT_MODIFIED = 304;
+ static final int HTTP_OK = 200;
+
+
+ @DataProvider(name = "positive")
+ public Object[][] positive() {
+ return new Object[][] {
+ { httpURI, "GET", HTTP_NOT_MODIFIED, HttpClient.Version.HTTP_1_1 },
+ { httpsURI, "GET", HTTP_NOT_MODIFIED, HttpClient.Version.HTTP_1_1 },
+ { httpURI, "GET", HTTP_NOT_MODIFIED, HttpClient.Version.HTTP_2 },
+ { httpsURI, "GET", HTTP_NOT_MODIFIED, HttpClient.Version.HTTP_2 },
+ { httpURI, "HEAD", HTTP_OK, HttpClient.Version.HTTP_1_1 },
+ { httpsURI, "HEAD", HTTP_OK, HttpClient.Version.HTTP_1_1 },
+ { httpURI, "HEAD", HTTP_OK, HttpClient.Version.HTTP_2 },
+ { httpsURI, "HEAD", HTTP_OK, HttpClient.Version.HTTP_2 },
+ { httpURI + "transfer/", "GET", HTTP_NOT_MODIFIED, HttpClient.Version.HTTP_1_1 },
+ { httpsURI + "transfer/", "GET", HTTP_NOT_MODIFIED, HttpClient.Version.HTTP_1_1 },
+ { httpURI + "transfer/", "GET", HTTP_NOT_MODIFIED, HttpClient.Version.HTTP_2 },
+ { httpsURI + "transfer/", "GET", HTTP_NOT_MODIFIED, HttpClient.Version.HTTP_2 },
+ { httpURI + "transfer/", "HEAD", HTTP_OK, HttpClient.Version.HTTP_1_1 },
+ { httpsURI + "transfer/", "HEAD", HTTP_OK, HttpClient.Version.HTTP_1_1 },
+ { httpURI + "transfer/", "HEAD", HTTP_OK, HttpClient.Version.HTTP_2 },
+ { httpsURI + "transfer/", "HEAD", HTTP_OK, HttpClient.Version.HTTP_2 }
+ };
+ }
+
+ static final AtomicLong requestCounter = new AtomicLong();
+
+ @Test(dataProvider = "positive")
+ void test(String uriString, String method,
+ int expResp, HttpClient.Version version) throws Exception {
+ out.printf("%n---- starting (%s) ----%n", uriString);
+ HttpClient client = HttpClient.newBuilder()
+ .followRedirects(Redirect.ALWAYS)
+ .sslContext(sslContext)
+ .build();
+
+ URI uri = URI.create(uriString);
+
+ HttpRequest.Builder requestBuilder = HttpRequest
+ .newBuilder(uri)
+ .method(method, HttpRequest.BodyPublishers.noBody());
+
+ if (version != null) {
+ requestBuilder.version(version);
+ }
+ HttpRequest request = requestBuilder.build();
+ out.println("Initial request: " + request.uri());
+
+ HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
+
+ out.println(" Got response: " + response);
+
+ assertEquals(response.statusCode(), expResp);
+ assertEquals(response.body(), "");
+ assertEquals(response.headers().firstValue("Content-length").get(), CONTENT_LEN);
+ }
+
+ // -- Infrastructure
+
+ @BeforeTest
+ public void setup() throws Exception {
+ sslContext = new SimpleSSLContext().get();
+ if (sslContext == null)
+ throw new AssertionError("Unexpected null sslContext");
+
+ InetSocketAddress sa = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0);
+
+ httpTestServer = HttpTestServer.of(HttpServer.create(sa, 0));
+ httpTestServer.addHandler(new HeadHandler(), "/");
+ httpURI = "http://" + httpTestServer.serverAuthority() + "/";
+ HttpsServer httpsServer = HttpsServer.create(sa, 0);
+ httpsServer.setHttpsConfigurator(new HttpsConfigurator(sslContext));
+ httpsTestServer = HttpTestServer.of(httpsServer);
+ httpsTestServer.addHandler(new HeadHandler(),"/");
+ httpsURI = "https://" + httpsTestServer.serverAuthority() + "/";
+
+ http2TestServer = HttpTestServer.of(new Http2TestServer("localhost", false, 0));
+ http2TestServer.addHandler(new HeadHandler(), "/");
+ http2URI = "http://" + http2TestServer.serverAuthority() + "/";
+ https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, 0));
+ https2TestServer.addHandler(new HeadHandler(), "/");
+ https2URI = "https://" + https2TestServer.serverAuthority() + "/";
+
+
+ httpTestServer.start();
+ httpsTestServer.start();
+ http2TestServer.start();
+ https2TestServer.start();
+ }
+
+ @AfterTest
+ public void teardown() throws Exception {
+ httpTestServer.stop();
+ httpsTestServer.stop();
+ http2TestServer.stop();
+ https2TestServer.stop();
+ }
+
+ static class HeadHandler implements HttpTestHandler {
+
+ @Override
+ public void handle(HttpTestExchange t) throws IOException {
+ readAllRequestData(t); // shouldn't be any
+ String method = t.getRequestMethod();
+ String path = t.getRequestURI().getPath();
+ HttpTestResponseHeaders rsph = t.getResponseHeaders();
+ if (path.contains("transfer"))
+ rsph.addHeader("Transfer-Encoding", "chunked");
+ rsph.addHeader("Content-length", CONTENT_LEN);
+ if (method.equals("HEAD")) {
+ t.sendResponseHeaders(HTTP_OK, -1);
+ } else if (method.equals("GET")) {
+ t.sendResponseHeaders(HTTP_NOT_MODIFIED, -1);
+ }
+ t.close();
+ }
+ }
+
+ static void readAllRequestData(HttpTestExchange t) throws IOException {
+ try (InputStream is = t.getRequestBody()) {
+ is.readAllBytes();
+ }
+ }
+}
--- a/test/jdk/java/net/httpclient/HeadersTest.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/HeadersTest.java Wed Jun 20 09:05:57 2018 -0700
@@ -32,6 +32,7 @@
import java.util.List;
import java.util.Map;
import java.util.Optional;
+import java.util.function.BiPredicate;
import static java.net.http.HttpClient.Builder.NO_PROXY;
/**
@@ -41,20 +42,11 @@
*/
public class HeadersTest {
+ static final BiPredicate<String,String> ACCEPT_ALL = (x, y) -> true;
+
static final URI TEST_URI = URI.create("http://www.foo.com/");
static final HttpClient client = HttpClient.newBuilder().proxy(NO_PROXY).build();
- static final class HttpHeadersStub extends HttpHeaders {
- Map<String, List<String>> map;
- HttpHeadersStub(Map<String, List<String>> map) {
- this.map = map;
- }
- @Override
- public Map<String, List<String>> map() {
- return map;
- }
- }
-
static void bad(String name) throws Exception {
HttpRequest.Builder builder = HttpRequest.newBuilder(TEST_URI);
try {
@@ -85,7 +77,7 @@
}
@Override public HttpHeaders headers() {
Map<String, List<String>> map = Map.of(name, List.of("foo"));
- return new HttpHeadersStub(map);
+ return HttpHeaders.of(map, ACCEPT_ALL);
}
};
client.send(req, HttpResponse.BodyHandlers.ofString());
@@ -127,7 +119,7 @@
}
@Override public HttpHeaders headers() {
Map<String, List<String>> map = Map.of("x-bad", List.of(value));
- return new HttpHeadersStub(map);
+ return HttpHeaders.of(map, ACCEPT_ALL);
}
};
client.send(req, HttpResponse.BodyHandlers.ofString());
@@ -170,7 +162,7 @@
@Override public HttpHeaders headers() {
Map<String, List<String>> map = new HashMap<>();
map.put(null, List.of("foo"));
- return new HttpHeadersStub(map);
+ return HttpHeaders.of(map, ACCEPT_ALL);
}
};
client.send(req, HttpResponse.BodyHandlers.ofString());
@@ -211,7 +203,7 @@
@Override public HttpHeaders headers() {
Map<String, List<String>> map = new HashMap<>();
map.put("x-bar", null);
- return new HttpHeadersStub(map);
+ return HttpHeaders.of(map, ACCEPT_ALL);
}
};
client.send(req, HttpResponse.BodyHandlers.ofString());
@@ -243,11 +235,10 @@
List<String> values = new ArrayList<>();
values.add("foo");
values.add(null);
- return new HttpHeadersStub(Map.of("x-bar", values));
+ return HttpHeaders.of(Map.of("x-bar", values), ACCEPT_ALL);
}
};
- client
- .send(req, HttpResponse.BodyHandlers.ofString());
+ client.send(req, HttpResponse.BodyHandlers.ofString());
throw new RuntimeException("Expected NPE for null header value");
} catch (NullPointerException expected) {
System.out.println("Got expected NPE: " + expected);
@@ -276,7 +267,7 @@
return Optional.empty();
}
@Override public HttpHeaders headers() {
- return new HttpHeadersStub(null);
+ return HttpHeaders.of(null, ACCEPT_ALL);
}
};
client.send(req, HttpResponse.BodyHandlers.ofString());
@@ -370,7 +361,7 @@
}
@Override public HttpHeaders headers() {
Map<String, List<String>> map = Map.of("x-good", List.of("foo"));
- return new HttpHeadersStub(map);
+ return HttpHeaders.of(map, ACCEPT_ALL);
}
};
client.send(req, HttpResponse.BodyHandlers.ofString());
@@ -416,7 +407,7 @@
}
@Override public HttpHeaders headers() {
Map<String, List<String>> map = Map.of("x-good", List.of("foo"));
- return new HttpHeadersStub(map);
+ return HttpHeaders.of(map, ACCEPT_ALL);
}
};
client.send(req, HttpResponse.BodyHandlers.ofString());
@@ -472,7 +463,7 @@
@Override
public HttpHeaders headers() {
Map<String, List<String>> map = Map.of("x-good", List.of("foo"));
- return new HttpHeadersStub(map);
+ return HttpHeaders.of(map, ACCEPT_ALL);
}
};
client.send(req, HttpResponse.BodyHandlers.ofString());
@@ -526,7 +517,7 @@
@Override
public HttpHeaders headers() {
Map<String, List<String>> map = Map.of("x-good", List.of("foo"));
- return new HttpHeadersStub(map);
+ return HttpHeaders.of(map, ACCEPT_ALL);
}
};
client.send(req, HttpResponse.BodyHandlers.ofString());
@@ -538,12 +529,10 @@
public static void main(String[] args) throws Exception {
bad("bad:header");
- bad("Foo\n");
good("X-Foo!");
good("Bar~");
good("x");
bad(" ");
- bad("Bar\r\n");
good("Hello#world");
good("Qwer#ert");
badValue("blah\r\n blah");
--- a/test/jdk/java/net/httpclient/HttpClientBuilderTest.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/HttpClientBuilderTest.java Wed Jun 20 09:05:57 2018 -0700
@@ -275,16 +275,7 @@
@Override public boolean expectContinue() { return false; }
@Override public URI uri() { return URI.create("http://foo.com/"); }
@Override public Optional<Version> version() { return Optional.empty(); }
- private final FixedHttpHeaders headers = new FixedHttpHeaders();
- @Override public HttpHeaders headers() { return headers; }
- public class FixedHttpHeaders extends HttpHeaders {
- private final Map<String, List<String>> map =
- new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
- @Override
- public Map<String, List<String>> map() {
- return map;
- }
- }
+ @Override public HttpHeaders headers() { return HttpHeaders.of(Map.of(), (x, y) -> true); }
}
// ---
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/HttpHeadersOf.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,422 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Tests for HttpHeaders.of factory method
+ * @run testng HttpHeadersOf
+ */
+
+import java.net.http.HttpHeaders;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiPredicate;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertThrows;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+public class HttpHeadersOf {
+
+ static final Class<NullPointerException> NPE = NullPointerException.class;
+ static final Class<NumberFormatException> NFE = NumberFormatException.class;
+ static final Class<UnsupportedOperationException> UOE = UnsupportedOperationException.class;
+
+ static final BiPredicate<String,String> ACCEPT_ALL =
+ new BiPredicate<>() {
+ @Override public boolean test(String name, String value) { return true; }
+ @Override public String toString() { return "ACCEPT_ALL"; }
+ };
+
+ static final BiPredicate<String,String> REJECT_ALL =
+ new BiPredicate<>() {
+ @Override public boolean test(String name, String value) { return false; }
+ @Override public String toString() { return "REJECT_ALL"; }
+ };
+
+ @DataProvider(name = "predicates")
+ public Object[][] predicates() {
+ return new Object[][] { { ACCEPT_ALL }, { REJECT_ALL } };
+ }
+
+ @Test(dataProvider = "predicates")
+ public void testNull(BiPredicate<String,String> filter) {
+ assertThrows(NPE, () -> HttpHeaders.of(null, null));
+ assertThrows(NPE, () -> HttpHeaders.of(null, filter));
+ assertThrows(NPE, () -> HttpHeaders.of(Map.of(), null));
+ assertThrows(NPE, () -> HttpHeaders.of(Map.of("name", List.of("value")), null));
+
+ // nulls in the Map
+ assertThrows(NPE, () -> HttpHeaders.of(Map.of(null, List.of("value)")), filter));
+ assertThrows(NPE, () -> HttpHeaders.of(Map.of("name", null), filter));
+ assertThrows(NPE, () -> HttpHeaders.of(Map.of("name", List.of(null)), filter));
+ assertThrows(NPE, () -> HttpHeaders.of(Map.of("name", List.of("aValue", null)), filter));
+ assertThrows(NPE, () -> HttpHeaders.of(Map.of("name", List.of(null, "aValue")), filter));
+ }
+
+
+ @DataProvider(name = "filterMaps")
+ public Object[][] filterMaps() {
+ List<Map<String, List<String>>> maps = List.of(
+ Map.of("A", List.of("B"), "X", List.of("Y", "Z")),
+ Map.of("A", List.of("B", "C"), "X", List.of("Y", "Z")),
+ Map.of("A", List.of("B", "C", "D"), "X", List.of("Y", "Z"))
+ );
+ return maps.stream().map(p -> new Object[] { p }).toArray(Object[][]::new);
+ }
+
+ @Test(dataProvider = "filterMaps")
+ public void testFilter(Map<String,List<String>> map) {
+ HttpHeaders headers = HttpHeaders.of(map, REJECT_ALL);
+ assertEquals(headers.map().size(), 0);
+ assertFalse(headers.firstValue("A").isPresent());
+ assertEquals(headers.allValues("A").size(), 0);
+
+ headers = HttpHeaders.of(map, (name, value) -> {
+ if (name.equals("A")) return true; else return false; });
+ assertEquals(headers.map().size(), 1);
+ assertTrue(headers.firstValue("A").isPresent());
+ assertEquals(headers.allValues("A"), map.get("A"));
+ assertEquals(headers.allValues("A").size(), map.get("A").size());
+ assertFalse(headers.firstValue("X").isPresent());
+
+ headers = HttpHeaders.of(map, (name, value) -> {
+ if (name.equals("X")) return true; else return false; });
+ assertEquals(headers.map().size(), 1);
+ assertTrue(headers.firstValue("X").isPresent());
+ assertEquals(headers.allValues("X"), map.get("X"));
+ assertEquals(headers.allValues("X").size(), map.get("X").size());
+ assertFalse(headers.firstValue("A").isPresent());
+ }
+
+
+ @DataProvider(name = "mapValues")
+ public Object[][] mapValues() {
+ List<Map<String, List<String>>> maps = List.of(
+ Map.of("A", List.of("B")),
+ Map.of("A", List.of("B", "C")),
+ Map.of("A", List.of("B", "C", "D")),
+
+ Map.of("A", List.of("B"), "X", List.of("Y", "Z")),
+ Map.of("A", List.of("B", "C"), "X", List.of("Y", "Z")),
+ Map.of("A", List.of("B", "C", "D"), "X", List.of("Y", "Z")),
+
+ Map.of("A", List.of("B"), "X", List.of("Y", "Z")),
+ Map.of("A", List.of("B", "C"), "X", List.of("Y", "Z")),
+ Map.of("A", List.of("B", "C", "D"), "X", List.of("Y", "Z")),
+
+ Map.of("X", List.of("Y", "Z"), "A", List.of("B")),
+ Map.of("X", List.of("Y", "Z"), "A", List.of("B", "C")),
+ Map.of("X", List.of("Y", "Z"), "A", List.of("B", "C", "D"))
+ );
+ return maps.stream().map(p -> new Object[] { p }).toArray(Object[][]::new);
+ }
+
+ @Test(dataProvider = "mapValues")
+ public void testMapValues(Map<String,List<String>> map) {
+ HttpHeaders headers = HttpHeaders.of(map, ACCEPT_ALL);
+
+ assertEquals(headers.map().size(), map.size());
+ assertTrue(headers.firstValue("A").isPresent());
+ assertTrue(headers.firstValue("a").isPresent());
+ assertEquals(headers.firstValue("A").get(), "B");
+ assertEquals(headers.firstValue("a").get(), "B");
+ assertEquals(headers.allValues("A"), map.get("A"));
+ assertEquals(headers.allValues("a"), map.get("A"));
+ assertEquals(headers.allValues("F").size(), 0);
+ assertTrue(headers.map().get("A").contains("B"));
+ assertFalse(headers.map().get("A").contains("F"));
+ assertThrows(NFE, () -> headers.firstValueAsLong("A"));
+
+ // a non-exhaustive list of mutators
+ assertThrows(UOE, () -> headers.map().put("Z", List.of("Z")));
+ assertThrows(UOE, () -> headers.map().remove("A"));
+ assertThrows(UOE, () -> headers.map().remove("A", "B"));
+ assertThrows(UOE, () -> headers.map().clear());
+ assertThrows(UOE, () -> headers.allValues("A").remove("B"));
+ assertThrows(UOE, () -> headers.allValues("A").remove(1));
+ assertThrows(UOE, () -> headers.allValues("A").clear());
+ assertThrows(UOE, () -> headers.allValues("A").add("Z"));
+ assertThrows(UOE, () -> headers.allValues("A").addAll(List.of("Z")));
+ assertThrows(UOE, () -> headers.allValues("A").add(1, "Z"));
+ }
+
+
+ @DataProvider(name = "caseInsensitivity")
+ public Object[][] caseInsensitivity() {
+ List<Map<String, List<String>>> maps = List.of(
+ Map.of("Accept-Encoding", List.of("gzip, deflate")),
+ Map.of("accept-encoding", List.of("gzip, deflate")),
+ Map.of("AccePT-ENCoding", List.of("gzip, deflate")),
+ Map.of("ACCept-EncodING", List.of("gzip, deflate")),
+ Map.of("ACCEPT-ENCODING", List.of("gzip, deflate"))
+ );
+ return maps.stream().map(p -> new Object[] { p }).toArray(Object[][]::new);
+ }
+
+ @Test(dataProvider = "caseInsensitivity")
+ public void testCaseInsensitivity(Map<String,List<String>> map) {
+ HttpHeaders headers = HttpHeaders.of(map, ACCEPT_ALL);
+
+ for (String name : List.of("Accept-Encoding", "accept-encoding",
+ "aCCept-EnCODing", "accepT-encodinG")) {
+ assertTrue(headers.firstValue(name).isPresent());
+ assertTrue(headers.allValues(name).contains("gzip, deflate"));
+ assertEquals(headers.firstValue(name).get(), "gzip, deflate");
+ assertEquals(headers.allValues(name).size(), 1);
+ assertEquals(headers.map().size(), 1);
+ assertEquals(headers.map().get(name).size(), 1);
+ assertEquals(headers.map().get(name).get(0), "gzip, deflate");
+ }
+ }
+
+ @Test
+ public void testEqualsAndHashCode() {
+ List<Map<String, List<String>>> maps = List.of(
+ Map.of("Accept-Encoding", List.of("gzip, deflate")),
+ Map.of("accept-encoding", List.of("gzip, deflate")),
+ Map.of("AccePT-ENCoding", List.of("gzip, deflate")),
+ Map.of("ACCept-EncodING", List.of("gzip, deflate")),
+ Map.of("ACCEPT-ENCODING", List.of("gzip, deflate"))
+ );
+ int mapDiffer = 0;
+ int mapHashDiffer = 0;
+ for (Map<String, List<String>> m1 : maps) {
+ HttpHeaders h1 = HttpHeaders.of(m1, ACCEPT_ALL);
+ for (Map<String, List<String>> m2 : maps) {
+ HttpHeaders h2 = HttpHeaders.of(m2, ACCEPT_ALL);
+ if (!m1.equals(m2)) mapDiffer++;
+ if (m1.hashCode() != m2.hashCode()) mapHashDiffer++;
+ assertEquals(h1, h2, "HttpHeaders differ");
+ assertEquals(h1.hashCode(), h2.hashCode(),
+ "hashCode differ for " + List.of(m1,m2));
+ }
+ }
+ assertTrue(mapDiffer > 0, "all maps were equal!");
+ assertTrue(mapHashDiffer > 0, "all maps had same hashCode!");
+ }
+
+ @DataProvider(name = "valueAsLong")
+ public Object[][] valueAsLong() {
+ return new Object[][] {
+ new Object[] { Map.of("Content-Length", List.of("10")), 10l },
+ new Object[] { Map.of("Content-Length", List.of("101")), 101l },
+ new Object[] { Map.of("Content-Length", List.of("56789")), 56789l },
+ new Object[] { Map.of("Content-Length", List.of(Long.toString(Long.MAX_VALUE))), Long.MAX_VALUE },
+ new Object[] { Map.of("Content-Length", List.of(Long.toString(Long.MIN_VALUE))), Long.MIN_VALUE }
+ };
+ }
+
+ @Test(dataProvider = "valueAsLong")
+ public void testValueAsLong(Map<String,List<String>> map, long expected) {
+ HttpHeaders headers = HttpHeaders.of(map, ACCEPT_ALL);
+ assertEquals(headers.firstValueAsLong("Content-Length").getAsLong(), expected);
+ }
+
+
+ @DataProvider(name = "duplicateNames")
+ public Object[][] duplicateNames() {
+ List<Map<String, List<String>>> maps = List.of(
+ Map.of("X-name", List.of(),
+ "x-name", List.of()),
+ Map.of("X-name", List.of(""),
+ "x-name", List.of("")),
+ Map.of("X-name", List.of("C"),
+ "x-name", List.of("D")),
+ Map.of("X-name", List.of("E"),
+ "Y-name", List.of("F"),
+ "X-Name", List.of("G")),
+ Map.of("X-chegar", List.of("H"),
+ "y-dfuchs", List.of("I"),
+ "Y-dfuchs", List.of("J")),
+ Map.of("X-name ", List.of("K"),
+ "X-Name", List.of("L")),
+ Map.of("X-name", List.of("M"),
+ "\rX-Name", List.of("N"))
+ );
+ return maps.stream().map(p -> new Object[] { p }).toArray(Object[][]::new);
+ }
+
+ @Test(dataProvider = "duplicateNames")
+ public void testDuplicates(Map<String,List<String>> map) {
+ HttpHeaders headers;
+ try {
+ headers = HttpHeaders.of(map, ACCEPT_ALL);
+ fail("UNEXPECTED: " + headers);
+ } catch (IllegalArgumentException iae) {
+ System.out.println("caught EXPECTED IAE:" + iae);
+ assertTrue(iae.getMessage().contains("duplicate"));
+ }
+ }
+
+
+ @DataProvider(name = "noSplittingJoining")
+ public Object[][] noSplittingJoining() {
+ List<Map<String, List<String>>> maps = List.of(
+ Map.of("A", List.of("B")),
+ Map.of("A", List.of("B", "C")),
+ Map.of("A", List.of("B", "C", "D")),
+ Map.of("A", List.of("B", "C", "D", "E")),
+ Map.of("A", List.of("B", "C", "D", "E", "F")),
+ Map.of("A", List.of("B, C")),
+ Map.of("A", List.of("B, C, D")),
+ Map.of("A", List.of("B, C, D, E")),
+ Map.of("A", List.of("B, C, D, E, F")),
+ Map.of("A", List.of("B, C", "D", "E", "F")),
+ Map.of("A", List.of("B", "C, D", "E", "F")),
+ Map.of("A", List.of("B", "C, D", "E, F")),
+ Map.of("A", List.of("B", "C, D, E", "F")),
+ Map.of("A", List.of("B", "C, D, E, F"))
+ );
+ return maps.stream().map(p -> new Object[] { p }).toArray(Object[][]::new);
+ }
+
+ @Test(dataProvider = "noSplittingJoining")
+ public void testNoSplittingJoining(Map<String,List<String>> map) {
+ HttpHeaders headers = HttpHeaders.of(map, ACCEPT_ALL);
+ Map<String,List<String>> headersMap = headers.map();
+
+ assertEquals(headers.map().size(), map.size());
+ for (Map.Entry<String,List<String>> entry : map.entrySet()) {
+ String headerName = entry.getKey();
+ List<String> headerValues = entry.getValue();
+ assertEquals(headerValues, headersMap.get(headerName));
+ assertEquals(headerValues, headers.allValues(headerName));
+ assertEquals(headerValues.get(0), headers.firstValue(headerName).get());
+ }
+ }
+
+
+ @DataProvider(name = "trimming")
+ public Object[][] trimming() {
+ List<Map<String, List<String>>> maps = List.of(
+ Map.of("A", List.of("B")),
+ Map.of(" A", List.of("B")),
+ Map.of("A ", List.of("B")),
+ Map.of("A", List.of(" B")),
+ Map.of("A", List.of("B ")),
+ Map.of("\tA", List.of("B")),
+ Map.of("A\t", List.of("B")),
+ Map.of("A", List.of("\tB")),
+ Map.of("A", List.of("B\t")),
+ Map.of("A\r", List.of("B")),
+ Map.of("A\n", List.of("B")),
+ Map.of("A\r\n", List.of("B"))
+ );
+ return maps.stream().map(p -> new Object[] { p }).toArray(Object[][]::new);
+ }
+
+ @Test(dataProvider = "trimming")
+ public void testTrimming(Map<String,List<String>> map) {
+ HttpHeaders headers = HttpHeaders.of(map, (name, value) -> {
+ assertEquals(name, "A");
+ assertEquals(value, "B");
+ return true;
+ });
+
+ assertEquals(headers.map().size(), 1);
+ assertEquals(headers.firstValue("A").get(), "B");
+ assertEquals(headers.allValues("A"), List.of("B"));
+ assertTrue(headers.map().get("A").equals(List.of("B")));
+ }
+
+
+ @DataProvider(name = "emptyKey")
+ public Object[][] emptyKey() {
+ List<Map<String, List<String>>> maps = List.of(
+ Map.of("", List.of("B")),
+ Map.of(" ", List.of("B")),
+ Map.of(" ", List.of("B")),
+ Map.of("\t", List.of("B")),
+ Map.of("\t\t", List.of("B"))
+ );
+ return maps.stream().map(p -> new Object[] { p }).toArray(Object[][]::new);
+ }
+
+ @Test(dataProvider = "emptyKey")
+ public void testEmptyKey(Map<String,List<String>> map) {
+ HttpHeaders headers;
+ try {
+ headers = HttpHeaders.of(map, ACCEPT_ALL);
+ fail("UNEXPECTED: " + headers);
+ } catch (IllegalArgumentException iae) {
+ System.out.println("caught EXPECTED IAE:" + iae);
+ assertTrue(iae.getMessage().contains("empty"));
+ }
+ }
+
+
+ @DataProvider(name = "emptyValue")
+ public Object[][] emptyValue() {
+ List<Map<String, List<String>>> maps = List.of(
+ Map.of("A", List.of("")),
+ Map.of("A", List.of("", "")),
+ Map.of("A", List.of("", "", " ")),
+ Map.of("A", List.of("\t")),
+ Map.of("A", List.of("\t\t"))
+ );
+ return maps.stream().map(p -> new Object[] { p }).toArray(Object[][]::new);
+ }
+
+ @Test(dataProvider = "emptyValue")
+ public void testEmptyValue(Map<String,List<String>> map) {
+ HttpHeaders headers = HttpHeaders.of(map, (name, value) -> {
+ assertEquals(value, "");
+ return true;
+ });
+ assertEquals(headers.map().size(), map.size());
+ assertEquals(headers.map().get("A").get(0), "");
+ headers.allValues("A").forEach(v -> assertEquals(v, ""));
+ assertEquals(headers.firstValue("A").get(), "");
+ }
+
+
+ @DataProvider(name = "noValues")
+ public Object[][] noValues() {
+ List<Map<String, List<String>>> maps = List.of(
+ Map.of("A", List.of()),
+ Map.of("A", List.of(), "B", List.of()),
+ Map.of("A", List.of(), "B", List.of(), "C", List.of()),
+ Map.of("A", new ArrayList()),
+ Map.of("A", new LinkedList())
+ );
+ return maps.stream().map(p -> new Object[] { p }).toArray(Object[][]::new);
+ }
+
+ @Test(dataProvider = "noValues")
+ public void testNoValues(Map<String,List<String>> map) {
+ HttpHeaders headers = HttpHeaders.of(map, (name, value) -> {
+ fail("UNEXPECTED call to filter");
+ return true;
+ });
+ assertEquals(headers.map().size(), 0);
+ assertEquals(headers.map().get("A"), null);
+ assertEquals(headers.allValues("A").size(), 0);
+ assertFalse(headers.firstValue("A").isPresent());
+ }
+}
--- a/test/jdk/java/net/httpclient/HttpInputStreamTest.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/HttpInputStreamTest.java Wed Jun 20 09:05:57 2018 -0700
@@ -47,7 +47,7 @@
/*
* @test
* @summary An example on how to read a response body with InputStream.
- * @run main/manual -Dtest.debug=true HttpInputStreamTest
+ * @run main/othervm/manual -Dtest.debug=true HttpInputStreamTest
* @author daniel fuchs
*/
public class HttpInputStreamTest {
--- a/test/jdk/java/net/httpclient/HttpServerAdapters.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/HttpServerAdapters.java Wed Jun 20 09:05:57 2018 -0700
@@ -27,11 +27,11 @@
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
+import jdk.internal.net.http.common.HttpHeadersBuilder;
import java.net.InetAddress;
import java.io.ByteArrayInputStream;
import java.net.http.HttpClient.Version;
-import jdk.internal.net.http.common.HttpHeadersImpl;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -41,6 +41,7 @@
import java.math.BigInteger;
import java.net.InetSocketAddress;
import java.net.URI;
+import java.net.http.HttpHeaders;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
@@ -95,26 +96,26 @@
}
/**
- * A version agnostic adapter class for HTTP Headers.
+ * A version agnostic adapter class for HTTP request Headers.
*/
- public static abstract class HttpTestHeaders {
+ public static abstract class HttpTestRequestHeaders {
public abstract Optional<String> firstValue(String name);
- public abstract void addHeader(String name, String value);
public abstract Set<String> keySet();
public abstract Set<Map.Entry<String, List<String>>> entrySet();
public abstract List<String> get(String name);
public abstract boolean containsKey(String name);
- public static HttpTestHeaders of(Headers headers) {
- return new Http1TestHeaders(headers);
- }
- public static HttpTestHeaders of(HttpHeadersImpl headers) {
- return new Http2TestHeaders(headers);
+ public static HttpTestRequestHeaders of(Headers headers) {
+ return new Http1TestRequestHeaders(headers);
}
- private final static class Http1TestHeaders extends HttpTestHeaders {
+ public static HttpTestRequestHeaders of(HttpHeaders headers) {
+ return new Http2TestRequestHeaders(headers);
+ }
+
+ private static final class Http1TestRequestHeaders extends HttpTestRequestHeaders {
private final Headers headers;
- Http1TestHeaders(Headers h) { this.headers = h; }
+ Http1TestRequestHeaders(Headers h) { this.headers = h; }
@Override
public Optional<String> firstValue(String name) {
if (headers.containsKey(name)) {
@@ -123,11 +124,6 @@
return Optional.empty();
}
@Override
- public void addHeader(String name, String value) {
- headers.add(name, value);
- }
-
- @Override
public Set<String> keySet() { return headers.keySet(); }
@Override
public Set<Map.Entry<String, List<String>>> entrySet() {
@@ -142,17 +138,14 @@
return headers.containsKey(name);
}
}
- private final static class Http2TestHeaders extends HttpTestHeaders {
- private final HttpHeadersImpl headers;
- Http2TestHeaders(HttpHeadersImpl h) { this.headers = h; }
+ private static final class Http2TestRequestHeaders extends HttpTestRequestHeaders {
+ private final HttpHeaders headers;
+ Http2TestRequestHeaders(HttpHeaders h) { this.headers = h; }
@Override
public Optional<String> firstValue(String name) {
return headers.firstValue(name);
}
@Override
- public void addHeader(String name, String value) {
- headers.addHeader(name, value);
- }
public Set<String> keySet() { return headers.map().keySet(); }
@Override
public Set<Map.Entry<String, List<String>>> entrySet() {
@@ -170,6 +163,37 @@
}
/**
+ * A version agnostic adapter class for HTTP response Headers.
+ */
+ public static abstract class HttpTestResponseHeaders {
+ public abstract void addHeader(String name, String value);
+
+ public static HttpTestResponseHeaders of(Headers headers) {
+ return new Http1TestResponseHeaders(headers);
+ }
+ public static HttpTestResponseHeaders of(HttpHeadersBuilder headersBuilder) {
+ return new Http2TestResponseHeaders(headersBuilder);
+ }
+
+ private final static class Http1TestResponseHeaders extends HttpTestResponseHeaders {
+ private final Headers headers;
+ Http1TestResponseHeaders(Headers h) { this.headers = h; }
+ @Override
+ public void addHeader(String name, String value) {
+ headers.add(name, value);
+ }
+ }
+ private final static class Http2TestResponseHeaders extends HttpTestResponseHeaders {
+ private final HttpHeadersBuilder headersBuilder;
+ Http2TestResponseHeaders(HttpHeadersBuilder hb) { this.headersBuilder = hb; }
+ @Override
+ public void addHeader(String name, String value) {
+ headersBuilder.addHeader(name, value);
+ }
+ }
+ }
+
+ /**
* A version agnostic adapter class for HTTP Server Exchange.
*/
public static abstract class HttpTestExchange {
@@ -177,17 +201,17 @@
public abstract Version getExchangeVersion();
public abstract InputStream getRequestBody();
public abstract OutputStream getResponseBody();
- public abstract HttpTestHeaders getRequestHeaders();
- public abstract HttpTestHeaders getResponseHeaders();
+ public abstract HttpTestRequestHeaders getRequestHeaders();
+ public abstract HttpTestResponseHeaders getResponseHeaders();
public abstract void sendResponseHeaders(int code, int contentLength) throws IOException;
public abstract URI getRequestURI();
public abstract String getRequestMethod();
public abstract void close();
- public void serverPush(URI uri, HttpTestHeaders headers, byte[] body) {
+ public void serverPush(URI uri, HttpHeaders headers, byte[] body) {
ByteArrayInputStream bais = new ByteArrayInputStream(body);
serverPush(uri, headers, bais);
}
- public void serverPush(URI uri, HttpTestHeaders headers, InputStream body) {
+ public void serverPush(URI uri, HttpHeaders headers, InputStream body) {
throw new UnsupportedOperationException("serverPush with " + getExchangeVersion());
}
public boolean serverPushAllowed() {
@@ -221,12 +245,12 @@
return exchange.getResponseBody();
}
@Override
- public HttpTestHeaders getRequestHeaders() {
- return HttpTestHeaders.of(exchange.getRequestHeaders());
+ public HttpTestRequestHeaders getRequestHeaders() {
+ return HttpTestRequestHeaders.of(exchange.getRequestHeaders());
}
@Override
- public HttpTestHeaders getResponseHeaders() {
- return HttpTestHeaders.of(exchange.getResponseHeaders());
+ public HttpTestResponseHeaders getResponseHeaders() {
+ return HttpTestResponseHeaders.of(exchange.getResponseHeaders());
}
@Override
public void sendResponseHeaders(int code, int contentLength) throws IOException {
@@ -268,12 +292,13 @@
return exchange.getResponseBody();
}
@Override
- public HttpTestHeaders getRequestHeaders() {
- return HttpTestHeaders.of(exchange.getRequestHeaders());
+ public HttpTestRequestHeaders getRequestHeaders() {
+ return HttpTestRequestHeaders.of(exchange.getRequestHeaders());
}
+
@Override
- public HttpTestHeaders getResponseHeaders() {
- return HttpTestHeaders.of(exchange.getResponseHeaders());
+ public HttpTestResponseHeaders getResponseHeaders() {
+ return HttpTestResponseHeaders.of(exchange.getResponseHeaders());
}
@Override
public void sendResponseHeaders(int code, int contentLength) throws IOException {
@@ -286,20 +311,8 @@
return exchange.serverPushAllowed();
}
@Override
- public void serverPush(URI uri, HttpTestHeaders headers, InputStream body) {
- HttpHeadersImpl headersImpl;
- if (headers instanceof HttpTestHeaders.Http2TestHeaders) {
- headersImpl = ((HttpTestHeaders.Http2TestHeaders)headers).headers.deepCopy();
- } else {
- headersImpl = new HttpHeadersImpl();
- for (Map.Entry<String, List<String>> e : headers.entrySet()) {
- String name = e.getKey();
- for (String v : e.getValue()) {
- headersImpl.addHeader(name, v);
- }
- }
- }
- exchange.serverPush(uri, headersImpl, body);
+ public void serverPush(URI uri, HttpHeaders headers, InputStream body) {
+ exchange.serverPush(uri, headers, body);
}
void doFilter(Filter.Chain filter) throws IOException {
throw new IOException("cannot use HTTP/1.1 filter with HTTP/2 server");
@@ -363,7 +376,7 @@
}
public static boolean expectException(HttpTestExchange e) {
- HttpTestHeaders h = e.getRequestHeaders();
+ HttpTestRequestHeaders h = e.getRequestHeaders();
Optional<String> expectException = h.firstValue("X-expect-exception");
if (expectException.isPresent()) {
return expectException.get().equalsIgnoreCase("true");
--- a/test/jdk/java/net/httpclient/ImmutableFlowItems.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/ImmutableFlowItems.java Wed Jun 20 09:05:57 2018 -0700
@@ -206,7 +206,7 @@
http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed";
http2URI_chunk = "http://" + http2TestServer.serverAuthority() + "/http2/chunk";
- https2TestServer = new Http2TestServer("localhost", true, 0);
+ https2TestServer = new Http2TestServer("localhost", true, sslContext);
https2TestServer.addHandler(h2_fixedLengthHandler, "/https2/fixed");
https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk");
https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed";
--- a/test/jdk/java/net/httpclient/InvalidInputStreamSubscriptionRequest.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/InvalidInputStreamSubscriptionRequest.java Wed Jun 20 09:05:57 2018 -0700
@@ -472,7 +472,7 @@
http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed";
http2URI_chunk = "http://" + http2TestServer.serverAuthority() + "/http2/chunk";
- https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, 0));
+ https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, sslContext));
https2TestServer.addHandler(h2_fixedLengthHandler, "/https2/fixed");
https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk");
https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed";
--- a/test/jdk/java/net/httpclient/InvalidSSLContextTest.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/InvalidSSLContextTest.java Wed Jun 20 09:05:57 2018 -0700
@@ -53,6 +53,8 @@
import org.testng.annotations.BeforeTest;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
+
+import static java.net.http.HttpClient.Builder.NO_PROXY;
import static java.net.http.HttpClient.Version.HTTP_1_1;
import static java.net.http.HttpClient.Version.HTTP_2;
@@ -75,6 +77,7 @@
public void testSync(Version version) throws Exception {
// client-side uses a different context to that of the server-side
HttpClient client = HttpClient.newBuilder()
+ .proxy(NO_PROXY)
.sslContext(SSLContext.getDefault())
.build();
@@ -85,8 +88,9 @@
try {
HttpResponse<?> response = client.send(request, BodyHandlers.discarding());
Assert.fail("UNEXPECTED response" + response);
- } catch (SSLException sslex) {
- System.out.println("Caught expected: " + sslex);
+ } catch (IOException ex) {
+ System.out.println("Caught expected: " + ex);
+ assertExceptionOrCause(SSLException.class, ex);
}
}
@@ -94,6 +98,7 @@
public void testAsync(Version version) throws Exception {
// client-side uses a different context to that of the server-side
HttpClient client = HttpClient.newBuilder()
+ .proxy(NO_PROXY)
.sslContext(SSLContext.getDefault())
.build();
@@ -117,21 +122,26 @@
if (cause == null) {
Assert.fail("Unexpected null cause: " + error);
}
- assertException(clazz, cause);
+ assertExceptionOrCause(clazz, cause);
} else {
- assertException(clazz, error);
+ assertExceptionOrCause(clazz, error);
}
return null;
}).join();
}
- static void assertException(Class<? extends Throwable> clazz, Throwable t) {
+ static void assertExceptionOrCause(Class<? extends Throwable> clazz, Throwable t) {
if (t == null) {
Assert.fail("Expected " + clazz + ", caught nothing");
}
- if (!clazz.isInstance(t)) {
- Assert.fail("Expected " + clazz + ", caught " + t);
- }
+ final Throwable original = t;
+ do {
+ if (clazz.isInstance(t)) {
+ return; // found
+ }
+ } while ((t = t.getCause()) != null);
+ original.printStackTrace(System.out);
+ Assert.fail("Expected " + clazz + "in " + original);
}
@BeforeTest
@@ -163,7 +173,7 @@
s.startHandshake();
s.close();
Assert.fail("SERVER: UNEXPECTED ");
- } catch (SSLHandshakeException he) {
+ } catch (SSLException he) {
System.out.println("SERVER: caught expected " + he);
} catch (IOException e) {
System.out.println("SERVER: caught: " + e);
--- a/test/jdk/java/net/httpclient/InvalidSubscriptionRequest.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/InvalidSubscriptionRequest.java Wed Jun 20 09:05:57 2018 -0700
@@ -404,7 +404,7 @@
http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed";
http2URI_chunk = "http://" + http2TestServer.serverAuthority() + "/http2/chunk";
- https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, 0));
+ https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, sslContext));
https2TestServer.addHandler(h2_fixedLengthHandler, "/https2/fixed");
https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk");
https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed";
--- a/test/jdk/java/net/httpclient/LineBodyHandlerTest.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/LineBodyHandlerTest.java Wed Jun 20 09:05:57 2018 -0700
@@ -661,7 +661,7 @@
http2TestServer.addHandler(new HttpTestEchoHandler(), "/http2/echo");
http2URI = "http://" + http2TestServer.serverAuthority() + "/http2/echo";
- https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, 0));
+ https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, sslContext));
https2TestServer.addHandler(new HttpTestEchoHandler(), "/https2/echo");
https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/echo";
--- a/test/jdk/java/net/httpclient/MappingResponseSubscriber.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/MappingResponseSubscriber.java Wed Jun 20 09:05:57 2018 -0700
@@ -88,7 +88,7 @@
String https2URI_fixed;
String https2URI_chunk;
- static final int ITERATION_COUNT = 10;
+ static final int ITERATION_COUNT = 3;
// a shared executor helps reduce the amount of threads created by the test
static final Executor executor = Executors.newCachedThreadPool();
@@ -224,7 +224,7 @@
http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed";
http2URI_chunk = "http://" + http2TestServer.serverAuthority() + "/http2/chunk";
- https2TestServer = new Http2TestServer("localhost", true, 0);
+ https2TestServer = new Http2TestServer("localhost", true, sslContext);
https2TestServer.addHandler(h2_fixedLengthHandler, "/https2/fixed");
https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk");
https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed";
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/MaxStreams.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,239 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @bug 8196389
+ * @summary Should HttpClient support SETTINGS_MAX_CONCURRENT_STREAMS from the server
+ *
+ * @modules java.base/sun.net.www.http
+ * java.net.http/jdk.internal.net.http.common
+ * java.net.http/jdk.internal.net.http.frame
+ * java.net.http/jdk.internal.net.http.hpack
+ * java.logging
+ * jdk.httpserver
+ * @library /lib/testlibrary http2/server
+ * @build Http2TestServer
+ * @build jdk.testlibrary.SimpleSSLContext
+ * @run testng/othervm -ea -esa MaxStreams
+ */
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.URI;
+import java.util.List;
+import java.util.LinkedList;
+import java.util.Properties;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Semaphore;
+import javax.net.ssl.SSLContext;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandler;
+import java.net.http.HttpResponse.BodyHandlers;
+import jdk.testlibrary.SimpleSSLContext;
+import org.testng.annotations.AfterTest;
+import org.testng.annotations.BeforeTest;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.net.http.HttpResponse.BodyHandlers.discarding;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.fail;
+
+public class MaxStreams {
+
+ Http2TestServer http2TestServer; // HTTP/2 ( h2c )
+ Http2TestServer https2TestServer; // HTTP/2 ( h2 )
+ final Http2FixedHandler handler = new Http2FixedHandler();
+ SSLContext ctx;
+ String http2FixedURI;
+ String https2FixedURI;
+ volatile CountDownLatch latch;
+ ExecutorService exec;
+ final Semaphore canStartTestRun = new Semaphore(1);
+
+ // we send an initial warm up request, then MAX_STREAMS+1 requests
+ // in parallel. The last of them should hit the limit.
+ // Then we wait for all the responses and send a further request
+ // which should succeed. The server should see (and respond to)
+ // MAX_STREAMS+2 requests per test run.
+
+ static final int MAX_STREAMS = 10;
+ static final String RESPONSE = "Hello world";
+
+ @DataProvider(name = "uris")
+ public Object[][] variants() {
+ return new Object[][]{
+ {http2FixedURI},
+ {https2FixedURI},
+ {http2FixedURI},
+ {https2FixedURI}
+ };
+ }
+
+
+ @Test(dataProvider = "uris", timeOut=20000)
+ void testAsString(String uri) throws Exception {
+ canStartTestRun.acquire();
+ latch = new CountDownLatch(1);
+ handler.setLatch(latch);
+ HttpClient client = HttpClient.newBuilder().sslContext(ctx).build();
+ List<CompletableFuture<HttpResponse<String>>> responses = new LinkedList<>();
+
+ HttpRequest request = HttpRequest.newBuilder(URI.create(uri))
+ .version(HttpClient.Version.HTTP_2)
+ .GET()
+ .build();
+ // send warmup to ensure we only have one Http2Connection
+ HttpResponse<String> warmup = client.send(request, BodyHandlers.ofString());
+ if (warmup.statusCode() != 200 || !warmup.body().equals(RESPONSE))
+ throw new RuntimeException();
+
+ for (int i=0;i<MAX_STREAMS+1; i++) {
+ responses.add(client.sendAsync(request, BodyHandlers.ofString()));
+ }
+
+ // wait until we get local exception before allow server to proceed
+ try {
+ CompletableFuture.anyOf(responses.toArray(new CompletableFuture<?>[0])).join();
+ } catch (Exception ee) {
+ System.err.println("Expected exception 1 " + ee);
+ }
+
+ latch.countDown();
+
+ // check the first MAX_STREAMS requests succeeded
+ try {
+ CompletableFuture.allOf(responses.toArray(new CompletableFuture<?>[0])).join();
+ System.err.println("Did not get Expected exception 2 ");
+ } catch (Exception ee) {
+ System.err.println("Expected exception 2 " + ee);
+ }
+ int count = 0;
+ int failures = 0;
+ for (CompletableFuture<HttpResponse<String>> cf : responses) {
+ HttpResponse<String> r = null;
+ try {
+ count++;
+ r = cf.join();
+ if (r.statusCode() != 200 || !r.body().equals(RESPONSE))
+ throw new RuntimeException();
+ } catch (Throwable t) {
+ failures++;
+ System.err.printf("Failure %d at count %d\n", failures, count);
+ System.err.println(t);
+ t.printStackTrace();
+ }
+ }
+ if (failures != 1) {
+ String msg = "Expected 1 failure. Got " + failures;
+ throw new RuntimeException(msg);
+ }
+
+ // make sure it succeeds now as number of streams == 0 now
+ HttpResponse<String> warmdown = client.send(request, BodyHandlers.ofString());
+ if (warmdown.statusCode() != 200 || !warmdown.body().equals(RESPONSE))
+ throw new RuntimeException();
+ System.err.println("Test OK");
+ }
+
+ @BeforeTest
+ public void setup() throws Exception {
+ ctx = (new SimpleSSLContext()).get();
+ exec = Executors.newCachedThreadPool();
+
+ InetSocketAddress sa = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0);
+
+ Properties props = new Properties();
+ props.setProperty("http2server.settings.max_concurrent_streams", Integer.toString(MAX_STREAMS));
+ http2TestServer = new Http2TestServer("localhost", false, 0, exec, 10, props, null);
+ http2TestServer.addHandler(handler, "/http2/fixed");
+ http2FixedURI = "http://" + http2TestServer.serverAuthority()+ "/http2/fixed";
+ http2TestServer.start();
+
+ https2TestServer = new Http2TestServer("localhost", true, 0, exec, 10, props, ctx);
+ https2TestServer.addHandler(handler, "/http2/fixed");
+ https2FixedURI = "https://" + https2TestServer.serverAuthority()+ "/http2/fixed";
+ https2TestServer.start();
+ }
+
+ @AfterTest
+ public void teardown() throws Exception {
+ System.err.println("Stopping test server now");
+ http2TestServer.stop();
+ }
+
+ class Http2FixedHandler implements Http2Handler {
+ final AtomicInteger counter = new AtomicInteger(0);
+ CountDownLatch latch;
+
+ synchronized void setLatch(CountDownLatch latch) {
+ this.latch = latch;
+ }
+
+ synchronized CountDownLatch getLatch() {
+ return latch;
+ }
+
+ @Override
+ public void handle(Http2TestExchange t) throws IOException {
+ int c = -1;
+ try (InputStream is = t.getRequestBody();
+ OutputStream os = t.getResponseBody()) {
+
+ is.readAllBytes();
+ c = counter.getAndIncrement();
+ if (c > 0 && c <= MAX_STREAMS) {
+ // Wait for latch.
+ try {
+ // don't send any replies until all requests are sent
+ System.err.println("latch await");
+ getLatch().await();
+ System.err.println("latch resume");
+ } catch (InterruptedException ee) {}
+ }
+ t.sendResponseHeaders(200, RESPONSE.length());
+ os.write(RESPONSE.getBytes());
+ } finally {
+ // client issues MAX_STREAMS + 3 requests in total
+ // but server should only see MAX_STREAMS + 2 in total. One is rejected by client
+ // counter c captured before increment so final value is MAX_STREAMS + 1
+ if (c == MAX_STREAMS + 1) {
+ counter.set(0);
+ canStartTestRun.release();
+ }
+ }
+ }
+ }
+}
--- a/test/jdk/java/net/httpclient/MethodsTest.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/MethodsTest.java Wed Jun 20 09:05:57 2018 -0700
@@ -21,8 +21,6 @@
* questions.
*/
-import jdk.internal.net.http.common.HttpHeadersImpl;
-
import java.io.IOException;
import java.net.http.HttpClient;
import java.net.http.HttpHeaders;
@@ -30,10 +28,10 @@
import java.net.URI;
import java.net.http.HttpResponse;
import java.time.Duration;
+import java.util.Map;
import java.util.Optional;
import static java.net.http.HttpClient.Builder.NO_PROXY;
-
/**
* @test
* @bug 8199135
@@ -75,7 +73,7 @@
return Optional.empty();
}
@Override public HttpHeaders headers() {
- return new HttpHeadersImpl();
+ return HttpHeaders.of(Map.of(), (x, y) -> true);
}
};
client.send(req, HttpResponse.BodyHandlers.ofString());
--- a/test/jdk/java/net/httpclient/NonAsciiCharsInURI.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/NonAsciiCharsInURI.java Wed Jun 20 09:05:57 2018 -0700
@@ -216,7 +216,7 @@
http2TestServer.addHandler(handler, "/http2");
http2URI = "http://" + http2TestServer.serverAuthority() + "/http2";
- https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, 0));
+ https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, sslContext));
https2TestServer.addHandler(handler, "/https2");
https2URI = "https://" + https2TestServer.serverAuthority() + "/https2";
--- a/test/jdk/java/net/httpclient/RedirectMethodChange.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/RedirectMethodChange.java Wed Jun 20 09:05:57 2018 -0700
@@ -212,7 +212,7 @@
http2TestServer.addHandler(handler, "/http2/");
http2URI = "http://" + http2TestServer.serverAuthority() + "/http2/test/rmt";
- https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, 0));
+ https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, sslContext));
targetURI = "https://" + https2TestServer.serverAuthority() + "/https2/redirect/rmt";
handler = new RedirMethodChgeHandler(targetURI);
https2TestServer.addHandler(handler, "/https2/");
@@ -283,14 +283,14 @@
}
if (newtest) {
- HttpTestHeaders hdrs = he.getRequestHeaders();
+ HttpTestRequestHeaders hdrs = he.getRequestHeaders();
String value = hdrs.firstValue("X-Redirect-Code").get();
int redirectCode = Integer.parseInt(value);
expectedMethod = hdrs.firstValue("X-Expect-Method").get();
if (!readAndCheckBody(he))
return;
- hdrs = he.getResponseHeaders();
- hdrs.addHeader("Location", targetURL);
+ HttpTestResponseHeaders headersbuilder = he.getResponseHeaders();
+ headersbuilder.addHeader("Location", targetURL);
he.sendResponseHeaders(redirectCode, 0);
inTest = true;
} else {
--- a/test/jdk/java/net/httpclient/RedirectWithCookie.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/RedirectWithCookie.java Wed Jun 20 09:05:57 2018 -0700
@@ -167,7 +167,7 @@
http2TestServer = HttpTestServer.of(new Http2TestServer("localhost", false, 0));
http2TestServer.addHandler(new CookieRedirectHandler(), "/http2/cookie/");
http2URI = "http://" + http2TestServer.serverAuthority() + "/http2/cookie/redirect";
- https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, 0));
+ https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, sslContext));
https2TestServer.addHandler(new CookieRedirectHandler(), "/https2/cookie/");
https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/cookie/redirect";
--- a/test/jdk/java/net/httpclient/RequestBuilderTest.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/RequestBuilderTest.java Wed Jun 20 09:05:57 2018 -0700
@@ -209,6 +209,7 @@
for (HttpRequest r : requests) {
assertEquals(r.headers().map().size(), 1);
assertTrue(r.headers().firstValue("A").isPresent());
+ assertTrue(r.headers().firstValue("a").isPresent());
assertEquals(r.headers().firstValue("A").get(), "B");
assertEquals(r.headers().allValues("A"), List.of("B"));
assertEquals(r.headers().allValues("C").size(), 0);
@@ -310,6 +311,30 @@
assertThrows(UOE, () -> r.headers().allValues("A").addAll(List.of("Z")));
assertThrows(UOE, () -> r.headers().allValues("A").add(1, "Z"));
}
+
+ // case-insensitivity
+ requests = List.of(
+ newBuilder(uri)
+ .header("Accept-Encoding", "gzip, deflate").build(),
+ newBuilder(uri)
+ .header("accept-encoding", "gzip, deflate").build(),
+ newBuilder(uri)
+ .header("AccePt-EncodINg", "gzip, deflate").build(),
+ newBuilder(uri)
+ .header("AcCEpt-EncoDIng", "gzip, deflate").build()
+ );
+ for (HttpRequest r : requests) {
+ for (String name : List.of("Accept-Encoding", "accept-encoding",
+ "aCCept-EnCODing", "accepT-encodinG")) {
+ assertTrue(r.headers().firstValue(name).isPresent());
+ assertTrue(r.headers().allValues(name).contains("gzip, deflate"));
+ assertEquals(r.headers().firstValue(name).get(), "gzip, deflate");
+ assertEquals(r.headers().allValues(name).size(), 1);
+ assertEquals(r.headers().map().size(), 1);
+ assertEquals(r.headers().map().get(name).size(), 1);
+ assertEquals(r.headers().map().get(name).get(0), "gzip, deflate");
+ }
+ }
}
private static final Set<String> RESTRICTED = Set.of("connection", "content-length",
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/ResponseBodyBeforeError.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,530 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Tests that all response body is delivered to the BodySubscriber
+ * before an abortive error terminates the flow
+ * @library /lib/testlibrary
+ * @build jdk.testlibrary.SimpleSSLContext
+ * @run testng/othervm ResponseBodyBeforeError
+ */
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UncheckedIOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodySubscriber;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Flow;
+import jdk.testlibrary.SimpleSSLContext;
+import org.testng.annotations.AfterTest;
+import org.testng.annotations.BeforeTest;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLServerSocketFactory;
+import static java.lang.System.out;
+import static java.net.http.HttpClient.Builder.NO_PROXY;
+import static java.net.http.HttpResponse.BodyHandlers.ofString;
+import static java.nio.charset.StandardCharsets.US_ASCII;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.fail;
+
+public class ResponseBodyBeforeError {
+
+ ReplyingServer variableLengthServer;
+ ReplyingServer variableLengthHttpsServer;
+ ReplyingServer fixedLengthServer;
+ ReplyingServer fixedLengthHttpsServer;
+
+ String httpURIVarLen;
+ String httpsURIVarLen;
+ String httpURIFixLen;
+ String httpsURIFixLen;
+
+ SSLContext sslContext;
+
+ static final String EXPECTED_RESPONSE_BODY =
+ "<html><body><h1>Heading</h1><p>Some Text</p></body></html>";
+
+ @DataProvider(name = "sanity")
+ public Object[][] sanity() {
+ return new Object[][]{
+ { httpURIVarLen + "?length=all" },
+ { httpsURIVarLen + "?length=all" },
+ { httpURIFixLen + "?length=all" },
+ { httpsURIFixLen + "?length=all" },
+ };
+ }
+
+ @Test(dataProvider = "sanity")
+ void sanity(String url) throws Exception {
+ HttpClient client = HttpClient.newBuilder()
+ .proxy(NO_PROXY)
+ .sslContext(sslContext)
+ .build();
+ HttpRequest request = HttpRequest.newBuilder(URI.create(url)).build();
+ HttpResponse<String> response = client.send(request, ofString());
+ String body = response.body();
+ assertEquals(body, EXPECTED_RESPONSE_BODY);
+ client.sendAsync(request, ofString())
+ .thenApply(resp -> resp.body())
+ .thenAccept(b -> assertEquals(b, EXPECTED_RESPONSE_BODY))
+ .join();
+ }
+
+ @DataProvider(name = "uris")
+ public Object[][] variants() {
+ Object[][] cases = new Object[][] {
+ // The length query string is the total number of response body
+ // bytes in the reply, before the server closes the connection. The
+ // second arg is a partial-expected-body that the body subscriber
+ // should receive before onError is invoked.
+
+ { httpURIFixLen + "?length=0", "" },
+ { httpURIFixLen + "?length=1", "<" },
+ { httpURIFixLen + "?length=2", "<h" },
+ { httpURIFixLen + "?length=10", "<html><bod" },
+ { httpURIFixLen + "?length=19", "<html><body><h1>Hea" },
+ { httpURIFixLen + "?length=31", "<html><body><h1>Heading</h1><p>" },
+
+ { httpsURIFixLen + "?length=0", "" },
+ { httpsURIFixLen + "?length=1", "<" },
+ { httpsURIFixLen + "?length=2", "<h" },
+ { httpsURIFixLen + "?length=10", "<html><bod" },
+ { httpsURIFixLen + "?length=19", "<html><body><h1>Hea" },
+ { httpsURIFixLen + "?length=31", "<html><body><h1>Heading</h1><p>" },
+
+ // accounts for chunk framing
+ { httpURIVarLen + "?length=0", "" },
+ { httpURIVarLen + "?length=1", "" },
+ { httpURIVarLen + "?length=2", "" },
+ { httpURIVarLen + "?length=4", "<" },
+ { httpURIVarLen + "?length=5", "<h" },
+ { httpURIVarLen + "?length=18", "<html><bod" },
+ { httpURIVarLen + "?length=20", "<html><body>" },
+ { httpURIVarLen + "?length=21", "<html><body>" }, // boundary around chunk framing
+ { httpURIVarLen + "?length=22", "<html><body>" },
+ { httpURIVarLen + "?length=23", "<html><body>" },
+ { httpURIVarLen + "?length=24", "<html><body>" },
+ { httpURIVarLen + "?length=25", "<html><body>" },
+ { httpURIVarLen + "?length=26", "<html><body>" },
+ { httpURIVarLen + "?length=27", "<html><body><" },
+ { httpURIVarLen + "?length=51", "<html><body><h1>Heading</h1><p>" },
+
+ { httpsURIVarLen + "?length=0", "" },
+ { httpsURIVarLen + "?length=1", "" },
+ { httpsURIVarLen + "?length=2", "" },
+ { httpsURIVarLen + "?length=4", "<" },
+ { httpsURIVarLen + "?length=5", "<h" },
+ { httpsURIVarLen + "?length=18", "<html><bod" },
+ { httpsURIVarLen + "?length=20", "<html><body>" },
+ { httpsURIVarLen + "?length=21", "<html><body>" },
+ { httpsURIVarLen + "?length=22", "<html><body>" },
+ { httpsURIVarLen + "?length=23", "<html><body>" },
+ { httpsURIVarLen + "?length=24", "<html><body>" },
+ { httpsURIVarLen + "?length=25", "<html><body>" },
+ { httpsURIVarLen + "?length=26", "<html><body>" },
+ { httpsURIVarLen + "?length=27", "<html><body><" },
+ { httpsURIVarLen + "?length=51", "<html><body><h1>Heading</h1><p>" },
+ };
+
+ List<Object[]> list = new ArrayList<>();
+ Arrays.asList(cases).stream()
+ .map(e -> new Object[] {e[0], e[1], true}) // reuse client
+ .forEach(list::add);
+ Arrays.asList(cases).stream()
+ .map(e -> new Object[] {e[0], e[1], false}) // do not reuse client
+ .forEach(list::add);
+ return list.stream().toArray(Object[][]::new);
+ }
+
+ static final int ITERATION_COUNT = 3;
+
+ @Test(dataProvider = "uris")
+ void testSynchronousAllRequestBody(String url,
+ String expectedPatrialBody,
+ boolean sameClient)
+ throws Exception
+ {
+ out.print("---\n");
+ HttpClient client = null;
+ for (int i=0; i< ITERATION_COUNT; i++) {
+ if (!sameClient || client == null)
+ client = HttpClient.newBuilder()
+ .proxy(NO_PROXY)
+ .sslContext(sslContext)
+ .build();
+ HttpRequest request = HttpRequest.newBuilder(URI.create(url)).build();
+ CustomBodySubscriber bs = new CustomBodySubscriber();
+ try {
+ HttpResponse<String> response = client.send(request, r -> bs);
+ String body = response.body();
+ out.println(response + ": " + body);
+ fail("UNEXPECTED RESPONSE: " + response);
+ } catch (IOException expected) {
+ String pm = bs.receivedAsString();
+ out.println("partial body received: " + pm);
+ assertEquals(pm, expectedPatrialBody);
+ }
+ }
+ }
+
+ @Test(dataProvider = "uris")
+ void testAsynchronousAllRequestBody(String url,
+ String expectedPatrialBody,
+ boolean sameClient)
+ throws Exception
+ {
+ out.print("---\n");
+ HttpClient client = null;
+ for (int i=0; i< ITERATION_COUNT; i++) {
+ if (!sameClient || client == null)
+ client = HttpClient.newBuilder()
+ .proxy(NO_PROXY)
+ .sslContext(sslContext)
+ .build();
+ HttpRequest request = HttpRequest.newBuilder(URI.create(url)).build();
+ CustomBodySubscriber bs = new CustomBodySubscriber();
+ try {
+ HttpResponse<String> response = client.sendAsync(request, r -> bs).get();
+ String body = response.body();
+ out.println(response + ": " + body);
+ fail("UNEXPECTED RESPONSE: " + response);
+ } catch (ExecutionException ee) {
+ if (ee.getCause() instanceof IOException) {
+ String pm = bs.receivedAsString();
+ out.println("partial body received: " + pm);
+ assertEquals(pm, expectedPatrialBody);
+ } else {
+ throw ee;
+ }
+ }
+ }
+ }
+
+ static final class CustomBodySubscriber implements BodySubscriber<String> {
+
+ Flow.Subscription subscription;
+ private final List<ByteBuffer> received = new ArrayList<>();
+ private final CompletableFuture<String> cf = new CompletableFuture<>();
+
+ @Override
+ public CompletionStage<String> getBody() {
+ return cf;
+ }
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ out.println("CustomBodySubscriber got onSubscribe: ");
+ this.subscription = subscription;
+ subscription.request(1);
+ }
+
+ @Override
+ public void onNext(List<ByteBuffer> items) {
+ out.println("CustomBodySubscriber got onNext: " + items);
+ received.addAll(items);
+ subscription.request(1);
+ }
+
+ @Override
+ public void onError(Throwable expected) {
+ out.println("CustomBodySubscriber got expected: " + expected);
+ cf.completeExceptionally(expected);
+ }
+
+ String receivedAsString() {
+ int size = received.stream().mapToInt(ByteBuffer::remaining).sum();
+ byte[] res = new byte[size];
+ int from = 0;
+ for (ByteBuffer b : received) {
+ int l = b.remaining();
+ b.get(res, from, l);
+ from += l;
+ }
+ return new String(res, UTF_8);
+ }
+
+ @Override
+ public void onComplete() {
+ out.println("CustomBodySubscriber got complete: ");
+ assert false : "Unexpected onComplete";
+ }
+ }
+
+ // -- infra
+
+ /**
+ * A server that replies with headers and a, possibly partial, reply, before
+ * closing the connection. The number of body bytes of written, is
+ * controllable through the "length" query string param in the requested
+ * URI.
+ */
+ static abstract class ReplyingServer extends Thread implements Closeable {
+
+ private final String name;
+ private final ServerSocket ss;
+ private volatile boolean closed;
+
+ private ReplyingServer(String name) throws IOException {
+ super(name);
+ this.name = name;
+ ss = newServerSocket();
+ ss.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0));
+ this.start();
+ }
+
+ protected ServerSocket newServerSocket() throws IOException {
+ return new ServerSocket();
+ }
+
+ abstract String responseHeaders();
+
+ abstract String responseBody();
+
+ @Override
+ public void run() {
+ while (!closed) {
+ try (Socket s = ss.accept()) {
+ out.print(name + ": got connection ");
+ InputStream is = s.getInputStream();
+ URI requestMethod = readRequestMethod(is);
+ out.print(requestMethod + " ");
+ URI uriPath = readRequestPath(is);
+ out.println(uriPath);
+ readRequestHeaders(is);
+
+ String query = uriPath.getRawQuery();
+ assert query != null;
+ String qv = query.split("=")[1];
+ int len;
+ if (qv.equals("all")) {
+ len = responseBody().getBytes(US_ASCII).length;
+ } else {
+ len = Integer.parseInt(query.split("=")[1]);
+ }
+
+ OutputStream os = s.getOutputStream();
+ os.write(responseHeaders().getBytes(US_ASCII));
+ out.println(name + ": headers written, writing " + len + " body bytes");
+ byte[] responseBytes = responseBody().getBytes(US_ASCII);
+ for (int i = 0; i< len; i++) {
+ os.write(responseBytes[i]);
+ os.flush();
+ }
+ } catch (IOException e) {
+ if (!closed)
+ throw new UncheckedIOException("Unexpected", e);
+ }
+ }
+ }
+
+ static final byte[] requestEnd = new byte[] { '\r', '\n', '\r', '\n' };
+
+ // Read the request method
+ static URI readRequestMethod(InputStream is) throws IOException {
+ StringBuilder sb = new StringBuilder();
+ int r;
+ while ((r = is.read()) != -1 && r != 0x20) {
+ sb.append((char)r);
+ }
+ return URI.create(sb.toString());
+ }
+
+ // Read the request URI path
+ static URI readRequestPath(InputStream is) throws IOException {
+ StringBuilder sb = new StringBuilder();
+ int r;
+ while ((r = is.read()) != -1 && r != 0x20) {
+ sb.append((char)r);
+ }
+ return URI.create(sb.toString());
+ }
+
+ // Read until the end of a HTTP request headers
+ static void readRequestHeaders(InputStream is) throws IOException {
+ int requestEndCount = 0, r;
+ while ((r = is.read()) != -1) {
+ if (r == requestEnd[requestEndCount]) {
+ requestEndCount++;
+ if (requestEndCount == 4) {
+ break;
+ }
+ } else {
+ requestEndCount = 0;
+ }
+ }
+ }
+
+ public int getPort() { return ss.getLocalPort(); }
+
+ @Override
+ public void close() {
+ if (closed)
+ return;
+ closed = true;
+ try {
+ ss.close();
+ } catch (IOException e) {
+ throw new UncheckedIOException("Unexpected", e);
+ }
+ }
+ }
+
+ /** A server that issues a possibly-partial chunked reply. */
+ static class PlainVariableLengthServer extends ReplyingServer {
+
+ static final String CHUNKED_RESPONSE_BODY =
+ "6\r\n"+ "<html>\r\n" +
+ "6\r\n"+ "<body>\r\n" +
+ "10\r\n"+ "<h1>Heading</h1>\r\n" +
+ "10\r\n"+ "<p>Some Text</p>\r\n" +
+ "7\r\n"+ "</body>\r\n" +
+ "7\r\n"+ "</html>\r\n" +
+ "0\r\n"+ "\r\n";
+
+ static final String RESPONSE_HEADERS =
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Type: text/html; charset=utf-8\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "Connection: close\r\n\r\n";
+
+
+ PlainVariableLengthServer() throws IOException {
+ super("PlainVariableLengthServer");
+ }
+
+ protected PlainVariableLengthServer(String name) throws IOException {
+ super(name);
+ }
+
+ @Override
+ String responseHeaders( ) { return RESPONSE_HEADERS; }
+
+ @Override
+ String responseBody( ) { return CHUNKED_RESPONSE_BODY; }
+ }
+
+ /** A server that issues a, possibly-partial, chunked reply over SSL */
+ static final class SSLVariableLengthServer extends PlainVariableLengthServer {
+ SSLVariableLengthServer() throws IOException {
+ super("SSLVariableLengthServer");
+ }
+ @Override
+ public ServerSocket newServerSocket() throws IOException {
+ return SSLServerSocketFactory.getDefault().createServerSocket();
+ }
+ }
+
+ /** A server that issues a, possibly-partial, fixed-length reply. */
+ static class PlainFixedLengthServer extends ReplyingServer {
+
+ static final String RESPONSE_BODY = EXPECTED_RESPONSE_BODY;
+
+ static final String RESPONSE_HEADERS =
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Type: text/html; charset=utf-8\r\n" +
+ "Content-Length: " + RESPONSE_BODY.length() + "\r\n" +
+ "Connection: close\r\n\r\n";
+
+ PlainFixedLengthServer() throws IOException {
+ super("PlainFixedLengthServer");
+ }
+
+ protected PlainFixedLengthServer(String name) throws IOException {
+ super(name);
+ }
+
+ @Override
+ String responseHeaders( ) { return RESPONSE_HEADERS; }
+
+ @Override
+ String responseBody( ) { return RESPONSE_BODY; }
+ }
+
+ /** A server that issues a, possibly-partial, fixed-length reply over SSL */
+ static final class SSLFixedLengthServer extends PlainFixedLengthServer {
+ SSLFixedLengthServer() throws IOException {
+ super("SSLFixedLengthServer");
+ }
+ @Override
+ public ServerSocket newServerSocket() throws IOException {
+ return SSLServerSocketFactory.getDefault().createServerSocket();
+ }
+ }
+
+ static String serverAuthority(ReplyingServer server) {
+ return InetAddress.getLoopbackAddress().getHostName() + ":"
+ + server.getPort();
+ }
+
+ @BeforeTest
+ public void setup() throws Exception {
+ sslContext = new SimpleSSLContext().get();
+ if (sslContext == null)
+ throw new AssertionError("Unexpected null sslContext");
+ SSLContext.setDefault(sslContext);
+
+ variableLengthServer = new PlainVariableLengthServer();
+ httpURIVarLen = "http://" + serverAuthority(variableLengthServer)
+ + "/http1/variable/foo";
+
+ variableLengthHttpsServer = new SSLVariableLengthServer();
+ httpsURIVarLen = "https://" + serverAuthority(variableLengthHttpsServer)
+ + "/https1/variable/bar";
+
+ fixedLengthServer = new PlainFixedLengthServer();
+ httpURIFixLen = "http://" + serverAuthority(fixedLengthServer)
+ + "/http1/fixed/baz";
+
+ fixedLengthHttpsServer = new SSLFixedLengthServer();
+ httpsURIFixLen = "https://" + serverAuthority(fixedLengthHttpsServer)
+ + "/https1/fixed/foz";
+ }
+
+ @AfterTest
+ public void teardown() throws Exception {
+ variableLengthServer.close();
+ variableLengthHttpsServer.close();
+ fixedLengthServer.close();
+ fixedLengthHttpsServer.close();
+ }
+}
--- a/test/jdk/java/net/httpclient/ResponsePublisher.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/ResponsePublisher.java Wed Jun 20 09:05:57 2018 -0700
@@ -426,7 +426,7 @@
http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed";
http2URI_chunk = "http://" + http2TestServer.serverAuthority() + "/http2/chunk";
- https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, 0));
+ https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, sslContext));
https2TestServer.addHandler(h2_fixedLengthHandler, "/https2/fixed");
https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk");
https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed";
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/RetryPost.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,237 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Ensure that the POST method is retied when the property is set.
+ * @run testng/othervm -Djdk.httpclient.enableAllMethodRetry RetryPost
+ * @run testng/othervm -Djdk.httpclient.enableAllMethodRetry=true RetryPost
+ */
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UncheckedIOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpResponse;
+import org.testng.annotations.AfterTest;
+import org.testng.annotations.BeforeTest;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+import static java.lang.System.out;
+import static java.net.http.HttpClient.Builder.NO_PROXY;
+import static java.net.http.HttpResponse.BodyHandlers.ofString;
+import static java.nio.charset.StandardCharsets.US_ASCII;
+import static org.testng.Assert.assertEquals;
+
+public class RetryPost {
+
+ FixedLengthServer fixedLengthServer;
+ String httpURIFixLen;
+
+ static final String RESPONSE_BODY =
+ "You use a glass mirror to see your face: you use works of art to see your soul.";
+
+ @DataProvider(name = "uris")
+ public Object[][] variants() {
+ return new Object[][] {
+ { httpURIFixLen, true },
+ { httpURIFixLen, false },
+ };
+ }
+
+ static final int ITERATION_COUNT = 3;
+
+ static final String REQUEST_BODY = "Body";
+
+ @Test(dataProvider = "uris")
+ void testSynchronousPOST(String url, boolean sameClient) throws Exception {
+ out.print("---\n");
+ HttpClient client = null;
+ for (int i=0; i< ITERATION_COUNT; i++) {
+ if (!sameClient || client == null)
+ client = HttpClient.newBuilder().proxy(NO_PROXY).build();
+ HttpRequest request = HttpRequest.newBuilder(URI.create(url))
+ .POST(BodyPublishers.ofString(REQUEST_BODY))
+ .build();
+ HttpResponse<String> response = client.send(request, ofString());
+ String body = response.body();
+ out.println(response + ": " + body);
+ assertEquals(response.statusCode(), 200);
+ assertEquals(body, RESPONSE_BODY);
+ }
+ }
+
+ @Test(dataProvider = "uris")
+ void testAsynchronousPOST(String url, boolean sameClient) {
+ out.print("---\n");
+ HttpClient client = null;
+ for (int i=0; i< ITERATION_COUNT; i++) {
+ if (!sameClient || client == null)
+ client = HttpClient.newBuilder().proxy(NO_PROXY).build();
+ HttpRequest request = HttpRequest.newBuilder(URI.create(url))
+ .POST(BodyPublishers.ofString(REQUEST_BODY))
+ .build();
+ client.sendAsync(request, ofString())
+ .thenApply(r -> { out.println(r + ": " + r.body()); return r; })
+ .thenApply(r -> { assertEquals(r.statusCode(), 200); return r; })
+ .thenApply(HttpResponse::body)
+ .thenAccept(b -> assertEquals(b, RESPONSE_BODY))
+ .join();
+ }
+ }
+
+
+ /**
+ * A server that issues a valid fixed-length reply on even requests, and
+ * immediately closes the connection on odd requests ( tick-tock ).
+ */
+ static class FixedLengthServer extends Thread implements AutoCloseable {
+
+ static final String RESPONSE_HEADERS =
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Type: text/html; charset=utf-8\r\n" +
+ "Content-Length: " + RESPONSE_BODY.length() + "\r\n" +
+ "Connection: close\r\n\r\n";
+
+ static final String RESPONSE = RESPONSE_HEADERS + RESPONSE_BODY;
+
+ private final ServerSocket ss;
+ private volatile boolean closed;
+ private int invocationTimes;
+
+ FixedLengthServer() throws IOException {
+ super("FixedLengthServer");
+ ss = new ServerSocket();
+ ss.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0));
+ this.start();
+ }
+
+ public int getPort() { return ss.getLocalPort(); }
+
+ @Override
+ public void run() {
+ while (!closed) {
+ try (Socket s = ss.accept()) {
+ invocationTimes++;
+ out.print("FixedLengthServer: got connection ");
+ if ((invocationTimes & 0x1) == 0x1) {
+ out.println(" closing immediately");
+ s.close();
+ continue;
+ }
+ InputStream is = s.getInputStream();
+ URI requestMethod = readRequestMethod(is);
+ out.print(requestMethod + " ");
+ URI uriPath = readRequestPath(is);
+ out.println(uriPath);
+ readRequestHeaders(is);
+ byte[] body = is.readNBytes(4);
+ assert body.length == REQUEST_BODY.length() :
+ "Unexpected request body " + body.length;
+
+ OutputStream os = s.getOutputStream();
+ out.println("Server: writing response bytes");
+ byte[] responseBytes = RESPONSE.getBytes(US_ASCII);
+ os.write(responseBytes);
+ } catch (IOException e) {
+ if (!closed)
+ throw new UncheckedIOException("Unexpected", e);
+ }
+ }
+ }
+
+ @Override
+ public void close() {
+ if (closed)
+ return;
+ closed = true;
+ try {
+ ss.close();
+ } catch (IOException e) {
+ throw new UncheckedIOException("Unexpected", e);
+ }
+ }
+
+ static final byte[] requestEnd = new byte[] { '\r', '\n', '\r', '\n' };
+
+ // Read the request method
+ static URI readRequestMethod(InputStream is) throws IOException {
+ StringBuilder sb = new StringBuilder();
+ int r;
+ while ((r = is.read()) != -1 && r != 0x20) {
+ sb.append((char)r);
+ }
+ return URI.create(sb.toString());
+ }
+
+ // Read the request URI path
+ static URI readRequestPath(InputStream is) throws IOException {
+ StringBuilder sb = new StringBuilder();
+ int r;
+ while ((r = is.read()) != -1 && r != 0x20) {
+ sb.append((char)r);
+ }
+ return URI.create(sb.toString());
+ }
+
+ // Read until the end of a HTTP request headers
+ static void readRequestHeaders(InputStream is) throws IOException {
+ int requestEndCount = 0, r;
+ while ((r = is.read()) != -1) {
+ if (r == requestEnd[requestEndCount]) {
+ requestEndCount++;
+ if (requestEndCount == 4) {
+ break;
+ }
+ } else {
+ requestEndCount = 0;
+ }
+ }
+ }
+ }
+
+ static String serverAuthority(FixedLengthServer server) {
+ return InetAddress.getLoopbackAddress().getHostName() + ":"
+ + server.getPort();
+ }
+
+ @BeforeTest
+ public void setup() throws Exception {
+ fixedLengthServer = new FixedLengthServer();
+ httpURIFixLen = "http://" + serverAuthority(fixedLengthServer)
+ + "/http1/fixed/baz";
+ }
+
+ @AfterTest
+ public void teardown() throws Exception {
+ fixedLengthServer.close();
+ }
+}
\ No newline at end of file
--- a/test/jdk/java/net/httpclient/RetryWithCookie.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/RetryWithCookie.java Wed Jun 20 09:05:57 2018 -0700
@@ -165,7 +165,7 @@
http2TestServer = HttpTestServer.of(new Http2TestServer("localhost", false, 0));
http2TestServer.addHandler(new CookieRetryHandler(), "/http2/cookie/");
http2URI = "http://" + http2TestServer.serverAuthority() + "/http2/cookie/retry";
- https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, 0));
+ https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, sslContext));
https2TestServer.addHandler(new CookieRetryHandler(), "/https2/cookie/");
https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/cookie/retry";
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/ShortResponseBody.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,668 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Tests Exception detail message when too few response bytes are
+ * received before a socket exception or eof.
+ * @library /lib/testlibrary
+ * @build jdk.testlibrary.SimpleSSLContext
+ * @run testng/othervm
+ * -Djdk.httpclient.HttpClient.log=headers,errors,channel
+ * ShortResponseBody
+ */
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UncheckedIOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpResponse;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.Stream;
+import jdk.testlibrary.SimpleSSLContext;
+import org.testng.annotations.AfterTest;
+import org.testng.annotations.BeforeTest;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLServerSocketFactory;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.SSLSocket;
+import static java.lang.System.out;
+import static java.net.http.HttpClient.Builder.NO_PROXY;
+import static java.net.http.HttpResponse.BodyHandlers.ofString;
+import static java.nio.charset.StandardCharsets.US_ASCII;
+import static java.util.stream.Collectors.toList;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.fail;
+
+public class ShortResponseBody {
+
+ Server closeImmediatelyServer;
+ Server closeImmediatelyHttpsServer;
+ Server variableLengthServer;
+ Server variableLengthHttpsServer;
+ Server fixedLengthServer;
+
+ String httpURIClsImed;
+ String httpsURIClsImed;
+ String httpURIVarLen;
+ String httpsURIVarLen;
+ String httpURIFixLen;
+
+ SSLContext sslContext;
+ SSLParameters sslParameters;
+
+ static final String EXPECTED_RESPONSE_BODY =
+ "<html><body><h1>Heading</h1><p>Some Text</p></body></html>";
+
+ final static AtomicLong ids = new AtomicLong();
+ final ThreadFactory factory = new ThreadFactory() {
+ @Override
+ public Thread newThread(Runnable r) {
+ Thread thread = new Thread(r, "HttpClient-Worker-" + ids.incrementAndGet());
+ thread.setDaemon(true);
+ return thread;
+ }
+ };
+ final ExecutorService service = Executors.newCachedThreadPool(factory);
+
+ @DataProvider(name = "sanity")
+ public Object[][] sanity() {
+ return new Object[][]{
+ { httpURIVarLen + "?length=all" },
+ { httpsURIVarLen + "?length=all" },
+ { httpURIFixLen + "?length=all" },
+ };
+ }
+
+ @Test(dataProvider = "sanity")
+ void sanity(String url) throws Exception {
+ HttpClient client = newHttpClient();
+ HttpRequest request = HttpRequest.newBuilder(URI.create(url)).build();
+ HttpResponse<String> response = client.send(request, ofString());
+ String body = response.body();
+ assertEquals(body, EXPECTED_RESPONSE_BODY);
+ client.sendAsync(request, ofString())
+ .thenApply(resp -> resp.body())
+ .thenAccept(b -> assertEquals(b, EXPECTED_RESPONSE_BODY))
+ .join();
+ }
+
+ @DataProvider(name = "uris")
+ public Object[][] variants() {
+ String[][] cases = new String[][] {
+ // The length query string is the total number of bytes in the reply,
+ // including headers, before the server closes the connection. The
+ // second arg is a partial-expected-detail message in the exception.
+ { httpURIVarLen + "?length=0", "no bytes" }, // EOF without receiving anything
+ { httpURIVarLen + "?length=1", "status line" }, // EOF during status-line
+ { httpURIVarLen + "?length=2", "status line" },
+ { httpURIVarLen + "?length=10", "status line" },
+ { httpURIVarLen + "?length=19", "header" }, // EOF during Content-Type header
+ { httpURIVarLen + "?length=30", "header" },
+ { httpURIVarLen + "?length=45", "header" },
+ { httpURIVarLen + "?length=48", "header" },
+ { httpURIVarLen + "?length=51", "header" },
+ { httpURIVarLen + "?length=98", "header" }, // EOF during Connection header
+ { httpURIVarLen + "?length=100", "header" },
+ { httpURIVarLen + "?length=101", "header" },
+ { httpURIVarLen + "?length=104", "header" },
+ { httpURIVarLen + "?length=106", "chunked transfer encoding" }, // EOF during chunk header ( length )
+ { httpURIVarLen + "?length=110", "chunked transfer encoding" }, // EOF during chunk response body data
+
+ { httpsURIVarLen + "?length=0", "no bytes" },
+ { httpsURIVarLen + "?length=1", "status line" },
+ { httpsURIVarLen + "?length=2", "status line" },
+ { httpsURIVarLen + "?length=10", "status line" },
+ { httpsURIVarLen + "?length=19", "header" },
+ { httpsURIVarLen + "?length=30", "header" },
+ { httpsURIVarLen + "?length=45", "header" },
+ { httpsURIVarLen + "?length=48", "header" },
+ { httpsURIVarLen + "?length=51", "header" },
+ { httpsURIVarLen + "?length=98", "header" },
+ { httpsURIVarLen + "?length=100", "header" },
+ { httpsURIVarLen + "?length=101", "header" },
+ { httpsURIVarLen + "?length=104", "header" },
+ { httpsURIVarLen + "?length=106", "chunked transfer encoding" },
+ { httpsURIVarLen + "?length=110", "chunked transfer encoding" },
+
+ { httpURIFixLen + "?length=0", "no bytes" }, // EOF without receiving anything
+ { httpURIFixLen + "?length=1", "status line" }, // EOF during status-line
+ { httpURIFixLen + "?length=2", "status line" },
+ { httpURIFixLen + "?length=10", "status line" },
+ { httpURIFixLen + "?length=19", "header" }, // EOF during Content-Type header
+ { httpURIFixLen + "?length=30", "header" },
+ { httpURIFixLen + "?length=45", "header" },
+ { httpURIFixLen + "?length=48", "header" },
+ { httpURIFixLen + "?length=51", "header" },
+ { httpURIFixLen + "?length=78", "header" }, // EOF during Connection header
+ { httpURIFixLen + "?length=79", "header" },
+ { httpURIFixLen + "?length=86", "header" },
+ { httpURIFixLen + "?length=104", "fixed content-length" }, // EOF during body
+ { httpURIFixLen + "?length=106", "fixed content-length" },
+ { httpURIFixLen + "?length=110", "fixed content-length" },
+
+ // ## ADD https fixed
+
+ { httpURIClsImed, "no bytes"},
+ { httpsURIClsImed, "no bytes"},
+ };
+
+ List<Object[]> list = new ArrayList<>();
+ Arrays.asList(cases).stream()
+ .map(e -> new Object[] {e[0], e[1], true}) // reuse client
+ .forEach(list::add);
+ Arrays.asList(cases).stream()
+ .map(e -> new Object[] {e[0], e[1], false}) // do not reuse client
+ .forEach(list::add);
+ return list.stream().toArray(Object[][]::new);
+ }
+
+ static final int ITERATION_COUNT = 3;
+
+ HttpClient newHttpClient() {
+ return HttpClient.newBuilder()
+ .proxy(NO_PROXY)
+ .sslContext(sslContext)
+ .sslParameters(sslParameters)
+ .executor(service)
+ .build();
+ }
+
+ @Test(dataProvider = "uris")
+ void testSynchronousGET(String url, String expectedMsg, boolean sameClient)
+ throws Exception
+ {
+ out.print("---\n");
+ HttpClient client = null;
+ for (int i=0; i< ITERATION_COUNT; i++) {
+ if (!sameClient || client == null)
+ client = newHttpClient();
+ HttpRequest request = HttpRequest.newBuilder(URI.create(url)).build();
+ try {
+ HttpResponse<String> response = client.send(request, ofString());
+ String body = response.body();
+ out.println(response + ": " + body);
+ fail("UNEXPECTED RESPONSE: " + response);
+ } catch (IOException ioe) {
+ out.println("Caught expected exception:" + ioe);
+ String msg = ioe.getMessage();
+ assertTrue(msg.contains(expectedMsg), "exception msg:[" + msg + "]");
+ // synchronous API must have the send method on the stack
+ assertSendMethodOnStack(ioe);
+ assertNoConnectionExpiredException(ioe);
+ }
+ }
+ }
+
+ @Test(dataProvider = "uris")
+ void testAsynchronousGET(String url, String expectedMsg, boolean sameClient)
+ throws Exception
+ {
+ out.print("---\n");
+ HttpClient client = null;
+ for (int i=0; i< ITERATION_COUNT; i++) {
+ if (!sameClient || client == null)
+ client = newHttpClient();
+ HttpRequest request = HttpRequest.newBuilder(URI.create(url)).build();
+ try {
+ HttpResponse<String> response = client.sendAsync(request, ofString()).get();
+ String body = response.body();
+ out.println(response + ": " + body);
+ fail("UNEXPECTED RESPONSE: " + response);
+ } catch (ExecutionException ee) {
+ if (ee.getCause() instanceof IOException) {
+ IOException ioe = (IOException) ee.getCause();
+ out.println("Caught expected exception:" + ioe);
+ String msg = ioe.getMessage();
+ assertTrue(msg.contains(expectedMsg), "exception msg:[" + msg + "]");
+ assertNoConnectionExpiredException(ioe);
+ } else {
+ throw ee;
+ }
+ }
+ }
+ }
+
+ // can be used to prolong request body publication
+ static final class InfiniteInputStream extends InputStream {
+ @Override
+ public int read() throws IOException {
+ return 1;
+ }
+
+ @Override
+ public int read(byte[] buf, int offset, int length) {
+ //int count = offset;
+ //length = Math.max(0, Math.min(buf.length - offset, length));
+ //for (; count < length; count++)
+ // buf[offset++] = 0x01;
+ //return count;
+ return Math.max(0, Math.min(buf.length - offset, length));
+ }
+ }
+
+ // POST tests are racy in what may be received before writing may cause a
+ // broken pipe or reset exception, before all the received data can be read.
+ // Any message up to, and including, the "expected" error message can occur.
+ // Strictly ordered list, in order of possible occurrence.
+ static final List<String> MSGS_ORDER =
+ List.of("no bytes", "status line", "header");
+
+
+ @Test(dataProvider = "uris")
+ void testSynchronousPOST(String url, String expectedMsg, boolean sameClient)
+ throws Exception
+ {
+ out.print("---\n");
+ HttpClient client = null;
+ for (int i=0; i< ITERATION_COUNT; i++) {
+ if (!sameClient || client == null)
+ client = newHttpClient();
+ HttpRequest request = HttpRequest.newBuilder(URI.create(url))
+ .POST(BodyPublishers.ofInputStream(() -> new InfiniteInputStream()))
+ .build();
+ try {
+ HttpResponse<String> response = client.send(request, ofString());
+ String body = response.body();
+ out.println(response + ": " + body);
+ fail("UNEXPECTED RESPONSE: " + response);
+ } catch (IOException ioe) {
+ out.println("Caught expected exception:" + ioe);
+ String msg = ioe.getMessage();
+
+ List<String> expectedMessages = new ArrayList<>();
+ expectedMessages.add(expectedMsg);
+ MSGS_ORDER.stream().takeWhile(s -> !s.equals(expectedMsg))
+ .forEach(expectedMessages::add);
+
+ assertTrue(expectedMessages.stream().anyMatch(s -> msg.indexOf(s) != -1),
+ "exception msg:[" + msg + "], not in [" + expectedMessages);
+ // synchronous API must have the send method on the stack
+ assertSendMethodOnStack(ioe);
+ assertNoConnectionExpiredException(ioe);
+ }
+ }
+ }
+
+ @Test(dataProvider = "uris")
+ void testAsynchronousPOST(String url, String expectedMsg, boolean sameClient)
+ throws Exception
+ {
+ out.print("---\n");
+ HttpClient client = null;
+ for (int i=0; i< ITERATION_COUNT; i++) {
+ if (!sameClient || client == null)
+ client = newHttpClient();
+ HttpRequest request = HttpRequest.newBuilder(URI.create(url))
+ .POST(BodyPublishers.ofInputStream(() -> new InfiniteInputStream()))
+ .build();
+ try {
+ HttpResponse<String> response = client.sendAsync(request, ofString()).get();
+ String body = response.body();
+ out.println(response + ": " + body);
+ fail("UNEXPECTED RESPONSE: " + response);
+ } catch (ExecutionException ee) {
+ if (ee.getCause() instanceof IOException) {
+ IOException ioe = (IOException) ee.getCause();
+ out.println("Caught expected exception:" + ioe);
+ String msg = ioe.getMessage();
+
+ List<String> expectedMessages = new ArrayList<>();
+ expectedMessages.add(expectedMsg);
+ MSGS_ORDER.stream().takeWhile(s -> !s.equals(expectedMsg))
+ .forEach(expectedMessages::add);
+
+ assertTrue(expectedMessages.stream().anyMatch(s -> msg.indexOf(s) != -1),
+ "exception msg:[" + msg + "], not in [" + expectedMessages);
+ assertNoConnectionExpiredException(ioe);
+ } else {
+ throw ee;
+ }
+ }
+ }
+ }
+
+ // Asserts that the "send" method appears in the stack of the given
+ // exception. The synchronous API must contain the send method on the stack.
+ static void assertSendMethodOnStack(IOException ioe) {
+ final String cn = "jdk.internal.net.http.HttpClientImpl";
+ List<StackTraceElement> list = Stream.of(ioe.getStackTrace())
+ .filter(ste -> ste.getClassName().equals(cn)
+ && ste.getMethodName().equals("send"))
+ .collect(toList());
+ if (list.size() != 1) {
+ ioe.printStackTrace(out);
+ fail(cn + ".send method not found in stack.");
+ }
+ }
+
+ // Asserts that the implementation-specific ConnectionExpiredException does
+ // NOT appear anywhere in the exception or its causal chain.
+ static void assertNoConnectionExpiredException(IOException ioe) {
+ Throwable throwable = ioe;
+ do {
+ String cn = throwable.getClass().getSimpleName();
+ if (cn.equals("ConnectionExpiredException")) {
+ ioe.printStackTrace(out);
+ fail("UNEXPECTED ConnectionExpiredException in:[" + ioe + "]");
+ }
+ } while ((throwable = throwable.getCause()) != null);
+ }
+
+ // -- infra
+
+ /**
+ * A server that, listens on a port, accepts new connections, and can be
+ * closed.
+ */
+ static abstract class Server extends Thread implements AutoCloseable {
+ protected final ServerSocket ss;
+ protected volatile boolean closed;
+
+ Server(String name) throws IOException {
+ super(name);
+ ss = newServerSocket();
+ ss.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0));
+ this.start();
+ }
+
+ protected ServerSocket newServerSocket() throws IOException {
+ return new ServerSocket();
+ }
+
+ public int getPort() { return ss.getLocalPort(); }
+
+ @Override
+ public void close() {
+ if (closed)
+ return;
+ closed = true;
+ try {
+ ss.close();
+ } catch (IOException e) {
+ throw new UncheckedIOException("Unexpected", e);
+ }
+ }
+ }
+
+ /**
+ * A server that closes the connection immediately, without reading or writing.
+ */
+ static class PlainCloseImmediatelyServer extends Server {
+ PlainCloseImmediatelyServer() throws IOException {
+ super("PlainCloseImmediatelyServer");
+ }
+
+ protected PlainCloseImmediatelyServer(String name) throws IOException {
+ super(name);
+ }
+
+ @Override
+ public void run() {
+ while (!closed) {
+ try (Socket s = ss.accept()) {
+ if (s instanceof SSLSocket) {
+ ((SSLSocket)s).startHandshake();
+ }
+ out.println("Server: got connection, closing immediately ");
+ } catch (IOException e) {
+ if (!closed)
+ throw new UncheckedIOException("Unexpected", e);
+ }
+ }
+ }
+ }
+
+ /**
+ * A server that closes the connection immediately, without reading or writing,
+ * after completing the SSL handshake.
+ */
+ static final class SSLCloseImmediatelyServer extends PlainCloseImmediatelyServer {
+ SSLCloseImmediatelyServer() throws IOException {
+ super("SSLCloseImmediatelyServer");
+ }
+ @Override
+ public ServerSocket newServerSocket() throws IOException {
+ return SSLServerSocketFactory.getDefault().createServerSocket();
+ }
+ }
+
+ /**
+ * A server that replies with headers and a, possibly partial, reply, before
+ * closing the connection. The number of bytes of written ( header + body),
+ * is controllable through the "length" query string param in the requested
+ * URI.
+ */
+ static abstract class ReplyingServer extends Server {
+
+ private final String name;
+
+ ReplyingServer(String name) throws IOException {
+ super(name);
+ this.name = name;
+ }
+
+ abstract String response();
+
+ @Override
+ public void run() {
+ while (!closed) {
+ try (Socket s = ss.accept()) {
+ out.print(name + ": got connection ");
+ InputStream is = s.getInputStream();
+ URI requestMethod = readRequestMethod(is);
+ out.print(requestMethod + " ");
+ URI uriPath = readRequestPath(is);
+ out.println(uriPath);
+ readRequestHeaders(is);
+
+ String query = uriPath.getRawQuery();
+ assert query != null;
+ String qv = query.split("=")[1];
+ int len;
+ if (qv.equals("all")) {
+ len = response().getBytes(US_ASCII).length;
+ } else {
+ len = Integer.parseInt(query.split("=")[1]);
+ }
+
+ OutputStream os = s.getOutputStream();
+ out.println(name + ": writing " + len + " bytes");
+ byte[] responseBytes = response().getBytes(US_ASCII);
+ for (int i = 0; i< len; i++) {
+ os.write(responseBytes[i]);
+ os.flush();
+ }
+ } catch (IOException e) {
+ if (!closed)
+ throw new UncheckedIOException("Unexpected", e);
+ }
+ }
+ }
+
+ static final byte[] requestEnd = new byte[] { '\r', '\n', '\r', '\n' };
+
+ // Read the request method
+ static URI readRequestMethod(InputStream is) throws IOException {
+ StringBuilder sb = new StringBuilder();
+ int r;
+ while ((r = is.read()) != -1 && r != 0x20) {
+ sb.append((char)r);
+ }
+ return URI.create(sb.toString());
+ }
+
+ // Read the request URI path
+ static URI readRequestPath(InputStream is) throws IOException {
+ StringBuilder sb = new StringBuilder();
+ int r;
+ while ((r = is.read()) != -1 && r != 0x20) {
+ sb.append((char)r);
+ }
+ return URI.create(sb.toString());
+ }
+
+ // Read until the end of a HTTP request headers
+ static void readRequestHeaders(InputStream is) throws IOException {
+ int requestEndCount = 0, r;
+ while ((r = is.read()) != -1) {
+ if (r == requestEnd[requestEndCount]) {
+ requestEndCount++;
+ if (requestEndCount == 4) {
+ break;
+ }
+ } else {
+ requestEndCount = 0;
+ }
+ }
+ }
+ }
+
+ /** A server that issues a, possibly-partial, chunked reply. */
+ static class PlainVariableLengthServer extends ReplyingServer {
+
+ static final String CHUNKED_RESPONSE_BODY =
+ "6\r\n"+ "<html>\r\n" +
+ "6\r\n"+ "<body>\r\n" +
+ "10\r\n"+ "<h1>Heading</h1>\r\n" +
+ "10\r\n"+ "<p>Some Text</p>\r\n" +
+ "7\r\n"+ "</body>\r\n" +
+ "7\r\n"+ "</html>\r\n" +
+ "0\r\n"+ "\r\n";
+
+ static final String RESPONSE_HEADERS =
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Type: text/html; charset=utf-8\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "Connection: close\r\n\r\n";
+
+ static final String RESPONSE = RESPONSE_HEADERS + CHUNKED_RESPONSE_BODY;
+
+ PlainVariableLengthServer() throws IOException {
+ super("PlainVariableLengthServer");
+ }
+
+ protected PlainVariableLengthServer(String name) throws IOException {
+ super(name);
+ }
+
+ @Override
+ String response( ) { return RESPONSE; }
+ }
+
+ /** A server that issues a, possibly-partial, chunked reply over SSL. */
+ static final class SSLVariableLengthServer extends PlainVariableLengthServer {
+ SSLVariableLengthServer() throws IOException {
+ super("SSLVariableLengthServer");
+ }
+ @Override
+ public ServerSocket newServerSocket() throws IOException {
+ return SSLServerSocketFactory.getDefault().createServerSocket();
+ }
+ }
+
+ /** A server that issues a fixed-length reply. */
+ static final class FixedLengthServer extends ReplyingServer {
+
+ static final String RESPONSE_BODY = EXPECTED_RESPONSE_BODY;
+
+ static final String RESPONSE_HEADERS =
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Type: text/html; charset=utf-8\r\n" +
+ "Content-Length: " + RESPONSE_BODY.length() + "\r\n" +
+ "Connection: close\r\n\r\n";
+
+ static final String RESPONSE = RESPONSE_HEADERS + RESPONSE_BODY;
+
+ FixedLengthServer() throws IOException {
+ super("FixedLengthServer");
+ }
+
+ @Override
+ String response( ) { return RESPONSE; }
+ }
+
+ static String serverAuthority(Server server) {
+ return InetAddress.getLoopbackAddress().getHostName() + ":"
+ + server.getPort();
+ }
+
+ @BeforeTest
+ public void setup() throws Exception {
+ sslContext = new SimpleSSLContext().get();
+ if (sslContext == null)
+ throw new AssertionError("Unexpected null sslContext");
+ SSLContext.setDefault(sslContext);
+
+ sslParameters = new SSLParameters();
+ sslParameters.setProtocols(new String[] {"TLSv1.2"});
+
+ closeImmediatelyServer = new PlainCloseImmediatelyServer();
+ httpURIClsImed = "http://" + serverAuthority(closeImmediatelyServer)
+ + "/http1/closeImmediately/foo";
+
+ closeImmediatelyHttpsServer = new SSLCloseImmediatelyServer();
+ httpsURIClsImed = "https://" + serverAuthority(closeImmediatelyHttpsServer)
+ + "/https1/closeImmediately/foo";
+
+ variableLengthServer = new PlainVariableLengthServer();
+ httpURIVarLen = "http://" + serverAuthority(variableLengthServer)
+ + "/http1/variable/bar";
+
+ variableLengthHttpsServer = new SSLVariableLengthServer();
+ httpsURIVarLen = "https://" + serverAuthority(variableLengthHttpsServer)
+ + "/https1/variable/bar";
+
+ fixedLengthServer = new FixedLengthServer();
+ httpURIFixLen = "http://" + serverAuthority(fixedLengthServer)
+ + "/http1/fixed/baz";
+ }
+
+ @AfterTest
+ public void teardown() throws Exception {
+ closeImmediatelyServer.close();
+ closeImmediatelyHttpsServer.close();
+ variableLengthServer.close();
+ variableLengthHttpsServer.close();
+ fixedLengthServer.close();
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/ShortResponseBodyWithRetry.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Run of ShortResponseBody with -Djdk.httpclient.enableAllMethodRetry
+ * @library /lib/testlibrary
+ * @build jdk.testlibrary.SimpleSSLContext
+ * @build ShortResponseBody
+ * @run testng/othervm
+ * -Djdk.httpclient.HttpClient.log=headers,errors,channel
+ * -Djdk.httpclient.enableAllMethodRetry
+ * ShortResponseBody
+ */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/SpecialHeadersTest.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,287 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Verify that some special headers - such as User-Agent
+ * can be specified by the caller.
+ * @bug 8203771
+ * @modules java.base/sun.net.www.http
+ * java.net.http/jdk.internal.net.http.common
+ * java.net.http/jdk.internal.net.http.frame
+ * java.net.http/jdk.internal.net.http.hpack
+ * java.logging
+ * jdk.httpserver
+ * @library /lib/testlibrary http2/server
+ * @build Http2TestServer HttpServerAdapters SpecialHeadersTest
+ * @build jdk.testlibrary.SimpleSSLContext
+ * @run testng/othervm
+ * -Djdk.httpclient.HttpClient.log=requests,headers,errors
+ * SpecialHeadersTest
+ */
+
+import com.sun.net.httpserver.HttpServer;
+import com.sun.net.httpserver.HttpsConfigurator;
+import com.sun.net.httpserver.HttpsServer;
+import jdk.testlibrary.SimpleSSLContext;
+import org.testng.annotations.AfterTest;
+import org.testng.annotations.BeforeTest;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import javax.net.ssl.SSLContext;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import static java.lang.System.err;
+import static java.lang.System.out;
+import static java.net.http.HttpClient.Builder.NO_PROXY;
+import static java.nio.charset.StandardCharsets.US_ASCII;
+import static org.testng.Assert.assertEquals;
+
+public class SpecialHeadersTest implements HttpServerAdapters {
+
+ SSLContext sslContext;
+ HttpTestServer httpTestServer; // HTTP/1.1 [ 4 servers ]
+ HttpTestServer httpsTestServer; // HTTPS/1.1
+ HttpTestServer http2TestServer; // HTTP/2 ( h2c )
+ HttpTestServer https2TestServer; // HTTP/2 ( h2 )
+ String httpURI;
+ String httpsURI;
+ String http2URI;
+ String https2URI;
+
+ static final String[][] headerNamesAndValues = new String[][]{
+ {"User-Agent: <DEFAULT>"},
+ {"User-Agent: camel-cased"},
+ {"user-agent: all-lower-case"},
+ {"user-Agent: mixed"},
+ };
+
+ @DataProvider(name = "variants")
+ public Object[][] variants() {
+ List<Object[]> list = new ArrayList<>();
+
+ for (boolean sameClient : new boolean[] { false, true }) {
+ Arrays.asList(headerNamesAndValues).stream()
+ .map(e -> new Object[] {httpURI, e[0], sameClient})
+ .forEach(list::add);
+ Arrays.asList(headerNamesAndValues).stream()
+ .map(e -> new Object[] {httpsURI, e[0], sameClient})
+ .forEach(list::add);
+ Arrays.asList(headerNamesAndValues).stream()
+ .map(e -> new Object[] {http2URI, e[0], sameClient})
+ .forEach(list::add);
+ Arrays.asList(headerNamesAndValues).stream()
+ .map(e -> new Object[] {https2URI, e[0], sameClient})
+ .forEach(list::add);
+ }
+ return list.stream().toArray(Object[][]::new);
+ }
+
+ static final int ITERATION_COUNT = 3; // checks upgrade and re-use
+
+ static String userAgent() {
+ return "Java-http-client/" + System.getProperty("java.version");
+ }
+
+ static final Map<String, String> DEFAULTS = Map.of("USER-AGENT", userAgent());
+
+ @Test(dataProvider = "variants")
+ void test(String uriString, String headerNameAndValue, boolean sameClient) throws Exception {
+ out.println("\n--- Starting ");
+
+ int index = headerNameAndValue.indexOf(":");
+ String name = headerNameAndValue.substring(0, index);
+ String v = headerNameAndValue.substring(index+1).trim();
+ String key = name.toUpperCase(Locale.ROOT);
+ boolean useDefault = "<DEFAULT>".equals(v);
+ String value = useDefault ? DEFAULTS.get(key) : v;
+
+ URI uri = URI.create(uriString+"?name="+key);
+
+ HttpClient client = null;
+ for (int i=0; i< ITERATION_COUNT; i++) {
+ if (!sameClient || client == null)
+ client = HttpClient.newBuilder()
+ .proxy(NO_PROXY)
+ .sslContext(sslContext)
+ .build();
+
+ HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(uri);
+ if (!useDefault) {
+ requestBuilder.header(name, value);
+ }
+ HttpRequest request = requestBuilder.build();
+ HttpResponse<String> resp = client.send(request, BodyHandlers.ofString());
+
+ out.println("Got response: " + resp);
+ out.println("Got body: " + resp.body());
+ assertEquals(resp.statusCode(), 200,
+ "Expected 200, got:" + resp.statusCode());
+
+ String receivedHeaderString = value == null ? null
+ : resp.headers().firstValue("X-"+key).get();
+ out.println("Got X-" + key + ": " + resp.headers().allValues("X-"+key));
+ if (value != null) {
+ assertEquals(receivedHeaderString, value);
+ assertEquals(resp.headers().allValues("X-"+key), List.of(value));
+ } else {
+ assertEquals(resp.headers().allValues("X-"+key).size(), 0);
+ }
+
+ }
+ }
+
+ @Test(dataProvider = "variants")
+ void testAsync(String uriString, String headerNameAndValue, boolean sameClient) {
+ out.println("\n--- Starting ");
+ int index = headerNameAndValue.indexOf(":");
+ String name = headerNameAndValue.substring(0, index);
+ String v = headerNameAndValue.substring(index+1).trim();
+ String key = name.toUpperCase(Locale.ROOT);
+ boolean useDefault = "<DEFAULT>".equals(v);
+ String value = useDefault ? DEFAULTS.get(key) : v;
+
+ URI uri = URI.create(uriString+"?name="+key);
+
+ HttpClient client = null;
+ for (int i=0; i< ITERATION_COUNT; i++) {
+ if (!sameClient || client == null)
+ client = HttpClient.newBuilder()
+ .proxy(NO_PROXY)
+ .sslContext(sslContext)
+ .build();
+
+ HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(uri);
+ if (!useDefault) {
+ requestBuilder.header(name, value);
+ }
+ HttpRequest request = requestBuilder.build();
+
+ client.sendAsync(request, BodyHandlers.ofString())
+ .thenApply(response -> {
+ out.println("Got response: " + response);
+ out.println("Got body: " + response.body());
+ assertEquals(response.statusCode(), 200);
+ return response;})
+ .thenAccept(resp -> {
+ String receivedHeaderString = value == null ? null
+ : resp.headers().firstValue("X-"+key).get();
+ out.println("Got X-" + key + ": " + resp.headers().allValues("X-"+key));
+ if (value != null) {
+ assertEquals(receivedHeaderString, value);
+ assertEquals(resp.headers().allValues("X-" + key), List.of(value));
+ } else {
+ assertEquals(resp.headers().allValues("X-" + key).size(), 1);
+ } })
+ .join();
+ }
+ }
+
+ static String serverAuthority(HttpTestServer server) {
+ return InetAddress.getLoopbackAddress().getHostName() + ":"
+ + server.getAddress().getPort();
+ }
+
+ @BeforeTest
+ public void setup() throws Exception {
+ sslContext = new SimpleSSLContext().get();
+ if (sslContext == null)
+ throw new AssertionError("Unexpected null sslContext");
+
+ HttpTestHandler handler = new HttpUriStringHandler();
+ InetSocketAddress sa = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0);
+ httpTestServer = HttpTestServer.of(HttpServer.create(sa, 0));
+ httpTestServer.addHandler(handler, "/http1");
+ httpURI = "http://" + serverAuthority(httpTestServer) + "/http1";
+
+ HttpsServer httpsServer = HttpsServer.create(sa, 0);
+ httpsServer.setHttpsConfigurator(new HttpsConfigurator(sslContext));
+ httpsTestServer = HttpTestServer.of(httpsServer);
+ httpsTestServer.addHandler(handler, "/https1");
+ httpsURI = "https://" + serverAuthority(httpsTestServer) + "/https1";
+
+ http2TestServer = HttpTestServer.of(new Http2TestServer("localhost", false, 0));
+ http2TestServer.addHandler(handler, "/http2");
+ http2URI = "http://" + http2TestServer.serverAuthority() + "/http2";
+
+ https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, sslContext));
+ https2TestServer.addHandler(handler, "/https2");
+ https2URI = "https://" + https2TestServer.serverAuthority() + "/https2";
+
+ httpTestServer.start();
+ httpsTestServer.start();
+ http2TestServer.start();
+ https2TestServer.start();
+ }
+
+ @AfterTest
+ public void teardown() throws Exception {
+ httpTestServer.stop();
+ httpsTestServer.stop();
+ http2TestServer.stop();
+ https2TestServer.stop();
+ }
+
+ /** A handler that returns, as its body, the exact received request URI. */
+ static class HttpUriStringHandler implements HttpTestHandler {
+ @Override
+ public void handle(HttpTestExchange t) throws IOException {
+ URI uri = t.getRequestURI();
+ String uriString = uri.toString();
+ out.println("Http1UriStringHandler received, uri: " + uriString);
+ String query = uri.getQuery();
+ String headerName = query.substring(query.indexOf("=")+1).trim();
+ try (InputStream is = t.getRequestBody();
+ OutputStream os = t.getResponseBody()) {
+ is.readAllBytes();
+ byte[] bytes = uriString.getBytes(US_ASCII);
+ t.getRequestHeaders().keySet().stream()
+ .filter(headerName::equalsIgnoreCase)
+ .forEach(h -> {
+ for (String v : t.getRequestHeaders().get(headerName)) {
+ t.getResponseHeaders().addHeader("X-"+h, v);
+ }
+ });
+ t.sendResponseHeaders(200, bytes.length);
+ os.write(bytes);
+ }
+ }
+ }
+}
--- a/test/jdk/java/net/httpclient/SplitResponse.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/SplitResponse.java Wed Jun 20 09:05:57 2018 -0700
@@ -24,6 +24,11 @@
import java.io.IOException;
import java.net.SocketException;
import java.net.URI;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
import java.util.concurrent.CompletableFuture;
import javax.net.ssl.SSLContext;
import javax.net.ServerSocketFactory;
@@ -32,6 +37,8 @@
import java.net.http.HttpClient.Version;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
+import java.util.stream.Stream;
+
import jdk.testlibrary.SimpleSSLContext;
import static java.lang.System.out;
import static java.lang.String.format;
@@ -44,7 +51,10 @@
* @library /lib/testlibrary
* @build jdk.testlibrary.SimpleSSLContext
* @build MockServer
- * @run main/othervm -Djdk.internal.httpclient.debug=true -Djdk.httpclient.HttpClient.log=all SplitResponse
+ * @run main/othervm
+ * -Djdk.internal.httpclient.debug=true
+ * -Djdk.httpclient.HttpClient.log=all
+ * SplitResponse HTTP connection:CLOSE mode:SYNC
*/
/**
@@ -106,20 +116,56 @@
return client;
}
+ enum Protocol {
+ HTTP, HTTPS
+ }
+ enum Connection {
+ KEEP_ALIVE,
+ CLOSE
+ }
+ enum Mode {
+ SYNC, ASYNC
+ }
+
+
public static void main(String[] args) throws Exception {
boolean useSSL = false;
- if (args != null && args.length == 1) {
- useSSL = "SSL".equals(args[0]);
+ if (args != null && args.length >= 1) {
+ useSSL = Protocol.valueOf(args[0]).equals(Protocol.HTTPS);
+ } else {
+ args = new String[] {"HTTP", "connection:KEEP_ALIVE:CLOSE", "mode:SYNC:ASYNC"};
}
+
+ LinkedHashSet<Mode> modes = new LinkedHashSet<>();
+ LinkedHashSet<Connection> keepAlive = new LinkedHashSet<>();
+ Stream.of(args).skip(1).forEach(s -> {
+ if (s.startsWith("connection:")) {
+ Stream.of(s.split(":")).skip(1).forEach(c -> {
+ keepAlive.add(Connection.valueOf(c));
+ });
+ } else if (s.startsWith("mode:")) {
+ Stream.of(s.split(":")).skip(1).forEach(m -> {
+ modes.add(Mode.valueOf(m));
+ });
+ } else {
+ System.err.println("Illegal argument: " + s);
+ System.err.println("Allowed syntax is: HTTP|HTTPS [connection:KEEP_ALIVE[:CLOSE]] [mode:SYNC[:ASYNC]");
+ throw new IllegalArgumentException(s);
+ }
+ });
+
+ if (keepAlive.isEmpty()) keepAlive.addAll(EnumSet.allOf(Connection.class));
+ if (modes.isEmpty()) modes.addAll(EnumSet.allOf(Mode.class));
+
SplitResponse sp = new SplitResponse(useSSL);
for (Version version : Version.values()) {
- for (boolean serverKeepalive : new boolean[]{ true, false }) {
+ for (Connection serverKeepalive : keepAlive) {
// Note: the mock server doesn't support Keep-Alive, but
// pretending that it might exercises code paths in and out of
// the connection pool, and retry logic
- for (boolean async : new boolean[]{ true, false }) {
- sp.test(version, serverKeepalive, async);
+ for (Mode mode : modes) {
+ sp.test(version,serverKeepalive == Connection.KEEP_ALIVE,mode == Mode.ASYNC);
}
}
}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/SplitResponseAsync.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/**
+ * @test
+ * @bug 8087112
+ * @library /lib/testlibrary
+ * @build jdk.testlibrary.SimpleSSLContext
+ * @build MockServer SplitResponse
+ * @run main/othervm
+ * -Djdk.internal.httpclient.debug=true
+ * -Djdk.httpclient.HttpClient.log=all
+ * SplitResponse HTTP connection:CLOSE mode:ASYNC
+ */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/SplitResponseKeepAlive.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/**
+ * @test
+ * @bug 8087112
+ * @library /lib/testlibrary
+ * @build jdk.testlibrary.SimpleSSLContext
+ * @build MockServer SplitResponse
+ * @run main/othervm
+ * -Djdk.internal.httpclient.debug=true
+ * -Djdk.httpclient.HttpClient.log=all
+ * SplitResponse HTTP connection:KEEP_ALIVE mode:SYNC
+ */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/SplitResponseKeepAliveAsync.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/**
+ * @test
+ * @bug 8087112
+ * @library /lib/testlibrary
+ * @build jdk.testlibrary.SimpleSSLContext
+ * @build MockServer SplitResponse
+ * @run main/othervm
+ * -Djdk.internal.httpclient.debug=true
+ * -Djdk.httpclient.HttpClient.log=all
+ * SplitResponse HTTP connection:KEEP_ALIVE mode:ASYNC
+ */
--- a/test/jdk/java/net/httpclient/SplitResponseSSL.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/SplitResponseSSL.java Wed Jun 20 09:05:57 2018 -0700
@@ -30,10 +30,5 @@
* @run main/othervm
* -Djdk.internal.httpclient.debug=true
* -Djdk.httpclient.HttpClient.log=all
- * SplitResponseSSL SSL
+ * SplitResponse HTTPS connection:CLOSE mode:SYNC
*/
-public class SplitResponseSSL {
- public static void main(String[] args) throws Exception {
- SplitResponse.main(args);
- }
-}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/SplitResponseSSLAsync.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/**
+ * @test
+ * @bug 8087112
+ * @library /lib/testlibrary
+ * @build jdk.testlibrary.SimpleSSLContext
+ * @build MockServer SplitResponse
+ * @run main/othervm
+ * -Djdk.internal.httpclient.debug=true
+ * -Djdk.httpclient.HttpClient.log=all
+ * SplitResponse HTTPS connection:CLOSE mode:ASYNC
+ */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/SplitResponseSSLKeepAlive.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/**
+ * @test
+ * @bug 8087112
+ * @library /lib/testlibrary
+ * @build jdk.testlibrary.SimpleSSLContext
+ * @build MockServer SplitResponse
+ * @run main/othervm
+ * -Djdk.internal.httpclient.debug=true
+ * -Djdk.httpclient.HttpClient.log=all
+ * SplitResponse HTTPS connection:KEEP_ALIVE mode:SYNC
+ */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/SplitResponseSSLKeepAliveAsync.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/**
+ * @test
+ * @bug 8087112
+ * @library /lib/testlibrary
+ * @build jdk.testlibrary.SimpleSSLContext
+ * @build MockServer SplitResponse
+ * @run main/othervm
+ * -Djdk.internal.httpclient.debug=true
+ * -Djdk.httpclient.HttpClient.log=all
+ * SplitResponse HTTPS connection:KEEP_ALIVE mode:ASYNC
+ */
--- a/test/jdk/java/net/httpclient/StreamingBody.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/StreamingBody.java Wed Jun 20 09:05:57 2018 -0700
@@ -136,7 +136,7 @@
http2TestServer = HttpTestServer.of(new Http2TestServer("localhost", false, 0));
http2TestServer.addHandler(new MessageHandler(), "/http2/streamingbody/");
http2URI = "http://" + http2TestServer.serverAuthority() + "/http2/streamingbody/y";
- https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, 0));
+ https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, sslContext));
https2TestServer.addHandler(new MessageHandler(), "/https2/streamingbody/");
https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/streamingbody/z";
--- a/test/jdk/java/net/httpclient/ThrowingPublishers.java Wed Jun 20 17:15:16 2018 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,685 +0,0 @@
-/*
- * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
-/*
- * @test
- * @summary Tests what happens when request publishers
- * throw unexpected exceptions.
- * @library /lib/testlibrary http2/server
- * @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters
- * ReferenceTracker ThrowingPublishers
- * @modules java.base/sun.net.www.http
- * java.net.http/jdk.internal.net.http.common
- * java.net.http/jdk.internal.net.http.frame
- * java.net.http/jdk.internal.net.http.hpack
- * @run testng/othervm -Djdk.internal.httpclient.debug=true
- * -Djdk.httpclient.enableAllMethodRetry=true
- * ThrowingPublishers
- */
-
-import com.sun.net.httpserver.HttpServer;
-import com.sun.net.httpserver.HttpsConfigurator;
-import com.sun.net.httpserver.HttpsServer;
-import jdk.testlibrary.SimpleSSLContext;
-import org.testng.annotations.AfterClass;
-import org.testng.annotations.AfterTest;
-import org.testng.annotations.BeforeTest;
-import org.testng.annotations.DataProvider;
-import org.testng.annotations.Test;
-
-import javax.net.ssl.SSLContext;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.UncheckedIOException;
-import java.net.InetAddress;
-import java.net.InetSocketAddress;
-import java.net.URI;
-import java.net.http.HttpClient;
-import java.net.http.HttpRequest;
-import java.net.http.HttpRequest.BodyPublisher;
-import java.net.http.HttpRequest.BodyPublishers;
-import java.net.http.HttpResponse;
-import java.net.http.HttpResponse.BodyHandler;
-import java.net.http.HttpResponse.BodyHandlers;
-import java.nio.ByteBuffer;
-import java.nio.charset.StandardCharsets;
-import java.util.EnumSet;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CompletionException;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Flow;
-import java.util.concurrent.SubmissionPublisher;
-import java.util.concurrent.atomic.AtomicLong;
-import java.util.function.BiPredicate;
-import java.util.function.Consumer;
-import java.util.function.Supplier;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import static java.lang.String.format;
-import static java.lang.System.out;
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertTrue;
-
-public class ThrowingPublishers implements HttpServerAdapters {
-
- SSLContext sslContext;
- HttpTestServer httpTestServer; // HTTP/1.1 [ 4 servers ]
- HttpTestServer httpsTestServer; // HTTPS/1.1
- HttpTestServer http2TestServer; // HTTP/2 ( h2c )
- HttpTestServer https2TestServer; // HTTP/2 ( h2 )
- String httpURI_fixed;
- String httpURI_chunk;
- String httpsURI_fixed;
- String httpsURI_chunk;
- String http2URI_fixed;
- String http2URI_chunk;
- String https2URI_fixed;
- String https2URI_chunk;
-
- static final int ITERATION_COUNT = 1;
- // a shared executor helps reduce the amount of threads created by the test
- static final Executor executor = new TestExecutor(Executors.newCachedThreadPool());
- static final ConcurrentMap<String, Throwable> FAILURES = new ConcurrentHashMap<>();
- static volatile boolean tasksFailed;
- static final AtomicLong serverCount = new AtomicLong();
- static final AtomicLong clientCount = new AtomicLong();
- static final long start = System.nanoTime();
- public static String now() {
- long now = System.nanoTime() - start;
- long secs = now / 1000_000_000;
- long mill = (now % 1000_000_000) / 1000_000;
- long nan = now % 1000_000;
- return String.format("[%d s, %d ms, %d ns] ", secs, mill, nan);
- }
-
- final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE;
- private volatile HttpClient sharedClient;
-
- static class TestExecutor implements Executor {
- final AtomicLong tasks = new AtomicLong();
- Executor executor;
- TestExecutor(Executor executor) {
- this.executor = executor;
- }
-
- @Override
- public void execute(Runnable command) {
- long id = tasks.incrementAndGet();
- executor.execute(() -> {
- try {
- command.run();
- } catch (Throwable t) {
- tasksFailed = true;
- System.out.printf(now() + "Task %s failed: %s%n", id, t);
- System.err.printf(now() + "Task %s failed: %s%n", id, t);
- FAILURES.putIfAbsent("Task " + id, t);
- throw t;
- }
- });
- }
- }
-
- @AfterClass
- static final void printFailedTests() {
- out.println("\n=========================");
- try {
- out.printf("%n%sCreated %d servers and %d clients%n",
- now(), serverCount.get(), clientCount.get());
- if (FAILURES.isEmpty()) return;
- out.println("Failed tests: ");
- FAILURES.entrySet().forEach((e) -> {
- out.printf("\t%s: %s%n", e.getKey(), e.getValue());
- e.getValue().printStackTrace(out);
- });
- if (tasksFailed) {
- System.out.println("WARNING: Some tasks failed");
- }
- } finally {
- out.println("\n=========================\n");
- }
- }
-
- private String[] uris() {
- return new String[] {
- httpURI_fixed,
- httpURI_chunk,
- httpsURI_fixed,
- httpsURI_chunk,
- http2URI_fixed,
- http2URI_chunk,
- https2URI_fixed,
- https2URI_chunk,
- };
- }
-
- @DataProvider(name = "noThrows")
- public Object[][] noThrows() {
- String[] uris = uris();
- Object[][] result = new Object[uris.length * 2][];
- //Object[][] result = new Object[uris.length][];
- int i = 0;
- for (boolean sameClient : List.of(false, true)) {
- //if (!sameClient) continue;
- for (String uri: uris()) {
- result[i++] = new Object[] {uri + "/noThrows", sameClient};
- }
- }
- assert i == uris.length * 2;
- // assert i == uris.length ;
- return result;
- }
-
- @DataProvider(name = "variants")
- public Object[][] variants() {
- String[] uris = uris();
- Object[][] result = new Object[uris.length * 2 * 2][];
- //Object[][] result = new Object[(uris.length/2) * 2 * 2][];
- int i = 0;
- for (Thrower thrower : List.of(
- new UncheckedIOExceptionThrower(),
- new UncheckedCustomExceptionThrower())) {
- for (boolean sameClient : List.of(false, true)) {
- for (String uri : uris()) {
- // if (uri.contains("http2") || uri.contains("https2")) continue;
- // if (!sameClient) continue;
- result[i++] = new Object[]{uri, sameClient, thrower};
- }
- }
- }
- assert i == uris.length * 2 * 2;
- //assert Stream.of(result).filter(o -> o != null).count() == result.length;
- return result;
- }
-
- private HttpClient makeNewClient() {
- clientCount.incrementAndGet();
- return TRACKER.track(HttpClient.newBuilder()
- .proxy(HttpClient.Builder.NO_PROXY)
- .executor(executor)
- .sslContext(sslContext)
- .build());
- }
-
- HttpClient newHttpClient(boolean share) {
- if (!share) return makeNewClient();
- HttpClient shared = sharedClient;
- if (shared != null) return shared;
- synchronized (this) {
- shared = sharedClient;
- if (shared == null) {
- shared = sharedClient = makeNewClient();
- }
- return shared;
- }
- }
-
- final String BODY = "Some string | that ? can | be split ? several | ways.";
-
- @Test(dataProvider = "noThrows")
- public void testNoThrows(String uri, boolean sameClient)
- throws Exception {
- HttpClient client = null;
- out.printf("%n%s testNoThrows(%s, %b)%n", now(), uri, sameClient);
- for (int i=0; i< ITERATION_COUNT; i++) {
- if (!sameClient || client == null)
- client = newHttpClient(sameClient);
-
- SubmissionPublisher<ByteBuffer> publisher
- = new SubmissionPublisher<>(executor,10);
- ThrowingBodyPublisher bodyPublisher = new ThrowingBodyPublisher((w) -> {},
- BodyPublishers.fromPublisher(publisher));
- CompletableFuture<Void> subscribedCF = bodyPublisher.subscribedCF();
- subscribedCF.whenComplete((r,t) -> System.out.println(now() + " subscribe completed " + t))
- .thenAcceptAsync((v) -> {
- Stream.of(BODY.split("\\|"))
- .forEachOrdered(s -> {
- System.out.println("submitting \"" + s +"\"");
- publisher.submit(ByteBuffer.wrap(s.getBytes(StandardCharsets.UTF_8)));
- });
- System.out.println("publishing done");
- publisher.close();
- },
- executor);
-
- HttpRequest req = HttpRequest.newBuilder(URI.create(uri))
- .POST(bodyPublisher)
- .build();
- BodyHandler<String> handler = BodyHandlers.ofString();
- CompletableFuture<HttpResponse<String>> response = client.sendAsync(req, handler);
-
- String body = response.join().body();
- assertEquals(body, Stream.of(BODY.split("\\|")).collect(Collectors.joining()));
- }
- }
-
- @Test(dataProvider = "variants")
- public void testThrowingAsString(String uri,
- boolean sameClient,
- Thrower thrower)
- throws Exception
- {
- String test = format("testThrowingAsString(%s, %b, %s)",
- uri, sameClient, thrower);
- List<byte[]> bytes = Stream.of(BODY.split("|"))
- .map(s -> s.getBytes(UTF_8))
- .collect(Collectors.toList());
- testThrowing(test, uri, sameClient, () -> BodyPublishers.ofByteArrays(bytes),
- this::shouldNotThrowInCancel, thrower,false);
- }
-
- private <T,U> void testThrowing(String name, String uri, boolean sameClient,
- Supplier<BodyPublisher> publishers,
- Finisher finisher, Thrower thrower, boolean async)
- throws Exception
- {
- out.printf("%n%s%s%n", now(), name);
- try {
- testThrowing(uri, sameClient, publishers, finisher, thrower, async);
- } catch (Error | Exception x) {
- FAILURES.putIfAbsent(name, x);
- throw x;
- }
- }
-
- private void testThrowing(String uri, boolean sameClient,
- Supplier<BodyPublisher> publishers,
- Finisher finisher, Thrower thrower,
- boolean async)
- throws Exception
- {
- HttpClient client = null;
- for (Where where : whereValues()) {
- //if (where == Where.ON_SUBSCRIBE) continue;
- //if (where == Where.ON_ERROR) continue;
- if (!sameClient || client == null)
- client = newHttpClient(sameClient);
-
- ThrowingBodyPublisher bodyPublisher =
- new ThrowingBodyPublisher(where.select(thrower), publishers.get());
- HttpRequest req = HttpRequest.
- newBuilder(URI.create(uri))
- .header("X-expect-exception", "true")
- .POST(bodyPublisher)
- .build();
- BodyHandler<String> handler = BodyHandlers.ofString();
- System.out.println("try throwing in " + where);
- HttpResponse<String> response = null;
- if (async) {
- try {
- response = client.sendAsync(req, handler).join();
- } catch (Error | Exception x) {
- Throwable cause = findCause(where, x, thrower);
- if (cause == null) throw causeNotFound(where, x);
- System.out.println(now() + "Got expected exception: " + cause);
- }
- } else {
- try {
- response = client.send(req, handler);
- } catch (Error | Exception t) {
- if (thrower.test(where, t)) {
- System.out.println(now() + "Got expected exception: " + t);
- } else throw causeNotFound(where, t);
- }
- }
- if (response != null) {
- finisher.finish(where, response, thrower);
- }
- }
- }
-
- enum Where {
- BEFORE_SUBSCRIBE, BEFORE_REQUEST, BEFORE_NEXT_REQUEST, BEFORE_CANCEL,
- AFTER_SUBSCRIBE, AFTER_REQUEST, AFTER_NEXT_REQUEST, AFTER_CANCEL;
- public Consumer<Where> select(Consumer<Where> consumer) {
- return new Consumer<Where>() {
- @Override
- public void accept(Where where) {
- if (Where.this == where) {
- consumer.accept(where);
- }
- }
- };
- }
- }
-
- // can be used to reduce the surface of the test when diagnosing
- // some failure
- Set<Where> whereValues() {
- //return EnumSet.of(Where.BEFORE_CANCEL, Where.AFTER_CANCEL);
- return EnumSet.allOf(Where.class);
- }
-
- interface Thrower extends Consumer<Where>, BiPredicate<Where,Throwable> {
-
- }
-
- interface Finisher<T,U> {
- U finish(Where w, HttpResponse<T> resp, Thrower thrower) throws IOException;
- }
-
- final <T,U> U shouldNotThrowInCancel(Where w, HttpResponse<T> resp, Thrower thrower) {
- switch (w) {
- case BEFORE_CANCEL: return null;
- case AFTER_CANCEL: return null;
- default: break;
- }
- return shouldHaveThrown(w, resp, thrower);
- }
-
-
- final <T,U> U shouldHaveThrown(Where w, HttpResponse<T> resp, Thrower thrower) {
- String msg = "Expected exception not thrown in " + w
- + "\n\tReceived: " + resp
- + "\n\tWith body: " + resp.body();
- System.out.println(msg);
- throw new RuntimeException(msg);
- }
-
-
- private static Throwable findCause(Where w,
- Throwable x,
- BiPredicate<Where, Throwable> filter) {
- while (x != null && !filter.test(w,x)) x = x.getCause();
- return x;
- }
-
- static AssertionError causeNotFound(Where w, Throwable t) {
- return new AssertionError("Expected exception not found in " + w, t);
- }
-
- static boolean isConnectionClosedLocally(Throwable t) {
- if (t instanceof CompletionException) t = t.getCause();
- if (t instanceof ExecutionException) t = t.getCause();
- if (t instanceof IOException) {
- String msg = t.getMessage();
- return msg == null ? false
- : msg.contains("connection closed locally");
- }
- return false;
- }
-
- static final class UncheckedCustomExceptionThrower implements Thrower {
- @Override
- public void accept(Where where) {
- out.println(now() + "Throwing in " + where);
- throw new UncheckedCustomException(where.name());
- }
-
- @Override
- public boolean test(Where w, Throwable throwable) {
- switch (w) {
- case AFTER_REQUEST:
- case BEFORE_NEXT_REQUEST:
- case AFTER_NEXT_REQUEST:
- if (isConnectionClosedLocally(throwable)) return true;
- break;
- default:
- break;
- }
- return UncheckedCustomException.class.isInstance(throwable);
- }
-
- @Override
- public String toString() {
- return "UncheckedCustomExceptionThrower";
- }
- }
-
- static final class UncheckedIOExceptionThrower implements Thrower {
- @Override
- public void accept(Where where) {
- out.println(now() + "Throwing in " + where);
- throw new UncheckedIOException(new CustomIOException(where.name()));
- }
-
- @Override
- public boolean test(Where w, Throwable throwable) {
- switch (w) {
- case AFTER_REQUEST:
- case BEFORE_NEXT_REQUEST:
- case AFTER_NEXT_REQUEST:
- if (isConnectionClosedLocally(throwable)) return true;
- break;
- default:
- break;
- }
- return UncheckedIOException.class.isInstance(throwable)
- && CustomIOException.class.isInstance(throwable.getCause());
- }
-
- @Override
- public String toString() {
- return "UncheckedIOExceptionThrower";
- }
- }
-
- static final class UncheckedCustomException extends RuntimeException {
- UncheckedCustomException(String message) {
- super(message);
- }
- UncheckedCustomException(String message, Throwable cause) {
- super(message, cause);
- }
- }
-
- static final class CustomIOException extends IOException {
- CustomIOException(String message) {
- super(message);
- }
- CustomIOException(String message, Throwable cause) {
- super(message, cause);
- }
- }
-
-
- static final class ThrowingBodyPublisher implements BodyPublisher {
- private final BodyPublisher publisher;
- private final CompletableFuture<Void> subscribedCF = new CompletableFuture<>();
- final Consumer<Where> throwing;
- ThrowingBodyPublisher(Consumer<Where> throwing, BodyPublisher publisher) {
- this.throwing = throwing;
- this.publisher = publisher;
- }
-
- @Override
- public long contentLength() {
- return publisher.contentLength();
- }
-
- @Override
- public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
- try {
- throwing.accept(Where.BEFORE_SUBSCRIBE);
- publisher.subscribe(new SubscriberWrapper(subscriber));
- subscribedCF.complete(null);
- throwing.accept(Where.AFTER_SUBSCRIBE);
- } catch (Throwable t) {
- subscribedCF.completeExceptionally(t);
- throw t;
- }
- }
-
- CompletableFuture<Void> subscribedCF() {
- return subscribedCF;
- }
-
- class SubscriptionWrapper implements Flow.Subscription {
- final Flow.Subscription subscription;
- final AtomicLong requestCount = new AtomicLong();
- SubscriptionWrapper(Flow.Subscription subscription) {
- this.subscription = subscription;
- }
- @Override
- public void request(long n) {
- long count = requestCount.incrementAndGet();
- System.out.printf("%s request-%d(%d)%n", now(), count, n);
- if (count > 1) throwing.accept(Where.BEFORE_NEXT_REQUEST);
- throwing.accept(Where.BEFORE_REQUEST);
- subscription.request(n);
- throwing.accept(Where.AFTER_REQUEST);
- if (count > 1) throwing.accept(Where.AFTER_NEXT_REQUEST);
- }
-
- @Override
- public void cancel() {
- throwing.accept(Where.BEFORE_CANCEL);
- subscription.cancel();
- throwing.accept(Where.AFTER_CANCEL);
- }
- }
-
- class SubscriberWrapper implements Flow.Subscriber<ByteBuffer> {
- final Flow.Subscriber<? super ByteBuffer> subscriber;
- SubscriberWrapper(Flow.Subscriber<? super ByteBuffer> subscriber) {
- this.subscriber = subscriber;
- }
- @Override
- public void onSubscribe(Flow.Subscription subscription) {
- subscriber.onSubscribe(new SubscriptionWrapper(subscription));
- }
- @Override
- public void onNext(ByteBuffer item) {
- subscriber.onNext(item);
- }
- @Override
- public void onComplete() {
- subscriber.onComplete();
- }
-
- @Override
- public void onError(Throwable throwable) {
- subscriber.onError(throwable);
- }
- }
- }
-
-
- @BeforeTest
- public void setup() throws Exception {
- sslContext = new SimpleSSLContext().get();
- if (sslContext == null)
- throw new AssertionError("Unexpected null sslContext");
-
- // HTTP/1.1
- HttpTestHandler h1_fixedLengthHandler = new HTTP_FixedLengthHandler();
- HttpTestHandler h1_chunkHandler = new HTTP_ChunkedHandler();
- InetSocketAddress sa = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0);
- httpTestServer = HttpTestServer.of(HttpServer.create(sa, 0));
- httpTestServer.addHandler(h1_fixedLengthHandler, "/http1/fixed");
- httpTestServer.addHandler(h1_chunkHandler, "/http1/chunk");
- httpURI_fixed = "http://" + httpTestServer.serverAuthority() + "/http1/fixed/x";
- httpURI_chunk = "http://" + httpTestServer.serverAuthority() + "/http1/chunk/x";
-
- HttpsServer httpsServer = HttpsServer.create(sa, 0);
- httpsServer.setHttpsConfigurator(new HttpsConfigurator(sslContext));
- httpsTestServer = HttpTestServer.of(httpsServer);
- httpsTestServer.addHandler(h1_fixedLengthHandler, "/https1/fixed");
- httpsTestServer.addHandler(h1_chunkHandler, "/https1/chunk");
- httpsURI_fixed = "https://" + httpsTestServer.serverAuthority() + "/https1/fixed/x";
- httpsURI_chunk = "https://" + httpsTestServer.serverAuthority() + "/https1/chunk/x";
-
- // HTTP/2
- HttpTestHandler h2_fixedLengthHandler = new HTTP_FixedLengthHandler();
- HttpTestHandler h2_chunkedHandler = new HTTP_ChunkedHandler();
-
- http2TestServer = HttpTestServer.of(new Http2TestServer("localhost", false, 0));
- http2TestServer.addHandler(h2_fixedLengthHandler, "/http2/fixed");
- http2TestServer.addHandler(h2_chunkedHandler, "/http2/chunk");
- http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed/x";
- http2URI_chunk = "http://" + http2TestServer.serverAuthority() + "/http2/chunk/x";
-
- https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, 0));
- https2TestServer.addHandler(h2_fixedLengthHandler, "/https2/fixed");
- https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk");
- https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed/x";
- https2URI_chunk = "https://" + https2TestServer.serverAuthority() + "/https2/chunk/x";
-
- serverCount.addAndGet(4);
- httpTestServer.start();
- httpsTestServer.start();
- http2TestServer.start();
- https2TestServer.start();
- }
-
- @AfterTest
- public void teardown() throws Exception {
- String sharedClientName =
- sharedClient == null ? null : sharedClient.toString();
- sharedClient = null;
- Thread.sleep(100);
- AssertionError fail = TRACKER.check(500);
- try {
- httpTestServer.stop();
- httpsTestServer.stop();
- http2TestServer.stop();
- https2TestServer.stop();
- } finally {
- if (fail != null) {
- if (sharedClientName != null) {
- System.err.println("Shared client name is: " + sharedClientName);
- }
- throw fail;
- }
- }
- }
-
- static class HTTP_FixedLengthHandler implements HttpTestHandler {
- @Override
- public void handle(HttpTestExchange t) throws IOException {
- out.println("HTTP_FixedLengthHandler received request to " + t.getRequestURI());
- byte[] resp;
- try (InputStream is = t.getRequestBody()) {
- resp = is.readAllBytes();
- }
- t.sendResponseHeaders(200, resp.length); //fixed content length
- try (OutputStream os = t.getResponseBody()) {
- os.write(resp);
- }
- }
- }
-
- static class HTTP_ChunkedHandler implements HttpTestHandler {
- @Override
- public void handle(HttpTestExchange t) throws IOException {
- out.println("HTTP_ChunkedHandler received request to " + t.getRequestURI());
- byte[] resp;
- try (InputStream is = t.getRequestBody()) {
- resp = is.readAllBytes();
- }
- t.sendResponseHeaders(200, -1); // chunked/variable
- try (OutputStream os = t.getResponseBody()) {
- os.write(resp);
- }
- }
- }
-
-}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/ThrowingPublishersCustomAfterCancel.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Tests what happens when request publishers
+ * throw unexpected exceptions.
+ * @library /lib/testlibrary http2/server
+ * @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters
+ * ReferenceTracker AbstractThrowingPublishers ThrowingPublishersCustomAfterCancel
+ * @modules java.base/sun.net.www.http
+ * java.net.http/jdk.internal.net.http.common
+ * java.net.http/jdk.internal.net.http.frame
+ * java.net.http/jdk.internal.net.http.hpack
+ * @run testng/othervm -Djdk.internal.httpclient.debug=true
+ * -Djdk.httpclient.enableAllMethodRetry=true
+ * ThrowingPublishersCustomAfterCancel
+ */
+
+import org.testng.annotations.Test;
+
+import java.util.Set;
+
+public class ThrowingPublishersCustomAfterCancel extends AbstractThrowingPublishers {
+
+ @Test(dataProvider = "afterCancelProviderCustom")
+ public void testThrowingAsString(String uri, boolean sameClient,
+ Thrower thrower, Set<Where> whereValues)
+ throws Exception
+ {
+ super.testThrowingAsStringImpl(uri, sameClient, thrower, whereValues);
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/ThrowingPublishersCustomBeforeCancel.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Tests what happens when request publishers
+ * throw unexpected exceptions.
+ * @library /lib/testlibrary http2/server
+ * @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters
+ * ReferenceTracker AbstractThrowingPublishers ThrowingPublishersCustomBeforeCancel
+ * @modules java.base/sun.net.www.http
+ * java.net.http/jdk.internal.net.http.common
+ * java.net.http/jdk.internal.net.http.frame
+ * java.net.http/jdk.internal.net.http.hpack
+ * @run testng/othervm -Djdk.internal.httpclient.debug=true
+ * -Djdk.httpclient.enableAllMethodRetry=true
+ * ThrowingPublishersCustomBeforeCancel
+ */
+
+import org.testng.annotations.Test;
+
+import java.util.Set;
+
+public class ThrowingPublishersCustomBeforeCancel extends AbstractThrowingPublishers {
+
+ @Test(dataProvider = "beforeCancelProviderCustom")
+ public void testThrowingAsString(String uri, boolean sameClient,
+ Thrower thrower, Set<Where> whereValues)
+ throws Exception
+ {
+ super.testThrowingAsStringImpl(uri, sameClient, thrower, whereValues);
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/ThrowingPublishersIOAfterCancel.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Tests what happens when request publishers
+ * throw unexpected exceptions.
+ * @library /lib/testlibrary http2/server
+ * @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters
+ * ReferenceTracker AbstractThrowingPublishers ThrowingPublishersIOAfterCancel
+ * @modules java.base/sun.net.www.http
+ * java.net.http/jdk.internal.net.http.common
+ * java.net.http/jdk.internal.net.http.frame
+ * java.net.http/jdk.internal.net.http.hpack
+ * @run testng/othervm -Djdk.internal.httpclient.debug=true
+ * -Djdk.httpclient.enableAllMethodRetry=true
+ * ThrowingPublishersIOAfterCancel
+ */
+
+import org.testng.annotations.Test;
+
+import java.util.Set;
+
+public class ThrowingPublishersIOAfterCancel extends AbstractThrowingPublishers {
+
+ @Test(dataProvider = "afterCancelProviderIO")
+ public void testThrowingAsString(String uri, boolean sameClient,
+ Thrower thrower, Set<Where> whereValues)
+ throws Exception
+ {
+ super.testThrowingAsStringImpl(uri, sameClient, thrower, whereValues);
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/ThrowingPublishersIOBeforeCancel.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Tests what happens when request publishers
+ * throw unexpected exceptions.
+ * @library /lib/testlibrary http2/server
+ * @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters
+ * ReferenceTracker AbstractThrowingPublishers ThrowingPublishersIOBeforeCancel
+ * @modules java.base/sun.net.www.http
+ * java.net.http/jdk.internal.net.http.common
+ * java.net.http/jdk.internal.net.http.frame
+ * java.net.http/jdk.internal.net.http.hpack
+ * @run testng/othervm -Djdk.internal.httpclient.debug=true
+ * -Djdk.httpclient.enableAllMethodRetry=true
+ * ThrowingPublishersIOBeforeCancel
+ */
+
+import org.testng.annotations.Test;
+
+import java.util.Set;
+
+public class ThrowingPublishersIOBeforeCancel extends AbstractThrowingPublishers {
+
+ @Test(dataProvider = "beforeCancelProviderIO")
+ public void testThrowingAsString(String uri, boolean sameClient,
+ Thrower thrower, Set<Where> whereValues)
+ throws Exception
+ {
+ super.testThrowingAsStringImpl(uri, sameClient, thrower, whereValues);
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/ThrowingPublishersInNextRequest.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Tests what happens when request publishers
+ * throw unexpected exceptions.
+ * @library /lib/testlibrary http2/server
+ * @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters
+ * ReferenceTracker AbstractThrowingPublishers ThrowingPublishersInNextRequest
+ * @modules java.base/sun.net.www.http
+ * java.net.http/jdk.internal.net.http.common
+ * java.net.http/jdk.internal.net.http.frame
+ * java.net.http/jdk.internal.net.http.hpack
+ * @run testng/othervm -Djdk.internal.httpclient.debug=true
+ * -Djdk.httpclient.enableAllMethodRetry=true
+ * ThrowingPublishersInNextRequest
+ */
+
+import org.testng.annotations.Test;
+
+import java.util.Set;
+
+public class ThrowingPublishersInNextRequest extends AbstractThrowingPublishers {
+
+ @Test(dataProvider = "nextRequestProvider")
+ public void testThrowingAsString(String uri, boolean sameClient,
+ Thrower thrower, Set<Where> whereValues)
+ throws Exception
+ {
+ super.testThrowingAsStringImpl(uri, sameClient, thrower, whereValues);
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/ThrowingPublishersInRequest.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Tests what happens when request publishers
+ * throw unexpected exceptions.
+ * @library /lib/testlibrary http2/server
+ * @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters
+ * ReferenceTracker AbstractThrowingPublishers ThrowingPublishersInRequest
+ * @modules java.base/sun.net.www.http
+ * java.net.http/jdk.internal.net.http.common
+ * java.net.http/jdk.internal.net.http.frame
+ * java.net.http/jdk.internal.net.http.hpack
+ * @run testng/othervm -Djdk.internal.httpclient.debug=true
+ * -Djdk.httpclient.enableAllMethodRetry=true
+ * ThrowingPublishersInRequest
+ */
+
+import org.testng.annotations.Test;
+
+import java.util.Set;
+
+public class ThrowingPublishersInRequest extends AbstractThrowingPublishers {
+
+ @Test(dataProvider = "requestProvider")
+ public void testThrowingAsString(String uri, boolean sameClient,
+ Thrower thrower, Set<Where> whereValues)
+ throws Exception
+ {
+ super.testThrowingAsStringImpl(uri, sameClient, thrower, whereValues);
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/ThrowingPublishersInSubscribe.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Tests what happens when request publishers
+ * throw unexpected exceptions.
+ * @library /lib/testlibrary http2/server
+ * @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters
+ * ReferenceTracker AbstractThrowingPublishers ThrowingPublishersInSubscribe
+ * @modules java.base/sun.net.www.http
+ * java.net.http/jdk.internal.net.http.common
+ * java.net.http/jdk.internal.net.http.frame
+ * java.net.http/jdk.internal.net.http.hpack
+ * @run testng/othervm -Djdk.internal.httpclient.debug=true
+ * -Djdk.httpclient.enableAllMethodRetry=true
+ * ThrowingPublishersInSubscribe
+ */
+
+import org.testng.annotations.Test;
+
+import java.util.Set;
+
+public class ThrowingPublishersInSubscribe extends AbstractThrowingPublishers {
+
+ @Test(dataProvider = "subscribeProvider")
+ public void testThrowingAsString(String uri, boolean sameClient,
+ Thrower thrower, Set<Where> whereValues)
+ throws Exception
+ {
+ super.testThrowingAsStringImpl(uri, sameClient, thrower, whereValues);
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/ThrowingPublishersSanity.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Tests what happens when request publishers
+ * throw unexpected exceptions.
+ * @library /lib/testlibrary http2/server
+ * @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters
+ * ReferenceTracker AbstractThrowingPublishers ThrowingPublishersSanity
+ * @modules java.base/sun.net.www.http
+ * java.net.http/jdk.internal.net.http.common
+ * java.net.http/jdk.internal.net.http.frame
+ * java.net.http/jdk.internal.net.http.hpack
+ * @run testng/othervm -Djdk.internal.httpclient.debug=true
+ * -Djdk.httpclient.enableAllMethodRetry=true
+ * ThrowingPublishersSanity
+ */
+
+import org.testng.annotations.Test;
+
+public class ThrowingPublishersSanity extends AbstractThrowingPublishers {
+
+ @Test(dataProvider = "sanity")
+ public void testSanity(String uri, boolean sameClient)
+ throws Exception {
+ super.testSanityImpl(uri,sameClient);
+ }
+}
--- a/test/jdk/java/net/httpclient/ThrowingPushPromises.java Wed Jun 20 17:15:16 2018 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,757 +0,0 @@
-/*
- * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
-/*
- * @test
- * @summary Tests what happens when push promise handlers and their
- * response body handlers and subscribers throw unexpected exceptions.
- * @library /lib/testlibrary http2/server
- * @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters
- * ReferenceTracker ThrowingPushPromises
- * @modules java.base/sun.net.www.http
- * java.net.http/jdk.internal.net.http.common
- * java.net.http/jdk.internal.net.http.frame
- * java.net.http/jdk.internal.net.http.hpack
- * @run testng/othervm -Djdk.internal.httpclient.debug=true ThrowingPushPromises
- */
-
-import jdk.internal.net.http.common.HttpHeadersImpl;
-import jdk.testlibrary.SimpleSSLContext;
-import org.testng.annotations.AfterTest;
-import org.testng.annotations.AfterClass;
-import org.testng.annotations.BeforeTest;
-import org.testng.annotations.DataProvider;
-import org.testng.annotations.Test;
-
-import javax.net.ssl.SSLContext;
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.io.UncheckedIOException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.http.HttpClient;
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-import java.net.http.HttpResponse.BodyHandler;
-import java.net.http.HttpResponse.BodyHandlers;
-import java.net.http.HttpResponse.BodySubscriber;
-import java.net.http.HttpResponse.PushPromiseHandler;
-import java.nio.ByteBuffer;
-import java.nio.charset.StandardCharsets;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CompletionException;
-import java.util.concurrent.CompletionStage;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Flow;
-import java.util.concurrent.atomic.AtomicLong;
-import java.util.function.Consumer;
-import java.util.function.Function;
-import java.util.function.Predicate;
-import java.util.function.Supplier;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import static java.lang.System.out;
-import static java.lang.System.err;
-import static java.lang.String.format;
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertTrue;
-
-public class ThrowingPushPromises implements HttpServerAdapters {
-
- SSLContext sslContext;
- HttpTestServer http2TestServer; // HTTP/2 ( h2c )
- HttpTestServer https2TestServer; // HTTP/2 ( h2 )
- String http2URI_fixed;
- String http2URI_chunk;
- String https2URI_fixed;
- String https2URI_chunk;
-
- static final int ITERATION_COUNT = 1;
- // a shared executor helps reduce the amount of threads created by the test
- static final Executor executor = new TestExecutor(Executors.newCachedThreadPool());
- static final ConcurrentMap<String, Throwable> FAILURES = new ConcurrentHashMap<>();
- static volatile boolean tasksFailed;
- static final AtomicLong serverCount = new AtomicLong();
- static final AtomicLong clientCount = new AtomicLong();
- static final long start = System.nanoTime();
- public static String now() {
- long now = System.nanoTime() - start;
- long secs = now / 1000_000_000;
- long mill = (now % 1000_000_000) / 1000_000;
- long nan = now % 1000_000;
- return String.format("[%d s, %d ms, %d ns] ", secs, mill, nan);
- }
-
- final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE;
- private volatile HttpClient sharedClient;
-
- static class TestExecutor implements Executor {
- final AtomicLong tasks = new AtomicLong();
- Executor executor;
- TestExecutor(Executor executor) {
- this.executor = executor;
- }
-
- @Override
- public void execute(Runnable command) {
- long id = tasks.incrementAndGet();
- executor.execute(() -> {
- try {
- command.run();
- } catch (Throwable t) {
- tasksFailed = true;
- out.printf(now() + "Task %s failed: %s%n", id, t);
- err.printf(now() + "Task %s failed: %s%n", id, t);
- FAILURES.putIfAbsent("Task " + id, t);
- throw t;
- }
- });
- }
- }
-
- @AfterClass
- static final void printFailedTests() {
- out.println("\n=========================");
- try {
- out.printf("%n%sCreated %d servers and %d clients%n",
- now(), serverCount.get(), clientCount.get());
- if (FAILURES.isEmpty()) return;
- out.println("Failed tests: ");
- FAILURES.entrySet().forEach((e) -> {
- out.printf("\t%s: %s%n", e.getKey(), e.getValue());
- e.getValue().printStackTrace(out);
- e.getValue().printStackTrace();
- });
- if (tasksFailed) {
- out.println("WARNING: Some tasks failed");
- }
- } finally {
- out.println("\n=========================\n");
- }
- }
-
- private String[] uris() {
- return new String[] {
- http2URI_fixed,
- http2URI_chunk,
- https2URI_fixed,
- https2URI_chunk,
- };
- }
-
- @DataProvider(name = "noThrows")
- public Object[][] noThrows() {
- String[] uris = uris();
- Object[][] result = new Object[uris.length * 2][];
-
- int i = 0;
- for (boolean sameClient : List.of(false, true)) {
- for (String uri: uris()) {
- result[i++] = new Object[] {uri, sameClient};
- }
- }
- assert i == uris.length * 2;
- return result;
- }
-
- @DataProvider(name = "variants")
- public Object[][] variants() {
- String[] uris = uris();
- Object[][] result = new Object[uris.length * 2 * 2][];
- int i = 0;
- for (Thrower thrower : List.of(
- new UncheckedIOExceptionThrower(),
- new UncheckedCustomExceptionThrower())) {
- for (boolean sameClient : List.of(false, true)) {
- for (String uri : uris()) {
- result[i++] = new Object[]{uri, sameClient, thrower};
- }
- }
- }
- assert i == uris.length * 2 * 2;
- return result;
- }
-
- private HttpClient makeNewClient() {
- clientCount.incrementAndGet();
- return TRACKER.track(HttpClient.newBuilder()
- .proxy(HttpClient.Builder.NO_PROXY)
- .executor(executor)
- .sslContext(sslContext)
- .build());
- }
-
- HttpClient newHttpClient(boolean share) {
- if (!share) return makeNewClient();
- HttpClient shared = sharedClient;
- if (shared != null) return shared;
- synchronized (this) {
- shared = sharedClient;
- if (shared == null) {
- shared = sharedClient = makeNewClient();
- }
- return shared;
- }
- }
-
- @Test(dataProvider = "noThrows")
- public void testNoThrows(String uri, boolean sameClient)
- throws Exception {
- HttpClient client = null;
- out.printf("%ntestNoThrows(%s, %b)%n", uri, sameClient);
- for (int i=0; i< ITERATION_COUNT; i++) {
- if (!sameClient || client == null)
- client = newHttpClient(sameClient);
-
- HttpRequest req = HttpRequest.newBuilder(URI.create(uri))
- .build();
- BodyHandler<Stream<String>> handler =
- new ThrowingBodyHandler((w) -> {},
- BodyHandlers.ofLines());
- Map<HttpRequest, CompletableFuture<HttpResponse<Stream<String>>>> pushPromises =
- new ConcurrentHashMap<>();
- PushPromiseHandler<Stream<String>> pushHandler = new PushPromiseHandler<>() {
- @Override
- public void applyPushPromise(HttpRequest initiatingRequest,
- HttpRequest pushPromiseRequest,
- Function<BodyHandler<Stream<String>>,
- CompletableFuture<HttpResponse<Stream<String>>>>
- acceptor) {
- pushPromises.putIfAbsent(pushPromiseRequest, acceptor.apply(handler));
- }
- };
- HttpResponse<Stream<String>> response =
- client.sendAsync(req, BodyHandlers.ofLines(), pushHandler).get();
- String body = response.body().collect(Collectors.joining("|"));
- assertEquals(URI.create(body).getPath(), URI.create(uri).getPath());
- for (HttpRequest promised : pushPromises.keySet()) {
- out.printf("%s Received promise: %s%n\tresponse: %s%n",
- now(), promised, pushPromises.get(promised).get());
- String promisedBody = pushPromises.get(promised).get().body()
- .collect(Collectors.joining("|"));
- assertEquals(promisedBody, promised.uri().toASCIIString());
- }
- assertEquals(3, pushPromises.size());
- }
- }
-
- @Test(dataProvider = "variants")
- public void testThrowingAsString(String uri,
- boolean sameClient,
- Thrower thrower)
- throws Exception
- {
- String test = format("testThrowingAsString(%s, %b, %s)",
- uri, sameClient, thrower);
- testThrowing(test, uri, sameClient, BodyHandlers::ofString,
- this::checkAsString, thrower);
- }
-
- @Test(dataProvider = "variants")
- public void testThrowingAsLines(String uri,
- boolean sameClient,
- Thrower thrower)
- throws Exception
- {
- String test = format("testThrowingAsLines(%s, %b, %s)",
- uri, sameClient, thrower);
- testThrowing(test, uri, sameClient, BodyHandlers::ofLines,
- this::checkAsLines, thrower);
- }
-
- @Test(dataProvider = "variants")
- public void testThrowingAsInputStream(String uri,
- boolean sameClient,
- Thrower thrower)
- throws Exception
- {
- String test = format("testThrowingAsInputStream(%s, %b, %s)",
- uri, sameClient, thrower);
- testThrowing(test, uri, sameClient, BodyHandlers::ofInputStream,
- this::checkAsInputStream, thrower);
- }
-
- private <T,U> void testThrowing(String name, String uri, boolean sameClient,
- Supplier<BodyHandler<T>> handlers,
- Finisher finisher, Thrower thrower)
- throws Exception
- {
- out.printf("%n%s%s%n", now(), name);
- try {
- testThrowing(uri, sameClient, handlers, finisher, thrower);
- } catch (Error | Exception x) {
- FAILURES.putIfAbsent(name, x);
- throw x;
- }
- }
-
- private <T,U> void testThrowing(String uri, boolean sameClient,
- Supplier<BodyHandler<T>> handlers,
- Finisher finisher, Thrower thrower)
- throws Exception
- {
- HttpClient client = null;
- for (Where where : Where.values()) {
- if (where == Where.ON_ERROR) continue;
- if (!sameClient || client == null)
- client = newHttpClient(sameClient);
-
- HttpRequest req = HttpRequest.
- newBuilder(URI.create(uri))
- .build();
- ConcurrentMap<HttpRequest, CompletableFuture<HttpResponse<T>>> promiseMap =
- new ConcurrentHashMap<>();
- Supplier<BodyHandler<T>> throwing = () ->
- new ThrowingBodyHandler(where.select(thrower), handlers.get());
- PushPromiseHandler<T> pushHandler = new ThrowingPromiseHandler<>(
- where.select(thrower),
- PushPromiseHandler.of((r) -> throwing.get(), promiseMap));
- out.println("try throwing in " + where);
- HttpResponse<T> response = null;
- try {
- response = client.sendAsync(req, handlers.get(), pushHandler).join();
- } catch (Error | Exception x) {
- throw x;
- }
- if (response != null) {
- finisher.finish(where, req.uri(), response, thrower, promiseMap);
- }
- }
- }
-
- enum Where {
- BODY_HANDLER, ON_SUBSCRIBE, ON_NEXT, ON_COMPLETE, ON_ERROR, GET_BODY, BODY_CF,
- BEFORE_ACCEPTING, AFTER_ACCEPTING;
- public Consumer<Where> select(Consumer<Where> consumer) {
- return new Consumer<Where>() {
- @Override
- public void accept(Where where) {
- if (Where.this == where) {
- consumer.accept(where);
- }
- }
- };
- }
- }
-
- interface Thrower extends Consumer<Where>, Predicate<Throwable> {
-
- }
-
- interface Finisher<T,U> {
- U finish(Where w, URI requestURI, HttpResponse<T> resp, Thrower thrower,
- Map<HttpRequest, CompletableFuture<HttpResponse<T>>> promises);
- }
-
- final <T,U> U shouldHaveThrown(Where w, HttpResponse<T> resp, Thrower thrower) {
- String msg = "Expected exception not thrown in " + w
- + "\n\tReceived: " + resp
- + "\n\tWith body: " + resp.body();
- System.out.println(msg);
- throw new RuntimeException(msg);
- }
-
- final List<String> checkAsString(Where w, URI reqURI,
- HttpResponse<String> resp,
- Thrower thrower,
- Map<HttpRequest, CompletableFuture<HttpResponse<String>>> promises) {
- Function<HttpResponse<String>, List<String>> extractor =
- (r) -> List.of(r.body());
- return check(w, reqURI, resp, thrower, promises, extractor);
- }
-
- final List<String> checkAsLines(Where w, URI reqURI,
- HttpResponse<Stream<String>> resp,
- Thrower thrower,
- Map<HttpRequest, CompletableFuture<HttpResponse<Stream<String>>>> promises) {
- Function<HttpResponse<Stream<String>>, List<String>> extractor =
- (r) -> r.body().collect(Collectors.toList());
- return check(w, reqURI, resp, thrower, promises, extractor);
- }
-
- final List<String> checkAsInputStream(Where w, URI reqURI,
- HttpResponse<InputStream> resp,
- Thrower thrower,
- Map<HttpRequest, CompletableFuture<HttpResponse<InputStream>>> promises)
- {
- Function<HttpResponse<InputStream>, List<String>> extractor = (r) -> {
- List<String> result;
- try (InputStream is = r.body()) {
- result = new BufferedReader(new InputStreamReader(is))
- .lines().collect(Collectors.toList());
- } catch (Throwable t) {
- throw new CompletionException(t);
- }
- return result;
- };
- return check(w, reqURI, resp, thrower, promises, extractor);
- }
-
- private final <T> List<String> check(Where w, URI reqURI,
- HttpResponse<T> resp,
- Thrower thrower,
- Map<HttpRequest, CompletableFuture<HttpResponse<T>>> promises,
- Function<HttpResponse<T>, List<String>> extractor)
- {
- List<String> result = extractor.apply(resp);
- for (HttpRequest req : promises.keySet()) {
- switch (w) {
- case BEFORE_ACCEPTING:
- throw new RuntimeException("No push promise should have been received" +
- " for " + reqURI + " in " + w + ": got " + promises.keySet());
- default:
- break;
- }
- HttpResponse<T> presp;
- try {
- presp = promises.get(req).join();
- } catch (Error | Exception x) {
- Throwable cause = findCause(x, thrower);
- if (cause != null) {
- out.println(now() + "Got expected exception in "
- + w + ": " + cause);
- continue;
- }
- throw x;
- }
- switch (w) {
- case BEFORE_ACCEPTING:
- case AFTER_ACCEPTING:
- case BODY_HANDLER:
- case GET_BODY:
- case BODY_CF:
- return shouldHaveThrown(w, presp, thrower);
- default:
- break;
- }
- List<String> presult = null;
- try {
- presult = extractor.apply(presp);
- } catch (Error | Exception x) {
- Throwable cause = findCause(x, thrower);
- if (cause != null) {
- out.println(now() + "Got expected exception for "
- + req + " in " + w + ": " + cause);
- continue;
- }
- throw x;
- }
- throw new RuntimeException("Expected exception not thrown for "
- + req + " in " + w);
- }
- final int expectedCount;
- switch (w) {
- case BEFORE_ACCEPTING:
- expectedCount = 0;
- break;
- default:
- expectedCount = 3;
- }
- assertEquals(promises.size(), expectedCount,
- "bad promise count for " + reqURI + " with " + w);
- assertEquals(result, List.of(reqURI.toASCIIString()));
- return result;
- }
-
- private static Throwable findCause(Throwable x,
- Predicate<Throwable> filter) {
- while (x != null && !filter.test(x)) x = x.getCause();
- return x;
- }
-
- static final class UncheckedCustomExceptionThrower implements Thrower {
- @Override
- public void accept(Where where) {
- out.println(now() + "Throwing in " + where);
- throw new UncheckedCustomException(where.name());
- }
-
- @Override
- public boolean test(Throwable throwable) {
- return UncheckedCustomException.class.isInstance(throwable);
- }
-
- @Override
- public String toString() {
- return "UncheckedCustomExceptionThrower";
- }
- }
-
- static final class UncheckedIOExceptionThrower implements Thrower {
- @Override
- public void accept(Where where) {
- out.println(now() + "Throwing in " + where);
- throw new UncheckedIOException(new CustomIOException(where.name()));
- }
-
- @Override
- public boolean test(Throwable throwable) {
- return UncheckedIOException.class.isInstance(throwable)
- && CustomIOException.class.isInstance(throwable.getCause());
- }
-
- @Override
- public String toString() {
- return "UncheckedIOExceptionThrower";
- }
- }
-
- static final class UncheckedCustomException extends RuntimeException {
- UncheckedCustomException(String message) {
- super(message);
- }
- UncheckedCustomException(String message, Throwable cause) {
- super(message, cause);
- }
- }
-
- static final class CustomIOException extends IOException {
- CustomIOException(String message) {
- super(message);
- }
- CustomIOException(String message, Throwable cause) {
- super(message, cause);
- }
- }
-
- static final class ThrowingPromiseHandler<T> implements PushPromiseHandler<T> {
- final Consumer<Where> throwing;
- final PushPromiseHandler<T> pushHandler;
- ThrowingPromiseHandler(Consumer<Where> throwing, PushPromiseHandler<T> pushHandler) {
- this.throwing = throwing;
- this.pushHandler = pushHandler;
- }
-
- @Override
- public void applyPushPromise(HttpRequest initiatingRequest,
- HttpRequest pushPromiseRequest,
- Function<BodyHandler<T>,
- CompletableFuture<HttpResponse<T>>> acceptor) {
- throwing.accept(Where.BEFORE_ACCEPTING);
- pushHandler.applyPushPromise(initiatingRequest, pushPromiseRequest, acceptor);
- throwing.accept(Where.AFTER_ACCEPTING);
- }
- }
-
- static final class ThrowingBodyHandler<T> implements BodyHandler<T> {
- final Consumer<Where> throwing;
- final BodyHandler<T> bodyHandler;
- ThrowingBodyHandler(Consumer<Where> throwing, BodyHandler<T> bodyHandler) {
- this.throwing = throwing;
- this.bodyHandler = bodyHandler;
- }
- @Override
- public BodySubscriber<T> apply(HttpResponse.ResponseInfo rinfo) {
- throwing.accept(Where.BODY_HANDLER);
- BodySubscriber<T> subscriber = bodyHandler.apply(rinfo);
- return new ThrowingBodySubscriber(throwing, subscriber);
- }
- }
-
- static final class ThrowingBodySubscriber<T> implements BodySubscriber<T> {
- private final BodySubscriber<T> subscriber;
- volatile boolean onSubscribeCalled;
- final Consumer<Where> throwing;
- ThrowingBodySubscriber(Consumer<Where> throwing, BodySubscriber<T> subscriber) {
- this.throwing = throwing;
- this.subscriber = subscriber;
- }
-
- @Override
- public void onSubscribe(Flow.Subscription subscription) {
- //out.println("onSubscribe ");
- onSubscribeCalled = true;
- throwing.accept(Where.ON_SUBSCRIBE);
- subscriber.onSubscribe(subscription);
- }
-
- @Override
- public void onNext(List<ByteBuffer> item) {
- // out.println("onNext " + item);
- assertTrue(onSubscribeCalled);
- throwing.accept(Where.ON_NEXT);
- subscriber.onNext(item);
- }
-
- @Override
- public void onError(Throwable throwable) {
- //out.println("onError");
- assertTrue(onSubscribeCalled);
- throwing.accept(Where.ON_ERROR);
- subscriber.onError(throwable);
- }
-
- @Override
- public void onComplete() {
- //out.println("onComplete");
- assertTrue(onSubscribeCalled, "onComplete called before onSubscribe");
- throwing.accept(Where.ON_COMPLETE);
- subscriber.onComplete();
- }
-
- @Override
- public CompletionStage<T> getBody() {
- throwing.accept(Where.GET_BODY);
- try {
- throwing.accept(Where.BODY_CF);
- } catch (Throwable t) {
- return CompletableFuture.failedFuture(t);
- }
- return subscriber.getBody();
- }
- }
-
-
- @BeforeTest
- public void setup() throws Exception {
- sslContext = new SimpleSSLContext().get();
- if (sslContext == null)
- throw new AssertionError("Unexpected null sslContext");
-
- // HTTP/2
- HttpTestHandler h2_fixedLengthHandler = new HTTP_FixedLengthHandler();
- HttpTestHandler h2_chunkedHandler = new HTTP_ChunkedHandler();
-
- http2TestServer = HttpTestServer.of(new Http2TestServer("localhost", false, 0));
- http2TestServer.addHandler(h2_fixedLengthHandler, "/http2/fixed");
- http2TestServer.addHandler(h2_chunkedHandler, "/http2/chunk");
- http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed/x";
- http2URI_chunk = "http://" + http2TestServer.serverAuthority() + "/http2/chunk/x";
-
- https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, 0));
- https2TestServer.addHandler(h2_fixedLengthHandler, "/https2/fixed");
- https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk");
- https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed/x";
- https2URI_chunk = "https://" + https2TestServer.serverAuthority() + "/https2/chunk/x";
-
- serverCount.addAndGet(2);
- http2TestServer.start();
- https2TestServer.start();
- }
-
- @AfterTest
- public void teardown() throws Exception {
- String sharedClientName =
- sharedClient == null ? null : sharedClient.toString();
- sharedClient = null;
- Thread.sleep(100);
- AssertionError fail = TRACKER.check(500);
- try {
- http2TestServer.stop();
- https2TestServer.stop();
- } finally {
- if (fail != null) {
- if (sharedClientName != null) {
- System.err.println("Shared client name is: " + sharedClientName);
- }
- throw fail;
- }
- }
- }
-
- private static void pushPromiseFor(HttpTestExchange t, URI requestURI, String pushPath, boolean fixed)
- throws IOException
- {
- try {
- URI promise = new URI(requestURI.getScheme(),
- requestURI.getAuthority(),
- pushPath, null, null);
- byte[] promiseBytes = promise.toASCIIString().getBytes(UTF_8);
- out.printf("TestServer: %s Pushing promise: %s%n", now(), promise);
- err.printf("TestServer: %s Pushing promise: %s%n", now(), promise);
- HttpTestHeaders headers = HttpTestHeaders.of(new HttpHeadersImpl());
- if (fixed) {
- headers.addHeader("Content-length", String.valueOf(promiseBytes.length));
- }
- t.serverPush(promise, headers, promiseBytes);
- } catch (URISyntaxException x) {
- throw new IOException(x.getMessage(), x);
- }
- }
-
- static class HTTP_FixedLengthHandler implements HttpTestHandler {
- @Override
- public void handle(HttpTestExchange t) throws IOException {
- out.println("HTTP_FixedLengthHandler received request to " + t.getRequestURI());
- try (InputStream is = t.getRequestBody()) {
- is.readAllBytes();
- }
- URI requestURI = t.getRequestURI();
- for (int i = 1; i<2; i++) {
- String path = requestURI.getPath() + "/before/promise-" + i;
- pushPromiseFor(t, requestURI, path, true);
- }
- byte[] resp = t.getRequestURI().toString().getBytes(StandardCharsets.UTF_8);
- t.sendResponseHeaders(200, resp.length); //fixed content length
- try (OutputStream os = t.getResponseBody()) {
- int bytes = resp.length/3;
- for (int i = 0; i<2; i++) {
- String path = requestURI.getPath() + "/after/promise-" + (i + 2);
- os.write(resp, i * bytes, bytes);
- os.flush();
- pushPromiseFor(t, requestURI, path, true);
- }
- os.write(resp, 2*bytes, resp.length - 2*bytes);
- }
- }
-
- }
-
- static class HTTP_ChunkedHandler implements HttpTestHandler {
- @Override
- public void handle(HttpTestExchange t) throws IOException {
- out.println("HTTP_ChunkedHandler received request to " + t.getRequestURI());
- byte[] resp = t.getRequestURI().toString().getBytes(StandardCharsets.UTF_8);
- try (InputStream is = t.getRequestBody()) {
- is.readAllBytes();
- }
- URI requestURI = t.getRequestURI();
- for (int i = 1; i<2; i++) {
- String path = requestURI.getPath() + "/before/promise-" + i;
- pushPromiseFor(t, requestURI, path, false);
- }
- t.sendResponseHeaders(200, -1); // chunked/variable
- try (OutputStream os = t.getResponseBody()) {
- int bytes = resp.length/3;
- for (int i = 0; i<2; i++) {
- String path = requestURI.getPath() + "/after/promise-" + (i + 2);
- os.write(resp, i * bytes, bytes);
- os.flush();
- pushPromiseFor(t, requestURI, path, false);
- }
- os.write(resp, 2*bytes, resp.length - 2*bytes);
- }
- }
- }
-
-}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/ThrowingPushPromisesAsInputStreamCustom.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Tests what happens when push promise handlers and their
+ * response body handlers and subscribers throw unexpected exceptions.
+ * @library /lib/testlibrary http2/server
+ * @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters
+ * ReferenceTracker AbstractThrowingPushPromises ThrowingPushPromisesAsInputStreamCustom
+ * @modules java.base/sun.net.www.http
+ * java.net.http/jdk.internal.net.http.common
+ * java.net.http/jdk.internal.net.http.frame
+ * java.net.http/jdk.internal.net.http.hpack
+ * @run testng/othervm -Djdk.internal.httpclient.debug=true ThrowingPushPromisesAsInputStreamCustom
+ */
+
+import org.testng.annotations.Test;
+
+public class ThrowingPushPromisesAsInputStreamCustom extends AbstractThrowingPushPromises {
+
+ @Test(dataProvider = "customVariants")
+ public void testThrowingAsInputStream(String uri, boolean sameClient, Thrower thrower)
+ throws Exception {
+ super.testThrowingAsInputStreamImpl(uri, sameClient, thrower);
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/ThrowingPushPromisesAsInputStreamIO.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Tests what happens when push promise handlers and their
+ * response body handlers and subscribers throw unexpected exceptions.
+ * @library /lib/testlibrary http2/server
+ * @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters
+ * ReferenceTracker AbstractThrowingPushPromises ThrowingPushPromisesAsInputStreamIO
+ * @modules java.base/sun.net.www.http
+ * java.net.http/jdk.internal.net.http.common
+ * java.net.http/jdk.internal.net.http.frame
+ * java.net.http/jdk.internal.net.http.hpack
+ * @run testng/othervm -Djdk.internal.httpclient.debug=true ThrowingPushPromisesAsInputStreamIO
+ */
+
+import org.testng.annotations.Test;
+
+public class ThrowingPushPromisesAsInputStreamIO extends AbstractThrowingPushPromises {
+
+ @Test(dataProvider = "ioVariants")
+ public void testThrowingAsInputStream(String uri, boolean sameClient, Thrower thrower)
+ throws Exception {
+ super.testThrowingAsInputStreamImpl(uri, sameClient, thrower);
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/ThrowingPushPromisesAsLinesCustom.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Tests what happens when push promise handlers and their
+ * response body handlers and subscribers throw unexpected exceptions.
+ * @library /lib/testlibrary http2/server
+ * @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters
+ * ReferenceTracker AbstractThrowingPushPromises ThrowingPushPromisesAsLinesCustom
+ * @modules java.base/sun.net.www.http
+ * java.net.http/jdk.internal.net.http.common
+ * java.net.http/jdk.internal.net.http.frame
+ * java.net.http/jdk.internal.net.http.hpack
+ * @run testng/othervm -Djdk.internal.httpclient.debug=true ThrowingPushPromisesAsLinesCustom
+ */
+
+import org.testng.annotations.Test;
+
+public class ThrowingPushPromisesAsLinesCustom extends AbstractThrowingPushPromises {
+
+ @Test(dataProvider = "customVariants")
+ public void testThrowingAsLines(String uri, boolean sameClient, Thrower thrower)
+ throws Exception {
+ super.testThrowingAsLinesImpl(uri, sameClient, thrower);
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/ThrowingPushPromisesAsLinesIO.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Tests what happens when push promise handlers and their
+ * response body handlers and subscribers throw unexpected exceptions.
+ * @library /lib/testlibrary http2/server
+ * @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters
+ * ReferenceTracker AbstractThrowingPushPromises ThrowingPushPromisesAsLinesIO
+ * @modules java.base/sun.net.www.http
+ * java.net.http/jdk.internal.net.http.common
+ * java.net.http/jdk.internal.net.http.frame
+ * java.net.http/jdk.internal.net.http.hpack
+ * @run testng/othervm -Djdk.internal.httpclient.debug=true ThrowingPushPromisesAsLinesIO
+ */
+
+import org.testng.annotations.Test;
+
+public class ThrowingPushPromisesAsLinesIO extends AbstractThrowingPushPromises {
+
+ @Test(dataProvider = "ioVariants")
+ public void testThrowingAsLines(String uri, boolean sameClient, Thrower thrower)
+ throws Exception {
+ super.testThrowingAsLinesImpl(uri, sameClient, thrower);
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/ThrowingPushPromisesAsStringCustom.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Tests what happens when push promise handlers and their
+ * response body handlers and subscribers throw unexpected exceptions.
+ * @library /lib/testlibrary http2/server
+ * @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters
+ * ReferenceTracker AbstractThrowingPushPromises ThrowingPushPromisesAsStringCustom
+ * @modules java.base/sun.net.www.http
+ * java.net.http/jdk.internal.net.http.common
+ * java.net.http/jdk.internal.net.http.frame
+ * java.net.http/jdk.internal.net.http.hpack
+ * @run testng/othervm -Djdk.internal.httpclient.debug=true ThrowingPushPromisesAsStringCustom
+ */
+
+import org.testng.annotations.Test;
+
+public class ThrowingPushPromisesAsStringCustom extends AbstractThrowingPushPromises {
+
+ @Test(dataProvider = "customVariants")
+ public void testThrowingAsString(String uri, boolean sameClient, Thrower thrower)
+ throws Exception {
+ super.testThrowingAsStringImpl(uri, sameClient, thrower);
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/ThrowingPushPromisesAsStringIO.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Tests what happens when push promise handlers and their
+ * response body handlers and subscribers throw unexpected exceptions.
+ * @library /lib/testlibrary http2/server
+ * @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters
+ * ReferenceTracker AbstractThrowingPushPromises ThrowingPushPromisesAsStringIO
+ * @modules java.base/sun.net.www.http
+ * java.net.http/jdk.internal.net.http.common
+ * java.net.http/jdk.internal.net.http.frame
+ * java.net.http/jdk.internal.net.http.hpack
+ * @run testng/othervm -Djdk.internal.httpclient.debug=true ThrowingPushPromisesAsStringIO
+ */
+
+import org.testng.annotations.Test;
+
+public class ThrowingPushPromisesAsStringIO extends AbstractThrowingPushPromises {
+
+ @Test(dataProvider = "ioVariants")
+ public void testThrowingAsString(String uri, boolean sameClient, Thrower thrower)
+ throws Exception {
+ super.testThrowingAsStringImpl(uri, sameClient, thrower);
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/ThrowingPushPromisesSanity.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Tests what happens when push promise handlers and their
+ * response body handlers and subscribers throw unexpected exceptions.
+ * @library /lib/testlibrary http2/server
+ * @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters
+ * ReferenceTracker AbstractThrowingPushPromises ThrowingPushPromisesSanity
+ * @modules java.base/sun.net.www.http
+ * java.net.http/jdk.internal.net.http.common
+ * java.net.http/jdk.internal.net.http.frame
+ * java.net.http/jdk.internal.net.http.hpack
+ * @run testng/othervm -Djdk.internal.httpclient.debug=true ThrowingPushPromisesSanity
+ */
+
+import org.testng.annotations.Test;
+
+public class ThrowingPushPromisesSanity extends AbstractThrowingPushPromises {
+
+ @Test(dataProvider = "sanity")
+ public void testSanity(String uri, boolean sameClient)
+ throws Exception {
+ super.testSanityImpl(uri, sameClient);
+ }
+}
--- a/test/jdk/java/net/httpclient/ThrowingSubscribers.java Wed Jun 20 17:15:16 2018 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,734 +0,0 @@
-/*
- * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
-/*
- * @test
- * @summary Tests what happens when response body handlers and subscribers
- * throw unexpected exceptions.
- * @library /lib/testlibrary http2/server
- * @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters
- * ReferenceTracker ThrowingSubscribers
- * @modules java.base/sun.net.www.http
- * java.net.http/jdk.internal.net.http.common
- * java.net.http/jdk.internal.net.http.frame
- * java.net.http/jdk.internal.net.http.hpack
- * @run testng/othervm -Djdk.internal.httpclient.debug=true ThrowingSubscribers
- */
-
-import com.sun.net.httpserver.HttpServer;
-import com.sun.net.httpserver.HttpsConfigurator;
-import com.sun.net.httpserver.HttpsServer;
-import jdk.testlibrary.SimpleSSLContext;
-import org.testng.annotations.AfterTest;
-import org.testng.annotations.AfterClass;
-import org.testng.annotations.BeforeTest;
-import org.testng.annotations.DataProvider;
-import org.testng.annotations.Test;
-
-import javax.net.ssl.SSLContext;
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.io.UncheckedIOException;
-import java.net.InetAddress;
-import java.net.InetSocketAddress;
-import java.net.URI;
-import java.net.http.HttpClient;
-import java.net.http.HttpHeaders;
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-import java.net.http.HttpResponse.BodyHandler;
-import java.net.http.HttpResponse.BodyHandlers;
-import java.net.http.HttpResponse.BodySubscriber;
-import java.nio.ByteBuffer;
-import java.nio.charset.StandardCharsets;
-import java.util.EnumSet;
-import java.util.List;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CompletionStage;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Flow;
-import java.util.concurrent.atomic.AtomicLong;
-import java.util.function.Consumer;
-import java.util.function.Predicate;
-import java.util.function.Supplier;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import static java.lang.System.out;
-import static java.lang.String.format;
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertTrue;
-
-public class ThrowingSubscribers implements HttpServerAdapters {
-
- SSLContext sslContext;
- HttpTestServer httpTestServer; // HTTP/1.1 [ 4 servers ]
- HttpTestServer httpsTestServer; // HTTPS/1.1
- HttpTestServer http2TestServer; // HTTP/2 ( h2c )
- HttpTestServer https2TestServer; // HTTP/2 ( h2 )
- String httpURI_fixed;
- String httpURI_chunk;
- String httpsURI_fixed;
- String httpsURI_chunk;
- String http2URI_fixed;
- String http2URI_chunk;
- String https2URI_fixed;
- String https2URI_chunk;
-
- static final int ITERATION_COUNT = 1;
- // a shared executor helps reduce the amount of threads created by the test
- static final Executor executor = new TestExecutor(Executors.newCachedThreadPool());
- static final ConcurrentMap<String, Throwable> FAILURES = new ConcurrentHashMap<>();
- static volatile boolean tasksFailed;
- static final AtomicLong serverCount = new AtomicLong();
- static final AtomicLong clientCount = new AtomicLong();
- static final long start = System.nanoTime();
- public static String now() {
- long now = System.nanoTime() - start;
- long secs = now / 1000_000_000;
- long mill = (now % 1000_000_000) / 1000_000;
- long nan = now % 1000_000;
- return String.format("[%d s, %d ms, %d ns] ", secs, mill, nan);
- }
-
- final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE;
- private volatile HttpClient sharedClient;
-
- static class TestExecutor implements Executor {
- final AtomicLong tasks = new AtomicLong();
- Executor executor;
- TestExecutor(Executor executor) {
- this.executor = executor;
- }
-
- @Override
- public void execute(Runnable command) {
- long id = tasks.incrementAndGet();
- executor.execute(() -> {
- try {
- command.run();
- } catch (Throwable t) {
- tasksFailed = true;
- System.out.printf(now() + "Task %s failed: %s%n", id, t);
- System.err.printf(now() + "Task %s failed: %s%n", id, t);
- FAILURES.putIfAbsent("Task " + id, t);
- throw t;
- }
- });
- }
- }
-
- @AfterClass
- static final void printFailedTests() {
- out.println("\n=========================");
- try {
- out.printf("%n%sCreated %d servers and %d clients%n",
- now(), serverCount.get(), clientCount.get());
- if (FAILURES.isEmpty()) return;
- out.println("Failed tests: ");
- FAILURES.entrySet().forEach((e) -> {
- out.printf("\t%s: %s%n", e.getKey(), e.getValue());
- e.getValue().printStackTrace(out);
- e.getValue().printStackTrace();
- });
- if (tasksFailed) {
- System.out.println("WARNING: Some tasks failed");
- }
- } finally {
- out.println("\n=========================\n");
- }
- }
-
- private String[] uris() {
- return new String[] {
- httpURI_fixed,
- httpURI_chunk,
- httpsURI_fixed,
- httpsURI_chunk,
- http2URI_fixed,
- http2URI_chunk,
- https2URI_fixed,
- https2URI_chunk,
- };
- }
-
- @DataProvider(name = "noThrows")
- public Object[][] noThrows() {
- String[] uris = uris();
- Object[][] result = new Object[uris.length * 2][];
- int i = 0;
- for (boolean sameClient : List.of(false, true)) {
- for (String uri: uris()) {
- result[i++] = new Object[] {uri, sameClient};
- }
- }
- assert i == uris.length * 2;
- return result;
- }
-
- @DataProvider(name = "variants")
- public Object[][] variants() {
- String[] uris = uris();
- Object[][] result = new Object[uris.length * 2 * 2][];
- int i = 0;
- for (Thrower thrower : List.of(
- new UncheckedIOExceptionThrower(),
- new UncheckedCustomExceptionThrower())) {
- for (boolean sameClient : List.of(false, true)) {
- for (String uri : uris()) {
- result[i++] = new Object[]{uri, sameClient, thrower};
- }
- }
- }
- assert i == uris.length * 2 * 2;
- return result;
- }
-
- private HttpClient makeNewClient() {
- clientCount.incrementAndGet();
- HttpClient client = HttpClient.newBuilder()
- .proxy(HttpClient.Builder.NO_PROXY)
- .executor(executor)
- .sslContext(sslContext)
- .build();
- return TRACKER.track(client);
- }
-
- HttpClient newHttpClient(boolean share) {
- if (!share) return makeNewClient();
- HttpClient shared = sharedClient;
- if (shared != null) return shared;
- synchronized (this) {
- shared = sharedClient;
- if (shared == null) {
- shared = sharedClient = makeNewClient();
- }
- return shared;
- }
- }
-
- enum SubscriberType {
- INLINE, // In line subscribers complete their CF on ON_COMPLETE
- // e.g. BodySubscribers::ofString
- OFFLINE; // Off line subscribers complete their CF immediately
- // but require the client to pull the data after the
- // CF completes (e.g. BodySubscribers::ofInputStream)
- }
-
- static EnumSet<Where> excludes(SubscriberType type) {
- EnumSet<Where> set = EnumSet.noneOf(Where.class);
-
- if (type == SubscriberType.OFFLINE) {
- // Throwing on onSubscribe needs some more work
- // for the case of InputStream, where the body has already
- // completed by the time the subscriber is subscribed.
- // The only way we have at that point to relay the exception
- // is to call onError on the subscriber, but should we if
- // Subscriber::onSubscribed has thrown an exception and
- // not completed normally?
- set.add(Where.ON_SUBSCRIBE);
- }
-
- // Don't know how to make the stack reliably cause onError
- // to be called without closing the connection.
- // And how do we get the exception if onError throws anyway?
- set.add(Where.ON_ERROR);
-
- return set;
- }
-
- @Test(dataProvider = "noThrows")
- public void testNoThrows(String uri, boolean sameClient)
- throws Exception {
- HttpClient client = null;
- out.printf("%ntestNoThrows(%s, %b)%n", uri, sameClient);
- for (int i=0; i< ITERATION_COUNT; i++) {
- if (!sameClient || client == null)
- client = newHttpClient(sameClient);
-
- HttpRequest req = HttpRequest.newBuilder(URI.create(uri))
- .build();
- BodyHandler<String> handler =
- new ThrowingBodyHandler((w) -> {},
- BodyHandlers.ofString());
- HttpResponse<String> response = client.send(req, handler);
- String body = response.body();
- assertEquals(URI.create(body).getPath(), URI.create(uri).getPath());
- }
- }
-
- @Test(dataProvider = "variants")
- public void testThrowingAsString(String uri,
- boolean sameClient,
- Thrower thrower)
- throws Exception
- {
- String test = format("testThrowingAsString(%s, %b, %s)",
- uri, sameClient, thrower);
- testThrowing(test, uri, sameClient, BodyHandlers::ofString,
- this::shouldHaveThrown, thrower,false,
- excludes(SubscriberType.INLINE));
- }
-
- @Test(dataProvider = "variants")
- public void testThrowingAsLines(String uri,
- boolean sameClient,
- Thrower thrower)
- throws Exception
- {
- String test = format("testThrowingAsLines(%s, %b, %s)",
- uri, sameClient, thrower);
- testThrowing(test, uri, sameClient, BodyHandlers::ofLines,
- this::checkAsLines, thrower,false,
- excludes(SubscriberType.OFFLINE));
- }
-
- @Test(dataProvider = "variants")
- public void testThrowingAsInputStream(String uri,
- boolean sameClient,
- Thrower thrower)
- throws Exception
- {
- String test = format("testThrowingAsInputStream(%s, %b, %s)",
- uri, sameClient, thrower);
- testThrowing(test, uri, sameClient, BodyHandlers::ofInputStream,
- this::checkAsInputStream, thrower,false,
- excludes(SubscriberType.OFFLINE));
- }
-
- @Test(dataProvider = "variants")
- public void testThrowingAsStringAsync(String uri,
- boolean sameClient,
- Thrower thrower)
- throws Exception
- {
- String test = format("testThrowingAsStringAsync(%s, %b, %s)",
- uri, sameClient, thrower);
- testThrowing(test, uri, sameClient, BodyHandlers::ofString,
- this::shouldHaveThrown, thrower, true,
- excludes(SubscriberType.INLINE));
- }
-
- @Test(dataProvider = "variants")
- public void testThrowingAsLinesAsync(String uri,
- boolean sameClient,
- Thrower thrower)
- throws Exception
- {
- String test = format("testThrowingAsLinesAsync(%s, %b, %s)",
- uri, sameClient, thrower);
- testThrowing(test, uri, sameClient, BodyHandlers::ofLines,
- this::checkAsLines, thrower,true,
- excludes(SubscriberType.OFFLINE));
- }
-
- @Test(dataProvider = "variants")
- public void testThrowingAsInputStreamAsync(String uri,
- boolean sameClient,
- Thrower thrower)
- throws Exception
- {
- String test = format("testThrowingAsInputStreamAsync(%s, %b, %s)",
- uri, sameClient, thrower);
- testThrowing(test, uri, sameClient, BodyHandlers::ofInputStream,
- this::checkAsInputStream, thrower,true,
- excludes(SubscriberType.OFFLINE));
- }
-
- private <T,U> void testThrowing(String name, String uri, boolean sameClient,
- Supplier<BodyHandler<T>> handlers,
- Finisher finisher, Thrower thrower,
- boolean async, EnumSet<Where> excludes)
- throws Exception
- {
- out.printf("%n%s%s%n", now(), name);
- try {
- testThrowing(uri, sameClient, handlers, finisher, thrower, async, excludes);
- } catch (Error | Exception x) {
- FAILURES.putIfAbsent(name, x);
- throw x;
- }
- }
-
- private <T,U> void testThrowing(String uri, boolean sameClient,
- Supplier<BodyHandler<T>> handlers,
- Finisher finisher, Thrower thrower,
- boolean async,
- EnumSet<Where> excludes)
- throws Exception
- {
- HttpClient client = null;
- for (Where where : EnumSet.complementOf(excludes)) {
-
- if (!sameClient || client == null)
- client = newHttpClient(sameClient);
-
- HttpRequest req = HttpRequest.
- newBuilder(URI.create(uri))
- .build();
- BodyHandler<T> handler =
- new ThrowingBodyHandler(where.select(thrower), handlers.get());
- System.out.println("try throwing in " + where);
- HttpResponse<T> response = null;
- if (async) {
- try {
- response = client.sendAsync(req, handler).join();
- } catch (Error | Exception x) {
- Throwable cause = findCause(x, thrower);
- if (cause == null) throw causeNotFound(where, x);
- System.out.println(now() + "Got expected exception: " + cause);
- }
- } else {
- try {
- response = client.send(req, handler);
- } catch (Error | Exception t) {
- if (thrower.test(t)) {
- System.out.println(now() + "Got expected exception: " + t);
- } else throw causeNotFound(where, t);
- }
- }
- if (response != null) {
- finisher.finish(where, response, thrower);
- }
- }
- }
-
- enum Where {
- BODY_HANDLER, ON_SUBSCRIBE, ON_NEXT, ON_COMPLETE, ON_ERROR, GET_BODY, BODY_CF;
- public Consumer<Where> select(Consumer<Where> consumer) {
- return new Consumer<Where>() {
- @Override
- public void accept(Where where) {
- if (Where.this == where) {
- consumer.accept(where);
- }
- }
- };
- }
- }
-
- static AssertionError causeNotFound(Where w, Throwable t) {
- return new AssertionError("Expected exception not found in " + w, t);
- }
-
- interface Thrower extends Consumer<Where>, Predicate<Throwable> {
-
- }
-
- interface Finisher<T,U> {
- U finish(Where w, HttpResponse<T> resp, Thrower thrower) throws IOException;
- }
-
- final <T,U> U shouldHaveThrown(Where w, HttpResponse<T> resp, Thrower thrower) {
- String msg = "Expected exception not thrown in " + w
- + "\n\tReceived: " + resp
- + "\n\tWith body: " + resp.body();
- System.out.println(msg);
- throw new RuntimeException(msg);
- }
-
- final List<String> checkAsLines(Where w, HttpResponse<Stream<String>> resp, Thrower thrower) {
- switch(w) {
- case BODY_HANDLER: return shouldHaveThrown(w, resp, thrower);
- case GET_BODY: return shouldHaveThrown(w, resp, thrower);
- case BODY_CF: return shouldHaveThrown(w, resp, thrower);
- default: break;
- }
- List<String> result = null;
- try {
- result = resp.body().collect(Collectors.toList());
- } catch (Error | Exception x) {
- Throwable cause = findCause(x, thrower);
- if (cause != null) {
- out.println(now() + "Got expected exception in " + w + ": " + cause);
- return result;
- }
- throw causeNotFound(w, x);
- }
- return shouldHaveThrown(w, resp, thrower);
- }
-
- final List<String> checkAsInputStream(Where w, HttpResponse<InputStream> resp,
- Thrower thrower)
- throws IOException
- {
- switch(w) {
- case BODY_HANDLER: return shouldHaveThrown(w, resp, thrower);
- case GET_BODY: return shouldHaveThrown(w, resp, thrower);
- case BODY_CF: return shouldHaveThrown(w, resp, thrower);
- default: break;
- }
- List<String> result = null;
- try (InputStreamReader r1 = new InputStreamReader(resp.body(), UTF_8);
- BufferedReader r = new BufferedReader(r1)) {
- try {
- result = r.lines().collect(Collectors.toList());
- } catch (Error | Exception x) {
- Throwable cause = findCause(x, thrower);
- if (cause != null) {
- out.println(now() + "Got expected exception in " + w + ": " + cause);
- return result;
- }
- throw causeNotFound(w, x);
- }
- }
- return shouldHaveThrown(w, resp, thrower);
- }
-
- private static Throwable findCause(Throwable x,
- Predicate<Throwable> filter) {
- while (x != null && !filter.test(x)) x = x.getCause();
- return x;
- }
-
- static final class UncheckedCustomExceptionThrower implements Thrower {
- @Override
- public void accept(Where where) {
- out.println(now() + "Throwing in " + where);
- throw new UncheckedCustomException(where.name());
- }
-
- @Override
- public boolean test(Throwable throwable) {
- return UncheckedCustomException.class.isInstance(throwable);
- }
-
- @Override
- public String toString() {
- return "UncheckedCustomExceptionThrower";
- }
- }
-
- static final class UncheckedIOExceptionThrower implements Thrower {
- @Override
- public void accept(Where where) {
- out.println(now() + "Throwing in " + where);
- throw new UncheckedIOException(new CustomIOException(where.name()));
- }
-
- @Override
- public boolean test(Throwable throwable) {
- return UncheckedIOException.class.isInstance(throwable)
- && CustomIOException.class.isInstance(throwable.getCause());
- }
-
- @Override
- public String toString() {
- return "UncheckedIOExceptionThrower";
- }
- }
-
- static final class UncheckedCustomException extends RuntimeException {
- UncheckedCustomException(String message) {
- super(message);
- }
- UncheckedCustomException(String message, Throwable cause) {
- super(message, cause);
- }
- }
-
- static final class CustomIOException extends IOException {
- CustomIOException(String message) {
- super(message);
- }
- CustomIOException(String message, Throwable cause) {
- super(message, cause);
- }
- }
-
- static final class ThrowingBodyHandler<T> implements BodyHandler<T> {
- final Consumer<Where> throwing;
- final BodyHandler<T> bodyHandler;
- ThrowingBodyHandler(Consumer<Where> throwing, BodyHandler<T> bodyHandler) {
- this.throwing = throwing;
- this.bodyHandler = bodyHandler;
- }
- @Override
- public BodySubscriber<T> apply(HttpResponse.ResponseInfo rinfo) {
- throwing.accept(Where.BODY_HANDLER);
- BodySubscriber<T> subscriber = bodyHandler.apply(rinfo);
- return new ThrowingBodySubscriber(throwing, subscriber);
- }
- }
-
- static final class ThrowingBodySubscriber<T> implements BodySubscriber<T> {
- private final BodySubscriber<T> subscriber;
- volatile boolean onSubscribeCalled;
- final Consumer<Where> throwing;
- ThrowingBodySubscriber(Consumer<Where> throwing, BodySubscriber<T> subscriber) {
- this.throwing = throwing;
- this.subscriber = subscriber;
- }
-
- @Override
- public void onSubscribe(Flow.Subscription subscription) {
- //out.println("onSubscribe ");
- onSubscribeCalled = true;
- throwing.accept(Where.ON_SUBSCRIBE);
- subscriber.onSubscribe(subscription);
- }
-
- @Override
- public void onNext(List<ByteBuffer> item) {
- // out.println("onNext " + item);
- assertTrue(onSubscribeCalled);
- throwing.accept(Where.ON_NEXT);
- subscriber.onNext(item);
- }
-
- @Override
- public void onError(Throwable throwable) {
- //out.println("onError");
- assertTrue(onSubscribeCalled);
- throwing.accept(Where.ON_ERROR);
- subscriber.onError(throwable);
- }
-
- @Override
- public void onComplete() {
- //out.println("onComplete");
- assertTrue(onSubscribeCalled, "onComplete called before onSubscribe");
- throwing.accept(Where.ON_COMPLETE);
- subscriber.onComplete();
- }
-
- @Override
- public CompletionStage<T> getBody() {
- throwing.accept(Where.GET_BODY);
- try {
- throwing.accept(Where.BODY_CF);
- } catch (Throwable t) {
- return CompletableFuture.failedFuture(t);
- }
- return subscriber.getBody();
- }
- }
-
-
- @BeforeTest
- public void setup() throws Exception {
- sslContext = new SimpleSSLContext().get();
- if (sslContext == null)
- throw new AssertionError("Unexpected null sslContext");
-
- // HTTP/1.1
- HttpTestHandler h1_fixedLengthHandler = new HTTP_FixedLengthHandler();
- HttpTestHandler h1_chunkHandler = new HTTP_ChunkedHandler();
- InetSocketAddress sa = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0);
- httpTestServer = HttpTestServer.of(HttpServer.create(sa, 0));
- httpTestServer.addHandler(h1_fixedLengthHandler, "/http1/fixed");
- httpTestServer.addHandler(h1_chunkHandler, "/http1/chunk");
- httpURI_fixed = "http://" + httpTestServer.serverAuthority() + "/http1/fixed/x";
- httpURI_chunk = "http://" + httpTestServer.serverAuthority() + "/http1/chunk/x";
-
- HttpsServer httpsServer = HttpsServer.create(sa, 0);
- httpsServer.setHttpsConfigurator(new HttpsConfigurator(sslContext));
- httpsTestServer = HttpTestServer.of(httpsServer);
- httpsTestServer.addHandler(h1_fixedLengthHandler, "/https1/fixed");
- httpsTestServer.addHandler(h1_chunkHandler, "/https1/chunk");
- httpsURI_fixed = "https://" + httpsTestServer.serverAuthority() + "/https1/fixed/x";
- httpsURI_chunk = "https://" + httpsTestServer.serverAuthority() + "/https1/chunk/x";
-
- // HTTP/2
- HttpTestHandler h2_fixedLengthHandler = new HTTP_FixedLengthHandler();
- HttpTestHandler h2_chunkedHandler = new HTTP_ChunkedHandler();
-
- http2TestServer = HttpTestServer.of(new Http2TestServer("localhost", false, 0));
- http2TestServer.addHandler(h2_fixedLengthHandler, "/http2/fixed");
- http2TestServer.addHandler(h2_chunkedHandler, "/http2/chunk");
- http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed/x";
- http2URI_chunk = "http://" + http2TestServer.serverAuthority() + "/http2/chunk/x";
-
- https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, 0));
- https2TestServer.addHandler(h2_fixedLengthHandler, "/https2/fixed");
- https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk");
- https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed/x";
- https2URI_chunk = "https://" + https2TestServer.serverAuthority() + "/https2/chunk/x";
-
- serverCount.addAndGet(4);
- httpTestServer.start();
- httpsTestServer.start();
- http2TestServer.start();
- https2TestServer.start();
- }
-
- @AfterTest
- public void teardown() throws Exception {
- String sharedClientName =
- sharedClient == null ? null : sharedClient.toString();
- sharedClient = null;
- Thread.sleep(100);
- AssertionError fail = TRACKER.check(500);
- try {
- httpTestServer.stop();
- httpsTestServer.stop();
- http2TestServer.stop();
- https2TestServer.stop();
- } finally {
- if (fail != null) {
- if (sharedClientName != null) {
- System.err.println("Shared client name is: " + sharedClientName);
- }
- throw fail;
- }
- }
- }
-
- static class HTTP_FixedLengthHandler implements HttpTestHandler {
- @Override
- public void handle(HttpTestExchange t) throws IOException {
- out.println("HTTP_FixedLengthHandler received request to " + t.getRequestURI());
- try (InputStream is = t.getRequestBody()) {
- is.readAllBytes();
- }
- byte[] resp = t.getRequestURI().toString().getBytes(StandardCharsets.UTF_8);
- t.sendResponseHeaders(200, resp.length); //fixed content length
- try (OutputStream os = t.getResponseBody()) {
- os.write(resp);
- }
- }
- }
-
- static class HTTP_ChunkedHandler implements HttpTestHandler {
- @Override
- public void handle(HttpTestExchange t) throws IOException {
- out.println("HTTP_ChunkedHandler received request to " + t.getRequestURI());
- byte[] resp = t.getRequestURI().toString().getBytes(StandardCharsets.UTF_8);
- try (InputStream is = t.getRequestBody()) {
- is.readAllBytes();
- }
- t.sendResponseHeaders(200, -1); // chunked/variable
- try (OutputStream os = t.getResponseBody()) {
- os.write(resp);
- }
- }
- }
-
-}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/ThrowingSubscribersAsInputStream.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Tests what happens when response body handlers and subscribers
+ * throw unexpected exceptions.
+ * @library /lib/testlibrary http2/server
+ * @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters
+ * ReferenceTracker ThrowingSubscribersAsInputStream AbstractThrowingSubscribers
+ * @modules java.base/sun.net.www.http
+ * java.net.http/jdk.internal.net.http.common
+ * java.net.http/jdk.internal.net.http.frame
+ * java.net.http/jdk.internal.net.http.hpack
+ * @run testng/othervm -Djdk.internal.httpclient.debug=true ThrowingSubscribersAsInputStream
+ */
+
+import org.testng.annotations.Test;
+
+public class ThrowingSubscribersAsInputStream extends AbstractThrowingSubscribers {
+
+ @Test(dataProvider = "variants")
+ public void testThrowingAsInputStream(String uri, boolean sameClient, Thrower thrower)
+ throws Exception {
+ super.testThrowingAsInputStreamImpl(uri, sameClient, thrower);
+ }
+
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/ThrowingSubscribersAsInputStreamAsync.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Tests what happens when response body handlers and subscribers
+ * throw unexpected exceptions.
+ * @library /lib/testlibrary http2/server
+ * @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters
+ * ReferenceTracker ThrowingSubscribersAsInputStreamAsync AbstractThrowingSubscribers
+ * @modules java.base/sun.net.www.http
+ * java.net.http/jdk.internal.net.http.common
+ * java.net.http/jdk.internal.net.http.frame
+ * java.net.http/jdk.internal.net.http.hpack
+ * @run testng/othervm -Djdk.internal.httpclient.debug=true ThrowingSubscribersAsInputStreamAsync
+ */
+
+import org.testng.annotations.Test;
+
+public class ThrowingSubscribersAsInputStreamAsync extends AbstractThrowingSubscribers {
+
+ @Test(dataProvider = "variants")
+ public void testThrowingAsInputStreamAsync(String uri, boolean sameClient, Thrower thrower)
+ throws Exception {
+ super.testThrowingAsInputStreamAsyncImpl(uri, sameClient, thrower);
+ }
+
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/ThrowingSubscribersAsLines.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Tests what happens when response body handlers and subscribers
+ * throw unexpected exceptions.
+ * @library /lib/testlibrary http2/server
+ * @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters
+ * ReferenceTracker ThrowingSubscribersAsLines AbstractThrowingSubscribers
+ * @modules java.base/sun.net.www.http
+ * java.net.http/jdk.internal.net.http.common
+ * java.net.http/jdk.internal.net.http.frame
+ * java.net.http/jdk.internal.net.http.hpack
+ * @run testng/othervm -Djdk.internal.httpclient.debug=true ThrowingSubscribersAsLines
+ */
+
+import org.testng.annotations.Test;
+
+public class ThrowingSubscribersAsLines extends AbstractThrowingSubscribers {
+
+ @Test(dataProvider = "variants")
+ public void testThrowingAsLines(String uri, boolean sameClient, Thrower thrower)
+ throws Exception {
+ super.testThrowingAsLinesImpl(uri, sameClient, thrower);
+ }
+
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/ThrowingSubscribersAsLinesAsync.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Tests what happens when response body handlers and subscribers
+ * throw unexpected exceptions.
+ * @library /lib/testlibrary http2/server
+ * @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters
+ * ReferenceTracker ThrowingSubscribersAsLinesAsync AbstractThrowingSubscribers
+ * @modules java.base/sun.net.www.http
+ * java.net.http/jdk.internal.net.http.common
+ * java.net.http/jdk.internal.net.http.frame
+ * java.net.http/jdk.internal.net.http.hpack
+ * @run testng/othervm -Djdk.internal.httpclient.debug=true ThrowingSubscribersAsLinesAsync
+ */
+
+import org.testng.annotations.Test;
+
+public class ThrowingSubscribersAsLinesAsync extends AbstractThrowingSubscribers {
+
+ @Test(dataProvider = "variants")
+ public void testThrowingAsLinesAsync(String uri, boolean sameClient, Thrower thrower)
+ throws Exception {
+ super.testThrowingAsLinesAsyncImpl(uri, sameClient, thrower);
+ }
+
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/ThrowingSubscribersAsString.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Tests what happens when response body handlers and subscribers
+ * throw unexpected exceptions.
+ * @library /lib/testlibrary http2/server
+ * @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters
+ * ReferenceTracker ThrowingSubscribersAsString AbstractThrowingSubscribers
+ * @modules java.base/sun.net.www.http
+ * java.net.http/jdk.internal.net.http.common
+ * java.net.http/jdk.internal.net.http.frame
+ * java.net.http/jdk.internal.net.http.hpack
+ * @run testng/othervm -Djdk.internal.httpclient.debug=true ThrowingSubscribersAsString
+ */
+
+import org.testng.annotations.Test;
+
+public class ThrowingSubscribersAsString extends AbstractThrowingSubscribers {
+
+ @Test(dataProvider = "variants")
+ public void testThrowingAsString(String uri, boolean sameClient, Thrower thrower)
+ throws Exception {
+ super.testThrowingAsStringImpl(uri, sameClient, thrower);
+ }
+
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/ThrowingSubscribersAsStringAsync.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Tests what happens when response body handlers and subscribers
+ * throw unexpected exceptions.
+ * @library /lib/testlibrary http2/server
+ * @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters
+ * ReferenceTracker ThrowingSubscribersAsStringAsync AbstractThrowingSubscribers
+ * @modules java.base/sun.net.www.http
+ * java.net.http/jdk.internal.net.http.common
+ * java.net.http/jdk.internal.net.http.frame
+ * java.net.http/jdk.internal.net.http.hpack
+ * @run testng/othervm -Djdk.internal.httpclient.debug=true ThrowingSubscribersAsStringAsync
+ */
+
+import org.testng.annotations.Test;
+
+public class ThrowingSubscribersAsStringAsync extends AbstractThrowingSubscribers {
+
+ @Test(dataProvider = "variants")
+ public void testThrowingAsStringAsync(String uri, boolean sameClient, Thrower thrower)
+ throws Exception {
+ super.testThrowingAsStringAsyncImpl(uri, sameClient, thrower);
+ }
+
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/ThrowingSubscribersSanity.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Tests what happens when response body handlers and subscribers
+ * throw unexpected exceptions.
+ * @library /lib/testlibrary http2/server
+ * @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters
+ * ReferenceTracker ThrowingSubscribersSanity AbstractThrowingSubscribers
+ * @modules java.base/sun.net.www.http
+ * java.net.http/jdk.internal.net.http.common
+ * java.net.http/jdk.internal.net.http.frame
+ * java.net.http/jdk.internal.net.http.hpack
+ * @run testng/othervm -Djdk.internal.httpclient.debug=true ThrowingSubscribersSanity
+ */
+
+import org.testng.annotations.Test;
+
+public class ThrowingSubscribersSanity extends AbstractThrowingSubscribers {
+
+ @Test(dataProvider = "sanity")
+ public void testSanity(String uri, boolean sameClient)
+ throws Exception {
+ super.testSanityImpl(uri, sameClient);
+ }
+
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/UnauthorizedTest.java Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,268 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @bug 8203882
+ * @summary (httpclient) Check that HttpClient throws IOException when
+ * receiving 401/407 with no WWW-Authenticate/Proxy-Authenticate
+ * header only in the case where an authenticator is configured
+ * for the client. If no authenticator is configured the client
+ * should simply let the caller deal with the unauthorized response.
+ * @modules java.base/sun.net.www.http
+ * java.net.http/jdk.internal.net.http.common
+ * java.net.http/jdk.internal.net.http.frame
+ * java.net.http/jdk.internal.net.http.hpack
+ * java.logging
+ * jdk.httpserver
+ * @library /lib/testlibrary /test/lib http2/server
+ * @build Http2TestServer
+ * @build jdk.testlibrary.SimpleSSLContext
+ * @run testng/othervm
+ * -Djdk.httpclient.HttpClient.log=headers
+ * UnauthorizedTest
+ */
+
+import com.sun.net.httpserver.HttpServer;
+import com.sun.net.httpserver.HttpsConfigurator;
+import com.sun.net.httpserver.HttpsServer;
+import jdk.testlibrary.SimpleSSLContext;
+import org.testng.annotations.AfterTest;
+import org.testng.annotations.BeforeTest;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import javax.net.ssl.SSLContext;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Authenticator;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.atomic.AtomicLong;
+
+import static java.lang.System.out;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+public class UnauthorizedTest implements HttpServerAdapters {
+
+ SSLContext sslContext;
+ HttpTestServer httpTestServer; // HTTP/1.1
+ HttpTestServer httpsTestServer; // HTTPS/1.1
+ HttpTestServer http2TestServer; // HTTP/2 ( h2c )
+ HttpTestServer https2TestServer; // HTTP/2 ( h2 )
+ String httpURI;
+ String httpsURI;
+ String http2URI;
+ String https2URI;
+ HttpClient authClient;
+ HttpClient noAuthClient;
+
+ static final int ITERATIONS = 3;
+
+ /*
+ * NOT_MODIFIED status code results from a conditional GET where
+ * the server does not (must not) return a response body because
+ * the condition specified in the request disallows it
+ */
+ static final int UNAUTHORIZED = 401;
+ static final int PROXY_UNAUTHORIZED = 407;
+ static final int HTTP_OK = 200;
+ static final String MESSAGE = "Unauthorized";
+
+ @DataProvider(name = "all")
+ public Object[][] positive() {
+ return new Object[][] {
+ { httpURI + "/server", UNAUTHORIZED, true, authClient},
+ { httpsURI + "/server", UNAUTHORIZED, true, authClient},
+ { http2URI + "/server", UNAUTHORIZED, true, authClient},
+ { https2URI + "/server", UNAUTHORIZED, true, authClient},
+ { httpURI + "/proxy", PROXY_UNAUTHORIZED, true, authClient},
+ { httpsURI + "/proxy", PROXY_UNAUTHORIZED, true, authClient},
+ { http2URI + "/proxy", PROXY_UNAUTHORIZED, true, authClient},
+ { https2URI + "/proxy", PROXY_UNAUTHORIZED, true, authClient},
+ { httpURI + "/server", UNAUTHORIZED, false, authClient},
+ { httpsURI + "/server", UNAUTHORIZED, false, authClient},
+ { http2URI + "/server", UNAUTHORIZED, false, authClient},
+ { https2URI + "/server", UNAUTHORIZED, false, authClient},
+ { httpURI + "/proxy", PROXY_UNAUTHORIZED, false, authClient},
+ { httpsURI + "/proxy", PROXY_UNAUTHORIZED, false, authClient},
+ { http2URI + "/proxy", PROXY_UNAUTHORIZED, false, authClient},
+ { https2URI + "/proxy", PROXY_UNAUTHORIZED, false, authClient},
+ { httpURI + "/server", UNAUTHORIZED, true, noAuthClient},
+ { httpsURI + "/server", UNAUTHORIZED, true, noAuthClient},
+ { http2URI + "/server", UNAUTHORIZED, true, noAuthClient},
+ { https2URI + "/server", UNAUTHORIZED, true, noAuthClient},
+ { httpURI + "/proxy", PROXY_UNAUTHORIZED, true, noAuthClient},
+ { httpsURI + "/proxy", PROXY_UNAUTHORIZED, true, noAuthClient},
+ { http2URI + "/proxy", PROXY_UNAUTHORIZED, true, noAuthClient},
+ { https2URI + "/proxy", PROXY_UNAUTHORIZED, true, noAuthClient},
+ { httpURI + "/server", UNAUTHORIZED, false, noAuthClient},
+ { httpsURI + "/server", UNAUTHORIZED, false, noAuthClient},
+ { http2URI + "/server", UNAUTHORIZED, false, noAuthClient},
+ { https2URI + "/server", UNAUTHORIZED, false, noAuthClient},
+ { httpURI + "/proxy", PROXY_UNAUTHORIZED, false, noAuthClient},
+ { httpsURI + "/proxy", PROXY_UNAUTHORIZED, false, noAuthClient},
+ { http2URI + "/proxy", PROXY_UNAUTHORIZED, false, noAuthClient},
+ { https2URI + "/proxy", PROXY_UNAUTHORIZED, false, noAuthClient},
+ };
+ }
+
+ static final AtomicLong requestCounter = new AtomicLong();
+
+ static final Authenticator authenticator = new Authenticator() {
+ };
+
+ @Test(dataProvider = "all")
+ void test(String uriString, int code, boolean async, HttpClient client) throws Throwable {
+ out.printf("%n---- starting (%s, %d, %s, %s) ----%n",
+ uriString, code, async ? "async" : "sync",
+ client.authenticator().isPresent() ? "authClient" : "noAuthClient");
+ URI uri = URI.create(uriString);
+
+ HttpRequest.Builder requestBuilder = HttpRequest
+ .newBuilder(uri)
+ .GET();
+
+ HttpRequest request = requestBuilder.build();
+ out.println("Initial request: " + request.uri());
+
+ boolean shouldThrow = client.authenticator().isPresent();
+ String header = (code==UNAUTHORIZED)?"WWW-Authenticate":"Proxy-Authenticate";
+
+ HttpResponse<String> response = null;
+ try {
+ if (async) {
+ response = client.send(request, BodyHandlers.ofString());
+ } else {
+ try {
+ response = client.sendAsync(request, BodyHandlers.ofString()).get();
+ } catch (ExecutionException ex) {
+ throw ex.getCause();
+ }
+ }
+ } catch (IOException ex) {
+ if (shouldThrow && ex.getMessage().contains(header)) {
+ System.out.println("Got expected exception: " + ex);
+ return;
+ } else throw ex;
+ }
+
+ out.println(" Got response: " + response);
+ assertEquals(response.statusCode(), code);
+ assertEquals(response.body(),
+ (code == UNAUTHORIZED ? "WWW-" : "Proxy-") + MESSAGE);
+ if (shouldThrow) {
+ throw new RuntimeException("Expected IOException not thrown.");
+ }
+ }
+
+ // -- Infrastructure
+
+ @BeforeTest
+ public void setup() throws Exception {
+ sslContext = new SimpleSSLContext().get();
+ if (sslContext == null)
+ throw new AssertionError("Unexpected null sslContext");
+
+ InetSocketAddress sa = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0);
+
+ httpTestServer = HttpTestServer.of(HttpServer.create(sa, 0));
+ httpTestServer.addHandler(new UnauthorizedHandler(), "/http1/");
+ httpURI = "http://" + httpTestServer.serverAuthority() + "/http1";
+ HttpsServer httpsServer = HttpsServer.create(sa, 0);
+ httpsServer.setHttpsConfigurator(new HttpsConfigurator(sslContext));
+ httpsTestServer = HttpTestServer.of(httpsServer);
+ httpsTestServer.addHandler(new UnauthorizedHandler(),"/https1/");
+ httpsURI = "https://" + httpsTestServer.serverAuthority() + "/https1";
+
+ http2TestServer = HttpTestServer.of(new Http2TestServer("localhost", false, 0));
+ http2TestServer.addHandler(new UnauthorizedHandler(), "/http2/");
+ http2URI = "http://" + http2TestServer.serverAuthority() + "/http2";
+ https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, sslContext));
+ https2TestServer.addHandler(new UnauthorizedHandler(), "/https2/");
+ https2URI = "https://" + https2TestServer.serverAuthority() + "/https2";
+
+ authClient = HttpClient.newBuilder()
+ .proxy(HttpClient.Builder.NO_PROXY)
+ .sslContext(sslContext)
+ .authenticator(authenticator)
+ .build();
+
+ noAuthClient = HttpClient.newBuilder()
+ .proxy(HttpClient.Builder.NO_PROXY)
+ .sslContext(sslContext)
+ .build();
+
+ httpTestServer.start();
+ httpsTestServer.start();
+ http2TestServer.start();
+ https2TestServer.start();
+ }
+
+ @AfterTest
+ public void teardown() throws Exception {
+ httpTestServer.stop();
+ httpsTestServer.stop();
+ http2TestServer.stop();
+ https2TestServer.stop();
+ }
+
+ static class UnauthorizedHandler implements HttpTestHandler {
+
+ @Override
+ public void handle(HttpTestExchange t) throws IOException {
+ readAllRequestData(t); // shouldn't be any
+ String method = t.getRequestMethod();
+ String path = t.getRequestURI().getPath();
+ HttpTestResponseHeaders rsph = t.getResponseHeaders();
+
+ int code;
+ if (path.contains("server")) {
+ code = UNAUTHORIZED;
+ } else {
+ code = PROXY_UNAUTHORIZED;
+ }
+ String message = (code == UNAUTHORIZED ? "WWW-" : "Proxy-") + MESSAGE;
+ byte[] bytes = message.getBytes(UTF_8);
+ t.sendResponseHeaders(code, bytes.length);
+ try (OutputStream os = t.getResponseBody()) {
+ os.write(bytes);
+ }
+ }
+ }
+
+ static void readAllRequestData(HttpTestExchange t) throws IOException {
+ try (InputStream is = t.getRequestBody()) {
+ is.readAllBytes();
+ }
+ }
+}
--- a/test/jdk/java/net/httpclient/examples/JavadocExamples.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/examples/JavadocExamples.java Wed Jun 20 09:05:57 2018 -0700
@@ -45,6 +45,7 @@
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Flow;
import java.util.regex.Pattern;
+import static java.nio.charset.StandardCharsets.UTF_8;
/*
* THE CONTENTS OF THIS FILE HAVE TO BE IN SYNC WITH THE EXAMPLES USED IN THE
@@ -156,6 +157,25 @@
HttpResponse<Void> respons4 = client
.send(request, BodyHandlers.discarding());
+
+ // HttpResponse.BodySubscribers class-level description
+ // Streams the response body to a File
+ HttpResponse<byte[]> response5 = client
+ .send(request, responseInfo -> BodySubscribers.ofByteArray());
+
+ // Accumulates the response body and returns it as a byte[]
+ HttpResponse<byte[]> response6 = client
+ .send(request, responseInfo -> BodySubscribers.ofByteArray());
+
+ // Discards the response body
+ HttpResponse<Void> response7 = client
+ .send(request, responseInfo -> BodySubscribers.discarding());
+
+ // Accumulates the response body as a String then maps it to its bytes
+ HttpResponse<byte[]> response8 = client
+ .send(request, responseInfo ->
+ BodySubscribers.mapping(BodySubscribers.ofString(UTF_8), String::getBytes));
+
}
/**
--- a/test/jdk/java/net/httpclient/http2/BadHeadersTest.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/http2/BadHeadersTest.java Wed Jun 20 09:05:57 2018 -0700
@@ -33,8 +33,7 @@
* @run testng/othervm -Djdk.internal.httpclient.debug=true BadHeadersTest
*/
-import jdk.internal.net.http.common.HttpHeadersImpl;
-import jdk.internal.net.http.common.Pair;
+import jdk.internal.net.http.common.HttpHeadersBuilder;
import jdk.internal.net.http.frame.ContinuationFrame;
import jdk.internal.net.http.frame.HeaderFrame;
import jdk.internal.net.http.frame.HeadersFrame;
@@ -44,41 +43,38 @@
import org.testng.annotations.BeforeTest;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
-
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-import java.net.Socket;
import java.net.URI;
import java.net.http.HttpClient;
+import java.net.http.HttpHeaders;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.nio.ByteBuffer;
import java.util.ArrayList;
-import java.util.LinkedHashMap;
import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.concurrent.CompletionException;
-import java.util.concurrent.atomic.AtomicInteger;
+import java.util.Map.Entry;
+import java.util.concurrent.ExecutionException;
import java.util.function.BiFunction;
-
import static java.util.List.of;
-import static jdk.internal.net.http.common.Pair.pair;
-import static org.testng.Assert.assertThrows;
+import static java.util.Map.entry;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
// Code copied from ContinuationFrameTest
public class BadHeadersTest {
- private static final List<List<Pair<String, String>>> BAD_HEADERS = of(
- of(pair(":status", "200"), pair(":hello", "GET")), // Unknown pseudo-header
- of(pair(":status", "200"), pair("hell o", "value")), // Space in the name
- of(pair(":status", "200"), pair("hello", "line1\r\n line2\r\n")), // Multiline value
- of(pair(":status", "200"), pair("hello", "DE" + ((char) 0x7F) + "L")), // Bad byte in value
- of(pair("hello", "world!"), pair(":status", "200")) // Pseudo header is not the first one
+ private static final List<List<Entry<String, String>>> BAD_HEADERS = of(
+ of(entry(":status", "200"), entry(":hello", "GET")), // Unknown pseudo-header
+ of(entry(":status", "200"), entry("hell o", "value")), // Space in the name
+ of(entry(":status", "200"), entry("hello", "line1\r\n line2\r\n")), // Multiline value
+ of(entry(":status", "200"), entry("hello", "DE" + ((char) 0x7F) + "L")), // Bad byte in value
+ of(entry("hello", "world!"), entry(":status", "200")) // Pseudo header is not the first one
);
SSLContext sslContext;
@@ -143,7 +139,35 @@
void test(String uri,
boolean sameClient,
BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> headerFramesSupplier)
- throws Exception
+ throws Exception
+ {
+ CFTHttp2TestExchange.setHeaderFrameSupplier(headerFramesSupplier);
+
+ HttpClient client = null;
+ for (int i=0; i< BAD_HEADERS.size(); i++) {
+ if (!sameClient || client == null)
+ client = HttpClient.newBuilder().sslContext(sslContext).build();
+
+ URI uriWithQuery = URI.create(uri + "?BAD_HEADERS=" + i);
+ HttpRequest request = HttpRequest.newBuilder(uriWithQuery)
+ .POST(BodyPublishers.ofString("Hello there!"))
+ .build();
+ System.out.println("\nSending request:" + uriWithQuery);
+ final HttpClient cc = client;
+ try {
+ HttpResponse<String> response = cc.send(request, BodyHandlers.ofString());
+ fail("Expected exception, got :" + response + ", " + response.body());
+ } catch (IOException ioe) {
+ System.out.println("Got EXPECTED: " + ioe);
+ assertDetailMessage(ioe, i);
+ }
+ }
+ }
+
+ @Test(dataProvider = "variants")
+ void testAsync(String uri,
+ boolean sameClient,
+ BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> headerFramesSupplier)
{
CFTHttp2TestExchange.setHeaderFrameSupplier(headerFramesSupplier);
@@ -152,31 +176,45 @@
if (!sameClient || client == null)
client = HttpClient.newBuilder().sslContext(sslContext).build();
- HttpRequest request = HttpRequest.newBuilder(URI.create(uri))
+ URI uriWithQuery = URI.create(uri + "?BAD_HEADERS=" + i);
+ HttpRequest request = HttpRequest.newBuilder(uriWithQuery)
.POST(BodyPublishers.ofString("Hello there!"))
.build();
+ System.out.println("\nSending request:" + uriWithQuery);
final HttpClient cc = client;
- if (i % 2 == 0) {
- assertThrows(IOException.class, () -> cc.send(request, BodyHandlers.ofString()));
- } else {
- Throwable t = null;
- try {
- cc.sendAsync(request, BodyHandlers.ofString()).join();
- } catch (Throwable t0) {
- t = t0;
+
+ Throwable t = null;
+ try {
+ HttpResponse<String> response = cc.sendAsync(request, BodyHandlers.ofString()).get();
+ fail("Expected exception, got :" + response + ", " + response.body());
+ } catch (Throwable t0) {
+ System.out.println("Got EXPECTED: " + t0);
+ if (t0 instanceof ExecutionException) {
+ t0 = t0.getCause();
}
- if (t == null) {
- throw new AssertionError("An exception was expected");
- }
- if (t instanceof CompletionException) {
- Throwable c = t.getCause();
- if (!(c instanceof IOException)) {
- throw new AssertionError("Unexpected exception", c);
- }
- } else if (!(t instanceof IOException)) {
- throw new AssertionError("Unexpected exception", t);
- }
+ t = t0;
}
+ assertDetailMessage(t, i);
+ }
+ }
+
+ // Assertions based on implementation specific detail messages. Keep in
+ // sync with implementation.
+ static void assertDetailMessage(Throwable throwable, int iterationIndex) {
+ assertTrue(throwable instanceof IOException,
+ "Expected IOException, got, " + throwable);
+ assertTrue(throwable.getMessage().contains("protocol error"),
+ "Expected \"protocol error\" in: " + throwable.getMessage());
+
+ if (iterationIndex == 0) { // unknown
+ assertTrue(throwable.getMessage().contains("Unknown pseudo-header"),
+ "Expected \"Unknown pseudo-header\" in: " + throwable.getMessage());
+ } else if (iterationIndex == 4) { // unexpected
+ assertTrue(throwable.getMessage().contains(" Unexpected pseudo-header"),
+ "Expected \" Unexpected pseudo-header\" in: " + throwable.getMessage());
+ } else {
+ assertTrue(throwable.getMessage().contains("Bad header"),
+ "Expected \"Bad header\" in: " + throwable.getMessage());
}
}
@@ -186,38 +224,12 @@
if (sslContext == null)
throw new AssertionError("Unexpected null sslContext");
- http2TestServer = new Http2TestServer("localhost", false, 0) {
- @Override
- protected Http2TestServerConnection createConnection(Http2TestServer http2TestServer,
- Socket socket,
- Http2TestExchangeSupplier exchangeSupplier)
- throws IOException {
- return new Http2TestServerConnection(http2TestServer, socket, exchangeSupplier) {
- @Override
- protected HttpHeadersImpl createNewResponseHeaders() {
- return new OrderedHttpHeaders();
- }
- };
- }
- };
+ http2TestServer = new Http2TestServer("localhost", false, 0);
http2TestServer.addHandler(new Http2EchoHandler(), "/http2/echo");
int port = http2TestServer.getAddress().getPort();
http2URI = "http://localhost:" + port + "/http2/echo";
- https2TestServer = new Http2TestServer("localhost", true, 0){
- @Override
- protected Http2TestServerConnection createConnection(Http2TestServer http2TestServer,
- Socket socket,
- Http2TestExchangeSupplier exchangeSupplier)
- throws IOException {
- return new Http2TestServerConnection(http2TestServer, socket, exchangeSupplier) {
- @Override
- protected HttpHeadersImpl createNewResponseHeaders() {
- return new OrderedHttpHeaders();
- }
- };
- }
- };
+ https2TestServer = new Http2TestServer("localhost", true, sslContext);
https2TestServer.addHandler(new Http2EchoHandler(), "/https2/echo");
port = https2TestServer.getAddress().getPort();
https2URI = "https://localhost:" + port + "/https2/echo";
@@ -239,16 +251,14 @@
static class Http2EchoHandler implements Http2Handler {
- private final AtomicInteger requestNo = new AtomicInteger();
-
@Override
public void handle(Http2TestExchange t) throws IOException {
try (InputStream is = t.getRequestBody();
OutputStream os = t.getResponseBody()) {
byte[] bytes = is.readAllBytes();
- int i = requestNo.incrementAndGet();
- List<Pair<String, String>> p = BAD_HEADERS.get(i % BAD_HEADERS.size());
- p.forEach(h -> t.getResponseHeaders().addHeader(h.first, h.second));
+ // Note: strictly ordered response headers will be added within
+ // the custom sendResponseHeaders implementation, based upon the
+ // query parameter
t.sendResponseHeaders(200, bytes.length);
os.write(bytes);
}
@@ -259,23 +269,34 @@
// allow headers to be sent with a number of CONTINUATION frames.
static class CFTHttp2TestExchange extends Http2TestExchangeImpl {
static volatile BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> headerFrameSupplier;
+ volatile int badHeadersIndex = -1;
static void setHeaderFrameSupplier(BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> hfs) {
headerFrameSupplier = hfs;
}
- CFTHttp2TestExchange(int streamid, String method, HttpHeadersImpl reqheaders,
- HttpHeadersImpl rspheaders, URI uri, InputStream is,
+ CFTHttp2TestExchange(int streamid, String method, HttpHeaders reqheaders,
+ HttpHeadersBuilder rspheadersBuilder, URI uri, InputStream is,
SSLSession sslSession, BodyOutputStream os,
Http2TestServerConnection conn, boolean pushAllowed) {
- super(streamid, method, reqheaders, rspheaders, uri, is, sslSession,
+ super(streamid, method, reqheaders, rspheadersBuilder, uri, is, sslSession,
os, conn, pushAllowed);
-
+ String query = uri.getQuery();
+ badHeadersIndex = Integer.parseInt(query.substring(query.indexOf("=") + 1));
+ assert badHeadersIndex >= 0 && badHeadersIndex < BAD_HEADERS.size() :
+ "Unexpected badHeadersIndex value: " + badHeadersIndex;
}
@Override
public void sendResponseHeaders(int rCode, long responseLength) throws IOException {
- List<ByteBuffer> encodeHeaders = conn.encodeHeaders(rspheaders);
+ assert rspheadersBuilder.build().map().size() == 0;
+ assert badHeadersIndex >= 0 && badHeadersIndex < BAD_HEADERS.size() :
+ "Unexpected badHeadersIndex value: " + badHeadersIndex;
+
+ List<Entry<String,String>> headers = BAD_HEADERS.get(badHeadersIndex);
+ System.out.println("Server replying with bad headers: " + headers);
+ List<ByteBuffer> encodeHeaders = conn.encodeHeadersOrdered(headers);
+
List<Http2Frame> headerFrames = headerFrameSupplier.apply(streamid, encodeHeaders);
assert headerFrames.size() > 0; // there must always be at least 1
@@ -291,36 +312,4 @@
System.err.println("Sent response headers " + rCode);
}
}
-
- /*
- * Use carefully. This class might not be suitable outside this test's
- * context. Pay attention working with multi Map view returned from map().
- * The reason is that header names must be lower-cased prior to any
- * operation that depends on whether or not the map contains a specific
- * element.
- */
- private static class OrderedHttpHeaders extends HttpHeadersImpl {
-
- private final Map<String, List<String>> map = new LinkedHashMap<>();
-
- @Override
- public void addHeader(String name, String value) {
- super.addHeader(name.toLowerCase(Locale.ROOT), value);
- }
-
- @Override
- public void setHeader(String name, String value) {
- super.setHeader(name.toLowerCase(Locale.ROOT), value);
- }
-
- @Override
- protected Map<String, List<String>> headersMap() {
- return map;
- }
-
- @Override
- protected HttpHeadersImpl newDeepCopy() {
- return new OrderedHttpHeaders();
- }
- }
}
--- a/test/jdk/java/net/httpclient/http2/BasicTest.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/http2/BasicTest.java Wed Jun 20 09:05:57 2018 -0700
@@ -222,7 +222,7 @@
httpsServer.addHandler((t -> {
SSLSession s = t.getSSLSession();
String prot = s.getProtocol();
- if (prot.equals("TLSv1.2")) {
+ if (prot.equals("TLSv1.2") || prot.equals("TLSv1.3")) {
t.sendResponseHeaders(200, -1);
} else {
System.err.printf("Protocols =%s\n", prot);
--- a/test/jdk/java/net/httpclient/http2/ContinuationFrameTest.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/http2/ContinuationFrameTest.java Wed Jun 20 09:05:57 2018 -0700
@@ -38,6 +38,7 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
+import java.net.http.HttpHeaders;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
@@ -49,7 +50,7 @@
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
-import jdk.internal.net.http.common.HttpHeadersImpl;
+import jdk.internal.net.http.common.HttpHeadersBuilder;
import jdk.internal.net.http.frame.ContinuationFrame;
import jdk.internal.net.http.frame.HeaderFrame;
import jdk.internal.net.http.frame.HeadersFrame;
@@ -168,7 +169,7 @@
int port = http2TestServer.getAddress().getPort();
http2URI = "http://localhost:" + port + "/http2/echo";
- https2TestServer = new Http2TestServer("localhost", true, 0);
+ https2TestServer = new Http2TestServer("localhost", true, sslContext);
https2TestServer.addHandler(new Http2EchoHandler(), "/https2/echo");
port = https2TestServer.getAddress().getPort();
https2URI = "https://localhost:" + port + "/https2/echo";
@@ -212,11 +213,11 @@
headerFrameSupplier = hfs;
}
- CFTHttp2TestExchange(int streamid, String method, HttpHeadersImpl reqheaders,
- HttpHeadersImpl rspheaders, URI uri, InputStream is,
+ CFTHttp2TestExchange(int streamid, String method, HttpHeaders reqheaders,
+ HttpHeadersBuilder rspheadersBuilder, URI uri, InputStream is,
SSLSession sslSession, BodyOutputStream os,
Http2TestServerConnection conn, boolean pushAllowed) {
- super(streamid, method, reqheaders, rspheaders, uri, is, sslSession,
+ super(streamid, method, reqheaders, rspheadersBuilder, uri, is, sslSession,
os, conn, pushAllowed);
}
@@ -226,11 +227,12 @@
this.responseLength = responseLength;
if (responseLength > 0 || responseLength < 0) {
long clen = responseLength > 0 ? responseLength : 0;
- rspheaders.setHeader("Content-length", Long.toString(clen));
+ rspheadersBuilder.setHeader("Content-length", Long.toString(clen));
}
- rspheaders.setHeader(":status", Integer.toString(rCode));
+ rspheadersBuilder.setHeader(":status", Integer.toString(rCode));
+ HttpHeaders headers = rspheadersBuilder.build();
- List<ByteBuffer> encodeHeaders = conn.encodeHeaders(rspheaders);
+ List<ByteBuffer> encodeHeaders = conn.encodeHeaders(headers);
List<Http2Frame> headerFrames = headerFrameSupplier.apply(streamid, encodeHeaders);
assert headerFrames.size() > 0; // there must always be at least 1
--- a/test/jdk/java/net/httpclient/http2/ErrorTest.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/http2/ErrorTest.java Wed Jun 20 09:05:57 2018 -0700
@@ -58,7 +58,7 @@
*/
public class ErrorTest {
- static final String[] CIPHER_SUITES = new String[]{ "TLS_KRB5_WITH_3DES_EDE_CBC_SHA" };
+ static final String[] CIPHER_SUITES = new String[]{ "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" };
static final String SIMPLE_STRING = "Hello world Goodbye world";
--- a/test/jdk/java/net/httpclient/http2/ImplicitPushCancel.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/http2/ImplicitPushCancel.java Wed Jun 20 09:05:57 2018 -0700
@@ -40,18 +40,18 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpHeaders;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.net.http.HttpResponse.PushPromiseHandler;
+import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
-
-import java.net.http.HttpClient;
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-import java.net.http.HttpResponse.BodyHandlers;
-import java.net.http.HttpResponse.PushPromiseHandler;
-import jdk.internal.net.http.common.HttpHeadersImpl;
import org.testng.annotations.AfterTest;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.Test;
@@ -177,7 +177,7 @@
for (Map.Entry<String,String> promise : promises.entrySet()) {
URI uri = requestURI.resolve(promise.getKey());
InputStream is = new ByteArrayInputStream(promise.getValue().getBytes(UTF_8));
- HttpHeadersImpl headers = new HttpHeadersImpl();
+ HttpHeaders headers = HttpHeaders.of(Collections.emptyMap(), (x, y) -> true);
exchange.serverPush(uri, headers, is);
}
System.err.println("Server: All pushes sent");
--- a/test/jdk/java/net/httpclient/http2/ServerPush.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/http2/ServerPush.java Wed Jun 20 09:05:57 2018 -0700
@@ -53,7 +53,6 @@
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.testng.Assert.*;
-
public class ServerPush {
static final int LOOPS = 13;
--- a/test/jdk/java/net/httpclient/http2/ServerPushWithDiffTypes.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/http2/ServerPushWithDiffTypes.java Wed Jun 20 09:05:57 2018 -0700
@@ -46,7 +46,8 @@
import java.net.http.HttpResponse.BodySubscribers;
import java.util.*;
import java.util.concurrent.*;
-import jdk.internal.net.http.common.HttpHeadersImpl;
+import java.util.function.BiPredicate;
+
import org.testng.annotations.Test;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.testng.Assert.assertEquals;
@@ -242,14 +243,16 @@
}
}
+ static final BiPredicate<String,String> ACCEPT_ALL = (x, y) -> true;
+
private void pushPromises(Http2TestExchange exchange) throws IOException {
URI requestURI = exchange.getRequestURI();
for (Map.Entry<String,String> promise : promises.entrySet()) {
URI uri = requestURI.resolve(promise.getKey());
InputStream is = new ByteArrayInputStream(promise.getValue().getBytes(UTF_8));
- HttpHeadersImpl headers = new HttpHeadersImpl();
+ Map<String,List<String>> map = Map.of("X-Promise", List.of(promise.getKey()));
+ HttpHeaders headers = HttpHeaders.of(map, ACCEPT_ALL);
// TODO: add some check on headers, maybe
- headers.addHeader("X-Promise", promise.getKey());
exchange.serverPush(uri, headers, is);
}
System.err.println("Server: All pushes sent");
--- a/test/jdk/java/net/httpclient/http2/java.net.http/jdk/internal/net/http/hpack/HeaderTableTest.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/http2/java.net.http/jdk/internal/net/http/hpack/HeaderTableTest.java Wed Jun 20 09:05:57 2018 -0700
@@ -26,9 +26,12 @@
import org.testng.annotations.Test;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Map;
+import java.util.Set;
import static org.testng.Assert.assertEquals;
+import static org.testng.AssertJUnit.assertTrue;
public class HeaderTableTest extends SimpleHeaderTableTest {
@@ -42,41 +45,39 @@
HeaderTable table = createHeaderTable(0);
Map<Integer, HeaderField> staticHeaderFields = createStaticEntries();
- Map<String, Integer> minimalIndexes = new HashMap<>();
+ Map<String, Set<Integer>> indexes = new HashMap<>();
for (Map.Entry<Integer, HeaderField> e : staticHeaderFields.entrySet()) {
Integer idx = e.getKey();
- String hName = e.getValue().name;
- Integer midx = minimalIndexes.get(hName);
- if (midx == null) {
- minimalIndexes.put(hName, idx);
- } else {
- minimalIndexes.put(hName, Math.min(idx, midx));
- }
+ String name = e.getValue().name;
+ indexes.merge(name, Set.of(idx), (v1, v2) -> {
+ HashSet<Integer> s = new HashSet<>();
+ s.addAll(v1);
+ s.addAll(v2);
+ return s;
+ });
}
- staticHeaderFields.entrySet().forEach(
- e -> {
- // lookup
- HeaderField actualHeaderField = table.get(e.getKey());
- HeaderField expectedHeaderField = e.getValue();
- assertEquals(actualHeaderField, expectedHeaderField);
+ staticHeaderFields.forEach((key, expectedHeaderField) -> {
+ // lookup
+ HeaderField actualHeaderField = table.get(key);
+ assertEquals(actualHeaderField.name, expectedHeaderField.name);
+ assertEquals(actualHeaderField.value, expectedHeaderField.value);
- // reverse lookup (name, value)
- String hName = expectedHeaderField.name;
- String hValue = expectedHeaderField.value;
- int expectedIndex = e.getKey();
- int actualIndex = table.indexOf(hName, hValue);
-
- assertEquals(actualIndex, expectedIndex);
+ // reverse lookup (name, value)
+ String hName = expectedHeaderField.name;
+ String hValue = expectedHeaderField.value;
+ int expectedIndex = key;
+ int actualIndex = table.indexOf(hName, hValue);
- // reverse lookup (name)
- int expectedMinimalIndex = minimalIndexes.get(hName);
- int actualMinimalIndex = table.indexOf(hName, "blah-blah");
+ assertEquals(actualIndex, expectedIndex);
- assertEquals(-actualMinimalIndex, expectedMinimalIndex);
- }
- );
+ // reverse lookup (name)
+ Set<Integer> expectedIndexes = indexes.get(hName);
+ int actualMinimalIndex = table.indexOf(hName, "blah-blah");
+
+ assertTrue(expectedIndexes.contains(-actualMinimalIndex));
+ });
}
@Test
--- a/test/jdk/java/net/httpclient/http2/java.net.http/jdk/internal/net/http/hpack/HuffmanTest.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/http2/java.net.http/jdk/internal/net/http/hpack/HuffmanTest.java Wed Jun 20 09:05:57 2018 -0700
@@ -22,24 +22,39 @@
*/
package jdk.internal.net.http.hpack;
+import jdk.internal.net.http.hpack.Huffman.Reader;
+import jdk.internal.net.http.hpack.Huffman.Writer;
import org.testng.annotations.Test;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
-import java.util.Stack;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-import static java.lang.Integer.parseInt;
-import static org.testng.Assert.*;
+import static jdk.internal.net.http.hpack.HPACK.bytesForBits;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
public final class HuffmanTest {
+ /*
+ * Implementations of Huffman.Reader and Huffman.Writer under test.
+ * Change them here.
+ */
+ private static final Supplier<Reader> READER = QuickHuffman.Reader::new;
+ private static final Supplier<Writer> WRITER = QuickHuffman.Writer::new;
+
//
// https://tools.ietf.org/html/rfc7541#appendix-B
//
- private static final String SPEC =
+ private static final String SPECIFICATION =
// @formatter:off
" code as bits as hex len\n" +
" sym aligned to MSB aligned in\n" +
@@ -303,137 +318,213 @@
" EOS (256) |11111111|11111111|11111111|111111 3fffffff [30]";
// @formatter:on
- @Test
- public void read_table() throws IOException {
- Pattern line = Pattern.compile(
- "\\(\\s*(?<ascii>\\d+)\\s*\\)\\s*(?<binary>(\\|(0|1)+)+)\\s*" +
- "(?<hex>[0-9a-zA-Z]+)\\s*\\[\\s*(?<len>\\d+)\\s*\\]");
- Matcher m = line.matcher(SPEC);
- int i = 0;
- while (m.find()) {
- String ascii = m.group("ascii");
- String binary = m.group("binary").replaceAll("\\|", "");
- String hex = m.group("hex");
- String len = m.group("len");
+ private static final Code EOS = new Code((char) 256, 0x3fffffff, 30);
+ private final SortedMap<Character, Code> CODES = readSpecification();
+
+ private static final class Code {
+
+ final char sym;
+ final int hex;
+ final int len;
+
+ public Code(char sym, int hex, int len) {
+ this.sym = sym;
+ this.hex = hex;
+ this.len = len;
+ }
- // Several sanity checks for the data read from the table, just to
- // make sure what we read makes sense
- assertEquals(parseInt(len), binary.length());
- assertEquals(parseInt(binary, 2), parseInt(hex, 16));
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Code code = (Code) o;
+ return sym == code.sym &&
+ hex == code.hex &&
+ len == code.len;
+ }
- int expected = parseInt(ascii);
+ @Override
+ public int hashCode() {
+ return Objects.hash(sym, hex, len);
+ }
+ }
- // TODO: find actual eos, do not hardcode it!
- byte[] bytes = intToBytes(0x3fffffff, 30,
- parseInt(hex, 16), parseInt(len));
-
- StringBuilder actual = new StringBuilder();
- NaiveHuffman.Reader t = new NaiveHuffman.Reader();
- t.read(ByteBuffer.wrap(bytes), actual, false, true);
+ private SortedMap<Character, Code> readSpecification() {
+ Pattern line = Pattern.compile(
+ "\\(\\s*(?<sym>\\d+)\\s*\\)\\s*(?<bits>(\\|([01])+)+)\\s*" +
+ "(?<hex>[0-9a-zA-Z]+)\\s*\\[\\s*(?<len>\\d+)\\s*\\]");
+ Matcher m = line.matcher(SPECIFICATION);
+ SortedMap<Character, Code> map = new TreeMap<>();
+ while (m.find()) {
+ String symString = m.group("sym");
+ String binaryString = m.group("bits").replaceAll("\\|", "");
+ String hexString = m.group("hex");
+ String lenString = m.group("len");
+ // several sanity checks for the data read from the table, just to
+ // make sure what we read makes sense:
+ int sym = Integer.parseInt(symString);
+ if (sym < 0 || sym > 65535) {
+ throw new IllegalArgumentException();
+ }
+ int binary = Integer.parseInt(binaryString, 2);
+ int len = Integer.parseInt(lenString);
+ if (binaryString.length() != len) {
+ throw new IllegalArgumentException();
+ }
+ int hex = Integer.parseInt(hexString, 16);
+ if (hex != binary) {
+ throw new IllegalArgumentException();
+ }
+ if (map.put((char) sym, new Code((char) sym, hex, len)) != null) {
+ // a mapping for sym already exists
+ throw new IllegalStateException();
+ }
+ }
+ if (map.size() != 257) {
+ throw new IllegalArgumentException();
+ }
+ return map;
+ }
- // What has been read MUST represent a single symbol
- assertEquals(actual.length(), 1, "ascii: " + ascii);
+ /*
+ * Encodes manually each symbol (character) from the specification and
+ * checks that Huffman.Reader decodes the result back to the initial
+ * character. This verifies that Huffman.Reader is implemented according to
+ * RFC 7541.
+ */
+ @Test
+ public void decodingConsistentWithSpecification() throws IOException {
+ Reader reader = READER.get();
+ for (Code code : CODES.values()) {
+ if (code.equals(EOS)) {
+ continue; // skip EOS
+ }
+ ByteBuffer input = encode(code);
+ StringBuilder output = new StringBuilder(1);
+ reader.read(input, output, true);
+ reader.reset();
- // It's a lot more visual to compare char as codes rather than
- // characters (as some of them might not be visible)
- assertEquals(actual.charAt(0), expected);
- i++;
+ // compare chars using their decimal representation (as some chars
+ // might not be printable/visible)
+ int expected = code.sym;
+ int actual = (int) output.charAt(0);
+ assertEquals(output.length(), 1); // exactly 1 character
+ assertEquals(actual, expected);
+ }
+ }
- // maybe not report EOS but rather throw an expected exception?
- }
- assertEquals(i, 257); // 256 + EOS
+ @Test
+ public void decodeEOS1() {
+ Reader reader = READER.get();
+ TestHelper.assertVoidThrows(
+ IOException.class,
+ () -> reader.read(encode(EOS), new StringBuilder(), true));
+ }
+
+ @Test
+ public void decodeEOS2() {
+ Reader reader = READER.get();
+ TestHelper.assertVoidThrows(
+ IOException.class,
+ () -> reader.read(encode(EOS), new StringBuilder(), false));
}
//
// https://tools.ietf.org/html/rfc7541#appendix-C.4.1
//
@Test
- public void read_1() {
- read("f1e3 c2e5 f23a 6ba0 ab90 f4ff", "www.example.com");
+ public void read01() {
+ readExhaustively("f1e3 c2e5 f23a 6ba0 ab90 f4ff", "www.example.com");
}
@Test
- public void write_1() {
- write("www.example.com", "f1e3 c2e5 f23a 6ba0 ab90 f4ff");
+ public void write01() {
+ writeExhaustively("www.example.com", "f1e3 c2e5 f23a 6ba0 ab90 f4ff");
}
//
// https://tools.ietf.org/html/rfc7541#appendix-C.4.2
//
@Test
- public void read_2() {
- read("a8eb 1064 9cbf", "no-cache");
+ public void read02() {
+ readExhaustively("a8eb 1064 9cbf", "no-cache");
}
@Test
- public void write_2() {
- write("no-cache", "a8eb 1064 9cbf");
+ public void write02() {
+ writeExhaustively("no-cache", "a8eb 1064 9cbf");
}
//
// https://tools.ietf.org/html/rfc7541#appendix-C.4.3
//
@Test
- public void read_3() {
- read("25a8 49e9 5ba9 7d7f", "custom-key");
+ public void read03() {
+ readExhaustively("25a8 49e9 5ba9 7d7f", "custom-key");
}
@Test
- public void write_3() {
- write("custom-key", "25a8 49e9 5ba9 7d7f");
+ public void write03() {
+ writeExhaustively("custom-key", "25a8 49e9 5ba9 7d7f");
}
//
// https://tools.ietf.org/html/rfc7541#appendix-C.4.3
//
@Test
- public void read_4() {
- read("25a8 49e9 5bb8 e8b4 bf", "custom-value");
+ public void read04() {
+ readExhaustively("25a8 49e9 5bb8 e8b4 bf", "custom-value");
}
@Test
- public void write_4() {
- write("custom-value", "25a8 49e9 5bb8 e8b4 bf");
+ public void write04() {
+ writeExhaustively("custom-value", "25a8 49e9 5bb8 e8b4 bf");
}
//
// https://tools.ietf.org/html/rfc7541#appendix-C.6.1
//
@Test
- public void read_5() {
- read("6402", "302");
+ public void read05() {
+ readExhaustively("6402", "302");
}
@Test
- public void write_5() {
- write("302", "6402");
+ public void write05() {
+ writeExhaustively("302", "6402");
}
//
// https://tools.ietf.org/html/rfc7541#appendix-C.6.1
//
@Test
- public void read_6() {
- read("aec3 771a 4b", "private");
+ public void read06() {
+ readExhaustively("aec3 771a 4b", "private");
}
@Test
- public void write_6() {
- write("private", "aec3 771a 4b");
+ public void write06() {
+ writeExhaustively("private", "aec3 771a 4b");
}
//
// https://tools.ietf.org/html/rfc7541#appendix-C.6.1
//
@Test
- public void read_7() {
- read("d07a be94 1054 d444 a820 0595 040b 8166 e082 a62d 1bff",
+ public void read07() {
+ readExhaustively(
+ "d07a be94 1054 d444 a820 0595 040b 8166 e082 a62d 1bff",
"Mon, 21 Oct 2013 20:13:21 GMT");
}
@Test
- public void write_7() {
- write("Mon, 21 Oct 2013 20:13:21 GMT",
+ public void write07() {
+ writeExhaustively(
+ "Mon, 21 Oct 2013 20:13:21 GMT",
"d07a be94 1054 d444 a820 0595 040b 8166 e082 a62d 1bff");
}
@@ -441,42 +532,44 @@
// https://tools.ietf.org/html/rfc7541#appendix-C.6.1
//
@Test
- public void read_8() {
- read("9d29 ad17 1863 c78f 0b97 c8e9 ae82 ae43 d3",
- "https://www.example.com");
+ public void read08() {
+ readExhaustively("9d29 ad17 1863 c78f 0b97 c8e9 ae82 ae43 d3",
+ "https://www.example.com");
}
@Test
- public void write_8() {
- write("https://www.example.com",
- "9d29 ad17 1863 c78f 0b97 c8e9 ae82 ae43 d3");
+ public void write08() {
+ writeExhaustively("https://www.example.com",
+ "9d29 ad17 1863 c78f 0b97 c8e9 ae82 ae43 d3");
}
//
// https://tools.ietf.org/html/rfc7541#appendix-C.6.2
//
@Test
- public void read_9() {
- read("640e ff", "307");
+ public void read09() {
+ readExhaustively("640e ff", "307");
}
@Test
- public void write_9() {
- write("307", "640e ff");
+ public void write09() {
+ writeExhaustively("307", "640e ff");
}
//
// https://tools.ietf.org/html/rfc7541#appendix-C.6.3
//
@Test
- public void read_10() {
- read("d07a be94 1054 d444 a820 0595 040b 8166 e084 a62d 1bff",
+ public void read10() {
+ readExhaustively(
+ "d07a be94 1054 d444 a820 0595 040b 8166 e084 a62d 1bff",
"Mon, 21 Oct 2013 20:13:22 GMT");
}
@Test
- public void write_10() {
- write("Mon, 21 Oct 2013 20:13:22 GMT",
+ public void write10() {
+ writeExhaustively(
+ "Mon, 21 Oct 2013 20:13:22 GMT",
"d07a be94 1054 d444 a820 0595 040b 8166 e084 a62d 1bff");
}
@@ -484,78 +577,179 @@
// https://tools.ietf.org/html/rfc7541#appendix-C.6.3
//
@Test
- public void read_11() {
- read("9bd9 ab", "gzip");
+ public void read11() {
+ readExhaustively("9bd9 ab", "gzip");
}
@Test
- public void write_11() {
- write("gzip", "9bd9 ab");
+ public void write11() {
+ writeExhaustively("gzip", "9bd9 ab");
}
//
// https://tools.ietf.org/html/rfc7541#appendix-C.6.3
//
@Test
- public void read_12() {
- read("94e7 821d d7f2 e6c7 b335 dfdf cd5b 3960 " +
- "d5af 2708 7f36 72c1 ab27 0fb5 291f 9587 " +
- "3160 65c0 03ed 4ee5 b106 3d50 07",
+ public void read12() {
+ // The number of possibilities here grow as 2^(n-1). There are 45 bytes
+ // in this input. So it would require 2^44 decoding operations. If we
+ // spend 1 microsecond per operation, it would take approximately
+ //
+ // ((10^15 * 10^(-6)) / 86400) / 365, or about 32 years
+ //
+ // Conclusion: too big to be read exhaustively
+ read("94e7 821d d7f2 e6c7 b335 dfdf cd5b 3960 "
+ + "d5af 2708 7f36 72c1 ab27 0fb5 291f 9587 "
+ + "3160 65c0 03ed 4ee5 b106 3d50 07",
"foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1");
}
@Test
- public void read_13() {
- read("6274 a6b4 0989 4de4 b27f 80",
- "/https2/fixed?0");
+ public void write12() {
+ write("foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1",
+ "94e7 821d d7f2 e6c7 b335 dfdf cd5b 3960 "
+ + "d5af 2708 7f36 72c1 ab27 0fb5 291f 9587 "
+ + "3160 65c0 03ed 4ee5 b106 3d50 07");
}
@Test
- public void test_trie_has_no_empty_nodes() {
- NaiveHuffman.Node root = NaiveHuffman.INSTANCE.getRoot();
- Stack<NaiveHuffman.Node> backlog = new Stack<>();
- backlog.push(root);
- while (!backlog.isEmpty()) {
- NaiveHuffman.Node n = backlog.pop();
- // The only type of nodes we couldn't possibly catch during
- // construction is an empty node: no children and no char
- if (n.left != null) {
- backlog.push(n.left);
- }
- if (n.right != null) {
- backlog.push(n.right);
- }
- assertFalse(!n.charIsSet && n.left == null && n.right == null,
- "Empty node in the trie");
- }
+ public void read13() {
+ readExhaustively("6274 a6b4 0989 4de4 b27f 80",
+ "/https2/fixed?0");
}
@Test
- public void test_trie_has_257_nodes() {
- int count = 0;
- NaiveHuffman.Node root = NaiveHuffman.INSTANCE.getRoot();
- Stack<NaiveHuffman.Node> backlog = new Stack<>();
- backlog.push(root);
- while (!backlog.isEmpty()) {
- NaiveHuffman.Node n = backlog.pop();
- if (n.left != null) {
- backlog.push(n.left);
+ public void roundTrip() throws IOException {
+
+ class Helper {
+ // Maps code's length to a character that is encoded with a code of
+ // that length. Which of the characters with the same code's length
+ // is picked is undefined.
+ private Map<Integer, Character> chars = new HashMap<>();
+ {
+ for (Map.Entry<Character, Code> e : CODES.entrySet()) {
+ chars.putIfAbsent(e.getValue().len, e.getKey());
+ }
}
- if (n.right != null) {
- backlog.push(n.right);
+
+ private CharSequence charsOfLength(int... lengths) {
+ StringBuilder b = new StringBuilder(lengths.length);
+ for (int length : lengths) {
+ Character c = chars.get(length);
+ if (c == null) {
+ throw new IllegalArgumentException(
+ "No code has length " + length);
+ }
+ b.append(c);
+ }
+ return b.toString();
}
- if (n.isLeaf()) {
- count++;
+
+ private void identity(CharSequence str) throws IOException {
+ Writer w = WRITER.get();
+ StringBuilder b = new StringBuilder(str.length());
+ int size = w.lengthOf(str);
+ ByteBuffer buffer = ByteBuffer.allocate(size);
+ w.from(str, 0, str.length()).write(buffer);
+ Reader r = READER.get();
+ r.read(buffer.flip(), b, true);
+ assertEquals(b.toString(), str);
+ }
+
+ private void roundTrip(int... lengths) throws IOException {
+ identity(charsOfLength(lengths));
}
}
- assertEquals(count, 257);
+
+ // The idea is to build a number of input strings that are encoded
+ // without the need for padding. The sizes of the encoded forms,
+ // therefore, must be 8, 16, 24, 32, 48, 56, 64 and 72 bits. Then check
+ // that they are encoded and then decoded into the same strings.
+
+ Helper h = new Helper();
+
+ // -- 8 bit code --
+
+ h.roundTrip( 8);
+
+ // -- 16 bit code --
+
+ h.roundTrip( 5, 11);
+ h.roundTrip( 5, 5, 6);
+
+ // -- 24 bit code --
+
+ h.roundTrip(24);
+ h.roundTrip( 5, 19);
+ h.roundTrip( 5, 5, 14);
+ h.roundTrip( 5, 5, 6, 8);
+
+ // -- 32 bit code --
+
+ h.roundTrip( 5, 27);
+ h.roundTrip( 5, 5, 22);
+ h.roundTrip( 5, 5, 7, 15);
+ h.roundTrip( 5, 5, 5, 5, 12);
+ h.roundTrip( 5, 5, 5, 5, 5, 7);
+
+ // -- 48 bit code --
+
+ h.roundTrip(20, 28);
+ h.roundTrip( 5, 13, 30);
+ h.roundTrip( 5, 5, 8, 30);
+ h.roundTrip( 5, 5, 5, 5, 28);
+ h.roundTrip( 5, 5, 5, 5, 5, 23);
+ h.roundTrip( 5, 5, 5, 5, 5, 8, 15);
+ h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 13);
+ h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 5, 8);
+
+ // -- 56 bit code --
+
+ h.roundTrip(26, 30);
+ h.roundTrip( 5, 21, 30);
+ h.roundTrip( 5, 5, 19, 27);
+ h.roundTrip( 5, 5, 5, 11, 30);
+ h.roundTrip( 5, 5, 5, 5, 6, 30);
+ h.roundTrip( 5, 5, 5, 5, 5, 5, 26);
+ h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 21);
+ h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 6, 15);
+ h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 5, 5, 11);
+ h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6);
+
+ // -- 64 bit code --
+
+ h.roundTrip( 6, 28, 30);
+ h.roundTrip( 5, 5, 24, 30);
+ h.roundTrip( 5, 5, 5, 19, 30);
+ h.roundTrip( 5, 5, 5, 5, 14, 30);
+ h.roundTrip( 5, 5, 5, 5, 5, 11, 28);
+ h.roundTrip( 5, 5, 5, 5, 5, 5, 6, 28);
+ h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 5, 24);
+ h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 5, 5, 19);
+ h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 14);
+ h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 8);
+
+ // -- 72 bit code --
+
+ h.roundTrip(12, 30, 30);
+ h.roundTrip( 5, 7, 30, 30);
+ h.roundTrip( 5, 5, 5, 27, 30);
+ h.roundTrip( 5, 5, 5, 5, 22, 30);
+ h.roundTrip( 5, 5, 5, 5, 5, 19, 28);
+ h.roundTrip( 5, 5, 5, 5, 5, 5, 12, 30);
+ h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 7, 30);
+ h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 5, 5, 27);
+ h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 22);
+ h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 7, 15);
+ h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 12);
+ h.roundTrip( 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 7);
}
@Test
- public void cant_encode_outside_byte() {
+ public void cannotEncodeOutsideByte() {
TestHelper.Block<Object> coding =
- () -> new NaiveHuffman.Writer()
- .from(((char) 256) + "", 0, 1)
+ () -> WRITER.get()
+ .from(String.valueOf((char) 256), 0, 1)
.write(ByteBuffer.allocate(1));
RuntimeException e =
TestHelper.assertVoidThrows(RuntimeException.class, coding);
@@ -565,25 +759,58 @@
private static void read(String hexdump, String decoded) {
ByteBuffer source = SpecHelper.toBytes(hexdump);
Appendable actual = new StringBuilder();
+ Reader reader = READER.get();
try {
- new QuickHuffman.Reader().read(source, actual, true);
+ reader.read(source, actual, true);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
assertEquals(actual.toString(), decoded);
}
+ private static void readExhaustively(String hexdump, String decoded) {
+ ByteBuffer EMPTY_BUFFER = ByteBuffer.allocate(0);
+ Reader reader = READER.get();
+ ByteBuffer source = SpecHelper.toBytes(hexdump);
+ StringBuilder actual = new StringBuilder();
+ BuffersTestingKit.forEachSplit(source, buffers -> {
+ try {
+ for (ByteBuffer b : buffers) {
+ reader.read(b, actual, false);
+ }
+ reader.read(EMPTY_BUFFER, actual, true);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ assertEquals(actual.toString(), decoded);
+ reader.reset();
+ actual.setLength(0);
+ });
+ }
+
private static void write(String decoded, String hexdump) {
- Huffman.Writer writer = new QuickHuffman.Writer();
+ Writer writer = WRITER.get();
int n = writer.lengthOf(decoded);
- ByteBuffer destination = ByteBuffer.allocate(n); // Extra margin (1) to test having more bytes in the destination than needed is ok
+ ByteBuffer destination = ByteBuffer.allocateDirect(n);
+ writer.from(decoded, 0, decoded.length());
+ boolean written = writer.write(destination);
+ assertTrue(written);
+ String actual = SpecHelper.toHexdump(destination.flip());
+ assertEquals(actual, hexdump);
+ writer.reset();
+ }
+
+ private static void writeExhaustively(String decoded, String hexdump) {
+ Writer writer = WRITER.get();
+ int n = writer.lengthOf(decoded);
+ ByteBuffer destination = ByteBuffer.allocate(n);
BuffersTestingKit.forEachSplit(destination, byteBuffers -> {
writer.from(decoded, 0, decoded.length());
boolean written = false;
for (ByteBuffer b : byteBuffers) {
int pos = b.position();
written = writer.write(b);
- b.position(pos);
+ b.position(pos); // "flip" to the saved position, for reading
}
assertTrue(written);
ByteBuffer concated = BuffersTestingKit.concat(byteBuffers);
@@ -593,45 +820,20 @@
});
}
- //
- // It's not very pretty, yes I know that
- //
- // hex:
- //
- // |31|30|...|N-1|...|01|00|
- // \ /
- // codeLength
- //
- // hex <<= 32 - codeLength; (align to MSB):
- //
- // |31|30|...|32-N|...|01|00|
- // \ /
- // codeLength
- //
- // EOS:
- //
- // |31|30|...|M-1|...|01|00|
- // \ /
- // eosLength
- //
- // eos <<= 32 - eosLength; (align to MSB):
- //
- // pad with MSBs of EOS:
- //
- // |31|30|...|32-N|32-N-1|...|01|00|
- // | 32|...|
- //
- // Finally, split into byte[]
- //
- private byte[] intToBytes(int eos, int eosLength, int hex, int codeLength) {
- hex <<= 32 - codeLength;
- eos >>= codeLength - (32 - eosLength);
- hex |= eos;
- int n = (int) Math.ceil(codeLength / 8.0);
+ /*
+ * Encodes a single character. This representation is padded, thus ready to
+ * be decoded.
+ */
+ private static ByteBuffer encode(Code code) {
+ int EOS_MSB = EOS.hex << (32 - EOS.len);
+ int padding = EOS_MSB >>> code.len;
+ int hexMSB = code.hex << (32 - code.len);
+ int c = hexMSB | padding;
+ int n = bytesForBits(code.len);
byte[] result = new byte[n];
for (int i = 0; i < n; i++) {
- result[i] = (byte) (hex >> (32 - 8 * (i + 1)));
+ result[i] = (byte) (c >> (32 - 8 * (i + 1)));
}
- return result;
+ return ByteBuffer.wrap(result);
}
}
--- a/test/jdk/java/net/httpclient/http2/java.net.http/jdk/internal/net/http/hpack/SimpleHeaderTableTest.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/http2/java.net.http/jdk/internal/net/http/hpack/SimpleHeaderTableTest.java Wed Jun 20 09:05:57 2018 -0700
@@ -47,7 +47,7 @@
// https://tools.ietf.org/html/rfc7541#appendix-A
//
// @formatter:off
- private static final String SPEC =
+ private static final String SPECIFICATION =
" | 1 | :authority | |\n" +
" | 2 | :method | GET |\n" +
" | 3 | :method | POST |\n" +
@@ -125,7 +125,8 @@
Map<Integer, HeaderField> staticHeaderFields = createStaticEntries();
staticHeaderFields.forEach((index, expectedHeaderField) -> {
SimpleHeaderTable.HeaderField actualHeaderField = table.get(index);
- assertEquals(actualHeaderField, expectedHeaderField);
+ assertEquals(actualHeaderField.name, expectedHeaderField.name);
+ assertEquals(actualHeaderField.value, expectedHeaderField.value);
});
}
@@ -318,7 +319,7 @@
static Map<Integer, HeaderField> createStaticEntries() {
Pattern line = Pattern.compile(
"\\|\\s*(?<index>\\d+?)\\s*\\|\\s*(?<name>.+?)\\s*\\|\\s*(?<value>.*?)\\s*\\|");
- Matcher m = line.matcher(SPEC);
+ Matcher m = line.matcher(SPECIFICATION);
Map<Integer, HeaderField> result = new HashMap<>();
while (m.find()) {
int index = Integer.parseInt(m.group("index"));
--- a/test/jdk/java/net/httpclient/http2/server/Http2EchoHandler.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/http2/server/Http2EchoHandler.java Wed Jun 20 09:05:57 2018 -0700
@@ -22,11 +22,11 @@
*/
import java.io.*;
+import java.net.http.HttpHeaders;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
-
-import jdk.internal.net.http.common.HttpHeadersImpl;
+import jdk.internal.net.http.common.HttpHeadersBuilder;
public class Http2EchoHandler implements Http2Handler {
static final Path CWD = Paths.get(".");
@@ -40,10 +40,10 @@
System.err.printf("EchoHandler received request to %s from %s\n",
t.getRequestURI(), t.getRemoteAddress());
InputStream is = t.getRequestBody();
- HttpHeadersImpl map = t.getRequestHeaders();
- HttpHeadersImpl map1 = t.getResponseHeaders();
- map1.addHeader("X-Hello", "world");
- map1.addHeader("X-Bye", "universe");
+ HttpHeaders map = t.getRequestHeaders();
+ HttpHeadersBuilder headersBuilder = t.getResponseHeaders();
+ headersBuilder.addHeader("X-Hello", "world");
+ headersBuilder.addHeader("X-Bye", "universe");
String fixedrequest = map.firstValue("XFixed").orElse(null);
File outfile = Files.createTempFile(CWD, "foo", "bar").toFile();
//System.err.println ("QQQ = " + outfile.toString());
--- a/test/jdk/java/net/httpclient/http2/server/Http2RedirectHandler.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/http2/server/Http2RedirectHandler.java Wed Jun 20 09:05:57 2018 -0700
@@ -25,7 +25,7 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.util.function.Supplier;
-import jdk.internal.net.http.common.HttpHeadersImpl;
+import jdk.internal.net.http.common.HttpHeadersBuilder;
public class Http2RedirectHandler implements Http2Handler {
@@ -44,8 +44,8 @@
System.err.printf("RedirectHandler request to %s from %s\n",
t.getRequestURI().toString(), t.getRemoteAddress().toString());
System.err.println("Redirecting to: " + location);
- HttpHeadersImpl map1 = t.getResponseHeaders();
- map1.addHeader("Location", location);
+ HttpHeadersBuilder headersBuilder = t.getResponseHeaders();
+ headersBuilder.addHeader("Location", location);
t.sendResponseHeaders(301, 1024);
byte[] bb = new byte[1024];
OutputStream os = t.getResponseBody();
--- a/test/jdk/java/net/httpclient/http2/server/Http2TestExchange.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/http2/server/Http2TestExchange.java Wed Jun 20 09:05:57 2018 -0700
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2017, 2018, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -26,16 +26,16 @@
import java.io.OutputStream;
import java.net.URI;
import java.net.InetSocketAddress;
+import java.net.http.HttpHeaders;
import java.util.concurrent.CompletableFuture;
-import java.util.function.Consumer;
import javax.net.ssl.SSLSession;
-import jdk.internal.net.http.common.HttpHeadersImpl;
+import jdk.internal.net.http.common.HttpHeadersBuilder;
public interface Http2TestExchange {
- HttpHeadersImpl getRequestHeaders();
+ HttpHeaders getRequestHeaders();
- HttpHeadersImpl getResponseHeaders();
+ HttpHeadersBuilder getResponseHeaders();
URI getRequestURI();
@@ -61,7 +61,7 @@
boolean serverPushAllowed();
- void serverPush(URI uri, HttpHeadersImpl headers, InputStream content);
+ void serverPush(URI uri, HttpHeaders headers, InputStream content);
/**
* Send a PING on this exchanges connection, and completes the returned CF
--- a/test/jdk/java/net/httpclient/http2/server/Http2TestExchangeImpl.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/http2/server/Http2TestExchangeImpl.java Wed Jun 20 09:05:57 2018 -0700
@@ -26,17 +26,19 @@
import java.io.IOException;
import java.net.URI;
import java.net.InetSocketAddress;
+import java.net.http.HttpHeaders;
+import java.util.List;
+import java.util.Map;
import java.util.concurrent.CompletableFuture;
-import java.util.function.Consumer;
import javax.net.ssl.SSLSession;
-import jdk.internal.net.http.common.HttpHeadersImpl;
+import jdk.internal.net.http.common.HttpHeadersBuilder;
import jdk.internal.net.http.frame.HeaderFrame;
import jdk.internal.net.http.frame.HeadersFrame;
public class Http2TestExchangeImpl implements Http2TestExchange {
- final HttpHeadersImpl reqheaders;
- final HttpHeadersImpl rspheaders;
+ final HttpHeaders reqheaders;
+ final HttpHeadersBuilder rspheadersBuilder;
final URI uri;
final String method;
final InputStream is;
@@ -52,8 +54,8 @@
Http2TestExchangeImpl(int streamid,
String method,
- HttpHeadersImpl reqheaders,
- HttpHeadersImpl rspheaders,
+ HttpHeaders reqheaders,
+ HttpHeadersBuilder rspheadersBuilder,
URI uri,
InputStream is,
SSLSession sslSession,
@@ -61,7 +63,7 @@
Http2TestServerConnection conn,
boolean pushAllowed) {
this.reqheaders = reqheaders;
- this.rspheaders = rspheaders;
+ this.rspheadersBuilder = rspheadersBuilder;
this.uri = uri;
this.method = method;
this.is = is;
@@ -74,7 +76,7 @@
}
@Override
- public HttpHeadersImpl getRequestHeaders() {
+ public HttpHeaders getRequestHeaders() {
return reqheaders;
}
@@ -84,8 +86,8 @@
}
@Override
- public HttpHeadersImpl getResponseHeaders() {
- return rspheaders;
+ public HttpHeadersBuilder getResponseHeaders() {
+ return rspheadersBuilder;
}
@Override
@@ -129,13 +131,14 @@
this.responseLength = responseLength;
if (responseLength > 0 || responseLength < 0) {
long clen = responseLength > 0 ? responseLength : 0;
- rspheaders.setHeader("Content-length", Long.toString(clen));
+ rspheadersBuilder.setHeader("Content-length", Long.toString(clen));
}
- rspheaders.setHeader(":status", Integer.toString(rCode));
+ rspheadersBuilder.setHeader(":status", Integer.toString(rCode));
+ HttpHeaders headers = rspheadersBuilder.build();
Http2TestServerConnection.ResponseHeaders response
- = new Http2TestServerConnection.ResponseHeaders(rspheaders);
+ = new Http2TestServerConnection.ResponseHeaders(headers);
response.streamid(streamid);
response.setFlag(HeaderFrame.END_HEADERS);
@@ -175,13 +178,19 @@
}
@Override
- public void serverPush(URI uri, HttpHeadersImpl headers, InputStream content) {
- OutgoingPushPromise pp = new OutgoingPushPromise(
- streamid, uri, headers, content);
- headers.setHeader(":method", "GET");
- headers.setHeader(":scheme", uri.getScheme());
- headers.setHeader(":authority", uri.getAuthority());
- headers.setHeader(":path", uri.getPath());
+ public void serverPush(URI uri, HttpHeaders headers, InputStream content) {
+ HttpHeadersBuilder headersBuilder = new HttpHeadersBuilder();
+ headersBuilder.setHeader(":method", "GET");
+ headersBuilder.setHeader(":scheme", uri.getScheme());
+ headersBuilder.setHeader(":authority", uri.getAuthority());
+ headersBuilder.setHeader(":path", uri.getPath());
+ for (Map.Entry<String,List<String>> entry : headers.map().entrySet()) {
+ for (String value : entry.getValue())
+ headersBuilder.addHeader(entry.getKey(), value);
+ }
+ HttpHeaders combinedHeaders = headersBuilder.build();
+ OutgoingPushPromise pp = new OutgoingPushPromise(streamid, uri, combinedHeaders, content);
+
try {
conn.outputQ.put(pp);
// writeLoop will spin up thread to read the InputStream
--- a/test/jdk/java/net/httpclient/http2/server/Http2TestExchangeSupplier.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/http2/server/Http2TestExchangeSupplier.java Wed Jun 20 09:05:57 2018 -0700
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2017, 2018, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -24,7 +24,8 @@
import javax.net.ssl.SSLSession;
import java.io.InputStream;
import java.net.URI;
-import jdk.internal.net.http.common.HttpHeadersImpl;
+import java.net.http.HttpHeaders;
+import jdk.internal.net.http.common.HttpHeadersBuilder;
/**
* A supplier of Http2TestExchanges. If the default Http2TestExchange impl is
@@ -39,8 +40,8 @@
Http2TestExchange get(int streamid,
String method,
- HttpHeadersImpl reqheaders,
- HttpHeadersImpl rspheaders,
+ HttpHeaders reqheaders,
+ HttpHeadersBuilder rspheadersBuilder,
URI uri,
InputStream is,
SSLSession sslSession,
--- a/test/jdk/java/net/httpclient/http2/server/Http2TestServer.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/http2/server/Http2TestServer.java Wed Jun 20 09:05:57 2018 -0700
@@ -53,6 +53,7 @@
final SSLContext sslContext;
final String serverName;
final HashMap<InetSocketAddress,Http2TestServerConnection> connections;
+ final Properties properties;
private static ThreadFactory defaultThreadFac =
(Runnable r) -> {
@@ -67,11 +68,11 @@
}
public Http2TestServer(String serverName, boolean secure, int port) throws Exception {
- this(serverName, secure, port, getDefaultExecutor(), 50, null);
+ this(serverName, secure, port, getDefaultExecutor(), 50, null, null);
}
public Http2TestServer(boolean secure, int port) throws Exception {
- this(null, secure, port, getDefaultExecutor(), 50, null);
+ this(null, secure, port, getDefaultExecutor(), 50, null, null);
}
public InetSocketAddress getAddress() {
@@ -85,19 +86,19 @@
public Http2TestServer(boolean secure,
SSLContext context) throws Exception {
- this(null, secure, 0, null, 50, context);
+ this(null, secure, 0, null, 50, null, context);
}
public Http2TestServer(String serverName, boolean secure,
SSLContext context) throws Exception {
- this(serverName, secure, 0, null, 50, context);
+ this(serverName, secure, 0, null, 50, null, context);
}
public Http2TestServer(boolean secure,
int port,
ExecutorService exec,
SSLContext context) throws Exception {
- this(null, secure, port, exec, 50, context);
+ this(null, secure, port, exec, 50, null, context);
}
public Http2TestServer(String serverName,
@@ -107,7 +108,7 @@
SSLContext context)
throws Exception
{
- this(serverName, secure, port, exec, 50, context);
+ this(serverName, secure, port, exec, 50, null, context);
}
/**
@@ -120,6 +121,7 @@
* @param port listen port
* @param exec executor service (cached thread pool is used if null)
* @param backlog the server socket backlog
+ * @param properties additional configuration properties
* @param context the SSLContext used when secure is true
*/
public Http2TestServer(String serverName,
@@ -127,19 +129,25 @@
int port,
ExecutorService exec,
int backlog,
+ Properties properties,
SSLContext context)
throws Exception
{
this.serverName = serverName;
if (secure) {
+ if (context != null)
+ this.sslContext = context;
+ else
+ this.sslContext = SSLContext.getDefault();
server = initSecure(port, backlog);
} else {
+ this.sslContext = context;
server = initPlaintext(port, backlog);
}
this.secure = secure;
this.exec = exec == null ? getDefaultExecutor() : exec;
this.handlers = Collections.synchronizedMap(new HashMap<>());
- this.sslContext = context;
+ this.properties = properties;
this.connections = new HashMap<>();
}
@@ -206,15 +214,12 @@
final ServerSocket initSecure(int port, int backlog) throws Exception {
ServerSocketFactory fac;
- if (sslContext != null) {
- fac = sslContext.getServerSocketFactory();
- } else {
- fac = SSLServerSocketFactory.getDefault();
- }
+ SSLParameters sslp = null;
+ fac = sslContext.getServerSocketFactory();
+ sslp = sslContext.getSupportedSSLParameters();
SSLServerSocket se = (SSLServerSocket) fac.createServerSocket();
se.setReuseAddress(false);
se.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), backlog);
- SSLParameters sslp = se.getSSLParameters();
sslp.setApplicationProtocols(new String[]{"h2"});
sslp.setEndpointIdentificationAlgorithm("HTTPS");
se.setSSLParameters(sslp);
@@ -264,7 +269,6 @@
socket.close();
}
System.err.println("TestServer: start exception: " + e);
- //throw e;
}
}
} catch (SecurityException se) {
@@ -285,7 +289,7 @@
Socket socket,
Http2TestExchangeSupplier exchangeSupplier)
throws IOException {
- return new Http2TestServerConnection(http2TestServer, socket, exchangeSupplier);
+ return new Http2TestServerConnection(http2TestServer, socket, exchangeSupplier, properties);
}
@Override
--- a/test/jdk/java/net/httpclient/http2/server/Http2TestServerConnection.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/http2/server/Http2TestServerConnection.java Wed Jun 20 09:05:57 2018 -0700
@@ -33,14 +33,14 @@
import java.net.InetAddress;
import javax.net.ssl.*;
import java.net.URISyntaxException;
+import java.net.http.HttpHeaders;
import java.nio.ByteBuffer;
-import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.function.Consumer;
-import jdk.internal.net.http.common.HttpHeadersImpl;
+import jdk.internal.net.http.common.HttpHeadersBuilder;
import jdk.internal.net.http.frame.*;
import jdk.internal.net.http.hpack.Decoder;
import jdk.internal.net.http.hpack.DecodingCallback;
@@ -71,6 +71,7 @@
final SettingsFrame serverSettings;
final ExecutorService exec;
final boolean secure;
+ final Properties properties;
volatile boolean stopping;
volatile int nextPushStreamId = 2;
ConcurrentLinkedQueue<PingRequest> pings = new ConcurrentLinkedQueue<>();
@@ -118,7 +119,8 @@
Http2TestServerConnection(Http2TestServer server,
Socket socket,
- Http2TestExchangeSupplier exchangeSupplier)
+ Http2TestExchangeSupplier exchangeSupplier,
+ Properties properties)
throws IOException
{
if (socket instanceof SSLSocket) {
@@ -132,8 +134,9 @@
this.outputQ = new Queue<>(sentinel);
this.random = new Random();
this.socket = socket;
+ this.properties = properties;
this.socket.setTcpNoDelay(true);
- this.serverSettings = SettingsFrame.getDefaultSettings();
+ this.serverSettings = getServerSettingProperties();
this.exec = server.exec;
this.secure = server.secure;
this.pushStreams = new HashSet<>();
@@ -141,6 +144,38 @@
os = new BufferedOutputStream(socket.getOutputStream());
}
+ static final String propPrefix = "http2server.settings.";
+
+ static final String[][] propIDs = {
+ {"header_table_size", Integer.toString(SettingsFrame.HEADER_TABLE_SIZE)},
+ {"enable_push", Integer.toString(SettingsFrame.ENABLE_PUSH)},
+ {"max_concurrent_streams", Integer.toString(SettingsFrame.MAX_CONCURRENT_STREAMS)},
+ {"initial_window_size", Integer.toString(SettingsFrame.INITIAL_WINDOW_SIZE)},
+ {"max_frame_size", Integer.toString(SettingsFrame.MAX_FRAME_SIZE)},
+ {"max_header_list_size", Integer.toString(SettingsFrame.MAX_HEADER_LIST_SIZE)}
+ };
+
+ private SettingsFrame getServerSettingProperties() {
+ SettingsFrame s = SettingsFrame.getDefaultSettings();
+ if (properties == null)
+ return s;
+ for (int i=0; i<propIDs.length; i++) {
+ String key = propIDs[i][0];
+ String numS = propIDs[i][1];
+ String prop = properties.getProperty(propPrefix + key);
+ if (prop != null) {
+ try {
+ System.err.println("TestServer: setting " + key + " property to: " +
+ prop);
+ int num = Integer.parseInt(numS);
+ System.err.println("TestServer: num = " + num);
+ s.setParameter(num, Integer.parseInt(prop));
+ } catch (NumberFormatException e) {/* ignore errors */}
+ }
+ }
+ return s;
+ }
+
/**
* Sends a PING frame on this connection, and completes the returned
* CF when the PING ack is received. The CF is given
@@ -420,11 +455,11 @@
outputQ.put(wup);
}
- HttpHeadersImpl decodeHeaders(List<HeaderFrame> frames) throws IOException {
- HttpHeadersImpl headers = createNewResponseHeaders();
+ HttpHeaders decodeHeaders(List<HeaderFrame> frames) throws IOException {
+ HttpHeadersBuilder headersBuilder = createNewHeadersBuilder();
DecodingCallback cb = (name, value) -> {
- headers.addHeader(name.toString(), value.toString());
+ headersBuilder.addHeader(name.toString(), value.toString());
};
for (HeaderFrame frame : frames) {
@@ -434,7 +469,7 @@
}
}
hpackIn.decode(EMPTY_BUFFER, true, cb);
- return headers;
+ return headersBuilder.build();
}
String getRequestLine(String request) {
@@ -451,8 +486,8 @@
return request.substring(start,end);
}
- void addHeaders(String headers, HttpHeadersImpl hdrs) {
- String[] hh = headers.split(CRLF);
+ static void addHeaders(String headersString, HttpHeadersBuilder headersBuilder) {
+ String[] hh = headersString.split(CRLF);
for (String header : hh) {
int colon = header.indexOf(':');
if (colon == -1)
@@ -461,14 +496,14 @@
String value = header.substring(colon+1);
while (value.startsWith(" "))
value = value.substring(1);
- hdrs.addHeader(name, value);
+ headersBuilder.addHeader(name, value);
}
}
// First stream (1) comes from a plaintext HTTP/1.1 request
@SuppressWarnings({"rawtypes","unchecked"})
void createPrimordialStream(Http1InitialRequest request) throws IOException {
- HttpHeadersImpl headers = createNewResponseHeaders();
+ HttpHeadersBuilder headersBuilder = createNewHeadersBuilder();
String requestLine = getRequestLine(request.headers);
String[] tokens = requestLine.split(" ");
if (!tokens[2].equals("HTTP/1.1")) {
@@ -485,18 +520,19 @@
throw new IOException("missing Host");
}
- headers.setHeader(":method", tokens[0]);
- headers.setHeader(":scheme", "http"); // always in this case
- headers.setHeader(":authority", host);
+ headersBuilder.setHeader(":method", tokens[0]);
+ headersBuilder.setHeader(":scheme", "http"); // always in this case
+ headersBuilder.setHeader(":authority", host);
String path = uri.getRawPath();
if (uri.getRawQuery() != null)
path = path + "?" + uri.getRawQuery();
- headers.setHeader(":path", path);
+ headersBuilder.setHeader(":path", path);
Queue q = new Queue(sentinel);
byte[] body = getRequestBody(request);
- addHeaders(getHeaders(request.headers), headers);
- headers.setHeader("Content-length", Integer.toString(body.length));
+ addHeaders(getHeaders(request.headers), headersBuilder);
+ headersBuilder.setHeader("Content-length", Integer.toString(body.length));
+ HttpHeaders headers = headersBuilder.build();
addRequestBodyToQueue(body, q);
streams.put(1, q);
@@ -534,7 +570,7 @@
}
}
boolean endStreamReceived = endStream;
- HttpHeadersImpl headers = decodeHeaders(frames);
+ HttpHeaders headers = decodeHeaders(frames);
// Strict to assert Client correctness. Not all servers are as strict,
// but some are known to be.
@@ -558,7 +594,7 @@
// for this stream/request delivered on Q
@SuppressWarnings({"rawtypes","unchecked"})
- void handleRequest(HttpHeadersImpl headers,
+ void handleRequest(HttpHeaders headers,
Queue queue,
int streamid,
boolean endStreamReceived)
@@ -572,7 +608,6 @@
String authority = headers.firstValue(":authority").orElse("");
//System.out.println("authority = " + authority);
System.err.printf("TestServer: %s %s\n", method, path);
- HttpHeadersImpl rspheaders = createNewResponseHeaders();
int winsize = clientSettings.getParameter(
SettingsFrame.INITIAL_WINDOW_SIZE);
//System.err.println ("Stream window size = " + winsize);
@@ -592,8 +627,9 @@
String us = scheme + "://" + authority + path;
URI uri = new URI(us);
boolean pushAllowed = clientSettings.getParameter(SettingsFrame.ENABLE_PUSH) == 1;
+ HttpHeadersBuilder rspheadersBuilder = createNewHeadersBuilder();
Http2TestExchange exchange = exchangeSupplier.get(streamid, method,
- headers, rspheaders, uri, bis, getSSLSession(),
+ headers, rspheadersBuilder, uri, bis, getSSLSession(),
bos, this, pushAllowed);
// give to user
@@ -620,8 +656,8 @@
}
}
- protected HttpHeadersImpl createNewResponseHeaders() {
- return new HttpHeadersImpl();
+ protected HttpHeadersBuilder createNewHeadersBuilder() {
+ return new HttpHeadersBuilder();
}
private SSLSession getSSLSession() {
@@ -710,7 +746,8 @@
}
}
- List<ByteBuffer> encodeHeaders(HttpHeadersImpl headers) {
+ /** Encodes an group of headers, without any ordering guarantees. */
+ List<ByteBuffer> encodeHeaders(HttpHeaders headers) {
List<ByteBuffer> buffers = new LinkedList<>();
ByteBuffer buf = getBuffer();
@@ -735,6 +772,30 @@
return buffers;
}
+ /** Encodes an ordered list of headers. */
+ List<ByteBuffer> encodeHeadersOrdered(List<Map.Entry<String,String>> headers) {
+ List<ByteBuffer> buffers = new LinkedList<>();
+
+ ByteBuffer buf = getBuffer();
+ boolean encoded;
+ for (Map.Entry<String, String> entry : headers) {
+ String value = entry.getValue();
+ String key = entry.getKey().toLowerCase();
+ do {
+ hpackOut.header(key, value);
+ encoded = hpackOut.encode(buf);
+ if (!encoded) {
+ buf.flip();
+ buffers.add(buf);
+ buf = getBuffer();
+ }
+ } while (!encoded);
+ }
+ buf.flip();
+ buffers.add(buf);
+ return buffers;
+ }
+
static void closeIgnore(Closeable c) {
try {
c.close();
@@ -812,9 +873,9 @@
// returns a minimal response with status 200
// that is the response to the push promise just sent
private ResponseHeaders getPushResponse(int streamid) {
- HttpHeadersImpl h = createNewResponseHeaders();
- h.addHeader(":status", "200");
- ResponseHeaders oh = new ResponseHeaders(h);
+ HttpHeadersBuilder hb = createNewHeadersBuilder();
+ hb.addHeader(":status", "200");
+ ResponseHeaders oh = new ResponseHeaders(hb.build());
oh.streamid(streamid);
oh.setFlag(HeaderFrame.END_HEADERS);
return oh;
@@ -1047,9 +1108,9 @@
// for the hashmap.
static class ResponseHeaders extends Http2Frame {
- HttpHeadersImpl headers;
+ HttpHeaders headers;
- ResponseHeaders(HttpHeadersImpl headers) {
+ ResponseHeaders(HttpHeaders headers) {
super(0, 0);
this.headers = headers;
}
--- a/test/jdk/java/net/httpclient/http2/server/OutgoingPushPromise.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/http2/server/OutgoingPushPromise.java Wed Jun 20 09:05:57 2018 -0700
@@ -21,22 +21,22 @@
* questions.
*/
-import java.io.*;
-import java.net.*;
-import jdk.internal.net.http.common.HttpHeadersImpl;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.http.HttpHeaders;
import jdk.internal.net.http.frame.Http2Frame;
// will be converted to a PushPromiseFrame in the writeLoop
// a thread is then created to produce the DataFrames from the InputStream
class OutgoingPushPromise extends Http2Frame {
- final HttpHeadersImpl headers;
+ final HttpHeaders headers;
final URI uri;
final InputStream is;
final int parentStream; // not the pushed streamid
public OutgoingPushPromise(int parentStream,
URI uri,
- HttpHeadersImpl headers,
+ HttpHeaders headers,
InputStream is) {
super(0,0);
this.uri = uri;
--- a/test/jdk/java/net/httpclient/http2/server/PushHandler.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/http2/server/PushHandler.java Wed Jun 20 09:05:57 2018 -0700
@@ -23,11 +23,16 @@
import java.io.*;
import java.net.*;
+import java.net.http.HttpHeaders;
import java.nio.file.*;
-import jdk.internal.net.http.common.HttpHeadersImpl;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiPredicate;
public class PushHandler implements Http2Handler {
+ static final BiPredicate<String,String> ACCEPT_ALL = (x, y) -> true;
+
final Path tempFile;
final int loops;
final long file_size;
@@ -50,8 +55,8 @@
for (int i=0; i<loops; i++) {
InputStream is = new FileInputStream(tempFile.toFile());
URI u = requestURI.resolve("/x/y/z/" + Integer.toString(i));
- HttpHeadersImpl h = new HttpHeadersImpl();
- h.addHeader("X-foo", "bar");
+ HttpHeaders h = HttpHeaders.of(Map.of("X-foo", List.of("bar")),
+ ACCEPT_ALL);
ee.serverPush(u, h, is);
}
System.err.println ("Server: sent all pushes");
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/noPermissions.policy Wed Jun 20 09:05:57 2018 -0700
@@ -0,0 +1,24 @@
+//
+// Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+// DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+//
+// This code is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License version 2 only, as
+// published by the Free Software Foundation.
+//
+// This code is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+// version 2 for more details (a copy is included in the LICENSE file that
+// accompanied this code).
+//
+// You should have received a copy of the GNU General Public License version
+// 2 along with this work; if not, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+//
+// Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+// or visit www.oracle.com if you need additional information or have any
+// questions.
+//
+
+grant codeBase "file:${test.classes}/*" { };
--- a/test/jdk/java/net/httpclient/offline/FixedHttpHeaders.java Wed Jun 20 17:15:16 2018 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,60 +0,0 @@
-/*
- * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.TreeMap;
-import java.net.http.HttpHeaders;
-
-/**
- * An HttpHeaders consisting of the given name value pairs.
- */
-public class FixedHttpHeaders extends HttpHeaders {
-
- private final Map<String, List<String>> map =
- new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-
- @Override
- public Map<String, List<String>> map() {
- return map;
- }
-
- /**
- * Creates an HttpHeaders of the given name value pairs.
- */
- public static HttpHeaders of(String... params) {
- Objects.requireNonNull(params);
- if ((params.length & 0x1) != 0)
- throw new IllegalArgumentException("odd number of params");
- FixedHttpHeaders headers = new FixedHttpHeaders();
- for (int i = 0; i < params.length; i += 2) {
- String name = params[i];
- String value = params[i + 1];
- headers.map.computeIfAbsent(name, k -> new ArrayList<>(1))
- .add(value);
- }
- return headers;
- }
-}
--- a/test/jdk/java/net/httpclient/offline/OfflineTesting.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/offline/OfflineTesting.java Wed Jun 20 09:05:57 2018 -0700
@@ -24,19 +24,26 @@
/*
* @test
* @summary Demonstrates how to achieve testing without network connections
- * @build FixedHttpHeaders DelegatingHttpClient FixedHttpResponse FixedResponseHttpClient
+ * @build DelegatingHttpClient FixedHttpResponse FixedResponseHttpClient
* @run testng/othervm OfflineTesting
*/
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
+import java.net.http.HttpHeaders;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiPredicate;
import org.testng.annotations.Test;
+import static java.lang.String.format;
import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Objects.requireNonNull;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;
@@ -49,8 +56,8 @@
return FixedResponseHttpClient.createClientFrom(
HttpClient.newBuilder(),
200,
- FixedHttpHeaders.of("Server", "nginx",
- "Content-Type", "text/html"),
+ headersOf("Server", "nginx",
+ "Content-Type", "text/html"),
"A response message");
}
@@ -94,11 +101,11 @@
HttpClient client = FixedResponseHttpClient.createClientFrom(
HttpClient.newBuilder(),
404,
- FixedHttpHeaders.of("Connection", "keep-alive",
- "Content-Length", "162",
- "Content-Type", "text/html",
- "Date", "Mon, 15 Jan 2018 15:01:16 GMT",
- "Server", "nginx"),
+ headersOf("Connection", "keep-alive",
+ "Content-Length", "162",
+ "Content-Type", "text/html",
+ "Date", "Mon, 15 Jan 2018 15:01:16 GMT",
+ "Server", "nginx"),
"<html>\n" +
"<head><title>404 Not Found</title></head>\n" +
"<body bgcolor=\"white\">\n" +
@@ -126,7 +133,7 @@
HttpClient client = FixedResponseHttpClient.createEchoClient(
HttpClient.newBuilder(),
200,
- FixedHttpHeaders.of("Connection", "keep-alive"));
+ headersOf("Connection", "keep-alive"));
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://openjdk.java.net/echo"))
@@ -146,7 +153,7 @@
HttpClient client = FixedResponseHttpClient.createEchoClient(
HttpClient.newBuilder(),
200,
- FixedHttpHeaders.of("Connection", "keep-alive"));
+ headersOf("Connection", "keep-alive"));
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://openjdk.java.net/echo"))
@@ -158,4 +165,26 @@
assertEquals(response.statusCode(), 200);
assertEquals(response.body(), "Hello chegar!!");
}
+
+ // ---
+
+ public static IllegalArgumentException newIAE(String message, Object... args) {
+ return new IllegalArgumentException(format(message, args));
+ }
+
+ static final BiPredicate<String,String> ACCEPT_ALL = (x, y) -> true;
+
+ static HttpHeaders headersOf(String... params) {
+ Map<String,List<String>> map = new HashMap<>();
+ requireNonNull(params);
+ if (params.length == 0 || params.length % 2 != 0) {
+ throw newIAE("wrong number, %d, of parameters", params.length);
+ }
+ for (int i = 0; i < params.length; i += 2) {
+ String name = params[i];
+ String value = params[i + 1];
+ map.put(name, List.of(value));
+ }
+ return HttpHeaders.of(map, ACCEPT_ALL);
+ }
}
--- a/test/jdk/java/net/httpclient/ssltest/CertificateTest.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/ssltest/CertificateTest.java Wed Jun 20 09:05:57 2018 -0700
@@ -22,6 +22,7 @@
*/
import java.io.File;
+import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpResponse.BodyHandlers;
@@ -30,6 +31,7 @@
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLParameters;
+import static java.net.http.HttpClient.Builder.NO_PROXY;
/*
* @test
@@ -112,6 +114,7 @@
Exception exception = null;
System.out.println("Making request to " + uri_s);
HttpClient client = HttpClient.newBuilder()
+ .proxy(NO_PROXY)
.sslContext(ctx)
.sslParameters(params)
.build();
@@ -128,7 +131,9 @@
error = "Test failed: good: status should be 200";
else if (!expectSuccess)
error = "Test failed: bad: status should not be 200";
- } catch (SSLException e) {
+ } catch (IOException e) {
+ // there must be an SSLException as the exception or cause
+ checkExceptionOrCause(SSLException.class, e);
System.err.println("Caught Exception " + e + ". expectSuccess = " + expectSuccess);
exception = e;
if (expectSuccess)
@@ -137,4 +142,16 @@
if (error != null)
throw new RuntimeException(error, exception);
}
+
+ static void checkExceptionOrCause(Class<? extends Throwable> clazz, Throwable t) {
+ final Throwable original = t;
+ do {
+ if (clazz.isInstance(t)) {
+ System.out.println("Found expected exception/cause: " + t);
+ return; // found
+ }
+ } while ((t = t.getCause()) != null);
+ original.printStackTrace(System.out);
+ throw new RuntimeException("Expected " + clazz + "in " + original);
+ }
}
--- a/test/jdk/java/net/httpclient/websocket/BlowupOutputQueue.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/websocket/BlowupOutputQueue.java Wed Jun 20 09:05:57 2018 -0700
@@ -87,6 +87,11 @@
listener = new MockListener() {
@Override
+ protected void onOpen0(WebSocket webSocket) {
+ /* do nothing */
+ }
+
+ @Override
protected void replenish(WebSocket webSocket) {
/* do nothing */
}
--- a/test/jdk/java/net/httpclient/websocket/MockListener.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/websocket/MockListener.java Wed Jun 20 09:05:57 2018 -0700
@@ -39,11 +39,15 @@
private final Predicate<? super Invocation> collectUntil;
public MockListener() {
- this(i -> i instanceof OnClose || i instanceof OnError);
+ this(1, MockListener::closeOrError);
+ }
+
+ public MockListener(long bufferSize) {
+ this(bufferSize, MockListener::closeOrError);
}
public MockListener(Predicate<? super Invocation> collectUntil) {
- this(2, collectUntil);
+ this(1, collectUntil);
}
/*
@@ -59,6 +63,10 @@
this.collectUntil = collectUntil;
}
+ private static boolean closeOrError(Invocation i) {
+ return i instanceof OnClose || i instanceof OnError;
+ }
+
@Override
public void onOpen(WebSocket webSocket) {
System.out.printf("onOpen(%s)%n", webSocket);
@@ -73,7 +81,9 @@
}
protected void onOpen0(WebSocket webSocket) {
- replenish(webSocket);
+ count = bufferSize - bufferSize / 2;
+ System.out.printf("request(%d)%n", bufferSize);
+ webSocket.request(bufferSize);
}
@Override
@@ -209,8 +219,9 @@
protected void replenish(WebSocket webSocket) {
if (--count <= 0) {
count = bufferSize - bufferSize / 2;
+ webSocket.request(count);
+ System.out.printf("request(%d)%n", count);
}
- webSocket.request(count);
}
public abstract static class Invocation {
--- a/test/jdk/java/net/httpclient/websocket/WebSocketTest.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/websocket/WebSocketTest.java Wed Jun 20 09:05:57 2018 -0700
@@ -25,11 +25,11 @@
* @test
* @build DummyWebSocketServer
* @run testng/othervm
- * -Djdk.internal.httpclient.websocket.debug=true
* WebSocketTest
*/
import org.testng.annotations.AfterTest;
+import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import java.io.IOException;
@@ -41,7 +41,10 @@
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Supplier;
import java.util.stream.Collectors;
+
import static java.net.http.HttpClient.Builder.NO_PROXY;
import static java.net.http.HttpClient.newBuilder;
import static java.net.http.WebSocket.NORMAL_CLOSURE;
@@ -197,6 +200,126 @@
assertFails(IOE, webSocket.sendPong(ByteBuffer.allocate(0)));
}
+ @DataProvider(name = "sequence")
+ public Object[][] data1() {
+ int[] CLOSE = {
+ 0x81, 0x00, // ""
+ 0x82, 0x00, // []
+ 0x89, 0x00, // <PING>
+ 0x8a, 0x00, // <PONG>
+ 0x88, 0x00, // <CLOSE>
+ };
+ int[] ERROR = {
+ 0x81, 0x00, // ""
+ 0x82, 0x00, // []
+ 0x89, 0x00, // <PING>
+ 0x8a, 0x00, // <PONG>
+ 0x8b, 0x00, // 0xB control frame (causes an error)
+ };
+ return new Object[][]{
+ {CLOSE, 1},
+ {CLOSE, 3},
+ {CLOSE, 4},
+ {CLOSE, Long.MAX_VALUE},
+ {ERROR, 1},
+ {ERROR, 3},
+ {ERROR, 4},
+ {ERROR, Long.MAX_VALUE},
+ };
+ }
+
+ @Test(dataProvider = "sequence")
+ public void listenerSequentialOrder(int[] binary, long requestSize)
+ throws IOException
+ {
+
+ server = Support.serverWithCannedData(binary);
+ server.open();
+
+ CompletableFuture<Void> violation = new CompletableFuture<>();
+
+ MockListener listener = new MockListener(requestSize) {
+
+ final AtomicBoolean guard = new AtomicBoolean();
+
+ private <T> T checkRunExclusively(Supplier<T> action) {
+ if (guard.getAndSet(true)) {
+ violation.completeExceptionally(new RuntimeException());
+ }
+ try {
+ return action.get();
+ } finally {
+ if (!guard.getAndSet(false)) {
+ violation.completeExceptionally(new RuntimeException());
+ }
+ }
+ }
+
+ @Override
+ public void onOpen(WebSocket webSocket) {
+ checkRunExclusively(() -> {
+ super.onOpen(webSocket);
+ return null;
+ });
+ }
+
+ @Override
+ public CompletionStage<?> onText(WebSocket webSocket,
+ CharSequence data,
+ boolean last) {
+ return checkRunExclusively(
+ () -> super.onText(webSocket, data, last));
+ }
+
+ @Override
+ public CompletionStage<?> onBinary(WebSocket webSocket,
+ ByteBuffer data,
+ boolean last) {
+ return checkRunExclusively(
+ () -> super.onBinary(webSocket, data, last));
+ }
+
+ @Override
+ public CompletionStage<?> onPing(WebSocket webSocket,
+ ByteBuffer message) {
+ return checkRunExclusively(
+ () -> super.onPing(webSocket, message));
+ }
+
+ @Override
+ public CompletionStage<?> onPong(WebSocket webSocket,
+ ByteBuffer message) {
+ return checkRunExclusively(
+ () -> super.onPong(webSocket, message));
+ }
+
+ @Override
+ public CompletionStage<?> onClose(WebSocket webSocket,
+ int statusCode,
+ String reason) {
+ return checkRunExclusively(
+ () -> super.onClose(webSocket, statusCode, reason));
+ }
+
+ @Override
+ public void onError(WebSocket webSocket, Throwable error) {
+ checkRunExclusively(() -> {
+ super.onError(webSocket, error);
+ return null;
+ });
+ }
+ };
+
+ webSocket = newBuilder().proxy(NO_PROXY).build().newWebSocketBuilder()
+ .buildAsync(server.getURI(), listener)
+ .join();
+
+
+ listener.invocations();
+ violation.complete(null); // won't affect if completed exceptionally
+ violation.join();
+ }
+
@Test
public void sendMethodsThrowIOE2() throws Exception {
server = Support.serverWithCannedData(0x88, 0x00);
--- a/test/jdk/java/net/httpclient/whitebox/ConnectionPoolTestDriver.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/whitebox/ConnectionPoolTestDriver.java Wed Jun 20 09:05:57 2018 -0700
@@ -31,5 +31,8 @@
* java.management
* @run main/othervm
* --add-reads java.net.http=java.management
- * java.net.http/jdk.internal.net.http.ConnectionPoolTest
+ * java.net.http/jdk.internal.net.http.ConnectionPoolTest testCacheCleaners
+ * @run main/othervm
+ * --add-reads java.net.http=java.management
+ * java.net.http/jdk.internal.net.http.ConnectionPoolTest testPoolSize
*/
--- a/test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/AuthenticationFilterTest.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/AuthenticationFilterTest.java Wed Jun 20 09:05:57 2018 -0700
@@ -23,7 +23,7 @@
package jdk.internal.net.http;
-import jdk.internal.net.http.common.HttpHeadersImpl;
+import jdk.internal.net.http.common.HttpHeadersBuilder;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import org.testng.annotations.AfterClass;
@@ -35,17 +35,19 @@
import java.net.ProxySelector;
import java.net.URI;
import java.net.URL;
-import java.net.http.HttpClient;
import java.net.http.HttpHeaders;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.security.AccessController;
import java.util.Arrays;
import java.util.Base64;
+import java.util.Collections;
+import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLong;
import java.net.http.HttpClient.Version;
+import java.util.function.BiPredicate;
import static java.lang.String.format;
import static java.lang.System.out;
@@ -163,6 +165,8 @@
});
}
+ static final BiPredicate<String,String> ACCEPT_ALL = (x, y) -> true;
+
private void doTestAuthentication(String uri, Version v, String proxy) throws Exception {
int colon = proxy == null ? -1 : proxy.lastIndexOf(":");
ProxySelector ps = proxy == null ? NO_PROXY
@@ -197,8 +201,9 @@
Exchange<?> exchange = new Exchange<>(req, multi);
out.println("\nSimulating unauthenticated request to " + uri);
filter.request(req, multi);
- assertFalse(req.getSystemHeaders().firstValue(authorization(true)).isPresent());
- assertFalse(req.getSystemHeaders().firstValue(authorization(false)).isPresent());
+ HttpHeaders hdrs = req.getSystemHeadersBuilder().build();
+ assertFalse(hdrs.firstValue(authorization(true)).isPresent());
+ assertFalse(hdrs.firstValue(authorization(false)).isPresent());
assertEquals(authenticator.COUNTER.get(), 0);
// Creates the Response to the first request, and call filter.response
@@ -207,9 +212,10 @@
// credentials, and will also cache the credentials in the multi exchange.
// The credentials shouldn't be put in the cache until the 200 response
// for that request arrives.
- HttpHeadersImpl headers = new HttpHeadersImpl();
- headers.addHeader(authenticate(proxy!=null),
- "Basic realm=\"earth\"");
+ HttpHeadersBuilder headersBuilder = new HttpHeadersBuilder();
+ headersBuilder.addHeader(authenticate(proxy!=null),
+ "Basic realm=\"earth\"");
+ HttpHeaders headers = headersBuilder.build();
Response response = new Response(req, exchange, headers, null, unauthorized, v);
out.println("Simulating " + unauthorized
+ " response from " + uri);
@@ -218,7 +224,7 @@
out.println("Checking filter's response to "
+ unauthorized + " from " + uri);
assertTrue(next != null, "next should not be null");
- String[] up = check(reqURI, next.getSystemHeaders(), proxy);
+ String[] up = check(reqURI, next.getSystemHeadersBuilder().build(), proxy);
assertEquals(authenticator.COUNTER.get(), 1);
// Now simulate a new successful exchange to get the credentials in the cache
@@ -230,10 +236,12 @@
exchange = new Exchange<>(next, multi);
filter.request(next, multi);
out.println("Checking credentials in request header after filter for " + uri);
- check(reqURI, next.getSystemHeaders(), proxy);
- check(next.uri(), next.getSystemHeaders(), proxy);
+ hdrs = next.getSystemHeadersBuilder().build();
+ check(reqURI, hdrs, proxy);
+ check(next.uri(), hdrs, proxy);
out.println("Simulating successful response 200 from " + uri);
- response = new Response(next, exchange, new HttpHeadersImpl(), null, 200, v);
+ HttpHeaders h = HttpHeaders.of(Collections.emptyMap(), ACCEPT_ALL);
+ response = new Response(next, exchange,h, null, 200, v);
next = filter.response(response);
assertTrue(next == null, "next should be null");
assertEquals(authenticator.COUNTER.get(), 1);
@@ -263,7 +271,7 @@
filter.request(req2, multi2);
out.println("Check that filter has added credentials from cache for " + reqURI2
+ " with proxy " + req2.proxy());
- String[] up2 = check(reqURI, req2.getSystemHeaders(), proxy);
+ String[] up2 = check(reqURI, req2.getSystemHeadersBuilder().build(), proxy);
assertTrue(Arrays.deepEquals(up, up2), format("%s:%s != %s:%s", up2[0], up2[1], up[0], up[1]));
assertEquals(authenticator.COUNTER.get(), 1);
@@ -293,24 +301,25 @@
HttpResponse.BodyHandlers.replacing(null),
null, AccessController.getContext());
filter.request(req3, multi3);
+ HttpHeaders h3 = req3.getSystemHeadersBuilder().build();
if (proxy == null) {
out.println("Check that filter has not added proxy credentials from cache for " + reqURI3);
- assert !req3.getSystemHeaders().firstValue(authorization(true)).isPresent()
+ assert !h3.firstValue(authorization(true)).isPresent()
: format("Unexpected proxy credentials found: %s",
- java.util.stream.Stream.of(getAuthorization(req3.getSystemHeaders(), true))
+ java.util.stream.Stream.of(getAuthorization(req3.getSystemHeadersBuilder().build(), true))
.collect(joining(":")));
- assertFalse(req3.getSystemHeaders().firstValue(authorization(true)).isPresent());
+ assertFalse(h3.firstValue(authorization(true)).isPresent());
} else {
out.println("Check that filter has added proxy credentials from cache for " + reqURI3);
- String[] up3 = check(reqURI, req3.getSystemHeaders(), proxy);
+ String[] up3 = check(reqURI, h3, proxy);
assertTrue(Arrays.deepEquals(up, up3), format("%s:%s != %s:%s", up3[0], up3[1], up[0], up[1]));
}
out.println("Check that filter has not added server credentials from cache for " + reqURI3);
- assert !req3.getSystemHeaders().firstValue(authorization(false)).isPresent()
+ assert !h3.firstValue(authorization(false)).isPresent()
: format("Unexpected server credentials found: %s",
- java.util.stream.Stream.of(getAuthorization(req3.getSystemHeaders(), false))
+ java.util.stream.Stream.of(getAuthorization(h3, false))
.collect(joining(":")));
- assertFalse(req3.getSystemHeaders().firstValue(authorization(false)).isPresent());
+ assertFalse(h3.firstValue(authorization(false)).isPresent());
assertEquals(authenticator.COUNTER.get(), 1);
// Now we will verify that credentials for proxies are not used for servers and
@@ -341,23 +350,24 @@
filter.request(req4, multi4);
out.println("Check that filter has not added proxy credentials from cache for "
+ reqURI4 + " (proxy: " + req4.proxy() + ")");
- assert !req4.getSystemHeaders().firstValue(authorization(true)).isPresent()
+ HttpHeaders h4 = req4.getSystemHeadersBuilder().build();
+ assert !h4.firstValue(authorization(true)).isPresent()
: format("Unexpected proxy credentials found: %s",
- java.util.stream.Stream.of(getAuthorization(req4.getSystemHeaders(), true))
+ java.util.stream.Stream.of(getAuthorization(h4, true))
.collect(joining(":")));
- assertFalse(req4.getSystemHeaders().firstValue(authorization(true)).isPresent());
+ assertFalse(h4.firstValue(authorization(true)).isPresent());
if (proxy != null) {
out.println("Check that filter has not added server credentials from cache for "
+ reqURI4 + " (proxy: " + req4.proxy() + ")");
- assert !req4.getSystemHeaders().firstValue(authorization(false)).isPresent()
+ assert !h4.firstValue(authorization(false)).isPresent()
: format("Unexpected server credentials found: %s",
- java.util.stream.Stream.of(getAuthorization(req4.getSystemHeaders(), false))
+ java.util.stream.Stream.of(getAuthorization(h4, false))
.collect(joining(":")));
- assertFalse(req4.getSystemHeaders().firstValue(authorization(false)).isPresent());
+ assertFalse(h4.firstValue(authorization(false)).isPresent());
} else {
out.println("Check that filter has added server credentials from cache for "
+ reqURI4 + " (proxy: " + req4.proxy() + ")");
- String[] up4 = check(reqURI, req4.getSystemHeaders(), proxy);
+ String[] up4 = check(reqURI, h4, proxy);
assertTrue(Arrays.deepEquals(up, up4), format("%s:%s != %s:%s", up4[0], up4[1], up[0], up[1]));
}
assertEquals(authenticator.COUNTER.get(), 1);
@@ -381,24 +391,26 @@
filter.request(req5, multi5);
out.println("Check that filter has not added server credentials from cache for "
+ reqURI + " (proxy: " + req5.proxy() + ")");
- assert !req5.getSystemHeaders().firstValue(authorization(false)).isPresent()
+ HttpHeaders h5 = req5.getSystemHeadersBuilder().build();
+ assert !h5.firstValue(authorization(false)).isPresent()
: format("Unexpected server credentials found: %s",
- java.util.stream.Stream.of(getAuthorization(req5.getSystemHeaders(), false))
+ java.util.stream.Stream.of(getAuthorization(h5, false))
.collect(joining(":")));
- assertFalse(req5.getSystemHeaders().firstValue(authorization(false)).isPresent());
+ assertFalse(h5.firstValue(authorization(false)).isPresent());
out.println("Check that filter has not added proxy credentials from cache for "
+ reqURI + " (proxy: " + req5.proxy() + ")");
- assert !req5.getSystemHeaders().firstValue(authorization(true)).isPresent()
+ assert !h5.firstValue(authorization(true)).isPresent()
: format("Unexpected proxy credentials found: %s",
- java.util.stream.Stream.of(getAuthorization(req5.getSystemHeaders(), true))
+ java.util.stream.Stream.of(getAuthorization(h5, true))
.collect(joining(":")));
- assertFalse(req5.getSystemHeaders().firstValue(authorization(true)).isPresent());
+ assertFalse(h5.firstValue(authorization(true)).isPresent());
assertEquals(authenticator.COUNTER.get(), 1);
// Now simulate a 401 response from the server
- HttpHeadersImpl headers5 = new HttpHeadersImpl();
- headers5.addHeader(authenticate(false),
- "Basic realm=\"earth\"");
+ HttpHeadersBuilder headers5Builder = new HttpHeadersBuilder();
+ headers5Builder.addHeader(authenticate(false),
+ "Basic realm=\"earth\"");
+ HttpHeaders headers5 = headers5Builder.build();
unauthorized = 401;
Response response5 = new Response(req5, exchange5, headers5, null, unauthorized, v);
out.println("Simulating " + unauthorized
@@ -409,12 +421,13 @@
out.println("Checking filter's response to "
+ unauthorized + " from " + uri);
assertTrue(next5 != null, "next5 should not be null");
- String[] up5 = check(reqURI, next5.getSystemHeaders(), null);
+ String[] up5 = check(reqURI, next5.getSystemHeadersBuilder().build(), null);
// now simulate a 200 response from the server
exchange5 = new Exchange<>(next5, multi5);
filter.request(next5, multi5);
- response5 = new Response(next5, exchange5, new HttpHeadersImpl(), null, 200, v);
+ h = HttpHeaders.of(Map.of(), ACCEPT_ALL);
+ response5 = new Response(next5, exchange5, h, null, 200, v);
filter.response(response5);
assertEquals(authenticator.COUNTER.get(), 2);
@@ -432,10 +445,11 @@
filter.request(req6, multi6);
out.println("Check that filter has added server credentials from cache for "
+ reqURI + " (proxy: " + req6.proxy() + ")");
- check(reqURI, req6.getSystemHeaders(), null);
+ HttpHeaders h6 = req6.getSystemHeadersBuilder().build();
+ check(reqURI, h6, null);
out.println("Check that filter has added proxy credentials from cache for "
+ reqURI + " (proxy: " + req6.proxy() + ")");
- String[] up6 = check(reqURI, req6.getSystemHeaders(), proxy);
+ String[] up6 = check(reqURI, h6, proxy);
assertTrue(Arrays.deepEquals(up, up6), format("%s:%s != %s:%s", up6[0], up6[1], up[0], up[1]));
assertEquals(authenticator.COUNTER.get(), 2);
}
@@ -456,18 +470,19 @@
out.println("Check that filter has not added server credentials from cache for "
+ reqURI7 + " (proxy: " + req7.proxy() + ") [resolved uri: "
+ reqURI7.resolve(".") + " should not match " + reqURI.resolve(".") + "]");
- assert !req7.getSystemHeaders().firstValue(authorization(false)).isPresent()
+ HttpHeaders h7 = req7.getSystemHeadersBuilder().build();
+ assert !h7.firstValue(authorization(false)).isPresent()
: format("Unexpected server credentials found: %s",
- java.util.stream.Stream.of(getAuthorization(req7.getSystemHeaders(), false))
+ java.util.stream.Stream.of(getAuthorization(h7, false))
.collect(joining(":")));
- assertFalse(req7.getSystemHeaders().firstValue(authorization(false)).isPresent());
+ assertFalse(h7.firstValue(authorization(false)).isPresent());
out.println("Check that filter has not added proxy credentials from cache for "
+ reqURI7 + " (proxy: " + req7.proxy() + ")");
- assert !req7.getSystemHeaders().firstValue(authorization(true)).isPresent()
+ assert !h7.firstValue(authorization(true)).isPresent()
: format("Unexpected proxy credentials found: %s",
- java.util.stream.Stream.of(getAuthorization(req7.getSystemHeaders(), true))
+ java.util.stream.Stream.of(getAuthorization(h7, true))
.collect(joining(":")));
- assertFalse(req7.getSystemHeaders().firstValue(authorization(true)).isPresent());
+ assertFalse(h7.firstValue(authorization(true)).isPresent());
assertEquals(authenticator.COUNTER.get(), 1);
}
--- a/test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/ConnectionPoolTest.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/ConnectionPoolTest.java Wed Jun 20 09:05:57 2018 -0700
@@ -31,6 +31,7 @@
import java.net.ProxySelector;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
+import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.Random;
@@ -67,14 +68,24 @@
}
public static void main(String[] args) throws Exception {
- testCacheCleaners();
+ if (args.length == 0) {
+ args = new String[] {"testCacheCleaners"};
+ }
+ for (String arg : args) {
+ if ("testCacheCleaners".equals(arg)) {
+ testCacheCleaners();
+ } else if ("testPoolSize".equals(arg)) {
+ assert args.length == 1 : "testPoolSize should be run in its own VM";
+ testPoolSize();
+ }
+ }
}
public static void testCacheCleaners() throws Exception {
ConnectionPool pool = new ConnectionPool(666);
HttpClient client = new HttpClientStub(pool);
InetSocketAddress proxy = InetSocketAddress.createUnresolved("bar", 80);
- System.out.println("Adding 10 connections to pool");
+ System.out.println("Adding 20 connections to pool");
Random random = new Random();
final int count = 20;
@@ -146,6 +157,74 @@
}
}
+ public static void testPoolSize() throws Exception {
+ final int MAX_POOL_SIZE = 10;
+ System.setProperty("jdk.httpclient.connectionPoolSize",
+ String.valueOf(MAX_POOL_SIZE));
+ ConnectionPool pool = new ConnectionPool(666);
+ HttpClient client = new HttpClientStub(pool);
+ InetSocketAddress proxy = InetSocketAddress.createUnresolved("bar", 80);
+ System.out.println("Adding 20 connections to pool");
+ Random random = new Random();
+
+ final int count = 20;
+ Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS);
+ int[] keepAlives = new int[count];
+ HttpConnectionStub[] connections = new HttpConnectionStub[count];
+ long purge = pool.purgeExpiredConnectionsAndReturnNextDeadline(now);
+ long expected = 0;
+ if (purge != expected) {
+ throw new RuntimeException("Bad purge delay: " + purge
+ + ", expected " + expected);
+ }
+ expected = Long.MAX_VALUE;
+ int previous = 0;
+ for (int i=0; i<count; i++) {
+ InetSocketAddress addr = InetSocketAddress.createUnresolved("foo"+i, 80);
+ keepAlives[i] = random.nextInt(10) * 10 + 5 + previous;
+ previous = keepAlives[i];
+ connections[i] = new HttpConnectionStub(client, addr, proxy, true);
+ System.out.println("Adding connection: " + now
+ + " keepAlive: " + keepAlives[i]
+ + " /" + connections[i]);
+ pool.returnToPool(connections[i], now, keepAlives[i]);
+ if (i < MAX_POOL_SIZE) {
+ expected = Math.min(expected, keepAlives[i] * 1000);
+ } else {
+ expected = keepAlives[i-MAX_POOL_SIZE+1] * 1000;
+ if (pool.contains(connections[i-MAX_POOL_SIZE])) {
+ throw new RuntimeException("Connection[" + i + "]/"
+ + connections[i] + " should have been removed");
+ }
+ }
+ purge = pool.purgeExpiredConnectionsAndReturnNextDeadline(now);
+ if (purge != expected) {
+ throw new RuntimeException("Bad purge delay for " + i + ": "
+ + purge + ", expected " + expected);
+ }
+ }
+
+ long opened = java.util.stream.Stream.of(connections)
+ .filter(HttpConnectionStub::connected).count();
+ if (opened != MAX_POOL_SIZE) {
+ throw new RuntimeException("Opened: expected "
+ + count + " got " + opened);
+ }
+ for (int i=0 ; i<count; i++) {
+ boolean closed = (i < count - MAX_POOL_SIZE);
+ if (connections[i].closed != closed) {
+ throw new RuntimeException("connection[" + i + "] should be "
+ + (closed ? "closed" : "opened"));
+ }
+ if (pool.contains(connections[i]) == closed) {
+ throw new RuntimeException("Connection[" + i + "]/"
+ + connections[i] + " should "
+ + (closed ? "" : "not ")
+ + "have been removed");
+ }
+ }
+ }
+
static <T> T error() {
throw new InternalError("Should not reach here: wrong test assumptions!");
}
--- a/test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/Http1HeaderParserTest.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/Http1HeaderParserTest.java Wed Jun 20 09:05:57 2018 -0700
@@ -32,6 +32,8 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.IntStream;
import sun.net.www.MessageHeader;
import org.testng.annotations.Test;
@@ -132,6 +134,11 @@
"Connection: keep-alive\r\n\r\n"
};
Arrays.stream(basic).forEach(responses::add);
+ // add some tests where some of the CRLF are replaced
+ // by a single LF
+ Arrays.stream(basic)
+ .map(Http1HeaderParserTest::mixedCRLF)
+ .forEach(responses::add);
String[] foldingTemplate =
{ "HTTP/1.1 200 OK\r\n" +
@@ -187,6 +194,12 @@
for (String template : foldingTemplate)
responses.add(template.replace("$NEWLINE", newLineChar));
}
+ // add some tests where some of the CRLF are replaced
+ // by a single LF
+ for (String newLineChar : new String[] { "\n", "\r", "\r\n" }) {
+ for (String template : foldingTemplate)
+ responses.add(mixedCRLF(template).replace("$NEWLINE", newLineChar));
+ }
String[] bad = // much of this is to retain parity with legacy MessageHeaders
{ "HTTP/1.1 200 OK\r\n" +
@@ -237,20 +250,77 @@
return responses.stream().map(p -> new Object[] { p }).toArray(Object[][]::new);
}
+ static final AtomicInteger index = new AtomicInteger();
+ static final AtomicInteger limit = new AtomicInteger(1);
+ static final AtomicBoolean useCRLF = new AtomicBoolean();
+ // A small method to replace part of the CRLF present in a string
+ // with simple LF. The method uses a deterministic algorithm based
+ // on current values of static index/limit/useCRLF counters.
+ // These counters are used to produce a stream of substitutes that
+ // looks like this:
+ // LF CRLF LF LF CRLF CRLF LF LF LF CRLF CRLF CRLF (then repeat from start)
+ static final String mixedCRLF(String headers) {
+ int next;
+ int start = 0;
+ int last = headers.lastIndexOf("\r\n");
+ String prev = "";
+ StringBuilder res = new StringBuilder();
+ while ((next = headers.indexOf("\r\n", start)) > 0) {
+ res.append(headers.substring(start, next));
+ if ("\n".equals(prev) && next == last) {
+ // for some reason the legacy MessageHeader parser will
+ // not consume the final LF if the headers are terminated
+ // by <LF><CRLF> instead of <CRLF><CRLF>. It consume
+ // <LF><CR> but leaves the last <LF> in the stream.
+ // Here we just make sure to avoid using <LF><CRLF>
+ // as that would cause the legacy parser to consume
+ // 1 byte less than the Http1HeadersParser - which
+ // does consume the last <LF>, as it should.
+ // if this is the last CRLF and the previous one
+ // was replaced by LF then use LF.
+ res.append(prev);
+ } else {
+ prev = useCRLF.get() ? "\r\n" : "\n";
+ res.append(prev);
+ }
+ // skip CRLF
+ start = next + 2;
+
+ // The idea is to substitute some of the CRLF with LF.
+ // Rather than doing this randomly, always use the following
+ // sequence:
+ // LF CRLF LF LF CRLF CRLF LF LF LF CRLF CRLF CRLF
+ index.incrementAndGet();
+ if (index.get() == limit.get()) {
+ index.set(0);
+ if (useCRLF.get()) limit.incrementAndGet();
+ if (limit.get() > 3) limit.set(1);
+ useCRLF.set(!useCRLF.get());
+ }
+ }
+ res.append(headers.substring(start));
+ return res.toString();
+ }
+
+
@Test(dataProvider = "responses")
public void verifyHeaders(String respString) throws Exception {
+ System.out.println("\ntesting:\n\t" + respString
+ .replace("\r\n", "<CRLF>")
+ .replace("\r", "<CR>")
+ .replace("\n","<LF>")
+ .replace("LF>", "LF>\n\t"));
byte[] bytes = respString.getBytes(US_ASCII);
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
MessageHeader m = new MessageHeader(bais);
Map<String,List<String>> messageHeaderMap = m.getHeaders();
- int available = bais.available();
+ int availableBytes = bais.available();
Http1HeaderParser decoder = new Http1HeaderParser();
ByteBuffer b = ByteBuffer.wrap(bytes);
decoder.parse(b);
+ System.out.printf("Http1HeaderParser parsed %d bytes out of %d%n", b.position(), bytes.length);
Map<String,List<String>> decoderMap1 = decoder.headers().map();
- assertEquals(available, b.remaining(),
- "stream available not equal to remaining");
// assert status-line
String statusLine1 = messageHeaderMap.get(null).get(0);
@@ -273,6 +343,9 @@
assertHeadersEqual(messageHeaderMap, decoderMap1,
"messageHeaderMap not equal to decoderMap1");
+ assertEquals(availableBytes, b.remaining(),
+ String.format("stream available (%d) not equal to remaining (%d)",
+ availableBytes, b.remaining()));
// byte at a time
decoder = new Http1HeaderParser();
List<ByteBuffer> buffers = IntStream.range(0, bytes.length)
@@ -280,9 +353,10 @@
.collect(toList());
while (decoder.parse(buffers.remove(0)) != true);
Map<String,List<String>> decoderMap2 = decoder.headers().map();
- assertEquals(available, buffers.size(),
+ assertEquals(availableBytes, buffers.size(),
"stream available not equals to remaining buffers");
assertEquals(decoderMap1, decoderMap2, "decoder maps not equal");
+
}
@DataProvider(name = "errors")
--- a/test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/RawChannelTest.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/RawChannelTest.java Wed Jun 20 09:05:57 2018 -0700
@@ -47,6 +47,7 @@
import jdk.internal.net.http.websocket.WebSocketRequest;
import org.testng.annotations.Test;
import static java.net.http.HttpResponse.BodyHandlers.discarding;
+import static java.util.concurrent.TimeUnit.SECONDS;
import static org.testng.Assert.assertEquals;
/*
@@ -104,6 +105,7 @@
// tell the server we have read the initial bytes, so
// that it makes sure there is something for us to
// read next in case the initialBytes have already drained the
+
// channel dry.
initialReadStall.countDown();
@@ -210,11 +212,19 @@
HttpRequest req = HttpRequest.newBuilder(uri).build();
// Switch on isWebSocket flag to prevent the connection from
// being returned to the pool.
- ((WebSocketRequest)req).isWebSocket(true);
HttpClient client = HttpClient.newHttpClient();
+ HttpClientImpl clientImpl = ((HttpClientFacade)client).impl;
+ HttpRequestImpl requestImpl = new HttpRequestImpl(req, null);
+ requestImpl.isWebSocket(true);
try {
- HttpResponse<?> r = client.send(req, discarding());
- r.body();
+ MultiExchange<Void> mex = new MultiExchange<>(req,
+ requestImpl,
+ clientImpl,
+ discarding(),
+ null,
+ null);
+ HttpResponse<?> r = mex.responseAsync(clientImpl.theExecutor())
+ .get(30, SECONDS);
return ((HttpResponseImpl) r).rawChannel();
} finally {
// Need to hold onto the client until the RawChannel is
--- a/test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/SelectorTest.java Wed Jun 20 17:15:16 2018 +0200
+++ b/test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/SelectorTest.java Wed Jun 20 09:05:57 2018 -0700
@@ -30,7 +30,6 @@
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import java.net.http.HttpClient;
-import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import org.testng.annotations.Test;
import jdk.internal.net.http.websocket.RawChannel;
@@ -150,11 +149,11 @@
static RawChannel getARawChannel(int port) throws Exception {
URI uri = URI.create("http://localhost:" + port + "/");
out.println("client connecting to " + uri.toString());
- HttpRequest req = HttpRequest.newBuilder(uri).build();
+ HttpRequestImpl req = new HttpRequestBuilderImpl(uri).buildForWebSocket();
// Otherwise HttpClient will think this is an ordinary connection and
// thus all ordinary procedures apply to it, e.g. it must be put into
// the cache
- ((HttpRequestImpl) req).isWebSocket(true);
+ req.isWebSocket(true);
HttpResponse<?> r = defaultClient().send(req, discarding());
r.body();
return ((HttpResponseImpl) r).rawChannel();
Binary file test/jdk/lib/testlibrary/jdk/testlibrary/testkeys has changed