8204679: HTTP Client refresh
Co-authored-by: Daniel Fuchs <daniel.fuchs@oracle.com> Co-authored-by: Michael McMahon <michael.x.mcmahon@oracle.com> Co-authored-by: Pavel Rappo <pavel.rappo@oracle.com> Reviewed-by: chegar, dfuchs, michaelm
This commit is contained in:
parent
8c5dfa21b3
commit
659fdd8dc4
@ -57,7 +57,7 @@ import jdk.internal.net.http.HttpClientBuilderImpl;
|
|||||||
* and can be used to send multiple requests.
|
* and can be used to send multiple requests.
|
||||||
*
|
*
|
||||||
* <p> An {@code HttpClient} provides configuration information, and resource
|
* <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
|
* <p> A {@link BodyHandler BodyHandler} must be supplied for each {@link
|
||||||
* HttpRequest} sent. The {@code BodyHandler} determines how to handle the
|
* HttpRequest} sent. The {@code BodyHandler} determines how to handle the
|
||||||
@ -232,11 +232,10 @@ public abstract class HttpClient {
|
|||||||
*
|
*
|
||||||
* <p> If this method is not invoked prior to {@linkplain #build()
|
* <p> If this method is not invoked prior to {@linkplain #build()
|
||||||
* building}, a default executor is created for each newly built {@code
|
* building}, a default executor is created for each newly built {@code
|
||||||
* HttpClient}. The default executor uses a {@linkplain
|
* HttpClient}.
|
||||||
* Executors#newCachedThreadPool(ThreadFactory) cached thread pool},
|
|
||||||
* with a custom thread factory.
|
|
||||||
*
|
*
|
||||||
* @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
|
* factory creates threads that run with an access control context that
|
||||||
* has no permissions.
|
* has no permissions.
|
||||||
*
|
*
|
||||||
@ -451,7 +450,7 @@ public abstract class HttpClient {
|
|||||||
* then the response, containing the {@code 3XX} response code, is returned,
|
* then the response, containing the {@code 3XX} response code, is returned,
|
||||||
* where it can be handled manually.
|
* 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}
|
* HttpClient.Builder#followRedirects(Redirect) Builder.followRedirects}
|
||||||
* method.
|
* method.
|
||||||
*
|
*
|
||||||
|
@ -25,62 +25,68 @@
|
|||||||
|
|
||||||
package java.net.http;
|
package java.net.http;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.OptionalLong;
|
import java.util.OptionalLong;
|
||||||
import static java.util.Collections.emptyList;
|
import java.util.TreeMap;
|
||||||
import static java.util.Collections.unmodifiableList;
|
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;
|
import static java.util.Objects.requireNonNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A read-only view of a set of HTTP headers.
|
* A read-only view of a set of HTTP headers.
|
||||||
*
|
*
|
||||||
* <p> An {@code HttpHeaders} is not created directly, but rather returned from
|
* <p> An {@code HttpHeaders} is not typically created directly, but rather
|
||||||
* an {@link HttpResponse HttpResponse}. Specific HTTP headers can be set for
|
* returned from an {@link HttpRequest#headers() HttpRequest} or an
|
||||||
* {@linkplain HttpRequest requests} through the one of the request builder's
|
* {@link HttpResponse#headers() HttpResponse}. Specific HTTP headers can be
|
||||||
* {@link HttpRequest.Builder#header(String, String) headers} methods.
|
* 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
|
* <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
|
* {@code Map} returned by the {@link #map() map} method, operate without regard
|
||||||
* case when retrieving the header value.
|
* 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.
|
* <p> {@code HttpHeaders} instances are immutable.
|
||||||
*
|
*
|
||||||
* @since 11
|
* @since 11
|
||||||
*/
|
*/
|
||||||
public abstract class HttpHeaders {
|
public final class HttpHeaders {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an HttpHeaders.
|
* Returns an {@link Optional} containing the first header string value of
|
||||||
*/
|
* the given named (and possibly multi-valued) header. If the header is not
|
||||||
protected HttpHeaders() {}
|
* present, then the returned {@code Optional} is empty.
|
||||||
|
|
||||||
/**
|
|
||||||
* 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()}
|
|
||||||
*
|
*
|
||||||
* @param name the header name
|
* @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) {
|
public Optional<String> firstValue(String name) {
|
||||||
return allValues(name).stream().findFirst();
|
return allValues(name).stream().findFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an {@link OptionalLong} containing the first value of the
|
* Returns an {@link OptionalLong} containing the first header string value
|
||||||
* named header field. If the header is not present, then the Optional is
|
* of the named header field. If the header is not present, then the
|
||||||
* empty. If the header is present but contains a value that does not parse
|
* Optional is empty. If the header is present but contains a value that
|
||||||
* as a {@code Long} value, then an exception is thrown.
|
* 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()}
|
|
||||||
*
|
*
|
||||||
* @param name the header name
|
* @param name the header name
|
||||||
* @return an {@code OptionalLong}
|
* @return an {@code OptionalLong}
|
||||||
@ -92,23 +98,19 @@ public abstract class HttpHeaders {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an unmodifiable List of all of the values of the given named
|
* Returns an unmodifiable List of all of the header string values of the
|
||||||
* header. Always returns a List, which may be empty if the header is not
|
* given named header. Always returns a List, which may be empty if the
|
||||||
* present.
|
* header is not present.
|
||||||
*
|
|
||||||
* @implSpec
|
|
||||||
* The default implementation invokes, among other things, the
|
|
||||||
* {@code map().get(name)} to retrieve the list of header values.
|
|
||||||
*
|
*
|
||||||
* @param name the header name
|
* @param name the header name
|
||||||
* @return a List of String values
|
* @return a List of headers string values
|
||||||
*/
|
*/
|
||||||
public List<String> allValues(String name) {
|
public List<String> allValues(String name) {
|
||||||
requireNonNull(name);
|
requireNonNull(name);
|
||||||
List<String> values = map().get(name);
|
List<String> values = map().get(name);
|
||||||
// Making unmodifiable list out of empty in order to make a list which
|
// Making unmodifiable list out of empty in order to make a list which
|
||||||
// throws UOE unconditionally
|
// throws UOE unconditionally
|
||||||
return values != null ? values : unmodifiableList(emptyList());
|
return values != null ? values : List.of();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -116,7 +118,9 @@ public abstract class HttpHeaders {
|
|||||||
*
|
*
|
||||||
* @return the Map
|
* @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.
|
* Tests this HTTP headers instance for equality with the given object.
|
||||||
@ -149,7 +153,11 @@ public abstract class HttpHeaders {
|
|||||||
* @return the hash-code value for this HTTP headers
|
* @return the hash-code value for this HTTP headers
|
||||||
*/
|
*/
|
||||||
public final int hashCode() {
|
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 @@ public abstract class HttpHeaders {
|
|||||||
sb.append(" }");
|
sb.append(" }");
|
||||||
return sb.toString();
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -89,10 +89,12 @@ public abstract class HttpRequest {
|
|||||||
* <p> Instances of {@code HttpRequest.Builder} are created by calling {@link
|
* <p> Instances of {@code HttpRequest.Builder} are created by calling {@link
|
||||||
* HttpRequest#newBuilder(URI)} or {@link HttpRequest#newBuilder()}.
|
* HttpRequest#newBuilder(URI)} or {@link HttpRequest#newBuilder()}.
|
||||||
*
|
*
|
||||||
* <p> Each of the setter methods modifies the state of the builder
|
* <p> The builder can be used to configure per-request state, such as: the
|
||||||
* and returns the same instance. The methods are not synchronized and
|
* request URI, the request method (default is GET unless explicitly set),
|
||||||
* should not be called from multiple threads without external
|
* specific request headers, etc. Each of the setter methods modifies the
|
||||||
* synchronization. The {@link #build() build} method returns a new
|
* 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
|
* {@code HttpRequest} each time it is invoked. Once built an {@code
|
||||||
* HttpRequest} is immutable, and can be sent multiple times.
|
* HttpRequest} is immutable, and can be sent multiple times.
|
||||||
*
|
*
|
||||||
|
@ -830,13 +830,13 @@ public interface HttpResponse<T> {
|
|||||||
* BodySubscriber} provides implementations of many common body subscribers.
|
* BodySubscriber} provides implementations of many common body subscribers.
|
||||||
*
|
*
|
||||||
* <p> The object acts as a {@link Flow.Subscriber}<{@link List}<{@link
|
* <p> The object acts as a {@link Flow.Subscriber}<{@link List}<{@link
|
||||||
* ByteBuffer}>> to the HTTP client implementation, which publishes
|
* ByteBuffer}>> to the HTTP Client implementation, which publishes
|
||||||
* unmodifiable lists of read-only ByteBuffers containing the response body.
|
* lists of ByteBuffers containing the response body. The Flow of data, as
|
||||||
* The Flow of data, as well as the order of ByteBuffers in the Flow lists,
|
* well as the order of ByteBuffers in the Flow lists, is a strictly ordered
|
||||||
* is a strictly ordered representation of the response body. Both the Lists
|
* representation of the response body. Both the Lists and the ByteBuffers,
|
||||||
* and the ByteBuffers, once passed to the subscriber, are no longer used by
|
* once passed to the subscriber, are no longer used by the HTTP Client. The
|
||||||
* the HTTP client. The subscriber converts the incoming buffers of data to
|
* subscriber converts the incoming buffers of data to some higher-level
|
||||||
* some higher-level Java type {@code T}.
|
* Java type {@code T}.
|
||||||
*
|
*
|
||||||
* <p> The {@link #getBody()} method returns a
|
* <p> The {@link #getBody()} method returns a
|
||||||
* {@link CompletionStage}<{@code T}> that provides the response body
|
* {@link CompletionStage}<{@code T}> that provides the response body
|
||||||
@ -859,6 +859,9 @@ public interface HttpResponse<T> {
|
|||||||
* may cause the underlying HTTP connection to be closed and prevent it
|
* may cause the underlying HTTP connection to be closed and prevent it
|
||||||
* from being reused for subsequent operations.
|
* 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
|
* @param <T> the response body type
|
||||||
* @see BodySubscribers
|
* @see BodySubscribers
|
||||||
* @since 11
|
* @since 11
|
||||||
@ -888,20 +891,20 @@ public interface HttpResponse<T> {
|
|||||||
*
|
*
|
||||||
* <pre>{@code // Streams the response body to a File
|
* <pre>{@code // Streams the response body to a File
|
||||||
* HttpResponse<byte[]> response = client
|
* 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[]
|
* // Accumulates the response body and returns it as a byte[]
|
||||||
* HttpResponse<byte[]> response = client
|
* HttpResponse<byte[]> response = client
|
||||||
* .send(request, (statusCode, responseHeaders) -> BodySubscribers.ofByteArray());
|
* .send(request, responseInfo -> BodySubscribers.ofByteArray());
|
||||||
*
|
*
|
||||||
* // Discards the response body
|
* // Discards the response body
|
||||||
* HttpResponse<Void> response = client
|
* 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
|
* // Accumulates the response body as a String then maps it to its bytes
|
||||||
* HttpResponse<byte[]> response = client
|
* HttpResponse<byte[]> response = client
|
||||||
* .send(request, (sc, hdrs) ->
|
* .send(request, responseInfo ->
|
||||||
* BodySubscribers.mapping(BodySubscribers.ofString(), String::getBytes));
|
* BodySubscribers.mapping(BodySubscribers.ofString(UTF_8), String::getBytes));
|
||||||
* }</pre>
|
* }</pre>
|
||||||
*
|
*
|
||||||
* @since 11
|
* @since 11
|
||||||
|
@ -35,9 +35,9 @@ import java.util.concurrent.CompletionStage;
|
|||||||
/**
|
/**
|
||||||
* A WebSocket Client.
|
* 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
|
* 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
|
* remains closed. WebSocket messages are sent through a {@code WebSocket} and
|
||||||
* received through a {@code WebSocket.Listener} associated with it. Messages
|
* received through a {@code WebSocket.Listener} associated with it. Messages
|
||||||
@ -55,21 +55,22 @@ import java.util.concurrent.CompletionStage;
|
|||||||
*
|
*
|
||||||
* <p> A receive method is any of the {@code onText}, {@code onBinary},
|
* <p> A receive method is any of the {@code onText}, {@code onBinary},
|
||||||
* {@code onPing}, {@code onPong} and {@code onClose} methods of
|
* {@code onPing}, {@code onPong} and {@code onClose} methods of
|
||||||
* {@code Listener}. A receive method initiates a receive operation and returns
|
* {@code Listener}. WebSocket initiates a receive operation by invoking a
|
||||||
* a {@code CompletionStage} which completes once the operation has completed.
|
* 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>.
|
* <p> To control receiving of messages, a WebSocket maintains an
|
||||||
* This counter's value is a number of times the WebSocket has yet to invoke a
|
* <a id="counter">internal counter</a>. This counter's value is a number of
|
||||||
* receive method. While this counter is zero the WebSocket does not invoke
|
* times the WebSocket has yet to invoke a receive method. While this counter is
|
||||||
* receive methods. The counter is incremented by {@code n} when {@code
|
* zero the WebSocket does not invoke receive methods. The counter is
|
||||||
* request(n)} is called. The counter is decremented by one when the WebSocket
|
* incremented by {@code n} when {@code request(n)} is called. The counter is
|
||||||
* invokes a receive method. {@code onOpen} and {@code onError} are not receive
|
* decremented by one when the WebSocket invokes a receive method.
|
||||||
* methods. WebSocket invokes {@code onOpen} prior to any other methods on the
|
* {@code onOpen} and {@code onError} are not receive methods. WebSocket invokes
|
||||||
* listener. WebSocket invokes {@code onOpen} at most once. WebSocket may invoke
|
* {@code onOpen} prior to any other methods on the listener. WebSocket invokes
|
||||||
* {@code onError} at any given time. If the WebSocket invokes {@code onError}
|
* {@code onOpen} at most once. WebSocket may invoke {@code onError} at any
|
||||||
* or {@code onClose}, then no further listener's methods will be invoked, no
|
* given time. If the WebSocket invokes {@code onError} or {@code onClose}, then
|
||||||
* matter the value of the counter. For a newly built WebSocket the counter is
|
* no further listener's methods will be invoked, no matter the value of the
|
||||||
* zero. A WebSocket invokes methods on the listener in a thread-safe manner.
|
* counter. For a newly built WebSocket the counter is zero.
|
||||||
*
|
*
|
||||||
* <p> Unless otherwise stated, {@code null} arguments will cause methods
|
* <p> Unless otherwise stated, {@code null} arguments will cause methods
|
||||||
* of {@code WebSocket} to throw {@code NullPointerException}, similarly,
|
* of {@code WebSocket} to throw {@code NullPointerException}, similarly,
|
||||||
@ -105,13 +106,13 @@ public interface WebSocket {
|
|||||||
/**
|
/**
|
||||||
* A builder of {@linkplain WebSocket WebSocket Clients}.
|
* A builder of {@linkplain WebSocket WebSocket Clients}.
|
||||||
*
|
*
|
||||||
* <p> A builder can be created by invoking the
|
* <p> Builders are created by invoking
|
||||||
* {@link HttpClient#newWebSocketBuilder HttpClient.newWebSocketBuilder}
|
* {@link HttpClient#newWebSocketBuilder HttpClient.newWebSocketBuilder}.
|
||||||
* method. The intermediate (setter-like) methods change the state of the
|
* The intermediate (setter-like) methods change the state of the builder
|
||||||
* builder and return the same builder they have been invoked on. If an
|
* and return the same builder they have been invoked on. If an intermediate
|
||||||
* intermediate method is not invoked, an appropriate default value (or
|
* method is not invoked, an appropriate default value (or behavior) will be
|
||||||
* behavior) will be assumed. A {@code Builder} is not safe for use by
|
* assumed. A {@code Builder} is not safe for use by multiple threads
|
||||||
* multiple threads without external synchronization.
|
* without external synchronization.
|
||||||
*
|
*
|
||||||
* @since 11
|
* @since 11
|
||||||
*/
|
*/
|
||||||
@ -155,7 +156,7 @@ public interface WebSocket {
|
|||||||
* Sets a request for the given subprotocols.
|
* Sets a request for the given subprotocols.
|
||||||
*
|
*
|
||||||
* <p> After the {@code WebSocket} has been built, the actual
|
* <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()}.
|
* {@link WebSocket#getSubprotocol WebSocket.getSubprotocol()}.
|
||||||
*
|
*
|
||||||
* <p> Subprotocols are specified in the order of preference. The most
|
* <p> Subprotocols are specified in the order of preference. The most
|
||||||
@ -218,11 +219,17 @@ public interface WebSocket {
|
|||||||
* The receiving interface of {@code WebSocket}.
|
* The receiving interface of {@code WebSocket}.
|
||||||
*
|
*
|
||||||
* <p> A {@code WebSocket} invokes methods of the associated listener
|
* <p> A {@code WebSocket} invokes methods of the associated listener
|
||||||
* passing itself as an argument. When data has been received, the
|
* passing itself as an argument. These methods are invoked in a thread-safe
|
||||||
* {@code WebSocket} invokes a receive method. Methods {@code onText},
|
* manner, such that the next invocation may start only after the previous
|
||||||
* {@code onBinary}, {@code onPing} and {@code onPong} must return a
|
* one has finished.
|
||||||
* {@code CompletionStage} that completes once the message has been received
|
*
|
||||||
* by the listener.
|
* <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
|
* <p> An {@code IOException} raised in {@code WebSocket} will result in an
|
||||||
* invocation of {@code onError} with that exception (if the input is not
|
* invocation of {@code onError} with that exception (if the input is not
|
||||||
@ -231,11 +238,14 @@ public interface WebSocket {
|
|||||||
* exceptionally, the WebSocket will invoke {@code onError} with this
|
* exceptionally, the WebSocket will invoke {@code onError} with this
|
||||||
* exception.
|
* exception.
|
||||||
*
|
*
|
||||||
* <p> If a listener's method returns {@code null} rather than a
|
* @apiNote The strict sequential order of invocations from
|
||||||
* {@code CompletionStage}, {@code WebSocket} will behave as if the listener
|
* {@code WebSocket} to {@code Listener} means, in particular, that the
|
||||||
* returned a {@code CompletionStage} that is already completed normally.
|
* {@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
|
* with more than a single {@code WebSocket}. In this case invocations
|
||||||
* related to different instances of {@code WebSocket} may not be ordered
|
* related to different instances of {@code WebSocket} may not be ordered
|
||||||
* and may even happen concurrently.
|
* and may even happen concurrently.
|
||||||
|
@ -28,7 +28,7 @@ package java.net.http;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An exception used to signal the opening handshake failed.
|
* Thrown when the opening handshake has failed.
|
||||||
*
|
*
|
||||||
* @since 11
|
* @since 11
|
||||||
*/
|
*/
|
||||||
@ -55,6 +55,10 @@ public final class WebSocketHandshakeException extends IOException {
|
|||||||
* <p> The value may be unavailable ({@code null}) if this exception has
|
* <p> The value may be unavailable ({@code null}) if this exception has
|
||||||
* been serialized and then deserialized.
|
* 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
|
* @return server response
|
||||||
*/
|
*/
|
||||||
public HttpResponse<?> getResponse() {
|
public HttpResponse<?> getResponse() {
|
||||||
|
@ -42,20 +42,24 @@
|
|||||||
* Hypertext Transfer Protocol (HTTP/1.1)</a>, and
|
* Hypertext Transfer Protocol (HTTP/1.1)</a>, and
|
||||||
* <a href="https://tools.ietf.org/html/rfc6455">The WebSocket Protocol</a>.
|
* <a href="https://tools.ietf.org/html/rfc6455">The WebSocket Protocol</a>.
|
||||||
*
|
*
|
||||||
* <p> Asynchronous tasks and dependent actions of returned {@link
|
* <p> In general, asynchronous tasks execute in either the thread invoking
|
||||||
* java.util.concurrent.CompletableFuture} instances are executed on the threads
|
* the operation, e.g. {@linkplain HttpClient#send(HttpRequest, BodyHandler)
|
||||||
* supplied by the client's {@link java.util.concurrent.Executor}, where
|
* sending} an HTTP request, or by the threads supplied by the client's {@link
|
||||||
* practical.
|
* 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
|
* <p> {@code CompletableFuture}s returned by this API will throw {@link
|
||||||
* java.lang.UnsupportedOperationException} for their {@link
|
* UnsupportedOperationException} for their {@link
|
||||||
* java.util.concurrent.CompletableFuture#obtrudeValue(Object) obtrudeValue}
|
* CompletableFuture#obtrudeValue(Object) obtrudeValue}
|
||||||
* and {@link java.util.concurrent.CompletableFuture#obtrudeException(Throwable)
|
* and {@link CompletableFuture#obtrudeException(Throwable)
|
||||||
* obtrudeException} methods. Invoking the {@link
|
* obtrudeException} methods. Invoking the {@link CompletableFuture#cancel
|
||||||
* java.util.concurrent.CompletableFuture#cancel cancel} method on a {@code
|
* cancel} method on a {@code CompletableFuture} returned by this API may not
|
||||||
* CompletableFuture} returned by this API will not interrupt the underlying
|
* interrupt the underlying operation, but may be useful to complete,
|
||||||
* operation, but may be useful to complete, exceptionally, dependent stages
|
* exceptionally, dependent stages that have not already completed.
|
||||||
* that have not already completed.
|
|
||||||
*
|
*
|
||||||
* <p> Unless otherwise stated, {@code null} parameter values will cause methods
|
* <p> Unless otherwise stated, {@code null} parameter values will cause methods
|
||||||
* of all classes in this package to throw {@code NullPointerException}.
|
* of all classes in this package to throw {@code NullPointerException}.
|
||||||
@ -63,3 +67,9 @@
|
|||||||
* @since 11
|
* @since 11
|
||||||
*/
|
*/
|
||||||
package java.net.http;
|
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;
|
||||||
|
@ -34,7 +34,6 @@ import java.net.URISyntaxException;
|
|||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.WeakHashMap;
|
import java.util.WeakHashMap;
|
||||||
import java.net.http.HttpHeaders;
|
import java.net.http.HttpHeaders;
|
||||||
@ -59,9 +58,9 @@ class AuthenticationFilter implements HeaderFilter {
|
|||||||
static final int UNAUTHORIZED = 401;
|
static final int UNAUTHORIZED = 401;
|
||||||
static final int PROXY_UNAUTHORIZED = 407;
|
static final int PROXY_UNAUTHORIZED = 407;
|
||||||
|
|
||||||
private static final List<String> BASIC_DUMMY =
|
private static final String BASIC_DUMMY =
|
||||||
List.of("Basic " + Base64.getEncoder()
|
"Basic " + Base64.getEncoder()
|
||||||
.encodeToString("o:o".getBytes(ISO_8859_1)));
|
.encodeToString("o:o".getBytes(ISO_8859_1));
|
||||||
|
|
||||||
// A public no-arg constructor is required by FilterFactory
|
// A public no-arg constructor is required by FilterFactory
|
||||||
public AuthenticationFilter() {}
|
public AuthenticationFilter() {}
|
||||||
@ -182,14 +181,12 @@ class AuthenticationFilter implements HeaderFilter {
|
|||||||
String value = "Basic " + s;
|
String value = "Basic " + s;
|
||||||
if (proxy) {
|
if (proxy) {
|
||||||
if (r.isConnect()) {
|
if (r.isConnect()) {
|
||||||
if (!Utils.PROXY_TUNNEL_FILTER
|
if (!Utils.PROXY_TUNNEL_FILTER.test(hdrname, value)) {
|
||||||
.test(hdrname, List.of(value))) {
|
|
||||||
Log.logError("{0} disabled", hdrname);
|
Log.logError("{0} disabled", hdrname);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if (r.proxy() != null) {
|
} else if (r.proxy() != null) {
|
||||||
if (!Utils.PROXY_FILTER
|
if (!Utils.PROXY_FILTER.test(hdrname, value)) {
|
||||||
.test(hdrname, List.of(value))) {
|
|
||||||
Log.logError("{0} disabled", hdrname);
|
Log.logError("{0} disabled", hdrname);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -261,9 +258,16 @@ class AuthenticationFilter implements HeaderFilter {
|
|||||||
|
|
||||||
boolean proxy = status == PROXY_UNAUTHORIZED;
|
boolean proxy = status == PROXY_UNAUTHORIZED;
|
||||||
String authname = proxy ? "Proxy-Authenticate" : "WWW-Authenticate";
|
String authname = proxy ? "Proxy-Authenticate" : "WWW-Authenticate";
|
||||||
String authval = hdrs.firstValue(authname).orElseThrow(() -> {
|
String authval = hdrs.firstValue(authname).orElse(null);
|
||||||
return new IOException("Invalid auth header");
|
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);
|
HeaderParser parser = new HeaderParser(authval);
|
||||||
String scheme = parser.findKey(0);
|
String scheme = parser.findKey(0);
|
||||||
|
|
||||||
|
@ -53,6 +53,8 @@ final class ConnectionPool {
|
|||||||
|
|
||||||
static final long KEEP_ALIVE = Utils.getIntegerNetProperty(
|
static final long KEEP_ALIVE = Utils.getIntegerNetProperty(
|
||||||
"jdk.httpclient.keepalive.timeout", 1200); // seconds
|
"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);
|
final Logger debug = Utils.getDebugLogger(this::dbgString, Utils.DEBUG);
|
||||||
|
|
||||||
// Pools of idle connections
|
// Pools of idle connections
|
||||||
@ -160,6 +162,7 @@ final class ConnectionPool {
|
|||||||
CleanupTrigger cleanup = registerCleanupTrigger(conn);
|
CleanupTrigger cleanup = registerCleanupTrigger(conn);
|
||||||
|
|
||||||
// it's possible that cleanup may have been called.
|
// it's possible that cleanup may have been called.
|
||||||
|
HttpConnection toClose = null;
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
if (cleanup.isDone()) {
|
if (cleanup.isDone()) {
|
||||||
return;
|
return;
|
||||||
@ -167,6 +170,10 @@ final class ConnectionPool {
|
|||||||
conn.close();
|
conn.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (MAX_POOL_SIZE > 0 && expiryList.size() >= MAX_POOL_SIZE) {
|
||||||
|
toClose = expiryList.removeOldest();
|
||||||
|
if (toClose != null) removeFromPool(toClose);
|
||||||
|
}
|
||||||
if (conn instanceof PlainHttpConnection) {
|
if (conn instanceof PlainHttpConnection) {
|
||||||
putConnection(conn, plainPool);
|
putConnection(conn, plainPool);
|
||||||
} else {
|
} else {
|
||||||
@ -175,6 +182,13 @@ final class ConnectionPool {
|
|||||||
}
|
}
|
||||||
expiryList.add(conn, now, keepAlive);
|
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);
|
//System.out.println("Return to pool: " + conn);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -314,6 +328,8 @@ final class ConnectionPool {
|
|||||||
private final LinkedList<ExpiryEntry> list = new LinkedList<>();
|
private final LinkedList<ExpiryEntry> list = new LinkedList<>();
|
||||||
private volatile boolean mayContainEntries;
|
private volatile boolean mayContainEntries;
|
||||||
|
|
||||||
|
int size() { return list.size(); }
|
||||||
|
|
||||||
// A loosely accurate boolean whose value is computed
|
// A loosely accurate boolean whose value is computed
|
||||||
// at the end of each operation performed on ExpiryList;
|
// at the end of each operation performed on ExpiryList;
|
||||||
// Does not require synchronizing on the ConnectionPool.
|
// Does not require synchronizing on the ConnectionPool.
|
||||||
@ -329,6 +345,13 @@ final class ConnectionPool {
|
|||||||
else return Optional.of(list.getLast().expiry);
|
else return Optional.of(list.getLast().expiry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// should only be called while holding a synchronization
|
||||||
// lock on the ConnectionPool
|
// lock on the ConnectionPool
|
||||||
void add(HttpConnection conn) {
|
void add(HttpConnection conn) {
|
||||||
@ -419,17 +442,38 @@ final class ConnectionPool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void cleanup(HttpConnection c, Throwable error) {
|
// Remove a connection from the pool.
|
||||||
if (debug.on())
|
// should only be called while holding a synchronization
|
||||||
debug.log("%s : ConnectionPool.cleanup(%s)",
|
// lock on the ConnectionPool
|
||||||
String.valueOf(c.getConnectionFlow()), error);
|
private void removeFromPool(HttpConnection c) {
|
||||||
synchronized(this) {
|
assert Thread.holdsLock(this);
|
||||||
if (c instanceof PlainHttpConnection) {
|
if (c instanceof PlainHttpConnection) {
|
||||||
removeFromPool(c, plainPool);
|
removeFromPool(c, plainPool);
|
||||||
} else {
|
} else {
|
||||||
assert c.isSecure();
|
assert c.isSecure();
|
||||||
removeFromPool(c, sslPool);
|
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) {
|
||||||
|
removeFromPool(c);
|
||||||
expiryList.remove(c);
|
expiryList.remove(c);
|
||||||
}
|
}
|
||||||
c.close();
|
c.close();
|
||||||
|
@ -31,7 +31,7 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.net.http.HttpHeaders;
|
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.Log;
|
||||||
import jdk.internal.net.http.common.Utils;
|
import jdk.internal.net.http.common.Utils;
|
||||||
|
|
||||||
@ -50,7 +50,7 @@ class CookieFilter implements HeaderFilter {
|
|||||||
Map<String,List<String>> cookies = cookieHandler.get(r.uri(), userheaders);
|
Map<String,List<String>> cookies = cookieHandler.get(r.uri(), userheaders);
|
||||||
|
|
||||||
// add the returned cookies
|
// add the returned cookies
|
||||||
HttpHeadersImpl systemHeaders = r.getSystemHeaders();
|
HttpHeadersBuilder systemHeadersBuilder = r.getSystemHeadersBuilder();
|
||||||
if (cookies.isEmpty()) {
|
if (cookies.isEmpty()) {
|
||||||
Log.logTrace("Request: no cookie to add for {0}", r.uri());
|
Log.logTrace("Request: no cookie to add for {0}", r.uri());
|
||||||
} else {
|
} else {
|
||||||
@ -65,7 +65,7 @@ class CookieFilter implements HeaderFilter {
|
|||||||
if (values == null || values.isEmpty()) continue;
|
if (values == null || values.isEmpty()) continue;
|
||||||
for (String val : values) {
|
for (String val : values) {
|
||||||
if (Utils.isValidValue(val)) {
|
if (Utils.isValidValue(val)) {
|
||||||
systemHeaders.addHeader(hdrname, val);
|
systemHeadersBuilder.addHeader(hdrname, val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -307,6 +307,7 @@ final class Exchange<T> {
|
|||||||
Function<ExchangeImpl<T>,CompletableFuture<Response>> andThen) {
|
Function<ExchangeImpl<T>,CompletableFuture<Response>> andThen) {
|
||||||
t = Utils.getCompletionCause(t);
|
t = Utils.getCompletionCause(t);
|
||||||
if (t instanceof ProxyAuthenticationRequired) {
|
if (t instanceof ProxyAuthenticationRequired) {
|
||||||
|
if (debug.on()) debug.log("checkFor407: ProxyAuthenticationRequired: building synthetic response");
|
||||||
bodyIgnored = MinimalFuture.completedFuture(null);
|
bodyIgnored = MinimalFuture.completedFuture(null);
|
||||||
Response proxyResponse = ((ProxyAuthenticationRequired)t).proxyResponse;
|
Response proxyResponse = ((ProxyAuthenticationRequired)t).proxyResponse;
|
||||||
HttpConnection c = ex == null ? null : ex.connection();
|
HttpConnection c = ex == null ? null : ex.connection();
|
||||||
@ -315,8 +316,10 @@ final class Exchange<T> {
|
|||||||
proxyResponse.version, true);
|
proxyResponse.version, true);
|
||||||
return MinimalFuture.completedFuture(syntheticResponse);
|
return MinimalFuture.completedFuture(syntheticResponse);
|
||||||
} else if (t != null) {
|
} else if (t != null) {
|
||||||
|
if (debug.on()) debug.log("checkFor407: no response - %s", t);
|
||||||
return MinimalFuture.failedFuture(t);
|
return MinimalFuture.failedFuture(t);
|
||||||
} else {
|
} else {
|
||||||
|
if (debug.on()) debug.log("checkFor407: all clear");
|
||||||
return andThen.apply(ex);
|
return andThen.apply(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -332,6 +335,7 @@ final class Exchange<T> {
|
|||||||
int rcode = r1.statusCode();
|
int rcode = r1.statusCode();
|
||||||
if (rcode == 100) {
|
if (rcode == 100) {
|
||||||
Log.logTrace("Received 100-Continue: sending body");
|
Log.logTrace("Received 100-Continue: sending body");
|
||||||
|
if (debug.on()) debug.log("Received 100-Continue for %s", r1);
|
||||||
CompletableFuture<Response> cf =
|
CompletableFuture<Response> cf =
|
||||||
exchImpl.sendBodyAsync()
|
exchImpl.sendBodyAsync()
|
||||||
.thenCompose(exIm -> exIm.getResponseAsync(parentExecutor));
|
.thenCompose(exIm -> exIm.getResponseAsync(parentExecutor));
|
||||||
@ -341,6 +345,7 @@ final class Exchange<T> {
|
|||||||
} else {
|
} else {
|
||||||
Log.logTrace("Expectation failed: Received {0}",
|
Log.logTrace("Expectation failed: Received {0}",
|
||||||
rcode);
|
rcode);
|
||||||
|
if (debug.on()) debug.log("Expect-Continue failed (%d) for: %s", rcode, r1);
|
||||||
if (upgrading && rcode == 101) {
|
if (upgrading && rcode == 101) {
|
||||||
IOException failed = new IOException(
|
IOException failed = new IOException(
|
||||||
"Unable to handle 101 while waiting for 100");
|
"Unable to handle 101 while waiting for 100");
|
||||||
@ -357,6 +362,7 @@ final class Exchange<T> {
|
|||||||
// send the request body and proceed.
|
// send the request body and proceed.
|
||||||
private CompletableFuture<Response> sendRequestBody(ExchangeImpl<T> ex) {
|
private CompletableFuture<Response> sendRequestBody(ExchangeImpl<T> ex) {
|
||||||
assert !request.expectContinue();
|
assert !request.expectContinue();
|
||||||
|
if (debug.on()) debug.log("sendRequestBody");
|
||||||
CompletableFuture<Response> cf = ex.sendBodyAsync()
|
CompletableFuture<Response> cf = ex.sendBodyAsync()
|
||||||
.thenCompose(exIm -> exIm.getResponseAsync(parentExecutor));
|
.thenCompose(exIm -> exIm.getResponseAsync(parentExecutor));
|
||||||
cf = wrapForUpgrade(cf);
|
cf = wrapForUpgrade(cf);
|
||||||
|
@ -32,8 +32,8 @@ import java.util.function.Function;
|
|||||||
import java.net.http.HttpResponse;
|
import java.net.http.HttpResponse;
|
||||||
import jdk.internal.net.http.common.Logger;
|
import jdk.internal.net.http.common.Logger;
|
||||||
import jdk.internal.net.http.common.MinimalFuture;
|
import jdk.internal.net.http.common.MinimalFuture;
|
||||||
import static java.net.http.HttpClient.Version.HTTP_1_1;
|
|
||||||
import jdk.internal.net.http.common.Utils;
|
import jdk.internal.net.http.common.Utils;
|
||||||
|
import static java.net.http.HttpClient.Version.HTTP_1_1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Splits request so that headers and body can be sent separately with optional
|
* Splits request so that headers and body can be sent separately with optional
|
||||||
|
@ -27,7 +27,6 @@ package jdk.internal.net.http;
|
|||||||
|
|
||||||
import java.io.EOFException;
|
import java.io.EOFException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.System.Logger.Level;
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
@ -42,7 +41,9 @@ import java.util.concurrent.atomic.AtomicReference;
|
|||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import jdk.internal.net.http.common.Demand;
|
import jdk.internal.net.http.common.Demand;
|
||||||
import jdk.internal.net.http.common.FlowTube.TubeSubscriber;
|
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.Logger;
|
||||||
|
import jdk.internal.net.http.common.MinimalFuture;
|
||||||
import jdk.internal.net.http.common.SequentialScheduler;
|
import jdk.internal.net.http.common.SequentialScheduler;
|
||||||
import jdk.internal.net.http.common.ConnectionExpiredException;
|
import jdk.internal.net.http.common.ConnectionExpiredException;
|
||||||
import jdk.internal.net.http.common.Utils;
|
import jdk.internal.net.http.common.Utils;
|
||||||
@ -166,6 +167,7 @@ class Http1AsyncReceiver {
|
|||||||
= new ConcurrentLinkedDeque<>();
|
= new ConcurrentLinkedDeque<>();
|
||||||
private final SequentialScheduler scheduler =
|
private final SequentialScheduler scheduler =
|
||||||
SequentialScheduler.synchronizedScheduler(this::flush);
|
SequentialScheduler.synchronizedScheduler(this::flush);
|
||||||
|
final MinimalFuture<Void> whenFinished;
|
||||||
private final Executor executor;
|
private final Executor executor;
|
||||||
private final Http1TubeSubscriber subscriber = new Http1TubeSubscriber();
|
private final Http1TubeSubscriber subscriber = new Http1TubeSubscriber();
|
||||||
private final AtomicReference<Http1AsyncDelegate> pendingDelegateRef;
|
private final AtomicReference<Http1AsyncDelegate> pendingDelegateRef;
|
||||||
@ -184,6 +186,7 @@ class Http1AsyncReceiver {
|
|||||||
public Http1AsyncReceiver(Executor executor, Http1Exchange<?> owner) {
|
public Http1AsyncReceiver(Executor executor, Http1Exchange<?> owner) {
|
||||||
this.pendingDelegateRef = new AtomicReference<>();
|
this.pendingDelegateRef = new AtomicReference<>();
|
||||||
this.executor = executor;
|
this.executor = executor;
|
||||||
|
this.whenFinished = new MinimalFuture<>();
|
||||||
this.owner = owner;
|
this.owner = owner;
|
||||||
this.client = owner.client;
|
this.client = owner.client;
|
||||||
}
|
}
|
||||||
@ -261,6 +264,14 @@ class Http1AsyncReceiver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.
|
* Must be called from within the scheduler main loop.
|
||||||
* Handles any pending errors by calling delegate.onReadError().
|
* Handles any pending errors by calling delegate.onReadError().
|
||||||
@ -284,6 +295,10 @@ class Http1AsyncReceiver {
|
|||||||
+ "\t\t queue.isEmpty: " + queue.isEmpty());
|
+ "\t\t queue.isEmpty: " + queue.isEmpty());
|
||||||
scheduler.stop();
|
scheduler.stop();
|
||||||
delegate.onReadError(x);
|
delegate.onReadError(x);
|
||||||
|
whenFinished.completeExceptionally(x);
|
||||||
|
if (Log.channel()) {
|
||||||
|
Log.logChannel("HTTP/1 read subscriber stopped for: {0}", describe());
|
||||||
|
}
|
||||||
if (stopRequested) {
|
if (stopRequested) {
|
||||||
// This is the special case where the subscriber
|
// This is the special case where the subscriber
|
||||||
// has requested an illegal number of items.
|
// has requested an illegal number of items.
|
||||||
@ -464,27 +479,35 @@ class Http1AsyncReceiver {
|
|||||||
// throw ConnectionExpiredException
|
// throw ConnectionExpiredException
|
||||||
// to try & force a retry of the request.
|
// to try & force a retry of the request.
|
||||||
retry = false;
|
retry = false;
|
||||||
ex = new ConnectionExpiredException(
|
ex = new ConnectionExpiredException(ex);
|
||||||
"subscription is finished", ex);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
error = ex;
|
error = ex;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
final Throwable t = (recorded == null ? ex : recorded);
|
final Throwable t = (recorded == null ? ex : recorded);
|
||||||
if (debug.on())
|
if (debug.on())
|
||||||
debug.log("recorded " + t + "\n\t delegate: " + delegate
|
debug.log("recorded " + t + "\n\t delegate: " + delegate
|
||||||
+ "\t\t queue.isEmpty: " + queue.isEmpty(), ex);
|
+ "\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) {
|
if (queue.isEmpty() || pendingDelegateRef.get() != null || stopRequested) {
|
||||||
// This callback is called from within the selector thread.
|
// This callback is called from within the selector thread.
|
||||||
// Use an executor here to avoid doing the heavy lifting in the
|
// Use an executor here to avoid doing the heavy lifting in the
|
||||||
// selector.
|
// selector.
|
||||||
|
if (Log.errors()) {
|
||||||
|
Log.logError("HTTP/1 propagating recorded error: {0} - {1}", describe(), t);
|
||||||
|
}
|
||||||
scheduler.runOrSchedule(executor);
|
scheduler.runOrSchedule(executor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void stop() {
|
void stop() {
|
||||||
if (debug.on()) debug.log("stopping");
|
if (debug.on()) debug.log("stopping");
|
||||||
|
if (Log.channel() && !scheduler.isStopped()) {
|
||||||
|
Log.logChannel("HTTP/1 read subscriber stopped for {0}", describe());
|
||||||
|
}
|
||||||
scheduler.stop();
|
scheduler.stop();
|
||||||
// make sure ref count is handled properly by
|
// make sure ref count is handled properly by
|
||||||
// closing the delegate.
|
// closing the delegate.
|
||||||
@ -492,6 +515,7 @@ class Http1AsyncReceiver {
|
|||||||
if (previous != null) previous.close(error);
|
if (previous != null) previous.close(error);
|
||||||
delegate = null;
|
delegate = null;
|
||||||
owner = null;
|
owner = null;
|
||||||
|
whenFinished.complete(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -514,12 +538,18 @@ class Http1AsyncReceiver {
|
|||||||
// supports being called multiple time.
|
// supports being called multiple time.
|
||||||
// doesn't cancel the previous subscription, since that is
|
// doesn't cancel the previous subscription, since that is
|
||||||
// most probably the same as the new subscription.
|
// 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;
|
assert this.subscription == null || dropped == false;
|
||||||
this.subscription = subscription;
|
this.subscription = subscription;
|
||||||
dropped = false;
|
dropped = false;
|
||||||
canRequestMore.set(true);
|
canRequestMore.set(true);
|
||||||
if (delegate != null) {
|
if (delegate != null) {
|
||||||
scheduler.runOrSchedule(executor);
|
scheduler.runOrSchedule(executor);
|
||||||
|
} else {
|
||||||
|
if (debug.on()) debug.log("onSubscribe: read delegate not present yet");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,7 +26,6 @@
|
|||||||
package jdk.internal.net.http;
|
package jdk.internal.net.http;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.System.Logger.Level;
|
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.http.HttpResponse.BodyHandler;
|
import java.net.http.HttpResponse.BodyHandler;
|
||||||
import java.net.http.HttpResponse.BodySubscriber;
|
import java.net.http.HttpResponse.BodySubscriber;
|
||||||
@ -46,6 +45,7 @@ import jdk.internal.net.http.common.SequentialScheduler;
|
|||||||
import jdk.internal.net.http.common.MinimalFuture;
|
import jdk.internal.net.http.common.MinimalFuture;
|
||||||
import jdk.internal.net.http.common.Utils;
|
import jdk.internal.net.http.common.Utils;
|
||||||
import static java.net.http.HttpClient.Version.HTTP_1_1;
|
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.
|
* Encapsulates one HTTP/1.1 request/response exchange.
|
||||||
@ -134,6 +134,9 @@ class Http1Exchange<T> extends ExchangeImpl<T> {
|
|||||||
subscription.request(n);
|
subscription.request(n);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A current-state message suitable for inclusion in an exception detail message. */
|
||||||
|
abstract String currentStateMessage();
|
||||||
|
|
||||||
final boolean isSubscribed() {
|
final boolean isSubscribed() {
|
||||||
return subscription != null;
|
return subscription != null;
|
||||||
}
|
}
|
||||||
@ -159,6 +162,7 @@ class Http1Exchange<T> extends ExchangeImpl<T> {
|
|||||||
@Override public void onNext(ByteBuffer item) { error(); }
|
@Override public void onNext(ByteBuffer item) { error(); }
|
||||||
@Override public void onError(Throwable throwable) { error(); }
|
@Override public void onError(Throwable throwable) { error(); }
|
||||||
@Override public void onComplete() { error(); }
|
@Override public void onComplete() { error(); }
|
||||||
|
@Override String currentStateMessage() { return null; }
|
||||||
private void error() {
|
private void error() {
|
||||||
throw new InternalError("should not reach here");
|
throw new InternalError("should not reach here");
|
||||||
}
|
}
|
||||||
@ -193,35 +197,6 @@ class Http1Exchange<T> extends ExchangeImpl<T> {
|
|||||||
}
|
}
|
||||||
this.requestAction = new Http1Request(request, this);
|
this.requestAction = new Http1Request(request, this);
|
||||||
this.asyncReceiver = new Http1AsyncReceiver(executor, 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
|
@Override
|
||||||
@ -244,16 +219,16 @@ class Http1Exchange<T> extends ExchangeImpl<T> {
|
|||||||
// create the response before sending the request headers, so that
|
// create the response before sending the request headers, so that
|
||||||
// the response can set the appropriate receivers.
|
// the response can set the appropriate receivers.
|
||||||
if (debug.on()) debug.log("Sending headers only");
|
if (debug.on()) debug.log("Sending headers only");
|
||||||
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
|
// If the first attempt to read something triggers EOF, or
|
||||||
// IOException("channel reset by peer"), we're going to retry.
|
// IOException("channel reset by peer"), we're going to retry.
|
||||||
// Instruct the asyncReceiver to throw ConnectionExpiredException
|
// Instruct the asyncReceiver to throw ConnectionExpiredException
|
||||||
// to force a retry.
|
// to force a retry.
|
||||||
asyncReceiver.setRetryOnError(true);
|
asyncReceiver.setRetryOnError(true);
|
||||||
|
if (response == null) {
|
||||||
|
response = new Http1Response<>(connection, this, asyncReceiver);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (debug.on()) debug.log("response created in advance");
|
||||||
|
|
||||||
CompletableFuture<Void> connectCF;
|
CompletableFuture<Void> connectCF;
|
||||||
if (!connection.connected()) {
|
if (!connection.connected()) {
|
||||||
@ -296,6 +271,8 @@ class Http1Exchange<T> extends ExchangeImpl<T> {
|
|||||||
return cf;
|
return cf;
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
if (debug.on()) debug.log("Failed to send headers: %s", t);
|
if (debug.on()) debug.log("Failed to send headers: %s", t);
|
||||||
|
headersSentCF.completeExceptionally(t);
|
||||||
|
bodySentCF.completeExceptionally(t);
|
||||||
connection.close();
|
connection.close();
|
||||||
cf.completeExceptionally(t);
|
cf.completeExceptionally(t);
|
||||||
return cf;
|
return cf;
|
||||||
@ -303,28 +280,52 @@ class Http1Exchange<T> extends ExchangeImpl<T> {
|
|||||||
.thenCompose(unused -> headersSentCF);
|
.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
|
@Override
|
||||||
CompletableFuture<ExchangeImpl<T>> sendBodyAsync() {
|
CompletableFuture<ExchangeImpl<T>> sendBodyAsync() {
|
||||||
assert headersSentCF.isDone();
|
assert headersSentCF.isDone();
|
||||||
|
if (debug.on()) debug.log("sendBodyAsync");
|
||||||
try {
|
try {
|
||||||
bodySubscriber = requestAction.continueRequest();
|
bodySubscriber = requestAction.continueRequest();
|
||||||
|
if (debug.on()) debug.log("bodySubscriber is %s",
|
||||||
|
bodySubscriber == null ? null : bodySubscriber.getClass());
|
||||||
if (bodySubscriber == null) {
|
if (bodySubscriber == null) {
|
||||||
bodySubscriber = Http1BodySubscriber.completeSubscriber(debug);
|
bodySubscriber = Http1BodySubscriber.completeSubscriber(debug);
|
||||||
appendToOutgoing(Http1BodySubscriber.COMPLETED);
|
appendToOutgoing(Http1BodySubscriber.COMPLETED);
|
||||||
} else {
|
} else {
|
||||||
// start
|
// start
|
||||||
bodySubscriber.whenSubscribed
|
bodySubscriber.whenSubscribed
|
||||||
|
.thenAccept((s) -> cancelIfFailed(s))
|
||||||
.thenAccept((s) -> requestMoreBody());
|
.thenAccept((s) -> requestMoreBody());
|
||||||
}
|
}
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
cancelImpl(t);
|
cancelImpl(t);
|
||||||
bodySentCF.completeExceptionally(t);
|
bodySentCF.completeExceptionally(t);
|
||||||
}
|
}
|
||||||
return bodySentCF;
|
return Utils.wrapForDebug(debug, "sendBodyAsync", bodySentCF);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
CompletableFuture<Response> getResponseAsync(Executor executor) {
|
CompletableFuture<Response> getResponseAsync(Executor executor) {
|
||||||
|
if (debug.on()) debug.log("reading headers");
|
||||||
CompletableFuture<Response> cf = response.readHeadersAsync(executor);
|
CompletableFuture<Response> cf = response.readHeadersAsync(executor);
|
||||||
Throwable cause;
|
Throwable cause;
|
||||||
synchronized (lock) {
|
synchronized (lock) {
|
||||||
@ -348,7 +349,7 @@ class Http1Exchange<T> extends ExchangeImpl<T> {
|
|||||||
debug.log(acknowledged ? ("completed response with " + cause)
|
debug.log(acknowledged ? ("completed response with " + cause)
|
||||||
: ("response already completed, ignoring " + cause));
|
: ("response already completed, ignoring " + cause));
|
||||||
}
|
}
|
||||||
return cf;
|
return Utils.wrapForDebug(debug, "getResponseAsync", cf);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -421,7 +422,6 @@ class Http1Exchange<T> extends ExchangeImpl<T> {
|
|||||||
&& response != null && response.finished()) {
|
&& response != null && response.finished()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
connection.close(); // TODO: ensure non-blocking if holding the lock
|
|
||||||
writePublisher.writeScheduler.stop();
|
writePublisher.writeScheduler.stop();
|
||||||
if (operations.isEmpty()) {
|
if (operations.isEmpty()) {
|
||||||
Log.logTrace("Http1Exchange: request [{0}/timeout={1}ms] no pending operation."
|
Log.logTrace("Http1Exchange: request [{0}/timeout={1}ms] no pending operation."
|
||||||
@ -444,6 +444,7 @@ class Http1Exchange<T> extends ExchangeImpl<T> {
|
|||||||
operations.clear();
|
operations.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
Log.logError("Http1Exchange.cancel: count=" + count);
|
Log.logError("Http1Exchange.cancel: count=" + count);
|
||||||
if (toComplete != null) {
|
if (toComplete != null) {
|
||||||
// We might be in the selector thread in case of timeout, when
|
// We might be in the selector thread in case of timeout, when
|
||||||
@ -466,6 +467,9 @@ class Http1Exchange<T> extends ExchangeImpl<T> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
connection.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void runInline(Runnable run) {
|
private void runInline(Runnable run) {
|
||||||
@ -511,7 +515,7 @@ class Http1Exchange<T> extends ExchangeImpl<T> {
|
|||||||
|
|
||||||
private void requestMoreBody() {
|
private void requestMoreBody() {
|
||||||
try {
|
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);
|
bodySubscriber.request(1);
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
if (debug.on()) debug.log("Subscription::request failed", t);
|
if (debug.on()) debug.log("Subscription::request failed", t);
|
||||||
@ -527,12 +531,25 @@ class Http1Exchange<T> extends ExchangeImpl<T> {
|
|||||||
final Executor exec = client.theExecutor();
|
final Executor exec = client.theExecutor();
|
||||||
final DataPair dp = outgoing.pollFirst();
|
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
|
if (dp == null) // publisher has not published anything yet
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
synchronized (lock) {
|
|
||||||
if (dp.throwable != null) {
|
if (dp.throwable != null) {
|
||||||
|
synchronized (lock) {
|
||||||
state = State.ERROR;
|
state = State.ERROR;
|
||||||
|
}
|
||||||
exec.execute(() -> {
|
exec.execute(() -> {
|
||||||
headersSentCF.completeExceptionally(dp.throwable);
|
headersSentCF.completeExceptionally(dp.throwable);
|
||||||
bodySentCF.completeExceptionally(dp.throwable);
|
bodySentCF.completeExceptionally(dp.throwable);
|
||||||
@ -543,14 +560,18 @@ class Http1Exchange<T> extends ExchangeImpl<T> {
|
|||||||
|
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case HEADERS:
|
case HEADERS:
|
||||||
|
synchronized (lock) {
|
||||||
state = State.BODY;
|
state = State.BODY;
|
||||||
|
}
|
||||||
// completeAsync, since dependent tasks should run in another thread
|
// completeAsync, since dependent tasks should run in another thread
|
||||||
if (debug.on()) debug.log("initiating completion of headersSentCF");
|
if (debug.on()) debug.log("initiating completion of headersSentCF");
|
||||||
headersSentCF.completeAsync(() -> this, exec);
|
headersSentCF.completeAsync(() -> this, exec);
|
||||||
break;
|
break;
|
||||||
case BODY:
|
case BODY:
|
||||||
if (dp.data == Http1BodySubscriber.COMPLETED) {
|
if (dp.data == Http1BodySubscriber.COMPLETED) {
|
||||||
|
synchronized (lock) {
|
||||||
state = State.COMPLETING;
|
state = State.COMPLETING;
|
||||||
|
}
|
||||||
if (debug.on()) debug.log("initiating completion of bodySentCF");
|
if (debug.on()) debug.log("initiating completion of bodySentCF");
|
||||||
bodySentCF.completeAsync(() -> this, exec);
|
bodySentCF.completeAsync(() -> this, exec);
|
||||||
} else {
|
} else {
|
||||||
@ -567,7 +588,6 @@ class Http1Exchange<T> extends ExchangeImpl<T> {
|
|||||||
|
|
||||||
return dp;
|
return dp;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/** A Publisher of HTTP/1.1 headers and request body. */
|
/** A Publisher of HTTP/1.1 headers and request body. */
|
||||||
final class Http1Publisher implements FlowTube.TubePublisher {
|
final class Http1Publisher implements FlowTube.TubePublisher {
|
||||||
@ -608,6 +628,14 @@ class Http1Exchange<T> extends ExchangeImpl<T> {
|
|||||||
public void run() {
|
public void run() {
|
||||||
assert state != State.COMPLETED : "Unexpected state:" + state;
|
assert state != State.COMPLETED : "Unexpected state:" + state;
|
||||||
if (debug.on()) debug.log("WriteTask");
|
if (debug.on()) debug.log("WriteTask");
|
||||||
|
|
||||||
|
if (cancelled) {
|
||||||
|
if (debug.on()) debug.log("handling cancellation");
|
||||||
|
writeScheduler.stop();
|
||||||
|
getOutgoing();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (subscriber == null) {
|
if (subscriber == null) {
|
||||||
if (debug.on()) debug.log("no subscriber yet");
|
if (debug.on()) debug.log("no subscriber yet");
|
||||||
return;
|
return;
|
||||||
@ -615,6 +643,8 @@ class Http1Exchange<T> extends ExchangeImpl<T> {
|
|||||||
if (debug.on()) debug.log(() -> "hasOutgoing = " + hasOutgoing());
|
if (debug.on()) debug.log(() -> "hasOutgoing = " + hasOutgoing());
|
||||||
while (hasOutgoing() && demand.tryDecrement()) {
|
while (hasOutgoing() && demand.tryDecrement()) {
|
||||||
DataPair dp = getOutgoing();
|
DataPair dp = getOutgoing();
|
||||||
|
if (dp == null)
|
||||||
|
break;
|
||||||
|
|
||||||
if (dp.throwable != null) {
|
if (dp.throwable != null) {
|
||||||
if (debug.on()) debug.log("onError");
|
if (debug.on()) debug.log("onError");
|
||||||
@ -661,7 +691,7 @@ class Http1Exchange<T> extends ExchangeImpl<T> {
|
|||||||
if (cancelled)
|
if (cancelled)
|
||||||
return; //no-op
|
return; //no-op
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
writeScheduler.stop();
|
writeScheduler.runOrSchedule(client.theExecutor());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,6 +35,7 @@ import java.util.Map;
|
|||||||
import java.net.http.HttpHeaders;
|
import java.net.http.HttpHeaders;
|
||||||
import static java.lang.String.format;
|
import static java.lang.String.format;
|
||||||
import static java.util.Objects.requireNonNull;
|
import static java.util.Objects.requireNonNull;
|
||||||
|
import static jdk.internal.net.http.common.Utils.ACCEPT_ALL;
|
||||||
|
|
||||||
class Http1HeaderParser {
|
class Http1HeaderParser {
|
||||||
|
|
||||||
@ -49,10 +50,13 @@ class Http1HeaderParser {
|
|||||||
private HttpHeaders headers;
|
private HttpHeaders headers;
|
||||||
private Map<String,List<String>> privateMap = new HashMap<>();
|
private Map<String,List<String>> privateMap = new HashMap<>();
|
||||||
|
|
||||||
enum State { STATUS_LINE,
|
enum State { INITIAL,
|
||||||
|
STATUS_LINE,
|
||||||
STATUS_LINE_FOUND_CR,
|
STATUS_LINE_FOUND_CR,
|
||||||
|
STATUS_LINE_FOUND_LF,
|
||||||
STATUS_LINE_END,
|
STATUS_LINE_END,
|
||||||
STATUS_LINE_END_CR,
|
STATUS_LINE_END_CR,
|
||||||
|
STATUS_LINE_END_LF,
|
||||||
HEADER,
|
HEADER,
|
||||||
HEADER_FOUND_CR,
|
HEADER_FOUND_CR,
|
||||||
HEADER_FOUND_LF,
|
HEADER_FOUND_LF,
|
||||||
@ -60,7 +64,7 @@ class Http1HeaderParser {
|
|||||||
HEADER_FOUND_CR_LF_CR,
|
HEADER_FOUND_CR_LF_CR,
|
||||||
FINISHED }
|
FINISHED }
|
||||||
|
|
||||||
private State state = State.STATUS_LINE;
|
private State state = State.INITIAL;
|
||||||
|
|
||||||
/** Returns the status-line. */
|
/** Returns the status-line. */
|
||||||
String statusLine() { return statusLine; }
|
String statusLine() { return statusLine; }
|
||||||
@ -69,7 +73,29 @@ class Http1HeaderParser {
|
|||||||
int responseCode() { return responseCode; }
|
int responseCode() { return responseCode; }
|
||||||
|
|
||||||
/** Returns the headers, possibly empty. */
|
/** 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
|
* Parses HTTP/1.X status-line and headers from the given bytes. Must be
|
||||||
@ -84,18 +110,25 @@ class Http1HeaderParser {
|
|||||||
boolean parse(ByteBuffer input) throws ProtocolException {
|
boolean parse(ByteBuffer input) throws ProtocolException {
|
||||||
requireNonNull(input, "null input");
|
requireNonNull(input, "null input");
|
||||||
|
|
||||||
while (input.hasRemaining() && state != State.FINISHED) {
|
while (canContinueParsing(input)) {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
|
case INITIAL:
|
||||||
|
state = State.STATUS_LINE;
|
||||||
|
break;
|
||||||
case STATUS_LINE:
|
case STATUS_LINE:
|
||||||
readResumeStatusLine(input);
|
readResumeStatusLine(input);
|
||||||
break;
|
break;
|
||||||
|
// fallthrough
|
||||||
case STATUS_LINE_FOUND_CR:
|
case STATUS_LINE_FOUND_CR:
|
||||||
|
case STATUS_LINE_FOUND_LF:
|
||||||
readStatusLineFeed(input);
|
readStatusLineFeed(input);
|
||||||
break;
|
break;
|
||||||
case STATUS_LINE_END:
|
case STATUS_LINE_END:
|
||||||
maybeStartHeaders(input);
|
maybeStartHeaders(input);
|
||||||
break;
|
break;
|
||||||
|
// fallthrough
|
||||||
case STATUS_LINE_END_CR:
|
case STATUS_LINE_END_CR:
|
||||||
|
case STATUS_LINE_END_LF:
|
||||||
maybeEndHeaders(input);
|
maybeEndHeaders(input);
|
||||||
break;
|
break;
|
||||||
case HEADER:
|
case HEADER:
|
||||||
@ -121,21 +154,35 @@ class Http1HeaderParser {
|
|||||||
return state == State.FINISHED;
|
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) {
|
private void readResumeStatusLine(ByteBuffer input) {
|
||||||
char c = 0;
|
char c = 0;
|
||||||
while (input.hasRemaining() && (c =(char)input.get()) != CR) {
|
while (input.hasRemaining() && (c =(char)input.get()) != CR) {
|
||||||
|
if (c == LF) break;
|
||||||
sb.append(c);
|
sb.append(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (c == CR) {
|
if (c == CR) {
|
||||||
state = State.STATUS_LINE_FOUND_CR;
|
state = State.STATUS_LINE_FOUND_CR;
|
||||||
|
} else if (c == LF) {
|
||||||
|
state = State.STATUS_LINE_FOUND_LF;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void readStatusLineFeed(ByteBuffer input) throws ProtocolException {
|
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) {
|
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());
|
c, sb.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,6 +205,8 @@ class Http1HeaderParser {
|
|||||||
char c = (char)input.get();
|
char c = (char)input.get();
|
||||||
if (c == CR) {
|
if (c == CR) {
|
||||||
state = State.STATUS_LINE_END_CR;
|
state = State.STATUS_LINE_END_CR;
|
||||||
|
} else if (c == LF) {
|
||||||
|
state = State.STATUS_LINE_END_LF;
|
||||||
} else {
|
} else {
|
||||||
sb.append(c);
|
sb.append(c);
|
||||||
state = State.HEADER;
|
state = State.HEADER;
|
||||||
@ -165,15 +214,15 @@ class Http1HeaderParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void maybeEndHeaders(ByteBuffer input) throws ProtocolException {
|
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;
|
assert sb.length() == 0;
|
||||||
char c = (char)input.get();
|
char c = state == State.STATUS_LINE_END_LF ? LF : (char)input.get();
|
||||||
if (c == LF) {
|
if (c == LF) {
|
||||||
headers = ImmutableHeaders.of(privateMap);
|
headers = HttpHeaders.of(privateMap, ACCEPT_ALL);
|
||||||
privateMap = null;
|
privateMap = null;
|
||||||
state = State.FINISHED; // no headers
|
state = State.FINISHED; // no headers
|
||||||
} else {
|
} else {
|
||||||
throw protocolException("Unexpected \"%s\", after status-line CR", c);
|
throw protocolException("Unexpected \"%s\", after status line CR", c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,8 +261,8 @@ class Http1HeaderParser {
|
|||||||
|
|
||||||
private void resumeOrLF(ByteBuffer input) {
|
private void resumeOrLF(ByteBuffer input) {
|
||||||
assert state == State.HEADER_FOUND_CR || state == State.HEADER_FOUND_LF;
|
assert state == State.HEADER_FOUND_CR || state == State.HEADER_FOUND_LF;
|
||||||
char c = (char)input.get();
|
char c = state == State.HEADER_FOUND_LF ? LF : (char)input.get();
|
||||||
if (c == LF && state == State.HEADER_FOUND_CR) {
|
if (c == LF) {
|
||||||
// header value will be flushed by
|
// header value will be flushed by
|
||||||
// resumeOrSecondCR if next line does not
|
// resumeOrSecondCR if next line does not
|
||||||
// begin by SP or HT
|
// begin by SP or HT
|
||||||
@ -231,7 +280,7 @@ class Http1HeaderParser {
|
|||||||
private void resumeOrSecondCR(ByteBuffer input) {
|
private void resumeOrSecondCR(ByteBuffer input) {
|
||||||
assert state == State.HEADER_FOUND_CR_LF;
|
assert state == State.HEADER_FOUND_CR_LF;
|
||||||
char c = (char)input.get();
|
char c = (char)input.get();
|
||||||
if (c == CR) {
|
if (c == CR || c == LF) {
|
||||||
if (sb.length() > 0) {
|
if (sb.length() > 0) {
|
||||||
// no continuation line - flush
|
// no continuation line - flush
|
||||||
// previous header value.
|
// previous header value.
|
||||||
@ -239,7 +288,13 @@ class Http1HeaderParser {
|
|||||||
sb = new StringBuilder();
|
sb = new StringBuilder();
|
||||||
addHeaderFromString(headerString);
|
addHeaderFromString(headerString);
|
||||||
}
|
}
|
||||||
|
if (c == CR) {
|
||||||
state = State.HEADER_FOUND_CR_LF_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) {
|
} else if (c == SP || c == HT) {
|
||||||
assert sb.length() != 0;
|
assert sb.length() != 0;
|
||||||
sb.append(SP); // continuation line
|
sb.append(SP); // continuation line
|
||||||
@ -262,7 +317,7 @@ class Http1HeaderParser {
|
|||||||
char c = (char)input.get();
|
char c = (char)input.get();
|
||||||
if (c == LF) {
|
if (c == LF) {
|
||||||
state = State.FINISHED;
|
state = State.FINISHED;
|
||||||
headers = ImmutableHeaders.of(privateMap);
|
headers = HttpHeaders.of(privateMap, ACCEPT_ALL);
|
||||||
privateMap = null;
|
privateMap = null;
|
||||||
} else {
|
} else {
|
||||||
throw protocolException("Unexpected \"%s\", after CR LF CR", c);
|
throw protocolException("Unexpected \"%s\", after CR LF CR", c);
|
||||||
|
@ -26,7 +26,6 @@
|
|||||||
package jdk.internal.net.http;
|
package jdk.internal.net.http;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.System.Logger.Level;
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -39,11 +38,12 @@ import java.util.function.BiPredicate;
|
|||||||
import java.net.http.HttpHeaders;
|
import java.net.http.HttpHeaders;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
import jdk.internal.net.http.Http1Exchange.Http1BodySubscriber;
|
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.Log;
|
||||||
import jdk.internal.net.http.common.Logger;
|
import jdk.internal.net.http.common.Logger;
|
||||||
import jdk.internal.net.http.common.Utils;
|
import jdk.internal.net.http.common.Utils;
|
||||||
|
|
||||||
|
import static java.lang.String.format;
|
||||||
import static java.nio.charset.StandardCharsets.US_ASCII;
|
import static java.nio.charset.StandardCharsets.US_ASCII;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -52,7 +52,7 @@ import static java.nio.charset.StandardCharsets.US_ASCII;
|
|||||||
class Http1Request {
|
class Http1Request {
|
||||||
|
|
||||||
private static final String COOKIE_HEADER = "Cookie";
|
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);
|
(k,v) -> !COOKIE_HEADER.equalsIgnoreCase(k);
|
||||||
|
|
||||||
private final HttpRequestImpl request;
|
private final HttpRequestImpl request;
|
||||||
@ -60,7 +60,7 @@ class Http1Request {
|
|||||||
private final HttpConnection connection;
|
private final HttpConnection connection;
|
||||||
private final HttpRequest.BodyPublisher requestPublisher;
|
private final HttpRequest.BodyPublisher requestPublisher;
|
||||||
private final HttpHeaders userHeaders;
|
private final HttpHeaders userHeaders;
|
||||||
private final HttpHeadersImpl systemHeaders;
|
private final HttpHeadersBuilder systemHeadersBuilder;
|
||||||
private volatile boolean streaming;
|
private volatile boolean streaming;
|
||||||
private volatile long contentLength;
|
private volatile long contentLength;
|
||||||
|
|
||||||
@ -73,7 +73,7 @@ class Http1Request {
|
|||||||
this.connection = http1Exchange.connection();
|
this.connection = http1Exchange.connection();
|
||||||
this.requestPublisher = request.requestPublisher; // may be null
|
this.requestPublisher = request.requestPublisher; // may be null
|
||||||
this.userHeaders = request.getUserHeaders();
|
this.userHeaders = request.getUserHeaders();
|
||||||
this.systemHeaders = request.getSystemHeaders();
|
this.systemHeadersBuilder = request.getSystemHeadersBuilder();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void logHeaders(String completeHeaders) {
|
private void logHeaders(String completeHeaders) {
|
||||||
@ -92,12 +92,13 @@ class Http1Request {
|
|||||||
|
|
||||||
|
|
||||||
private void collectHeaders0(StringBuilder sb) {
|
private void collectHeaders0(StringBuilder sb) {
|
||||||
BiPredicate<String,List<String>> filter =
|
BiPredicate<String,String> filter =
|
||||||
connection.headerFilter(request);
|
connection.headerFilter(request);
|
||||||
|
|
||||||
// Filter out 'Cookie:' headers, we will collect them at the end.
|
// Filter out 'Cookie:' headers, we will collect them at the end.
|
||||||
BiPredicate<String,List<String>> nocookies =
|
BiPredicate<String,String> nocookies = NOCOOKIES.and(filter);
|
||||||
NOCOOKIES.and(filter);
|
|
||||||
|
HttpHeaders systemHeaders = systemHeadersBuilder.build();
|
||||||
|
|
||||||
// If we're sending this request through a tunnel,
|
// If we're sending this request through a tunnel,
|
||||||
// then don't send any preemptive proxy-* headers that
|
// then don't send any preemptive proxy-* headers that
|
||||||
@ -112,8 +113,7 @@ class Http1Request {
|
|||||||
|
|
||||||
// Gather all 'Cookie:' headers and concatenate their
|
// Gather all 'Cookie:' headers and concatenate their
|
||||||
// values in a single line.
|
// values in a single line.
|
||||||
collectCookies(sb, COOKIE_HEADER,
|
collectCookies(sb, systemHeaders, userHeaders);
|
||||||
systemHeaders, userHeaders, filter);
|
|
||||||
|
|
||||||
// terminate headers
|
// terminate headers
|
||||||
sb.append('\r').append('\n');
|
sb.append('\r').append('\n');
|
||||||
@ -142,20 +142,16 @@ class Http1Request {
|
|||||||
// any illegal character for header field values.
|
// any illegal character for header field values.
|
||||||
//
|
//
|
||||||
private void collectCookies(StringBuilder sb,
|
private void collectCookies(StringBuilder sb,
|
||||||
String key,
|
|
||||||
HttpHeaders system,
|
HttpHeaders system,
|
||||||
HttpHeaders user,
|
HttpHeaders user) {
|
||||||
BiPredicate<String, List<String>> filter) {
|
List<String> systemList = system.allValues(COOKIE_HEADER);
|
||||||
List<String> systemList = system.allValues(key);
|
List<String> userList = user.allValues(COOKIE_HEADER);
|
||||||
if (systemList != null && !filter.test(key, systemList)) systemList = null;
|
|
||||||
List<String> userList = user.allValues(key);
|
|
||||||
if (userList != null && !filter.test(key, userList)) userList = null;
|
|
||||||
boolean found = false;
|
boolean found = false;
|
||||||
if (systemList != null) {
|
if (systemList != null) {
|
||||||
for (String cookie : systemList) {
|
for (String cookie : systemList) {
|
||||||
if (!found) {
|
if (!found) {
|
||||||
found = true;
|
found = true;
|
||||||
sb.append(key).append(':').append(' ');
|
sb.append(COOKIE_HEADER).append(':').append(' ');
|
||||||
} else {
|
} else {
|
||||||
sb.append(';').append(' ');
|
sb.append(';').append(' ');
|
||||||
}
|
}
|
||||||
@ -166,7 +162,7 @@ class Http1Request {
|
|||||||
for (String cookie : userList) {
|
for (String cookie : userList) {
|
||||||
if (!found) {
|
if (!found) {
|
||||||
found = true;
|
found = true;
|
||||||
sb.append(key).append(':').append(' ');
|
sb.append(COOKIE_HEADER).append(':').append(' ');
|
||||||
} else {
|
} else {
|
||||||
sb.append(';').append(' ');
|
sb.append(';').append(' ');
|
||||||
}
|
}
|
||||||
@ -176,13 +172,15 @@ class Http1Request {
|
|||||||
if (found) sb.append('\r').append('\n');
|
if (found) sb.append('\r').append('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
private void collectHeaders1(StringBuilder sb, HttpHeaders headers,
|
private void collectHeaders1(StringBuilder sb,
|
||||||
BiPredicate<String, List<String>> filter) {
|
HttpHeaders headers,
|
||||||
|
BiPredicate<String,String> filter) {
|
||||||
for (Map.Entry<String,List<String>> entry : headers.map().entrySet()) {
|
for (Map.Entry<String,List<String>> entry : headers.map().entrySet()) {
|
||||||
String key = entry.getKey();
|
String key = entry.getKey();
|
||||||
List<String> values = entry.getValue();
|
List<String> values = entry.getValue();
|
||||||
if (!filter.test(key, values)) continue;
|
|
||||||
for (String value : values) {
|
for (String value : values) {
|
||||||
|
if (!filter.test(key, value))
|
||||||
|
continue;
|
||||||
sb.append(key).append(':').append(' ')
|
sb.append(key).append(':').append(' ')
|
||||||
.append(value)
|
.append(value)
|
||||||
.append('\r').append('\n');
|
.append('\r').append('\n');
|
||||||
@ -279,7 +277,7 @@ class Http1Request {
|
|||||||
|
|
||||||
URI uri = request.uri();
|
URI uri = request.uri();
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
systemHeaders.setHeader("Host", hostString());
|
systemHeadersBuilder.setHeader("Host", hostString());
|
||||||
}
|
}
|
||||||
if (requestPublisher == null) {
|
if (requestPublisher == null) {
|
||||||
// Not a user request, or maybe a method, e.g. GET, with no body.
|
// Not a user request, or maybe a method, e.g. GET, with no body.
|
||||||
@ -289,13 +287,13 @@ class Http1Request {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (contentLength == 0) {
|
if (contentLength == 0) {
|
||||||
systemHeaders.setHeader("Content-Length", "0");
|
systemHeadersBuilder.setHeader("Content-Length", "0");
|
||||||
} else if (contentLength > 0) {
|
} else if (contentLength > 0) {
|
||||||
systemHeaders.setHeader("Content-Length", Long.toString(contentLength));
|
systemHeadersBuilder.setHeader("Content-Length", Long.toString(contentLength));
|
||||||
streaming = false;
|
streaming = false;
|
||||||
} else {
|
} else {
|
||||||
streaming = true;
|
streaming = true;
|
||||||
systemHeaders.setHeader("Transfer-encoding", "chunked");
|
systemHeadersBuilder.setHeader("Transfer-encoding", "chunked");
|
||||||
}
|
}
|
||||||
collectHeaders0(sb);
|
collectHeaders0(sb);
|
||||||
String hs = sb.toString();
|
String hs = sb.toString();
|
||||||
@ -349,6 +347,11 @@ class Http1Request {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String currentStateMessage() {
|
||||||
|
return "streaming request body " + (complete ? "complete" : "incomplete");
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(Throwable throwable) {
|
public void onError(Throwable throwable) {
|
||||||
if (complete)
|
if (complete)
|
||||||
@ -416,6 +419,12 @@ class Http1Request {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String currentStateMessage() {
|
||||||
|
return format("fixed content-length: %d, bytes sent: %d",
|
||||||
|
contentLength, contentWritten);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(Throwable throwable) {
|
public void onError(Throwable throwable) {
|
||||||
if (debug.on()) debug.log("onError");
|
if (debug.on()) debug.log("onError");
|
||||||
|
@ -44,9 +44,10 @@ import jdk.internal.net.http.common.Log;
|
|||||||
import jdk.internal.net.http.common.Logger;
|
import jdk.internal.net.http.common.Logger;
|
||||||
import jdk.internal.net.http.common.MinimalFuture;
|
import jdk.internal.net.http.common.MinimalFuture;
|
||||||
import jdk.internal.net.http.common.Utils;
|
import jdk.internal.net.http.common.Utils;
|
||||||
|
|
||||||
import static java.net.http.HttpClient.Version.HTTP_1_1;
|
import static java.net.http.HttpClient.Version.HTTP_1_1;
|
||||||
import static java.net.http.HttpResponse.BodySubscribers.discarding;
|
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).
|
* Handles a HTTP/1.1 response (headers + body).
|
||||||
@ -76,6 +77,7 @@ class Http1Response<T> {
|
|||||||
final Logger debug = Utils.getDebugLogger(this::dbgString, Utils.DEBUG);
|
final Logger debug = Utils.getDebugLogger(this::dbgString, Utils.DEBUG);
|
||||||
final static AtomicLong responseCount = new AtomicLong();
|
final static AtomicLong responseCount = new AtomicLong();
|
||||||
final long id = responseCount.incrementAndGet();
|
final long id = responseCount.incrementAndGet();
|
||||||
|
private Http1HeaderParser hd;
|
||||||
|
|
||||||
Http1Response(HttpConnection conn,
|
Http1Response(HttpConnection conn,
|
||||||
Http1Exchange<T> exchange,
|
Http1Exchange<T> exchange,
|
||||||
@ -87,6 +89,11 @@ class Http1Response<T> {
|
|||||||
this.asyncReceiver = asyncReceiver;
|
this.asyncReceiver = asyncReceiver;
|
||||||
headersReader = new HeadersReader(this::advance);
|
headersReader = new HeadersReader(this::advance);
|
||||||
bodyReader = new BodyReader(this::advance);
|
bodyReader = new BodyReader(this::advance);
|
||||||
|
|
||||||
|
hd = new Http1HeaderParser();
|
||||||
|
readProgress = State.READING_HEADERS;
|
||||||
|
headersReader.start(hd);
|
||||||
|
asyncReceiver.subscribe(headersReader);
|
||||||
}
|
}
|
||||||
|
|
||||||
String dbgTag;
|
String dbgTag;
|
||||||
@ -150,19 +157,36 @@ class Http1Response<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private volatile boolean firstTimeAround = true;
|
||||||
|
|
||||||
public CompletableFuture<Response> readHeadersAsync(Executor executor) {
|
public CompletableFuture<Response> readHeadersAsync(Executor executor) {
|
||||||
if (debug.on())
|
if (debug.on())
|
||||||
debug.log("Reading Headers: (remaining: "
|
debug.log("Reading Headers: (remaining: "
|
||||||
+ asyncReceiver.remaining() +") " + readProgress);
|
+ asyncReceiver.remaining() +") " + readProgress);
|
||||||
|
|
||||||
|
if (firstTimeAround) {
|
||||||
|
if (debug.on()) debug.log("First time around");
|
||||||
|
firstTimeAround = false;
|
||||||
|
} else {
|
||||||
// with expect continue we will resume reading headers + body.
|
// with expect continue we will resume reading headers + body.
|
||||||
asyncReceiver.unsubscribe(bodyReader);
|
asyncReceiver.unsubscribe(bodyReader);
|
||||||
bodyReader.reset();
|
bodyReader.reset();
|
||||||
Http1HeaderParser hd = new Http1HeaderParser();
|
|
||||||
|
hd = new Http1HeaderParser();
|
||||||
readProgress = State.READING_HEADERS;
|
readProgress = State.READING_HEADERS;
|
||||||
|
headersReader.reset();
|
||||||
headersReader.start(hd);
|
headersReader.start(hd);
|
||||||
asyncReceiver.subscribe(headersReader);
|
asyncReceiver.subscribe(headersReader);
|
||||||
|
}
|
||||||
|
|
||||||
CompletableFuture<State> cf = headersReader.completion();
|
CompletableFuture<State> cf = headersReader.completion();
|
||||||
assert cf != null : "parsing not started";
|
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) -> {
|
Function<State, Response> lambda = (State completed) -> {
|
||||||
assert completed == State.READING_HEADERS;
|
assert completed == State.READING_HEADERS;
|
||||||
@ -207,7 +231,7 @@ class Http1Response<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int fixupContentLen(int clen) {
|
int fixupContentLen(int clen) {
|
||||||
if (request.method().equalsIgnoreCase("HEAD")) {
|
if (request.method().equalsIgnoreCase("HEAD") || responseCode == HTTP_NOT_MODIFIED) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (clen == -1) {
|
if (clen == -1) {
|
||||||
@ -289,13 +313,19 @@ class Http1Response<T> {
|
|||||||
try {
|
try {
|
||||||
userSubscriber.onComplete();
|
userSubscriber.onComplete();
|
||||||
} catch (Throwable x) {
|
} catch (Throwable x) {
|
||||||
propagateError(t = withError = Utils.getCompletionCause(x));
|
// Simply propagate the error by calling
|
||||||
// rethrow and let the caller deal with it.
|
// 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)
|
// (i.e: log and close the connection)
|
||||||
// arguably we could decide to not throw and let the
|
// Note that rethrowing here could introduce a
|
||||||
// connection be reused since we should have received and
|
// race that might cause the next send() operation to
|
||||||
// parsed all the bytes when we reach here.
|
// fail as the connection has already been put back
|
||||||
throw x;
|
// into the cache when we reach here.
|
||||||
|
propagateError(t = withError = Utils.getCompletionCause(x));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
propagateError(t);
|
propagateError(t);
|
||||||
@ -613,6 +643,7 @@ class Http1Response<T> {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public final void onReadError(Throwable t) {
|
public final void onReadError(Throwable t) {
|
||||||
|
t = wrapWithExtraDetail(t, parser::currentStateMessage);
|
||||||
Http1Response.this.onReadError(t);
|
Http1Response.this.onReadError(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -692,6 +723,7 @@ class Http1Response<T> {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public final void onReadError(Throwable t) {
|
public final void onReadError(Throwable t) {
|
||||||
|
t = wrapWithExtraDetail(t, parser::currentStateMessage);
|
||||||
Http1Response.this.onReadError(t);
|
Http1Response.this.onReadError(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,6 +25,9 @@
|
|||||||
|
|
||||||
package jdk.internal.net.http;
|
package jdk.internal.net.http;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
import java.net.ConnectException;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
@ -95,6 +98,7 @@ class Http2ClientImpl {
|
|||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
Http2Connection connection = connections.get(key);
|
Http2Connection connection = connections.get(key);
|
||||||
if (connection != null) {
|
if (connection != null) {
|
||||||
|
try {
|
||||||
if (connection.closed || !connection.reserveStream(true)) {
|
if (connection.closed || !connection.reserveStream(true)) {
|
||||||
if (debug.on())
|
if (debug.on())
|
||||||
debug.log("removing found closed or closing connection: %s", connection);
|
debug.log("removing found closed or closing connection: %s", connection);
|
||||||
@ -105,6 +109,10 @@ class Http2ClientImpl {
|
|||||||
debug.log("found connection in the pool: %s", connection);
|
debug.log("found connection in the pool: %s", connection);
|
||||||
return MinimalFuture.completedFuture(connection);
|
return MinimalFuture.completedFuture(connection);
|
||||||
}
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
// thrown by connection.reserveStream()
|
||||||
|
return MinimalFuture.failedFuture(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!req.secure() || failures.contains(key)) {
|
if (!req.secure() || failures.contains(key)) {
|
||||||
@ -119,6 +127,11 @@ class Http2ClientImpl {
|
|||||||
.whenComplete((conn, t) -> {
|
.whenComplete((conn, t) -> {
|
||||||
synchronized (Http2ClientImpl.this) {
|
synchronized (Http2ClientImpl.this) {
|
||||||
if (conn != null) {
|
if (conn != null) {
|
||||||
|
try {
|
||||||
|
conn.reserveStream(true);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UncheckedIOException(e); // shouldn't happen
|
||||||
|
}
|
||||||
offerConnection(conn);
|
offerConnection(conn);
|
||||||
} else {
|
} else {
|
||||||
Throwable cause = Utils.getCompletionCause(t);
|
Throwable cause = Utils.getCompletionCause(t);
|
||||||
|
@ -28,7 +28,6 @@ package jdk.internal.net.http;
|
|||||||
import java.io.EOFException;
|
import java.io.EOFException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.UncheckedIOException;
|
import java.io.UncheckedIOException;
|
||||||
import java.lang.System.Logger.Level;
|
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
@ -54,7 +53,7 @@ import java.net.http.HttpHeaders;
|
|||||||
import jdk.internal.net.http.HttpConnection.HttpPublisher;
|
import jdk.internal.net.http.HttpConnection.HttpPublisher;
|
||||||
import jdk.internal.net.http.common.FlowTube;
|
import jdk.internal.net.http.common.FlowTube;
|
||||||
import jdk.internal.net.http.common.FlowTube.TubeSubscriber;
|
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.Log;
|
||||||
import jdk.internal.net.http.common.Logger;
|
import jdk.internal.net.http.common.Logger;
|
||||||
import jdk.internal.net.http.common.MinimalFuture;
|
import jdk.internal.net.http.common.MinimalFuture;
|
||||||
@ -82,7 +81,6 @@ import jdk.internal.net.http.hpack.DecodingCallback;
|
|||||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
import static jdk.internal.net.http.frame.SettingsFrame.*;
|
import static jdk.internal.net.http.frame.SettingsFrame.*;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An Http2Connection. Encapsulates the socket(channel) and any SSLEngine used
|
* An Http2Connection. Encapsulates the socket(channel) and any SSLEngine used
|
||||||
* over it. Contains an HttpConnection which hides the SocketChannel SSL stuff.
|
* over it. Contains an HttpConnection which hides the SocketChannel SSL stuff.
|
||||||
@ -259,6 +257,8 @@ class Http2Connection {
|
|||||||
// assigning a stream to a connection.
|
// assigning a stream to a connection.
|
||||||
private int lastReservedClientStreamid = 1;
|
private int lastReservedClientStreamid = 1;
|
||||||
private int lastReservedServerStreamid = 0;
|
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 Encoder hpackOut;
|
||||||
private final Decoder hpackIn;
|
private final Decoder hpackIn;
|
||||||
final SettingsFrame clientSettings;
|
final SettingsFrame clientSettings;
|
||||||
@ -273,7 +273,7 @@ class Http2Connection {
|
|||||||
*/
|
*/
|
||||||
private final WindowController windowController = new WindowController();
|
private final WindowController windowController = new WindowController();
|
||||||
private final FramesController framesController = new FramesController();
|
private final FramesController framesController = new FramesController();
|
||||||
private final Http2TubeSubscriber subscriber = new Http2TubeSubscriber();
|
private final Http2TubeSubscriber subscriber;
|
||||||
final ConnectionWindowUpdateSender windowUpdater;
|
final ConnectionWindowUpdateSender windowUpdater;
|
||||||
private volatile Throwable cause;
|
private volatile Throwable cause;
|
||||||
private volatile Supplier<ByteBuffer> initial;
|
private volatile Supplier<ByteBuffer> initial;
|
||||||
@ -290,6 +290,7 @@ class Http2Connection {
|
|||||||
String key) {
|
String key) {
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
this.client2 = client2;
|
this.client2 = client2;
|
||||||
|
this.subscriber = new Http2TubeSubscriber(client2.client());
|
||||||
this.nextstreamid = nextstreamid;
|
this.nextstreamid = nextstreamid;
|
||||||
this.key = key;
|
this.key = key;
|
||||||
this.clientSettings = this.client2.getClientSettings();
|
this.clientSettings = this.client2.getClientSettings();
|
||||||
@ -310,7 +311,7 @@ class Http2Connection {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Case 1) Create from upgraded HTTP/1.1 connection.
|
* 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
|
* that initiated the connection, whose response will be delivered
|
||||||
* on a Stream.
|
* on a Stream.
|
||||||
*/
|
*/
|
||||||
@ -324,6 +325,7 @@ class Http2Connection {
|
|||||||
client2,
|
client2,
|
||||||
3, // stream 1 is registered during the upgrade
|
3, // stream 1 is registered during the upgrade
|
||||||
keyFor(connection));
|
keyFor(connection));
|
||||||
|
reserveStream(true);
|
||||||
Log.logTrace("Connection send window size {0} ", windowController.connectionWindowSize());
|
Log.logTrace("Connection send window size {0} ", windowController.connectionWindowSize());
|
||||||
|
|
||||||
Stream<?> initialStream = createStream(exchange);
|
Stream<?> initialStream = createStream(exchange);
|
||||||
@ -407,7 +409,8 @@ class Http2Connection {
|
|||||||
// call these before assigning a request/stream to a connection
|
// call these before assigning a request/stream to a connection
|
||||||
// if false returned then a new Http2Connection is required
|
// if false returned then a new Http2Connection is required
|
||||||
// if true, the the stream may be assigned to this connection
|
// 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) {
|
if (finalStream) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -424,6 +427,19 @@ class Http2Connection {
|
|||||||
lastReservedClientStreamid+=2;
|
lastReservedClientStreamid+=2;
|
||||||
else
|
else
|
||||||
lastReservedServerStreamid+=2;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -564,6 +580,14 @@ class Http2Connection {
|
|||||||
return serverSettings.getParameter(INITIAL_WINDOW_SIZE);
|
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() {
|
void close() {
|
||||||
Log.logTrace("Closing HTTP/2 connection: to {0}", connection.address());
|
Log.logTrace("Closing HTTP/2 connection: to {0}", connection.address());
|
||||||
GoAwayFrame f = new GoAwayFrame(0,
|
GoAwayFrame f = new GoAwayFrame(0,
|
||||||
@ -637,13 +661,19 @@ class Http2Connection {
|
|||||||
if (closed == true) return;
|
if (closed == true) return;
|
||||||
closed = true;
|
closed = true;
|
||||||
}
|
}
|
||||||
|
if (Log.errors()) {
|
||||||
|
if (!(t instanceof EOFException) || isActive()) {
|
||||||
Log.logError(t);
|
Log.logError(t);
|
||||||
|
} else if (t != null) {
|
||||||
|
Log.logError("Shutting down connection: {0}", t.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
Throwable initialCause = this.cause;
|
Throwable initialCause = this.cause;
|
||||||
if (initialCause == null) this.cause = t;
|
if (initialCause == null) this.cause = t;
|
||||||
client2.deleteConnection(this);
|
client2.deleteConnection(this);
|
||||||
List<Stream<?>> c = new LinkedList<>(streams.values());
|
List<Stream<?>> c = new LinkedList<>(streams.values());
|
||||||
for (Stream<?> s : c) {
|
for (Stream<?> s : c) {
|
||||||
s.cancelImpl(t);
|
s.connectionClosing(t);
|
||||||
}
|
}
|
||||||
connection.close();
|
connection.close();
|
||||||
}
|
}
|
||||||
@ -766,7 +796,7 @@ class Http2Connection {
|
|||||||
nextPushStream += 2;
|
nextPushStream += 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpHeadersImpl headers = decoder.headers();
|
HttpHeaders headers = decoder.headers();
|
||||||
HttpRequestImpl pushReq = HttpRequestImpl.createPushRequest(parentReq, headers);
|
HttpRequestImpl pushReq = HttpRequestImpl.createPushRequest(parentReq, headers);
|
||||||
Exchange<T> pushExch = new Exchange<>(pushReq, parent.exchange.multi);
|
Exchange<T> pushExch = new Exchange<>(pushReq, parent.exchange.multi);
|
||||||
Stream.PushedStream<T> pushStream = createPushStream(parent, pushExch);
|
Stream.PushedStream<T> pushStream = createPushStream(parent, pushExch);
|
||||||
@ -797,16 +827,44 @@ class Http2Connection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void resetStream(int streamid, int code) throws IOException {
|
void resetStream(int streamid, int code) throws IOException {
|
||||||
|
try {
|
||||||
|
if (connection.channel().isOpen()) {
|
||||||
|
// no need to try & send a reset frame if the
|
||||||
|
// connection channel is already closed.
|
||||||
Log.logError(
|
Log.logError(
|
||||||
"Resetting stream {0,number,integer} with error code {1,number,integer}",
|
"Resetting stream {0,number,integer} with error code {1,number,integer}",
|
||||||
streamid, code);
|
streamid, code);
|
||||||
ResetFrame frame = new ResetFrame(streamid, code);
|
ResetFrame frame = new ResetFrame(streamid, code);
|
||||||
sendFrame(frame);
|
sendFrame(frame);
|
||||||
|
} else if (debug.on()) {
|
||||||
|
debug.log("Channel already closed, no need to reset stream %d",
|
||||||
|
streamid);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
decrementStreamsCount(streamid);
|
||||||
closeStream(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) {
|
void closeStream(int streamid) {
|
||||||
if (debug.on()) debug.log("Closed stream %d", streamid);
|
if (debug.on()) debug.log("Closed stream %d", streamid);
|
||||||
|
boolean isClient = (streamid % 2) == 1;
|
||||||
Stream<?> s = streams.remove(streamid);
|
Stream<?> s = streams.remove(streamid);
|
||||||
if (s != null) {
|
if (s != null) {
|
||||||
// decrement the reference count on the HttpClientImpl
|
// decrement the reference count on the HttpClientImpl
|
||||||
@ -1148,14 +1206,19 @@ class Http2Connection {
|
|||||||
* A simple tube subscriber for reading from the connection flow.
|
* A simple tube subscriber for reading from the connection flow.
|
||||||
*/
|
*/
|
||||||
final class Http2TubeSubscriber implements TubeSubscriber {
|
final class Http2TubeSubscriber implements TubeSubscriber {
|
||||||
volatile Flow.Subscription subscription;
|
private volatile Flow.Subscription subscription;
|
||||||
volatile boolean completed;
|
private volatile boolean completed;
|
||||||
volatile boolean dropped;
|
private volatile boolean dropped;
|
||||||
volatile Throwable error;
|
private volatile Throwable error;
|
||||||
final ConcurrentLinkedQueue<ByteBuffer> queue
|
private final ConcurrentLinkedQueue<ByteBuffer> queue
|
||||||
= new ConcurrentLinkedQueue<>();
|
= new ConcurrentLinkedQueue<>();
|
||||||
final SequentialScheduler scheduler =
|
private final SequentialScheduler scheduler =
|
||||||
SequentialScheduler.synchronizedScheduler(this::processQueue);
|
SequentialScheduler.synchronizedScheduler(this::processQueue);
|
||||||
|
private final HttpClientImpl client;
|
||||||
|
|
||||||
|
Http2TubeSubscriber(HttpClientImpl client) {
|
||||||
|
this.client = Objects.requireNonNull(client);
|
||||||
|
}
|
||||||
|
|
||||||
final void processQueue() {
|
final void processQueue() {
|
||||||
try {
|
try {
|
||||||
@ -1179,6 +1242,12 @@ class Http2Connection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final void runOrSchedule() {
|
||||||
|
if (client.isSelectorThread()) {
|
||||||
|
scheduler.runOrSchedule(client.theExecutor());
|
||||||
|
} else scheduler.runOrSchedule();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSubscribe(Flow.Subscription subscription) {
|
public void onSubscribe(Flow.Subscription subscription) {
|
||||||
// supports being called multiple time.
|
// supports being called multiple time.
|
||||||
@ -1202,7 +1271,7 @@ class Http2Connection {
|
|||||||
if (debug.on()) debug.log(() -> "onNext: got " + Utils.remaining(item)
|
if (debug.on()) debug.log(() -> "onNext: got " + Utils.remaining(item)
|
||||||
+ " bytes in " + item.size() + " buffers");
|
+ " bytes in " + item.size() + " buffers");
|
||||||
queue.addAll(item);
|
queue.addAll(item);
|
||||||
scheduler.runOrSchedule(client().theExecutor());
|
runOrSchedule();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -1210,15 +1279,18 @@ class Http2Connection {
|
|||||||
if (debug.on()) debug.log(() -> "onError: " + throwable);
|
if (debug.on()) debug.log(() -> "onError: " + throwable);
|
||||||
error = throwable;
|
error = throwable;
|
||||||
completed = true;
|
completed = true;
|
||||||
scheduler.runOrSchedule(client().theExecutor());
|
runOrSchedule();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onComplete() {
|
public void onComplete() {
|
||||||
if (debug.on()) debug.log("EOF");
|
String msg = isActive()
|
||||||
error = new EOFException("EOF reached while reading");
|
? "EOF reached while reading"
|
||||||
|
: "Idle connection closed by HTTP/2 peer";
|
||||||
|
if (debug.on()) debug.log(msg);
|
||||||
|
error = new EOFException(msg);
|
||||||
completed = true;
|
completed = true;
|
||||||
scheduler.runOrSchedule(client().theExecutor());
|
runOrSchedule();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -1230,6 +1302,10 @@ class Http2Connection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
synchronized boolean isActive() {
|
||||||
|
return numReservedClientStreams > 0 || numReservedServerStreams > 0;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public final String toString() {
|
public final String toString() {
|
||||||
return dbgString();
|
return dbgString();
|
||||||
@ -1242,10 +1318,10 @@ class Http2Connection {
|
|||||||
|
|
||||||
static class HeaderDecoder extends ValidatingHeadersConsumer {
|
static class HeaderDecoder extends ValidatingHeadersConsumer {
|
||||||
|
|
||||||
HttpHeadersImpl headers;
|
HttpHeadersBuilder headersBuilder;
|
||||||
|
|
||||||
HeaderDecoder() {
|
HeaderDecoder() {
|
||||||
this.headers = new HttpHeadersImpl();
|
this.headersBuilder = new HttpHeadersBuilder();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -1253,11 +1329,11 @@ class Http2Connection {
|
|||||||
String n = name.toString();
|
String n = name.toString();
|
||||||
String v = value.toString();
|
String v = value.toString();
|
||||||
super.onDecoded(n, v);
|
super.onDecoded(n, v);
|
||||||
headers.addHeader(n, v);
|
headersBuilder.addHeader(n, v);
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpHeadersImpl headers() {
|
HttpHeaders headers() {
|
||||||
return headers;
|
return headersBuilder.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,11 +28,14 @@ package jdk.internal.net.http;
|
|||||||
import javax.net.ssl.SSLContext;
|
import javax.net.ssl.SSLContext;
|
||||||
import javax.net.ssl.SSLParameters;
|
import javax.net.ssl.SSLParameters;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
import java.lang.ref.Reference;
|
import java.lang.ref.Reference;
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
import java.net.Authenticator;
|
import java.net.Authenticator;
|
||||||
|
import java.net.ConnectException;
|
||||||
import java.net.CookieHandler;
|
import java.net.CookieHandler;
|
||||||
import java.net.ProxySelector;
|
import java.net.ProxySelector;
|
||||||
|
import java.net.http.HttpTimeoutException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.channels.CancelledKeyException;
|
import java.nio.channels.CancelledKeyException;
|
||||||
import java.nio.channels.ClosedChannelException;
|
import java.nio.channels.ClosedChannelException;
|
||||||
@ -56,13 +59,14 @@ import java.util.Optional;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
import java.util.concurrent.CompletionException;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.ThreadFactory;
|
import java.util.concurrent.ThreadFactory;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
import java.util.function.BooleanSupplier;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
@ -121,6 +125,34 @@ final class HttpClientImpl extends HttpClient implements Trackable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 CookieHandler cookieHandler;
|
||||||
private final Redirect followRedirects;
|
private final Redirect followRedirects;
|
||||||
private final Optional<ProxySelector> userProxySelector;
|
private final Optional<ProxySelector> userProxySelector;
|
||||||
@ -128,7 +160,7 @@ final class HttpClientImpl extends HttpClient implements Trackable {
|
|||||||
private final Authenticator authenticator;
|
private final Authenticator authenticator;
|
||||||
private final Version version;
|
private final Version version;
|
||||||
private final ConnectionPool connections;
|
private final ConnectionPool connections;
|
||||||
private final Executor executor;
|
private final DelegatingExecutor delegatingExecutor;
|
||||||
private final boolean isDefaultExecutor;
|
private final boolean isDefaultExecutor;
|
||||||
// Security parameters
|
// Security parameters
|
||||||
private final SSLContext sslContext;
|
private final SSLContext sslContext;
|
||||||
@ -240,12 +272,11 @@ final class HttpClientImpl extends HttpClient implements Trackable {
|
|||||||
ex = Executors.newCachedThreadPool(new DefaultThreadFactory(id));
|
ex = Executors.newCachedThreadPool(new DefaultThreadFactory(id));
|
||||||
isDefaultExecutor = true;
|
isDefaultExecutor = true;
|
||||||
} else {
|
} else {
|
||||||
ex = builder.executor;
|
|
||||||
isDefaultExecutor = false;
|
isDefaultExecutor = false;
|
||||||
}
|
}
|
||||||
|
delegatingExecutor = new DelegatingExecutor(this::isSelectorThread, ex);
|
||||||
facadeRef = new WeakReference<>(facadeFactory.createFacade(this));
|
facadeRef = new WeakReference<>(facadeFactory.createFacade(this));
|
||||||
client2 = new Http2ClientImpl(this);
|
client2 = new Http2ClientImpl(this);
|
||||||
executor = ex;
|
|
||||||
cookieHandler = builder.cookieHandler;
|
cookieHandler = builder.cookieHandler;
|
||||||
followRedirects = builder.followRedirects == null ?
|
followRedirects = builder.followRedirects == null ?
|
||||||
Redirect.NEVER : builder.followRedirects;
|
Redirect.NEVER : builder.followRedirects;
|
||||||
@ -489,20 +520,37 @@ final class HttpClientImpl extends HttpClient implements Trackable {
|
|||||||
send(HttpRequest req, BodyHandler<T> responseHandler)
|
send(HttpRequest req, BodyHandler<T> responseHandler)
|
||||||
throws IOException, InterruptedException
|
throws IOException, InterruptedException
|
||||||
{
|
{
|
||||||
|
CompletableFuture<HttpResponse<T>> cf = null;
|
||||||
try {
|
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) {
|
} catch (ExecutionException e) {
|
||||||
Throwable t = e.getCause();
|
final Throwable throwable = e.getCause();
|
||||||
if (t instanceof Error)
|
final String msg = throwable.getMessage();
|
||||||
throw (Error)t;
|
|
||||||
if (t instanceof RuntimeException)
|
if (throwable instanceof IllegalArgumentException) {
|
||||||
throw (RuntimeException)t;
|
throw new IllegalArgumentException(msg, throwable);
|
||||||
else if (t instanceof IOException)
|
} else if (throwable instanceof SecurityException) {
|
||||||
throw Utils.getIOException(t);
|
throw new SecurityException(msg, throwable);
|
||||||
else
|
} else if (throwable instanceof HttpTimeoutException) {
|
||||||
throw new InternalError("Unexpected exception", t);
|
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
|
@Override
|
||||||
public <T> CompletableFuture<HttpResponse<T>>
|
public <T> CompletableFuture<HttpResponse<T>>
|
||||||
@ -511,13 +559,20 @@ final class HttpClientImpl extends HttpClient implements Trackable {
|
|||||||
return sendAsync(userRequest, responseHandler, null);
|
return sendAsync(userRequest, responseHandler, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> CompletableFuture<HttpResponse<T>>
|
public <T> CompletableFuture<HttpResponse<T>>
|
||||||
sendAsync(HttpRequest userRequest,
|
sendAsync(HttpRequest userRequest,
|
||||||
BodyHandler<T> responseHandler,
|
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(userRequest);
|
||||||
Objects.requireNonNull(responseHandler);
|
Objects.requireNonNull(responseHandler);
|
||||||
|
|
||||||
@ -536,9 +591,17 @@ final class HttpClientImpl extends HttpClient implements Trackable {
|
|||||||
if (debugelapsed.on())
|
if (debugelapsed.on())
|
||||||
debugelapsed.log("ClientImpl (async) send %s", userRequest);
|
debugelapsed.log("ClientImpl (async) send %s", userRequest);
|
||||||
|
|
||||||
Executor executor = acc == null
|
// When using sendAsync(...) we explicitly pass the
|
||||||
? this.executor
|
// executor's delegate as exchange executor to force
|
||||||
: new PrivilegedExecutor(this.executor, acc);
|
// 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,
|
MultiExchange<T> mex = new MultiExchange<>(userRequest,
|
||||||
requestImpl,
|
requestImpl,
|
||||||
@ -547,15 +610,18 @@ final class HttpClientImpl extends HttpClient implements Trackable {
|
|||||||
pushPromiseHandler,
|
pushPromiseHandler,
|
||||||
acc);
|
acc);
|
||||||
CompletableFuture<HttpResponse<T>> res =
|
CompletableFuture<HttpResponse<T>> res =
|
||||||
mex.responseAsync().whenComplete((b,t) -> unreference());
|
mex.responseAsync(executor).whenComplete((b,t) -> unreference());
|
||||||
if (DEBUGELAPSED) {
|
if (DEBUGELAPSED) {
|
||||||
res = res.whenComplete(
|
res = res.whenComplete(
|
||||||
(b,t) -> debugCompleted("ClientImpl (async)", start, userRequest));
|
(b,t) -> debugCompleted("ClientImpl (async)", start, userRequest));
|
||||||
}
|
}
|
||||||
|
|
||||||
// makes sure that any dependent actions happen in the executor
|
// makes sure that any dependent actions happen in the CF default
|
||||||
res = res.whenCompleteAsync((r, t) -> { /* do nothing */}, executor);
|
// 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;
|
return res;
|
||||||
} catch(Throwable t) {
|
} catch(Throwable t) {
|
||||||
unreference();
|
unreference();
|
||||||
@ -654,6 +720,7 @@ final class HttpClientImpl extends HttpClient implements Trackable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
synchronized void shutdown() {
|
synchronized void shutdown() {
|
||||||
|
Log.logTrace("{0}: shutting down", getName());
|
||||||
if (debug.on()) debug.log("SelectorManager shutting down");
|
if (debug.on()) debug.log("SelectorManager shutting down");
|
||||||
closed = true;
|
closed = true;
|
||||||
try {
|
try {
|
||||||
@ -670,6 +737,7 @@ final class HttpClientImpl extends HttpClient implements Trackable {
|
|||||||
List<AsyncEvent> readyList = new ArrayList<>();
|
List<AsyncEvent> readyList = new ArrayList<>();
|
||||||
List<Runnable> resetList = new ArrayList<>();
|
List<Runnable> resetList = new ArrayList<>();
|
||||||
try {
|
try {
|
||||||
|
if (Log.channel()) Log.logChannel(getName() + ": starting");
|
||||||
while (!Thread.currentThread().isInterrupted()) {
|
while (!Thread.currentThread().isInterrupted()) {
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
assert errorList.isEmpty();
|
assert errorList.isEmpty();
|
||||||
@ -706,7 +774,7 @@ final class HttpClientImpl extends HttpClient implements Trackable {
|
|||||||
throw new IOException("Channel closed");
|
throw new IOException("Channel closed");
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.logTrace("HttpClientImpl: " + e);
|
Log.logTrace("{0}: {1}", getName(), e);
|
||||||
if (debug.on())
|
if (debug.on())
|
||||||
debug.log("Got " + e.getClass().getName()
|
debug.log("Got " + e.getClass().getName()
|
||||||
+ " while handling registration events");
|
+ " while handling registration events");
|
||||||
@ -738,7 +806,9 @@ final class HttpClientImpl extends HttpClient implements Trackable {
|
|||||||
// Check whether client is still alive, and if not,
|
// Check whether client is still alive, and if not,
|
||||||
// gracefully stop this thread
|
// gracefully stop this thread
|
||||||
if (!owner.isReferenced()) {
|
if (!owner.isReferenced()) {
|
||||||
Log.logTrace("HttpClient no longer referenced. Exiting...");
|
Log.logTrace("{0}: {1}",
|
||||||
|
getName(),
|
||||||
|
"HttpClient no longer referenced. Exiting...");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -780,7 +850,9 @@ final class HttpClientImpl extends HttpClient implements Trackable {
|
|||||||
// Check whether client is still alive, and if not,
|
// Check whether client is still alive, and if not,
|
||||||
// gracefully stop this thread
|
// gracefully stop this thread
|
||||||
if (!owner.isReferenced()) {
|
if (!owner.isReferenced()) {
|
||||||
Log.logTrace("HttpClient no longer referenced. Exiting...");
|
Log.logTrace("{0}: {1}",
|
||||||
|
getName(),
|
||||||
|
"HttpClient no longer referenced. Exiting...");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
owner.purgeTimeoutsAndReturnNextDeadline();
|
owner.purgeTimeoutsAndReturnNextDeadline();
|
||||||
@ -831,17 +903,18 @@ final class HttpClientImpl extends HttpClient implements Trackable {
|
|||||||
|
|
||||||
}
|
}
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
//e.printStackTrace();
|
|
||||||
if (!closed) {
|
if (!closed) {
|
||||||
// This terminates thread. So, better just print stack trace
|
// This terminates thread. So, better just print stack trace
|
||||||
String err = Utils.stackTrace(e);
|
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 (debug.on()) debug.log("shutting down", e);
|
||||||
if (Utils.ASSERTIONSENABLED && !debug.on()) {
|
if (Utils.ASSERTIONSENABLED && !debug.on()) {
|
||||||
e.printStackTrace(System.err); // always print the stack
|
e.printStackTrace(System.err); // always print the stack
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
if (Log.channel()) Log.logChannel(getName() + ": stopping");
|
||||||
shutdown();
|
shutdown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1013,13 +1086,15 @@ final class HttpClientImpl extends HttpClient implements Trackable {
|
|||||||
return Optional.ofNullable(authenticator);
|
return Optional.ofNullable(authenticator);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*package-private*/ final Executor theExecutor() {
|
/*package-private*/ final DelegatingExecutor theExecutor() {
|
||||||
return executor;
|
return delegatingExecutor;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public final Optional<Executor> executor() {
|
public final Optional<Executor> executor() {
|
||||||
return isDefaultExecutor ? Optional.empty() : Optional.of(executor);
|
return isDefaultExecutor
|
||||||
|
? Optional.empty()
|
||||||
|
: Optional.of(delegatingExecutor.delegate());
|
||||||
}
|
}
|
||||||
|
|
||||||
ConnectionPool connectionPool() {
|
ConnectionPool connectionPool() {
|
||||||
|
@ -27,7 +27,6 @@ package jdk.internal.net.http;
|
|||||||
|
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.System.Logger.Level;
|
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.channels.SocketChannel;
|
import java.nio.channels.SocketChannel;
|
||||||
@ -250,7 +249,7 @@ abstract class HttpConnection implements Closeable {
|
|||||||
* @param request
|
* @param request
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
BiPredicate<String,List<String>> headerFilter(HttpRequestImpl request) {
|
BiPredicate<String,String> headerFilter(HttpRequestImpl request) {
|
||||||
if (isTunnel()) {
|
if (isTunnel()) {
|
||||||
// talking to a server through a proxy tunnel
|
// talking to a server through a proxy tunnel
|
||||||
// don't send proxy-* headers to a plain server
|
// don't send proxy-* headers to a plain server
|
||||||
@ -280,12 +279,12 @@ abstract class HttpConnection implements Closeable {
|
|||||||
// start with "proxy-"
|
// start with "proxy-"
|
||||||
private static HttpHeaders proxyTunnelHeaders(HttpRequestImpl request) {
|
private static HttpHeaders proxyTunnelHeaders(HttpRequestImpl request) {
|
||||||
Map<String, List<String>> combined = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
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
|
combined.putAll(request.headers().map()); // let user override system
|
||||||
|
|
||||||
// keep only proxy-* - and also strip authorization headers
|
// keep only proxy-* - and also strip authorization headers
|
||||||
// for disabled schemes
|
// 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
|
/* Returns either a plain HTTP connection or a plain tunnelling connection
|
||||||
|
@ -32,9 +32,9 @@ import java.util.Optional;
|
|||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
import java.net.http.HttpRequest.BodyPublisher;
|
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 jdk.internal.net.http.common.Utils;
|
||||||
import static java.lang.String.format;
|
|
||||||
import static java.util.Objects.requireNonNull;
|
import static java.util.Objects.requireNonNull;
|
||||||
import static jdk.internal.net.http.common.Utils.isValidName;
|
import static jdk.internal.net.http.common.Utils.isValidName;
|
||||||
import static jdk.internal.net.http.common.Utils.isValidValue;
|
import static jdk.internal.net.http.common.Utils.isValidValue;
|
||||||
@ -42,7 +42,7 @@ import static jdk.internal.net.http.common.Utils.newIAE;
|
|||||||
|
|
||||||
public class HttpRequestBuilderImpl implements HttpRequest.Builder {
|
public class HttpRequestBuilderImpl implements HttpRequest.Builder {
|
||||||
|
|
||||||
private HttpHeadersImpl userHeaders;
|
private HttpHeadersBuilder headersBuilder;
|
||||||
private URI uri;
|
private URI uri;
|
||||||
private String method;
|
private String method;
|
||||||
private boolean expectContinue;
|
private boolean expectContinue;
|
||||||
@ -54,13 +54,13 @@ public class HttpRequestBuilderImpl implements HttpRequest.Builder {
|
|||||||
requireNonNull(uri, "uri must be non-null");
|
requireNonNull(uri, "uri must be non-null");
|
||||||
checkURI(uri);
|
checkURI(uri);
|
||||||
this.uri = uri;
|
this.uri = uri;
|
||||||
this.userHeaders = new HttpHeadersImpl();
|
this.headersBuilder = new HttpHeadersBuilder();
|
||||||
this.method = "GET"; // default, as per spec
|
this.method = "GET"; // default, as per spec
|
||||||
this.version = Optional.empty();
|
this.version = Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
public HttpRequestBuilderImpl() {
|
public HttpRequestBuilderImpl() {
|
||||||
this.userHeaders = new HttpHeadersImpl();
|
this.headersBuilder = new HttpHeadersBuilder();
|
||||||
this.method = "GET"; // default, as per spec
|
this.method = "GET"; // default, as per spec
|
||||||
this.version = Optional.empty();
|
this.version = Optional.empty();
|
||||||
}
|
}
|
||||||
@ -90,7 +90,7 @@ public class HttpRequestBuilderImpl implements HttpRequest.Builder {
|
|||||||
public HttpRequestBuilderImpl copy() {
|
public HttpRequestBuilderImpl copy() {
|
||||||
HttpRequestBuilderImpl b = new HttpRequestBuilderImpl();
|
HttpRequestBuilderImpl b = new HttpRequestBuilderImpl();
|
||||||
b.uri = this.uri;
|
b.uri = this.uri;
|
||||||
b.userHeaders = this.userHeaders.deepCopy();
|
b.headersBuilder = this.headersBuilder.structuralCopy();
|
||||||
b.method = this.method;
|
b.method = this.method;
|
||||||
b.expectContinue = this.expectContinue;
|
b.expectContinue = this.expectContinue;
|
||||||
b.bodyPublisher = bodyPublisher;
|
b.bodyPublisher = bodyPublisher;
|
||||||
@ -106,7 +106,7 @@ public class HttpRequestBuilderImpl implements HttpRequest.Builder {
|
|||||||
if (!isValidName(name)) {
|
if (!isValidName(name)) {
|
||||||
throw newIAE("invalid header name: \"%s\"", 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);
|
throw newIAE("restricted header name: \"%s\"", name);
|
||||||
}
|
}
|
||||||
if (!isValidValue(value)) {
|
if (!isValidValue(value)) {
|
||||||
@ -117,14 +117,14 @@ public class HttpRequestBuilderImpl implements HttpRequest.Builder {
|
|||||||
@Override
|
@Override
|
||||||
public HttpRequestBuilderImpl setHeader(String name, String value) {
|
public HttpRequestBuilderImpl setHeader(String name, String value) {
|
||||||
checkNameAndValue(name, value);
|
checkNameAndValue(name, value);
|
||||||
userHeaders.setHeader(name, value);
|
headersBuilder.setHeader(name, value);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HttpRequestBuilderImpl header(String name, String value) {
|
public HttpRequestBuilderImpl header(String name, String value) {
|
||||||
checkNameAndValue(name, value);
|
checkNameAndValue(name, value);
|
||||||
userHeaders.addHeader(name, value);
|
headersBuilder.addHeader(name, value);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,7 +155,7 @@ public class HttpRequestBuilderImpl implements HttpRequest.Builder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpHeadersImpl headers() { return userHeaders; }
|
HttpHeadersBuilder headersBuilder() { return headersBuilder; }
|
||||||
|
|
||||||
URI uri() { return uri; }
|
URI uri() { return uri; }
|
||||||
|
|
||||||
@ -213,6 +213,13 @@ public class HttpRequestBuilderImpl implements HttpRequest.Builder {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HttpRequest build() {
|
public HttpRequest build() {
|
||||||
|
if (uri == null)
|
||||||
|
throw new IllegalStateException("uri is null");
|
||||||
|
assert method != null;
|
||||||
|
return new ImmutableHttpRequest(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpRequestImpl buildForWebSocket() {
|
||||||
if (uri == null)
|
if (uri == null)
|
||||||
throw new IllegalStateException("uri is null");
|
throw new IllegalStateException("uri is null");
|
||||||
assert method != null;
|
assert method != null;
|
||||||
|
@ -41,16 +41,16 @@ import java.util.Optional;
|
|||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
import java.net.http.HttpHeaders;
|
import java.net.http.HttpHeaders;
|
||||||
import java.net.http.HttpRequest;
|
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.common.Utils;
|
||||||
import jdk.internal.net.http.websocket.WebSocketRequest;
|
import jdk.internal.net.http.websocket.WebSocketRequest;
|
||||||
|
|
||||||
import static jdk.internal.net.http.common.Utils.ALLOWED_HEADERS;
|
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 HttpHeaders userHeaders;
|
||||||
private final HttpHeadersImpl systemHeaders;
|
private final HttpHeadersBuilder systemHeadersBuilder;
|
||||||
private final URI uri;
|
private final URI uri;
|
||||||
private volatile Proxy proxy; // ensure safe publishing
|
private volatile Proxy proxy; // ensure safe publishing
|
||||||
private final InetSocketAddress authority; // only used when URI not specified
|
private final InetSocketAddress authority; // only used when URI not specified
|
||||||
@ -78,8 +78,8 @@ class HttpRequestImpl extends HttpRequest implements WebSocketRequest {
|
|||||||
public HttpRequestImpl(HttpRequestBuilderImpl builder) {
|
public HttpRequestImpl(HttpRequestBuilderImpl builder) {
|
||||||
String method = builder.method();
|
String method = builder.method();
|
||||||
this.method = method == null ? "GET" : method;
|
this.method = method == null ? "GET" : method;
|
||||||
this.userHeaders = ImmutableHeaders.of(builder.headers().map(), ALLOWED_HEADERS);
|
this.userHeaders = HttpHeaders.of(builder.headersBuilder().map(), ALLOWED_HEADERS);
|
||||||
this.systemHeaders = new HttpHeadersImpl();
|
this.systemHeadersBuilder = new HttpHeadersBuilder();
|
||||||
this.uri = builder.uri();
|
this.uri = builder.uri();
|
||||||
assert uri != null;
|
assert uri != null;
|
||||||
this.proxy = null;
|
this.proxy = null;
|
||||||
@ -106,21 +106,23 @@ class HttpRequestImpl extends HttpRequest implements WebSocketRequest {
|
|||||||
"uri must be non null");
|
"uri must be non null");
|
||||||
Duration timeout = request.timeout().orElse(null);
|
Duration timeout = request.timeout().orElse(null);
|
||||||
this.method = method == null ? "GET" : method;
|
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) {
|
if (request instanceof HttpRequestImpl) {
|
||||||
// all cases exception WebSocket should have a new system headers
|
// all cases exception WebSocket should have a new system headers
|
||||||
this.isWebSocket = ((HttpRequestImpl) request).isWebSocket;
|
this.isWebSocket = ((HttpRequestImpl) request).isWebSocket;
|
||||||
if (isWebSocket) {
|
if (isWebSocket) {
|
||||||
this.systemHeaders = ((HttpRequestImpl) request).systemHeaders;
|
this.systemHeadersBuilder = ((HttpRequestImpl)request).systemHeadersBuilder;
|
||||||
} else {
|
} else {
|
||||||
this.systemHeaders = new HttpHeadersImpl();
|
this.systemHeadersBuilder = new HttpHeadersBuilder();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
HttpRequestBuilderImpl.checkURI(requestURI);
|
HttpRequestBuilderImpl.checkURI(requestURI);
|
||||||
checkTimeout(timeout);
|
checkTimeout(timeout);
|
||||||
this.systemHeaders = new HttpHeadersImpl();
|
this.systemHeadersBuilder = new HttpHeadersBuilder();
|
||||||
|
}
|
||||||
|
if (!userHeaders.firstValue("User-Agent").isPresent()) {
|
||||||
|
this.systemHeadersBuilder.setHeader("User-Agent", USER_AGENT);
|
||||||
}
|
}
|
||||||
this.systemHeaders.setHeader("User-Agent", USER_AGENT);
|
|
||||||
this.uri = requestURI;
|
this.uri = requestURI;
|
||||||
if (isWebSocket) {
|
if (isWebSocket) {
|
||||||
// WebSocket determines and sets the proxy itself
|
// WebSocket determines and sets the proxy itself
|
||||||
@ -169,8 +171,10 @@ class HttpRequestImpl extends HttpRequest implements WebSocketRequest {
|
|||||||
this.method = method == null? "GET" : method;
|
this.method = method == null? "GET" : method;
|
||||||
this.userHeaders = other.userHeaders;
|
this.userHeaders = other.userHeaders;
|
||||||
this.isWebSocket = other.isWebSocket;
|
this.isWebSocket = other.isWebSocket;
|
||||||
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 = uri;
|
this.uri = uri;
|
||||||
this.proxy = other.proxy;
|
this.proxy = other.proxy;
|
||||||
this.expectContinue = other.expectContinue;
|
this.expectContinue = other.expectContinue;
|
||||||
@ -189,8 +193,8 @@ class HttpRequestImpl extends HttpRequest implements WebSocketRequest {
|
|||||||
// to the connection pool (we might need to revisit this constructor later)
|
// to the connection pool (we might need to revisit this constructor later)
|
||||||
assert "CONNECT".equalsIgnoreCase(method);
|
assert "CONNECT".equalsIgnoreCase(method);
|
||||||
this.method = method;
|
this.method = method;
|
||||||
this.systemHeaders = new HttpHeadersImpl();
|
this.systemHeadersBuilder = new HttpHeadersBuilder();
|
||||||
this.userHeaders = ImmutableHeaders.of(headers);
|
this.userHeaders = headers;
|
||||||
this.uri = URI.create("socket://" + authority.getHostString() + ":"
|
this.uri = URI.create("socket://" + authority.getHostString() + ":"
|
||||||
+ Integer.toString(authority.getPort()) + "/");
|
+ Integer.toString(authority.getPort()) + "/");
|
||||||
this.proxy = null;
|
this.proxy = null;
|
||||||
@ -218,14 +222,14 @@ class HttpRequestImpl extends HttpRequest implements WebSocketRequest {
|
|||||||
* parent.
|
* parent.
|
||||||
*/
|
*/
|
||||||
static HttpRequestImpl createPushRequest(HttpRequestImpl parent,
|
static HttpRequestImpl createPushRequest(HttpRequestImpl parent,
|
||||||
HttpHeadersImpl headers)
|
HttpHeaders headers)
|
||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
return new HttpRequestImpl(parent, headers);
|
return new HttpRequestImpl(parent, headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
// only used for push requests
|
// only used for push requests
|
||||||
private HttpRequestImpl(HttpRequestImpl parent, HttpHeadersImpl headers)
|
private HttpRequestImpl(HttpRequestImpl parent, HttpHeaders headers)
|
||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
this.method = headers.firstValue(":method")
|
this.method = headers.firstValue(":method")
|
||||||
@ -240,8 +244,8 @@ class HttpRequestImpl extends HttpRequest implements WebSocketRequest {
|
|||||||
sb.append(scheme).append("://").append(authority).append(path);
|
sb.append(scheme).append("://").append(authority).append(path);
|
||||||
this.uri = URI.create(sb.toString());
|
this.uri = URI.create(sb.toString());
|
||||||
this.proxy = null;
|
this.proxy = null;
|
||||||
this.userHeaders = ImmutableHeaders.of(headers.map(), ALLOWED_HEADERS);
|
this.userHeaders = HttpHeaders.of(headers.map(), ALLOWED_HEADERS);
|
||||||
this.systemHeaders = parent.systemHeaders;
|
this.systemHeadersBuilder = parent.systemHeadersBuilder;
|
||||||
this.expectContinue = parent.expectContinue;
|
this.expectContinue = parent.expectContinue;
|
||||||
this.secure = parent.secure;
|
this.secure = parent.secure;
|
||||||
this.requestPublisher = parent.requestPublisher;
|
this.requestPublisher = parent.requestPublisher;
|
||||||
@ -264,9 +268,9 @@ class HttpRequestImpl extends HttpRequest implements WebSocketRequest {
|
|||||||
InetSocketAddress authority() { return authority; }
|
InetSocketAddress authority() { return authority; }
|
||||||
|
|
||||||
void setH2Upgrade(Http2ClientImpl h2client) {
|
void setH2Upgrade(Http2ClientImpl h2client) {
|
||||||
systemHeaders.setHeader("Connection", "Upgrade, HTTP2-Settings");
|
systemHeadersBuilder.setHeader("Connection", "Upgrade, HTTP2-Settings");
|
||||||
systemHeaders.setHeader("Upgrade", "h2c");
|
systemHeadersBuilder.setHeader("Upgrade", "h2c");
|
||||||
systemHeaders.setHeader("HTTP2-Settings", h2client.getSettingsString());
|
systemHeadersBuilder.setHeader("HTTP2-Settings", h2client.getSettingsString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -332,18 +336,18 @@ class HttpRequestImpl extends HttpRequest implements WebSocketRequest {
|
|||||||
|
|
||||||
HttpHeaders getUserHeaders() { return userHeaders; }
|
HttpHeaders getUserHeaders() { return userHeaders; }
|
||||||
|
|
||||||
HttpHeadersImpl getSystemHeaders() { return systemHeaders; }
|
HttpHeadersBuilder getSystemHeadersBuilder() { return systemHeadersBuilder; }
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<HttpClient.Version> version() { return version; }
|
public Optional<HttpClient.Version> version() { return version; }
|
||||||
|
|
||||||
void addSystemHeader(String name, String value) {
|
void addSystemHeader(String name, String value) {
|
||||||
systemHeaders.addHeader(name, value);
|
systemHeadersBuilder.addHeader(name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setSystemHeader(String name, String value) {
|
public void setSystemHeader(String name, String value) {
|
||||||
systemHeaders.setHeader(name, value);
|
systemHeadersBuilder.setHeader(name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
InetSocketAddress getAddress() {
|
InetSocketAddress getAddress() {
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -26,7 +26,7 @@
|
|||||||
package jdk.internal.net.http;
|
package jdk.internal.net.http;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.System.Logger.Level;
|
import java.net.ConnectException;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
@ -69,7 +69,7 @@ class MultiExchange<T> {
|
|||||||
final AccessControlContext acc;
|
final AccessControlContext acc;
|
||||||
final HttpClientImpl client;
|
final HttpClientImpl client;
|
||||||
final HttpResponse.BodyHandler<T> responseHandler;
|
final HttpResponse.BodyHandler<T> responseHandler;
|
||||||
final Executor executor;
|
final HttpClientImpl.DelegatingExecutor executor;
|
||||||
final AtomicInteger attempts = new AtomicInteger();
|
final AtomicInteger attempts = new AtomicInteger();
|
||||||
HttpRequestImpl currentreq; // used for retries & redirect
|
HttpRequestImpl currentreq; // used for retries & redirect
|
||||||
HttpRequestImpl previousreq; // used for retries & redirect
|
HttpRequestImpl previousreq; // used for retries & redirect
|
||||||
@ -124,8 +124,8 @@ class MultiExchange<T> {
|
|||||||
|
|
||||||
if (pushPromiseHandler != null) {
|
if (pushPromiseHandler != null) {
|
||||||
Executor executor = acc == null
|
Executor executor = acc == null
|
||||||
? this.executor
|
? this.executor.delegate()
|
||||||
: new PrivilegedExecutor(this.executor, acc);
|
: new PrivilegedExecutor(this.executor.delegate(), acc);
|
||||||
this.pushGroup = new PushGroup<>(pushPromiseHandler, request, executor);
|
this.pushGroup = new PushGroup<>(pushPromiseHandler, request, executor);
|
||||||
} else {
|
} else {
|
||||||
pushGroup = null;
|
pushGroup = null;
|
||||||
@ -193,7 +193,7 @@ class MultiExchange<T> {
|
|||||||
getExchange().cancel(cause);
|
getExchange().cancel(cause);
|
||||||
}
|
}
|
||||||
|
|
||||||
public CompletableFuture<HttpResponse<T>> responseAsync() {
|
public CompletableFuture<HttpResponse<T>> responseAsync(Executor executor) {
|
||||||
CompletableFuture<Void> start = new MinimalFuture<>();
|
CompletableFuture<Void> start = new MinimalFuture<>();
|
||||||
CompletableFuture<HttpResponse<T>> cf = responseAsync0(start);
|
CompletableFuture<HttpResponse<T>> cf = responseAsync0(start);
|
||||||
start.completeAsync( () -> null, executor); // trigger execution
|
start.completeAsync( () -> null, executor); // trigger execution
|
||||||
@ -285,13 +285,22 @@ class MultiExchange<T> {
|
|||||||
|
|
||||||
private static boolean retryPostValue() {
|
private static boolean retryPostValue() {
|
||||||
String s = Utils.getNetProperty("jdk.httpclient.enableAllMethodRetry");
|
String s = Utils.getNetProperty("jdk.httpclient.enableAllMethodRetry");
|
||||||
if (s == "" || "true".equals(s))
|
if (s == null)
|
||||||
return true;
|
|
||||||
return false;
|
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. */
|
/** True if ALL ( even non-idempotent ) requests can be automatic retried. */
|
||||||
private static final boolean RETRY_ALWAYS = retryPostValue();
|
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. */
|
/** Returns true is given request has an idempotent method. */
|
||||||
private static boolean isIdempotentRequest(HttpRequest request) {
|
private static boolean isIdempotentRequest(HttpRequest request) {
|
||||||
@ -307,13 +316,23 @@ class MultiExchange<T> {
|
|||||||
|
|
||||||
/** Returns true if the given request can be automatically retried. */
|
/** Returns true if the given request can be automatically retried. */
|
||||||
private static boolean canRetryRequest(HttpRequest request) {
|
private static boolean canRetryRequest(HttpRequest request) {
|
||||||
if (isIdempotentRequest(request))
|
|
||||||
return true;
|
|
||||||
if (RETRY_ALWAYS)
|
if (RETRY_ALWAYS)
|
||||||
return true;
|
return true;
|
||||||
|
if (isIdempotentRequest(request))
|
||||||
|
return true;
|
||||||
return false;
|
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
|
* Takes a Throwable and returns a suitable CompletableFuture that is
|
||||||
* completed exceptionally, or null.
|
* completed exceptionally, or null.
|
||||||
@ -326,21 +345,20 @@ class MultiExchange<T> {
|
|||||||
}
|
}
|
||||||
if (cancelled && t instanceof IOException) {
|
if (cancelled && t instanceof IOException) {
|
||||||
t = new HttpTimeoutException("request timed out");
|
t = new HttpTimeoutException("request timed out");
|
||||||
} else if (t instanceof ConnectionExpiredException) {
|
} else if (retryOnFailure(t)) {
|
||||||
Throwable cause = t;
|
Throwable cause = retryCause(t);
|
||||||
if (t.getCause() != null) {
|
|
||||||
cause = t.getCause(); // unwrap the ConnectionExpiredException
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (!(t instanceof ConnectException)) {
|
||||||
if (!canRetryRequest(currentreq)) {
|
if (!canRetryRequest(currentreq)) {
|
||||||
return failedFuture(cause); // fails with original cause
|
return failedFuture(cause); // fails with original cause
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// allow the retry mechanism to do its work
|
// allow the retry mechanism to do its work
|
||||||
retryCause = cause;
|
retryCause = cause;
|
||||||
if (!expiredOnce) {
|
if (!expiredOnce) {
|
||||||
if (debug.on())
|
if (debug.on())
|
||||||
debug.log("ConnectionExpiredException (async): retrying...", t);
|
debug.log(t.getClass().getSimpleName() + " (async): retrying...", t);
|
||||||
expiredOnce = true;
|
expiredOnce = true;
|
||||||
// The connection was abruptly closed.
|
// The connection was abruptly closed.
|
||||||
// We return null to retry the same request a second time.
|
// We return null to retry the same request a second time.
|
||||||
@ -350,9 +368,11 @@ class MultiExchange<T> {
|
|||||||
previousreq = currentreq;
|
previousreq = currentreq;
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
if (debug.on())
|
if (debug.on()) {
|
||||||
debug.log("ConnectionExpiredException (async): already retried once.", t);
|
debug.log(t.getClass().getSimpleName()
|
||||||
if (t.getCause() != null) t = t.getCause();
|
+ " (async): already retried once.", t);
|
||||||
|
}
|
||||||
|
t = cause;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return failedFuture(t);
|
return failedFuture(t);
|
||||||
@ -364,9 +384,10 @@ class MultiExchange<T> {
|
|||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
public void handle() {
|
public void handle() {
|
||||||
if (debug.on())
|
if (debug.on()) {
|
||||||
debug.log("Cancelling MultiExchange due to timeout for request %s",
|
debug.log("Cancelling MultiExchange due to timeout for request %s",
|
||||||
request);
|
request);
|
||||||
|
}
|
||||||
cancel(new HttpTimeoutException("request timed out"));
|
cancel(new HttpTimeoutException("request timed out"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,10 +26,8 @@
|
|||||||
package jdk.internal.net.http;
|
package jdk.internal.net.http;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.System.Logger.Level;
|
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.StandardSocketOptions;
|
import java.net.StandardSocketOptions;
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.nio.channels.SelectableChannel;
|
import java.nio.channels.SelectableChannel;
|
||||||
import java.nio.channels.SelectionKey;
|
import java.nio.channels.SelectionKey;
|
||||||
import java.nio.channels.SocketChannel;
|
import java.nio.channels.SocketChannel;
|
||||||
@ -91,14 +89,16 @@ class PlainHttpConnection extends HttpConnection {
|
|||||||
// complete async since the event runs on the SelectorManager thread
|
// complete async since the event runs on the SelectorManager thread
|
||||||
cf.completeAsync(() -> null, client().theExecutor());
|
cf.completeAsync(() -> null, client().theExecutor());
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
client().theExecutor().execute( () -> cf.completeExceptionally(e));
|
Throwable t = Utils.toConnectException(e);
|
||||||
|
client().theExecutor().execute( () -> cf.completeExceptionally(t));
|
||||||
|
close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void abort(IOException ioe) {
|
public void abort(IOException ioe) {
|
||||||
close();
|
|
||||||
client().theExecutor().execute( () -> cf.completeExceptionally(ioe));
|
client().theExecutor().execute( () -> cf.completeExceptionally(ioe));
|
||||||
|
close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,7 +114,7 @@ class PlainHttpConnection extends HttpConnection {
|
|||||||
try {
|
try {
|
||||||
finished = AccessController.doPrivileged(pa);
|
finished = AccessController.doPrivileged(pa);
|
||||||
} catch (PrivilegedActionException e) {
|
} catch (PrivilegedActionException e) {
|
||||||
cf.completeExceptionally(e.getCause());
|
throw e.getCause();
|
||||||
}
|
}
|
||||||
if (finished) {
|
if (finished) {
|
||||||
if (debug.on()) debug.log("connect finished without blocking");
|
if (debug.on()) debug.log("connect finished without blocking");
|
||||||
@ -125,7 +125,13 @@ class PlainHttpConnection extends HttpConnection {
|
|||||||
client().registerEvent(new ConnectEvent(cf));
|
client().registerEvent(new ConnectEvent(cf));
|
||||||
}
|
}
|
||||||
} catch (Throwable throwable) {
|
} 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;
|
return cf;
|
||||||
}
|
}
|
||||||
|
@ -43,6 +43,13 @@ class RedirectFilter implements HeaderFilter {
|
|||||||
static final int DEFAULT_MAX_REDIRECTS = 5;
|
static final int DEFAULT_MAX_REDIRECTS = 5;
|
||||||
URI uri;
|
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(
|
static final int max_redirects = Utils.getIntegerNetProperty(
|
||||||
"jdk.httpclient.redirects.retrylimit", DEFAULT_MAX_REDIRECTS
|
"jdk.httpclient.redirects.retrylimit", DEFAULT_MAX_REDIRECTS
|
||||||
);
|
);
|
||||||
@ -91,6 +98,10 @@ class RedirectFilter implements HeaderFilter {
|
|||||||
if (rcode == 200 || policy == HttpClient.Redirect.NEVER) {
|
if (rcode == 200 || policy == HttpClient.Redirect.NEVER) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (rcode == HTTP_NOT_MODIFIED)
|
||||||
|
return null;
|
||||||
|
|
||||||
if (rcode >= 300 && rcode <= 399) {
|
if (rcode >= 300 && rcode <= 399) {
|
||||||
URI redir = getRedirectedURI(r.headers());
|
URI redir = getRedirectedURI(r.headers());
|
||||||
String newMethod = redirectedMethod(rcode, method);
|
String newMethod = redirectedMethod(rcode, method);
|
||||||
|
@ -64,7 +64,7 @@ class Response {
|
|||||||
int statusCode,
|
int statusCode,
|
||||||
HttpClient.Version version,
|
HttpClient.Version version,
|
||||||
boolean isConnectResponse) {
|
boolean isConnectResponse) {
|
||||||
this.headers = ImmutableHeaders.of(headers);
|
this.headers = headers;
|
||||||
this.request = req;
|
this.request = req;
|
||||||
this.version = version;
|
this.version = version;
|
||||||
this.exchange = exchange;
|
this.exchange = exchange;
|
||||||
|
@ -26,7 +26,6 @@
|
|||||||
package jdk.internal.net.http;
|
package jdk.internal.net.http;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.System.Logger.Level;
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@ -34,9 +33,9 @@ import java.util.List;
|
|||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.net.http.HttpHeaders;
|
import java.net.http.HttpHeaders;
|
||||||
import java.net.http.HttpResponse;
|
import java.net.http.HttpResponse;
|
||||||
|
|
||||||
import jdk.internal.net.http.common.Logger;
|
import jdk.internal.net.http.common.Logger;
|
||||||
import jdk.internal.net.http.common.Utils;
|
import jdk.internal.net.http.common.Utils;
|
||||||
|
import static java.lang.String.format;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements chunked/fixed transfer encodings of HTTP/1.1 responses.
|
* Implements chunked/fixed transfer encodings of HTTP/1.1 responses.
|
||||||
@ -95,6 +94,9 @@ class ResponseContent {
|
|||||||
|
|
||||||
interface BodyParser extends Consumer<ByteBuffer> {
|
interface BodyParser extends Consumer<ByteBuffer> {
|
||||||
void onSubscribe(AbstractSubscription sub);
|
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
|
// Returns a parser that will take care of parsing the received byte
|
||||||
@ -144,6 +146,11 @@ class ResponseContent {
|
|||||||
pusher.onSubscribe(this.sub = sub);
|
pusher.onSubscribe(this.sub = sub);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String currentStateMessage() {
|
||||||
|
return format("chunked transfer encoding, state: %s", state);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void accept(ByteBuffer b) {
|
public void accept(ByteBuffer b) {
|
||||||
if (closedExceptionally != null) {
|
if (closedExceptionally != null) {
|
||||||
@ -424,6 +431,12 @@ class ResponseContent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String currentStateMessage() {
|
||||||
|
return format("fixed content-length: %d, bytes received: %d",
|
||||||
|
contentLength, contentLength - remaining);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void accept(ByteBuffer b) {
|
public void accept(ByteBuffer b) {
|
||||||
if (closedExceptionally != null) {
|
if (closedExceptionally != null) {
|
||||||
|
@ -41,6 +41,7 @@ import java.util.function.Supplier;
|
|||||||
import jdk.internal.net.http.common.BufferSupplier;
|
import jdk.internal.net.http.common.BufferSupplier;
|
||||||
import jdk.internal.net.http.common.Demand;
|
import jdk.internal.net.http.common.Demand;
|
||||||
import jdk.internal.net.http.common.FlowTube;
|
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.Logger;
|
||||||
import jdk.internal.net.http.common.SequentialScheduler;
|
import jdk.internal.net.http.common.SequentialScheduler;
|
||||||
import jdk.internal.net.http.common.SequentialScheduler.DeferredCompleter;
|
import jdk.internal.net.http.common.SequentialScheduler.DeferredCompleter;
|
||||||
@ -149,6 +150,10 @@ final class SocketTube implements FlowTube {
|
|||||||
void signalClosed() {
|
void signalClosed() {
|
||||||
// Ensures that the subscriber will be terminated and that future
|
// Ensures that the subscriber will be terminated and that future
|
||||||
// subscribers will be notified when the connection is closed.
|
// 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(
|
readPublisher.subscriptionImpl.signalError(
|
||||||
new IOException("connection closed locally"));
|
new IOException("connection closed locally"));
|
||||||
}
|
}
|
||||||
@ -364,6 +369,10 @@ final class SocketTube implements FlowTube {
|
|||||||
void startSubscription() {
|
void startSubscription() {
|
||||||
try {
|
try {
|
||||||
if (debug.on()) debug.log("write: starting subscription");
|
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();
|
assert client.isSelectorThread();
|
||||||
// make sure read registrations are handled before;
|
// make sure read registrations are handled before;
|
||||||
readPublisher.subscriptionImpl.handlePending();
|
readPublisher.subscriptionImpl.handlePending();
|
||||||
@ -409,6 +418,10 @@ final class SocketTube implements FlowTube {
|
|||||||
|
|
||||||
void signalError(Throwable error) {
|
void signalError(Throwable error) {
|
||||||
debug.log(() -> "write error: " + error);
|
debug.log(() -> "write error: " + error);
|
||||||
|
if (Log.channel()) {
|
||||||
|
Log.logChannel("Failed to write to channel ({0}: {1})",
|
||||||
|
channelDescr(), error);
|
||||||
|
}
|
||||||
completed = true;
|
completed = true;
|
||||||
readPublisher.signalError(error);
|
readPublisher.signalError(error);
|
||||||
}
|
}
|
||||||
@ -455,6 +468,7 @@ final class SocketTube implements FlowTube {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void cancel() {
|
public void cancel() {
|
||||||
|
if (debug.on()) debug.log("write: cancel");
|
||||||
dropSubscription();
|
dropSubscription();
|
||||||
upstreamSubscription.cancel();
|
upstreamSubscription.cancel();
|
||||||
}
|
}
|
||||||
@ -558,6 +572,10 @@ final class SocketTube implements FlowTube {
|
|||||||
if (!errorRef.compareAndSet(null, error)) {
|
if (!errorRef.compareAndSet(null, error)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (Log.channel()) {
|
||||||
|
Log.logChannel("Error signalled on channel {0}: {1}",
|
||||||
|
channelDescr(), error);
|
||||||
|
}
|
||||||
subscriptionImpl.handleError();
|
subscriptionImpl.handleError();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -665,6 +683,7 @@ final class SocketTube implements FlowTube {
|
|||||||
final void handleSubscribeEvent() {
|
final void handleSubscribeEvent() {
|
||||||
assert client.isSelectorThread();
|
assert client.isSelectorThread();
|
||||||
debug.log("subscribe event raised");
|
debug.log("subscribe event raised");
|
||||||
|
if (Log.channel()) Log.logChannel("Start reading from {0}", channelDescr());
|
||||||
readScheduler.runOrSchedule();
|
readScheduler.runOrSchedule();
|
||||||
if (readScheduler.isStopped() || completed) {
|
if (readScheduler.isStopped() || completed) {
|
||||||
// if already completed or stopped we can handle any
|
// if already completed or stopped we can handle any
|
||||||
@ -702,6 +721,10 @@ final class SocketTube implements FlowTube {
|
|||||||
@Override
|
@Override
|
||||||
public final void cancel() {
|
public final void cancel() {
|
||||||
pauseReadEvent();
|
pauseReadEvent();
|
||||||
|
if (Log.channel()) {
|
||||||
|
Log.logChannel("Read subscription cancelled for channel {0}",
|
||||||
|
channelDescr());
|
||||||
|
}
|
||||||
readScheduler.stop();
|
readScheduler.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -726,6 +749,10 @@ final class SocketTube implements FlowTube {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (debug.on()) debug.log("got read error: " + error);
|
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();
|
readScheduler.runOrSchedule();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -772,6 +799,10 @@ final class SocketTube implements FlowTube {
|
|||||||
if (debug.on())
|
if (debug.on())
|
||||||
debug.log("Sending error " + error
|
debug.log("Sending error " + error
|
||||||
+ " to subscriber " + subscriber);
|
+ " to subscriber " + subscriber);
|
||||||
|
if (Log.channel()) {
|
||||||
|
Log.logChannel("Raising error with subscriber for {0}: {1}",
|
||||||
|
channelDescr(), error);
|
||||||
|
}
|
||||||
current.errorRef.compareAndSet(null, error);
|
current.errorRef.compareAndSet(null, error);
|
||||||
current.signalCompletion();
|
current.signalCompletion();
|
||||||
readScheduler.stop();
|
readScheduler.stop();
|
||||||
@ -788,6 +819,10 @@ final class SocketTube implements FlowTube {
|
|||||||
if (bytes == EOF) {
|
if (bytes == EOF) {
|
||||||
if (!completed) {
|
if (!completed) {
|
||||||
if (debug.on()) debug.log("got read EOF");
|
if (debug.on()) debug.log("got read EOF");
|
||||||
|
if (Log.channel()) {
|
||||||
|
Log.logChannel("EOF read from channel: {0}",
|
||||||
|
channelDescr());
|
||||||
|
}
|
||||||
completed = true;
|
completed = true;
|
||||||
// safe to pause here because we're finished
|
// safe to pause here because we're finished
|
||||||
// anyway.
|
// anyway.
|
||||||
@ -849,6 +884,12 @@ final class SocketTube implements FlowTube {
|
|||||||
if (debug.on()) debug.log("Unexpected exception in read loop", t);
|
if (debug.on()) debug.log("Unexpected exception in read loop", t);
|
||||||
signalError(t);
|
signalError(t);
|
||||||
} finally {
|
} finally {
|
||||||
|
if (readScheduler.isStopped()) {
|
||||||
|
if (debug.on()) debug.log("Read scheduler stopped");
|
||||||
|
if (Log.channel()) {
|
||||||
|
Log.logChannel("Stopped reading from channel {0}", channelDescr());
|
||||||
|
}
|
||||||
|
}
|
||||||
handlePending();
|
handlePending();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1238,4 +1279,8 @@ final class SocketTube implements FlowTube {
|
|||||||
final String dbgString() {
|
final String dbgString() {
|
||||||
return "SocketTube("+id+")";
|
return "SocketTube("+id+")";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final String channelDescr() {
|
||||||
|
return String.valueOf(channel);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,9 +25,9 @@
|
|||||||
|
|
||||||
package jdk.internal.net.http;
|
package jdk.internal.net.http;
|
||||||
|
|
||||||
|
import java.io.EOFException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.UncheckedIOException;
|
import java.io.UncheckedIOException;
|
||||||
import java.lang.System.Logger.Level;
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -39,6 +39,7 @@ import java.util.concurrent.ConcurrentLinkedQueue;
|
|||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.concurrent.Flow;
|
import java.util.concurrent.Flow;
|
||||||
import java.util.concurrent.Flow.Subscription;
|
import java.util.concurrent.Flow.Subscription;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.function.BiPredicate;
|
import java.util.function.BiPredicate;
|
||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
@ -114,8 +115,8 @@ class Stream<T> extends ExchangeImpl<T> {
|
|||||||
final Http2Connection connection;
|
final Http2Connection connection;
|
||||||
final HttpRequestImpl request;
|
final HttpRequestImpl request;
|
||||||
final HeadersConsumer rspHeadersConsumer;
|
final HeadersConsumer rspHeadersConsumer;
|
||||||
final HttpHeadersImpl responseHeaders;
|
final HttpHeadersBuilder responseHeadersBuilder;
|
||||||
final HttpHeadersImpl requestPseudoHeaders;
|
final HttpHeaders requestPseudoHeaders;
|
||||||
volatile HttpResponse.BodySubscriber<T> responseSubscriber;
|
volatile HttpResponse.BodySubscriber<T> responseSubscriber;
|
||||||
final HttpRequest.BodyPublisher requestPublisher;
|
final HttpRequest.BodyPublisher requestPublisher;
|
||||||
volatile RequestSubscriber requestSubscriber;
|
volatile RequestSubscriber requestSubscriber;
|
||||||
@ -133,6 +134,8 @@ class Stream<T> extends ExchangeImpl<T> {
|
|||||||
private volatile boolean closed;
|
private volatile boolean closed;
|
||||||
private volatile boolean endStreamSent;
|
private volatile boolean endStreamSent;
|
||||||
|
|
||||||
|
final AtomicBoolean deRegistered = new AtomicBoolean(false);
|
||||||
|
|
||||||
// state flags
|
// state flags
|
||||||
private boolean requestSent, responseReceived;
|
private boolean requestSent, responseReceived;
|
||||||
|
|
||||||
@ -171,7 +174,7 @@ class Stream<T> extends ExchangeImpl<T> {
|
|||||||
Http2Frame frame = inputQ.peek();
|
Http2Frame frame = inputQ.peek();
|
||||||
if (frame instanceof ResetFrame) {
|
if (frame instanceof ResetFrame) {
|
||||||
inputQ.remove();
|
inputQ.remove();
|
||||||
handleReset((ResetFrame)frame);
|
handleReset((ResetFrame)frame, subscriber);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
DataFrame df = (DataFrame)frame;
|
DataFrame df = (DataFrame)frame;
|
||||||
@ -185,6 +188,7 @@ class Stream<T> extends ExchangeImpl<T> {
|
|||||||
Log.logTrace("responseSubscriber.onComplete");
|
Log.logTrace("responseSubscriber.onComplete");
|
||||||
if (debug.on()) debug.log("incoming: onComplete");
|
if (debug.on()) debug.log("incoming: onComplete");
|
||||||
sched.stop();
|
sched.stop();
|
||||||
|
connection.decrementStreamsCount(streamid);
|
||||||
subscriber.onComplete();
|
subscriber.onComplete();
|
||||||
onCompleteCalled = true;
|
onCompleteCalled = true;
|
||||||
setEndStreamReceived();
|
setEndStreamReceived();
|
||||||
@ -198,6 +202,7 @@ class Stream<T> extends ExchangeImpl<T> {
|
|||||||
Log.logTrace("responseSubscriber.onComplete");
|
Log.logTrace("responseSubscriber.onComplete");
|
||||||
if (debug.on()) debug.log("incoming: onComplete");
|
if (debug.on()) debug.log("incoming: onComplete");
|
||||||
sched.stop();
|
sched.stop();
|
||||||
|
connection.decrementStreamsCount(streamid);
|
||||||
subscriber.onComplete();
|
subscriber.onComplete();
|
||||||
onCompleteCalled = true;
|
onCompleteCalled = true;
|
||||||
setEndStreamReceived();
|
setEndStreamReceived();
|
||||||
@ -251,6 +256,10 @@ class Stream<T> extends ExchangeImpl<T> {
|
|||||||
return true; // end of stream
|
return true; // end of stream
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean deRegister() {
|
||||||
|
return deRegistered.compareAndSet(false, true);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
CompletableFuture<T> readBodyAsync(HttpResponse.BodyHandler<T> handler,
|
CompletableFuture<T> readBodyAsync(HttpResponse.BodyHandler<T> handler,
|
||||||
boolean returnConnectionToPool,
|
boolean returnConnectionToPool,
|
||||||
@ -258,6 +267,7 @@ class Stream<T> extends ExchangeImpl<T> {
|
|||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
Log.logTrace("Reading body on stream {0}", streamid);
|
Log.logTrace("Reading body on stream {0}", streamid);
|
||||||
|
debug.log("Getting BodySubscriber for: " + response);
|
||||||
BodySubscriber<T> bodySubscriber = handler.apply(new ResponseInfoImpl(response));
|
BodySubscriber<T> bodySubscriber = handler.apply(new ResponseInfoImpl(response));
|
||||||
CompletableFuture<T> cf = receiveData(bodySubscriber, executor);
|
CompletableFuture<T> cf = receiveData(bodySubscriber, executor);
|
||||||
|
|
||||||
@ -337,10 +347,9 @@ class Stream<T> extends ExchangeImpl<T> {
|
|||||||
this.windowController = windowController;
|
this.windowController = windowController;
|
||||||
this.request = e.request();
|
this.request = e.request();
|
||||||
this.requestPublisher = request.requestPublisher; // may be null
|
this.requestPublisher = request.requestPublisher; // may be null
|
||||||
responseHeaders = new HttpHeadersImpl();
|
this.responseHeadersBuilder = new HttpHeadersBuilder();
|
||||||
rspHeadersConsumer = new HeadersConsumer();
|
this.rspHeadersConsumer = new HeadersConsumer();
|
||||||
this.requestPseudoHeaders = new HttpHeadersImpl();
|
this.requestPseudoHeaders = createPseudoHeaders(request);
|
||||||
// NEW
|
|
||||||
this.windowUpdater = new StreamWindowUpdateSender(connection);
|
this.windowUpdater = new StreamWindowUpdateSender(connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -391,6 +400,7 @@ class Stream<T> extends ExchangeImpl<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected void handleResponse() throws IOException {
|
protected void handleResponse() throws IOException {
|
||||||
|
HttpHeaders responseHeaders = responseHeadersBuilder.build();
|
||||||
responseCode = (int)responseHeaders
|
responseCode = (int)responseHeaders
|
||||||
.firstValueAsLong(":status")
|
.firstValueAsLong(":status")
|
||||||
.orElseThrow(() -> new IOException("no statuscode in response"));
|
.orElseThrow(() -> new IOException("no statuscode in response"));
|
||||||
@ -423,6 +433,13 @@ class Stream<T> extends ExchangeImpl<T> {
|
|||||||
Log.logTrace("Ignoring RST_STREAM frame received on remotely closed stream {0}", streamid);
|
Log.logTrace("Ignoring RST_STREAM frame received on remotely closed stream {0}", streamid);
|
||||||
} else if (closed) {
|
} else if (closed) {
|
||||||
Log.logTrace("Ignoring RST_STREAM frame received on closed stream {0}", streamid);
|
Log.logTrace("Ignoring RST_STREAM frame received on closed stream {0}", streamid);
|
||||||
|
} else {
|
||||||
|
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 {
|
} else {
|
||||||
// put it in the input queue in order to read all
|
// put it in the input queue in order to read all
|
||||||
// pending data frames first. Indeed, a server may send
|
// pending data frames first. Indeed, a server may send
|
||||||
@ -436,13 +453,38 @@ class Stream<T> extends ExchangeImpl<T> {
|
|||||||
Log.logTrace("RST_STREAM pushed in queue for stream {0}", streamid);
|
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);
|
Log.logTrace("Handling RST_STREAM on stream {0}", streamid);
|
||||||
if (!closed) {
|
if (!closed) {
|
||||||
close();
|
synchronized (this) {
|
||||||
|
if (closed) {
|
||||||
|
if (debug.on()) debug.log("Stream already closed: ignoring RESET");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
closed = true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
int error = frame.getErrorCode();
|
int error = frame.getErrorCode();
|
||||||
completeResponseExceptionally(new IOException(ErrorFrame.stringForCode(error)));
|
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 {
|
} else {
|
||||||
Log.logTrace("Ignoring RST_STREAM frame received on closed stream {0}", streamid);
|
Log.logTrace("Ignoring RST_STREAM frame received on closed stream {0}", streamid);
|
||||||
}
|
}
|
||||||
@ -535,13 +577,12 @@ class Stream<T> extends ExchangeImpl<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private OutgoingHeaders<Stream<T>> headerFrame(long contentLength) {
|
private OutgoingHeaders<Stream<T>> headerFrame(long contentLength) {
|
||||||
HttpHeadersImpl h = request.getSystemHeaders();
|
HttpHeadersBuilder h = request.getSystemHeadersBuilder();
|
||||||
if (contentLength > 0) {
|
if (contentLength > 0) {
|
||||||
h.setHeader("content-length", Long.toString(contentLength));
|
h.setHeader("content-length", Long.toString(contentLength));
|
||||||
}
|
}
|
||||||
setPseudoHeaderFields();
|
HttpHeaders sysh = filterHeaders(h.build());
|
||||||
HttpHeaders sysh = filter(h);
|
HttpHeaders userh = filterHeaders(request.getUserHeaders());
|
||||||
HttpHeaders userh = filter(request.getUserHeaders());
|
|
||||||
OutgoingHeaders<Stream<T>> f = new OutgoingHeaders<>(sysh, userh, this);
|
OutgoingHeaders<Stream<T>> f = new OutgoingHeaders<>(sysh, userh, this);
|
||||||
if (contentLength == 0) {
|
if (contentLength == 0) {
|
||||||
f.setFlag(HeadersFrame.END_STREAM);
|
f.setFlag(HeadersFrame.END_STREAM);
|
||||||
@ -565,7 +606,7 @@ class Stream<T> extends ExchangeImpl<T> {
|
|||||||
// If nothing needs filtering then we can just use the
|
// If nothing needs filtering then we can just use the
|
||||||
// original headers.
|
// original headers.
|
||||||
private boolean needsFiltering(HttpHeaders 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) {
|
if (filter == Utils.PROXY_TUNNEL_FILTER || filter == Utils.PROXY_FILTER) {
|
||||||
// we're either connecting or proxying
|
// we're either connecting or proxying
|
||||||
// slight optimization: we only need to filter out
|
// slight optimization: we only need to filter out
|
||||||
@ -583,18 +624,17 @@ class Stream<T> extends ExchangeImpl<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private HttpHeaders filter(HttpHeaders headers) {
|
private HttpHeaders filterHeaders(HttpHeaders headers) {
|
||||||
HttpConnection conn = connection();
|
HttpConnection conn = connection();
|
||||||
BiPredicate<String, List<String>> filter =
|
BiPredicate<String, String> filter = conn.headerFilter(request);
|
||||||
conn.headerFilter(request);
|
|
||||||
if (needsFiltering(headers, filter)) {
|
if (needsFiltering(headers, filter)) {
|
||||||
return ImmutableHeaders.of(headers.map(), filter);
|
return HttpHeaders.of(headers.map(), filter);
|
||||||
}
|
}
|
||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setPseudoHeaderFields() {
|
private static HttpHeaders createPseudoHeaders(HttpRequest request) {
|
||||||
HttpHeadersImpl hdrs = requestPseudoHeaders;
|
HttpHeadersBuilder hdrs = new HttpHeadersBuilder();
|
||||||
String method = request.method();
|
String method = request.method();
|
||||||
hdrs.setHeader(":method", method);
|
hdrs.setHeader(":method", method);
|
||||||
URI uri = request.uri();
|
URI uri = request.uri();
|
||||||
@ -615,9 +655,10 @@ class Stream<T> extends ExchangeImpl<T> {
|
|||||||
path += "?" + query;
|
path += "?" + query;
|
||||||
}
|
}
|
||||||
hdrs.setHeader(":path", Utils.encode(path));
|
hdrs.setHeader(":path", Utils.encode(path));
|
||||||
|
return hdrs.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpHeadersImpl getRequestPseudoHeaders() {
|
HttpHeaders getRequestPseudoHeaders() {
|
||||||
return requestPseudoHeaders;
|
return requestPseudoHeaders;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -657,6 +698,7 @@ class Stream<T> extends ExchangeImpl<T> {
|
|||||||
if (streamid > 0) {
|
if (streamid > 0) {
|
||||||
if (debug.on()) debug.log("Released stream %d", streamid);
|
if (debug.on()) debug.log("Released stream %d", streamid);
|
||||||
// remove this stream from the Http2Connection map.
|
// remove this stream from the Http2Connection map.
|
||||||
|
connection.decrementStreamsCount(streamid);
|
||||||
connection.closeStream(streamid);
|
connection.closeStream(streamid);
|
||||||
} else {
|
} else {
|
||||||
if (debug.on()) debug.log("Can't release stream %d", streamid);
|
if (debug.on()) debug.log("Can't release stream %d", streamid);
|
||||||
@ -945,14 +987,18 @@ class Stream<T> extends ExchangeImpl<T> {
|
|||||||
if (!cf.isDone()) {
|
if (!cf.isDone()) {
|
||||||
Log.logTrace("Completing response (streamid={0}): {1}",
|
Log.logTrace("Completing response (streamid={0}): {1}",
|
||||||
streamid, cf);
|
streamid, cf);
|
||||||
cf.complete(resp);
|
if (debug.on())
|
||||||
|
debug.log("Completing responseCF(%d) with response headers", i);
|
||||||
response_cfs.remove(cf);
|
response_cfs.remove(cf);
|
||||||
|
cf.complete(resp);
|
||||||
return;
|
return;
|
||||||
} // else we found the previous response: just leave it alone.
|
} // else we found the previous response: just leave it alone.
|
||||||
}
|
}
|
||||||
cf = MinimalFuture.completedFuture(resp);
|
cf = MinimalFuture.completedFuture(resp);
|
||||||
Log.logTrace("Created completed future (streamid={0}): {1}",
|
Log.logTrace("Created completed future (streamid={0}): {1}",
|
||||||
streamid, cf);
|
streamid, cf);
|
||||||
|
if (debug.on())
|
||||||
|
debug.log("Adding completed responseCF(0) with response headers");
|
||||||
response_cfs.add(cf);
|
response_cfs.add(cf);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -983,8 +1029,8 @@ class Stream<T> extends ExchangeImpl<T> {
|
|||||||
for (int i = 0; i < response_cfs.size(); i++) {
|
for (int i = 0; i < response_cfs.size(); i++) {
|
||||||
CompletableFuture<Response> cf = response_cfs.get(i);
|
CompletableFuture<Response> cf = response_cfs.get(i);
|
||||||
if (!cf.isDone()) {
|
if (!cf.isDone()) {
|
||||||
cf.completeExceptionally(t);
|
|
||||||
response_cfs.remove(i);
|
response_cfs.remove(i);
|
||||||
|
cf.completeExceptionally(t);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1033,6 +1079,15 @@ class Stream<T> extends ExchangeImpl<T> {
|
|||||||
cancelImpl(cause);
|
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
|
// This method sends a RST_STREAM frame
|
||||||
void cancelImpl(Throwable e) {
|
void cancelImpl(Throwable e) {
|
||||||
errorRef.compareAndSet(null, e);
|
errorRef.compareAndSet(null, e);
|
||||||
@ -1062,8 +1117,15 @@ class Stream<T> extends ExchangeImpl<T> {
|
|||||||
try {
|
try {
|
||||||
// will send a RST_STREAM frame
|
// will send a RST_STREAM frame
|
||||||
if (streamid != 0) {
|
if (streamid != 0) {
|
||||||
|
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);
|
connection.resetStream(streamid, ResetFrame.CANCEL);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
Log.logError(ex);
|
Log.logError(ex);
|
||||||
}
|
}
|
||||||
@ -1184,6 +1246,7 @@ class Stream<T> extends ExchangeImpl<T> {
|
|||||||
// create and return the PushResponseImpl
|
// create and return the PushResponseImpl
|
||||||
@Override
|
@Override
|
||||||
protected void handleResponse() {
|
protected void handleResponse() {
|
||||||
|
HttpHeaders responseHeaders = responseHeadersBuilder.build();
|
||||||
responseCode = (int)responseHeaders
|
responseCode = (int)responseHeaders
|
||||||
.firstValueAsLong(":status")
|
.firstValueAsLong(":status")
|
||||||
.orElse(-1);
|
.orElse(-1);
|
||||||
@ -1252,7 +1315,8 @@ class Stream<T> extends ExchangeImpl<T> {
|
|||||||
|
|
||||||
void reset() {
|
void reset() {
|
||||||
super.reset();
|
super.reset();
|
||||||
responseHeaders.clear();
|
responseHeadersBuilder.clear();
|
||||||
|
debug.log("Response builder cleared, ready to receive new headers.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -1262,7 +1326,7 @@ class Stream<T> extends ExchangeImpl<T> {
|
|||||||
String n = name.toString();
|
String n = name.toString();
|
||||||
String v = value.toString();
|
String v = value.toString();
|
||||||
super.onDecoded(n, v);
|
super.onDecoded(n, v);
|
||||||
responseHeaders.addHeader(n, v);
|
responseHeadersBuilder.addHeader(n, v);
|
||||||
if (Log.headers() && Log.trace()) {
|
if (Log.headers() && Log.trace()) {
|
||||||
Log.logTrace("RECEIVED HEADER (streamid={0}): {1}: {2}",
|
Log.logTrace("RECEIVED HEADER (streamid={0}): {1}: {2}",
|
||||||
streamid, n, v);
|
streamid, n, v);
|
||||||
|
@ -35,13 +35,12 @@ public final class ConnectionExpiredException extends IOException {
|
|||||||
private static final long serialVersionUID = 0;
|
private static final long serialVersionUID = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a {@code ConnectionExpiredException} with the specified detail
|
* Constructs a {@code ConnectionExpiredException} with a detail message of
|
||||||
* message and cause.
|
* "subscription is finished" and the given cause.
|
||||||
*
|
*
|
||||||
* @param s the detail message
|
|
||||||
* @param cause the throwable cause
|
* @param cause the throwable cause
|
||||||
*/
|
*/
|
||||||
public ConnectionExpiredException(String s, Throwable cause) {
|
public ConnectionExpiredException(Throwable cause) {
|
||||||
super(s, cause);
|
super("subscription is finished", cause);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
|
* Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
|
||||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||||
*
|
*
|
||||||
* This code is free software; you can redistribute it and/or modify it
|
* This code is free software; you can redistribute it and/or modify it
|
||||||
@ -27,44 +27,31 @@ package jdk.internal.net.http.common;
|
|||||||
|
|
||||||
import java.net.http.HttpHeaders;
|
import java.net.http.HttpHeaders;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.TreeMap;
|
import java.util.TreeMap;
|
||||||
|
import static jdk.internal.net.http.common.Utils.ACCEPT_ALL;
|
||||||
|
|
||||||
/**
|
/** A mutable builder for collecting and building HTTP headers. */
|
||||||
* Implementation of HttpHeaders.
|
public class HttpHeadersBuilder {
|
||||||
*
|
|
||||||
* 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;
|
private final TreeMap<String, List<String>> headersMap;
|
||||||
|
|
||||||
public HttpHeadersImpl() {
|
public HttpHeadersBuilder() {
|
||||||
headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
headersMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public HttpHeadersBuilder structuralCopy() {
|
||||||
public Map<String, List<String>> map() {
|
HttpHeadersBuilder builder = new HttpHeadersBuilder();
|
||||||
return Collections.unmodifiableMap(headersMap());
|
for (Map.Entry<String, List<String>> entry : headersMap.entrySet()) {
|
||||||
}
|
|
||||||
|
|
||||||
// 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());
|
List<String> valuesCopy = new ArrayList<>(entry.getValue());
|
||||||
h1.headersMap().put(entry.getKey(), valuesCopy);
|
builder.headersMap.put(entry.getKey(), valuesCopy);
|
||||||
}
|
}
|
||||||
return h1;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addHeader(String name, String value) {
|
public void addHeader(String name, String value) {
|
||||||
headersMap().computeIfAbsent(name, k -> new ArrayList<>(1))
|
headersMap.computeIfAbsent(name, k -> new ArrayList<>(1))
|
||||||
.add(value);
|
.add(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,18 +59,27 @@ public class HttpHeadersImpl extends HttpHeaders {
|
|||||||
// headers typically have one value
|
// headers typically have one value
|
||||||
List<String> values = new ArrayList<>(1);
|
List<String> values = new ArrayList<>(1);
|
||||||
values.add(value);
|
values.add(value);
|
||||||
headersMap().put(name, values);
|
headersMap.put(name, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void clear() {
|
public void clear() {
|
||||||
headersMap().clear();
|
headersMap.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected HttpHeadersImpl newDeepCopy() {
|
public Map<String, List<String>> map() {
|
||||||
return new HttpHeadersImpl();
|
return headersMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Map<String, List<String>> headersMap() {
|
public HttpHeaders build() {
|
||||||
return headers;
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -43,7 +43,7 @@ import javax.net.ssl.SSLParameters;
|
|||||||
/**
|
/**
|
||||||
* -Djava.net.HttpClient.log=
|
* -Djava.net.HttpClient.log=
|
||||||
* errors,requests,headers,
|
* 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.
|
* Any of errors, requests, headers or content are optional.
|
||||||
*
|
*
|
||||||
@ -65,6 +65,7 @@ public abstract class Log implements System.Logger {
|
|||||||
public static final int FRAMES = 0x10;
|
public static final int FRAMES = 0x10;
|
||||||
public static final int SSL = 0x20;
|
public static final int SSL = 0x20;
|
||||||
public static final int TRACE = 0x40;
|
public static final int TRACE = 0x40;
|
||||||
|
public static final int CHANNEL = 0x80;
|
||||||
static int logging;
|
static int logging;
|
||||||
|
|
||||||
// Frame types: "control", "data", "window", "all"
|
// Frame types: "control", "data", "window", "all"
|
||||||
@ -99,11 +100,15 @@ public abstract class Log implements System.Logger {
|
|||||||
case "ssl":
|
case "ssl":
|
||||||
logging |= SSL;
|
logging |= SSL;
|
||||||
break;
|
break;
|
||||||
|
case "channel":
|
||||||
|
logging |= CHANNEL;
|
||||||
|
break;
|
||||||
case "trace":
|
case "trace":
|
||||||
logging |= TRACE;
|
logging |= TRACE;
|
||||||
break;
|
break;
|
||||||
case "all":
|
case "all":
|
||||||
logging |= CONTENT|HEADERS|REQUESTS|FRAMES|ERRORS|TRACE|SSL;
|
logging |= CONTENT|HEADERS|REQUESTS|FRAMES|ERRORS|TRACE|SSL| CHANNEL;
|
||||||
|
frametypes |= ALL;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// ignore bad values
|
// ignore bad values
|
||||||
@ -166,6 +171,10 @@ public abstract class Log implements System.Logger {
|
|||||||
return (logging & FRAMES) != 0;
|
return (logging & FRAMES) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean channel() {
|
||||||
|
return (logging & CHANNEL) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
public static void logError(String s, Object... s1) {
|
public static void logError(String s, Object... s1) {
|
||||||
if (errors()) {
|
if (errors()) {
|
||||||
logger.log(Level.INFO, "ERROR: " + s, s1);
|
logger.log(Level.INFO, "ERROR: " + s, s1);
|
||||||
@ -191,9 +200,21 @@ public abstract class Log implements System.Logger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
public static void logTrace(String s, Object... s1) {
|
||||||
if (trace()) {
|
if (trace()) {
|
||||||
String format = "TRACE: " + s;
|
String format = "MISC: " + s;
|
||||||
logger.log(Level.INFO, format, s1);
|
logger.log(Level.INFO, format, s1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,6 +46,7 @@ import java.util.concurrent.Flow;
|
|||||||
import java.util.concurrent.Flow.Subscriber;
|
import java.util.concurrent.Flow.Subscriber;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.IntBinaryOperator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements SSL using two SubscriberWrappers.
|
* Implements SSL using two SubscriberWrappers.
|
||||||
@ -87,13 +88,18 @@ public class SSLFlowDelegate {
|
|||||||
final Logger debug =
|
final Logger debug =
|
||||||
Utils.getDebugLogger(this::dbgString, Utils.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 Executor exec;
|
||||||
final Reader reader;
|
final Reader reader;
|
||||||
final Writer writer;
|
final Writer writer;
|
||||||
final SSLEngine engine;
|
final SSLEngine engine;
|
||||||
final String tubeName; // hack
|
final String tubeName; // hack
|
||||||
final CompletableFuture<String> alpnCF; // completes on initial handshake
|
final CompletableFuture<String> alpnCF; // completes on initial handshake
|
||||||
final static ByteBuffer SENTINEL = Utils.EMPTY_BYTEBUFFER;
|
|
||||||
volatile boolean close_notify_received;
|
volatile boolean close_notify_received;
|
||||||
final CompletableFuture<Void> readerCF;
|
final CompletableFuture<Void> readerCF;
|
||||||
final CompletableFuture<Void> writerCF;
|
final CompletableFuture<Void> writerCF;
|
||||||
@ -146,7 +152,8 @@ public class SSLFlowDelegate {
|
|||||||
// Writer to the downWriter.
|
// Writer to the downWriter.
|
||||||
connect(downReader, downWriter);
|
connect(downReader, downWriter);
|
||||||
|
|
||||||
//Monitor.add(this::monitor);
|
if (monProp != null && (monProp.equals("") || monProp.equalsIgnoreCase("true")))
|
||||||
|
Monitor.add(this::monitor);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -245,7 +252,10 @@ public class SSLFlowDelegate {
|
|||||||
final Logger debugr = Utils.getDebugLogger(this::dbgString, Utils.DEBUG);
|
final Logger debugr = Utils.getDebugLogger(this::dbgString, Utils.DEBUG);
|
||||||
|
|
||||||
private final class ReaderDownstreamPusher implements Runnable {
|
private final class ReaderDownstreamPusher implements Runnable {
|
||||||
@Override public void run() { processData(); }
|
@Override
|
||||||
|
public void run() {
|
||||||
|
processData();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Reader() {
|
Reader() {
|
||||||
@ -309,6 +319,7 @@ public class SSLFlowDelegate {
|
|||||||
|
|
||||||
// readBuf is kept ready for reading outside of this method
|
// readBuf is kept ready for reading outside of this method
|
||||||
private void addToReadBuf(List<ByteBuffer> buffers, boolean complete) {
|
private void addToReadBuf(List<ByteBuffer> buffers, boolean complete) {
|
||||||
|
assert Utils.remaining(buffers) > 0 || buffers.isEmpty();
|
||||||
synchronized (readBufferLock) {
|
synchronized (readBufferLock) {
|
||||||
for (ByteBuffer buf : buffers) {
|
for (ByteBuffer buf : buffers) {
|
||||||
readBuf.compact();
|
readBuf.compact();
|
||||||
@ -344,6 +355,7 @@ public class SSLFlowDelegate {
|
|||||||
// In this case we need to wait for more bytes than what
|
// In this case we need to wait for more bytes than what
|
||||||
// we had before calling unwrap() again.
|
// we had before calling unwrap() again.
|
||||||
volatile int minBytesRequired;
|
volatile int minBytesRequired;
|
||||||
|
|
||||||
// work function where it all happens
|
// work function where it all happens
|
||||||
final void processData() {
|
final void processData() {
|
||||||
try {
|
try {
|
||||||
@ -400,12 +412,11 @@ public class SSLFlowDelegate {
|
|||||||
outgoing(Utils.EMPTY_BB_LIST, true);
|
outgoing(Utils.EMPTY_BB_LIST, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (result.handshaking() && !complete) {
|
if (result.handshaking()) {
|
||||||
if (debugr.on()) debugr.log("handshaking");
|
|
||||||
if (doHandshake(result, READER)) {
|
|
||||||
resumeActivity();
|
|
||||||
}
|
|
||||||
handshaking = true;
|
handshaking = true;
|
||||||
|
if (debugr.on()) debugr.log("handshaking");
|
||||||
|
if (doHandshake(result, READER)) continue; // need unwrap
|
||||||
|
else break; // doHandshake will have triggered the write scheduler if necessary
|
||||||
} else {
|
} else {
|
||||||
if ((handshakeState.getAndSet(NOT_HANDSHAKING) & ~DOING_TASKS) == HANDSHAKING) {
|
if ((handshakeState.getAndSet(NOT_HANDSHAKING) & ~DOING_TASKS) == HANDSHAKING) {
|
||||||
handshaking = false;
|
handshaking = false;
|
||||||
@ -443,12 +454,19 @@ public class SSLFlowDelegate {
|
|||||||
|
|
||||||
EngineResult unwrapBuffer(ByteBuffer src) throws IOException {
|
EngineResult unwrapBuffer(ByteBuffer src) throws IOException {
|
||||||
ByteBuffer dst = getAppBuffer();
|
ByteBuffer dst = getAppBuffer();
|
||||||
|
int len = src.remaining();
|
||||||
while (true) {
|
while (true) {
|
||||||
SSLEngineResult sslResult = engine.unwrap(src, dst);
|
SSLEngineResult sslResult = engine.unwrap(src, dst);
|
||||||
switch (sslResult.getStatus()) {
|
switch (sslResult.getStatus()) {
|
||||||
case BUFFER_OVERFLOW:
|
case BUFFER_OVERFLOW:
|
||||||
// may happen only if app size buffer was changed.
|
// may happen if app size buffer was changed, or if
|
||||||
// get it again if app buffer size changed
|
// 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 =
|
int appSize = applicationBufferSize =
|
||||||
engine.getSession().getApplicationBufferSize();
|
engine.getSession().getApplicationBufferSize();
|
||||||
ByteBuffer b = ByteBuffer.allocate(appSize + dst.position());
|
ByteBuffer b = ByteBuffer.allocate(appSize + dst.position());
|
||||||
@ -457,11 +475,26 @@ public class SSLFlowDelegate {
|
|||||||
dst = b;
|
dst = b;
|
||||||
break;
|
break;
|
||||||
case CLOSED:
|
case CLOSED:
|
||||||
|
assert dst.position() == 0;
|
||||||
return doClosure(new EngineResult(sslResult));
|
return doClosure(new EngineResult(sslResult));
|
||||||
case BUFFER_UNDERFLOW:
|
case BUFFER_UNDERFLOW:
|
||||||
// handled implicitly by compaction/reallocation of readBuf
|
// handled implicitly by compaction/reallocation of readBuf
|
||||||
|
assert dst.position() == 0;
|
||||||
return new EngineResult(sslResult);
|
return new EngineResult(sslResult);
|
||||||
case OK:
|
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();
|
dst.flip();
|
||||||
return new EngineResult(sslResult, dst);
|
return new EngineResult(sslResult, dst);
|
||||||
}
|
}
|
||||||
@ -662,8 +695,8 @@ public class SSLFlowDelegate {
|
|||||||
}
|
}
|
||||||
cleanList(writeList); // tidy up the source list
|
cleanList(writeList); // tidy up the source list
|
||||||
sendResultBytes(result);
|
sendResultBytes(result);
|
||||||
if (handshaking && !completing) {
|
if (handshaking) {
|
||||||
if (needWrap()) {
|
if (!completing && needWrap()) {
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
@ -687,11 +720,30 @@ public class SSLFlowDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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")
|
@SuppressWarnings("fallthrough")
|
||||||
EngineResult wrapBuffers(ByteBuffer[] src) throws SSLException {
|
EngineResult wrapBuffers(ByteBuffer[] src) throws SSLException {
|
||||||
|
long len = Utils.remaining(src);
|
||||||
if (debugw.on())
|
if (debugw.on())
|
||||||
debugw.log("wrapping " + Utils.remaining(src) + " bytes");
|
debugw.log("wrapping " + len + " bytes");
|
||||||
ByteBuffer dst = getNetBuffer();
|
|
||||||
|
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) {
|
while (true) {
|
||||||
SSLEngineResult sslResult = engine.wrap(src, dst);
|
SSLEngineResult sslResult = engine.wrap(src, dst);
|
||||||
if (debugw.on()) debugw.log("SSLResult: " + sslResult);
|
if (debugw.on()) debugw.log("SSLResult: " + sslResult);
|
||||||
@ -702,7 +754,7 @@ public class SSLFlowDelegate {
|
|||||||
if (debugw.on()) debugw.log("BUFFER_OVERFLOW");
|
if (debugw.on()) debugw.log("BUFFER_OVERFLOW");
|
||||||
int netSize = packetBufferSize
|
int netSize = packetBufferSize
|
||||||
= engine.getSession().getPacketBufferSize();
|
= engine.getSession().getPacketBufferSize();
|
||||||
ByteBuffer b = ByteBuffer.allocate(netSize + dst.position());
|
ByteBuffer b = writeBuffer = ByteBuffer.allocate(netSize + dst.position());
|
||||||
dst.flip();
|
dst.flip();
|
||||||
b.put(dst);
|
b.put(dst);
|
||||||
dst = b;
|
dst = b;
|
||||||
@ -712,11 +764,27 @@ public class SSLFlowDelegate {
|
|||||||
// fallthrough. There could be some remaining data in dst.
|
// fallthrough. There could be some remaining data in dst.
|
||||||
// CLOSED will be handled by the caller.
|
// CLOSED will be handled by the caller.
|
||||||
case OK:
|
case OK:
|
||||||
|
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();
|
dst.flip();
|
||||||
final ByteBuffer dest = dst;
|
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())
|
if (debugw.on())
|
||||||
debugw.log("OK => produced: %d, not wrapped: %d",
|
debugw.log("OK => produced: %d bytes into %d, not wrapped: %d",
|
||||||
dest.remaining(), Utils.remaining(src));
|
dest.remaining(), dest.capacity(), Utils.remaining(src));
|
||||||
return new EngineResult(sslResult, dest);
|
return new EngineResult(sslResult, dest);
|
||||||
case BUFFER_UNDERFLOW:
|
case BUFFER_UNDERFLOW:
|
||||||
// Shouldn't happen. Doesn't returns when wrap()
|
// Shouldn't happen. Doesn't returns when wrap()
|
||||||
@ -799,8 +867,12 @@ public class SSLFlowDelegate {
|
|||||||
private static final int NOT_HANDSHAKING = 0;
|
private static final int NOT_HANDSHAKING = 0;
|
||||||
private static final int HANDSHAKING = 1;
|
private static final int HANDSHAKING = 1;
|
||||||
|
|
||||||
private static final int DOING_TASKS = 4; // bit added to above state
|
// Bit flags
|
||||||
private static final ByteBuffer HS_TRIGGER = ByteBuffer.allocate(0);
|
// 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 READER = 1;
|
||||||
private static final int WRITER = 2;
|
private static final int WRITER = 2;
|
||||||
@ -808,7 +880,7 @@ public class SSLFlowDelegate {
|
|||||||
private static String states(AtomicInteger state) {
|
private static String states(AtomicInteger state) {
|
||||||
int s = state.get();
|
int s = state.get();
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
int x = s & ~DOING_TASKS;
|
int x = s & ~TASK_BITS;
|
||||||
switch (x) {
|
switch (x) {
|
||||||
case NOT_HANDSHAKING:
|
case NOT_HANDSHAKING:
|
||||||
sb.append(" NOT_HANDSHAKING ");
|
sb.append(" NOT_HANDSHAKING ");
|
||||||
@ -821,6 +893,8 @@ public class SSLFlowDelegate {
|
|||||||
}
|
}
|
||||||
if ((s & DOING_TASKS) > 0)
|
if ((s & DOING_TASKS) > 0)
|
||||||
sb.append("|DOING_TASKS");
|
sb.append("|DOING_TASKS");
|
||||||
|
if ((s & REQUESTING_TASKS) > 0)
|
||||||
|
sb.append("|REQUESTING_TASKS");
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -833,18 +907,37 @@ public class SSLFlowDelegate {
|
|||||||
final ConcurrentLinkedQueue<String> stateList =
|
final ConcurrentLinkedQueue<String> stateList =
|
||||||
debug.on() ? new ConcurrentLinkedQueue<>() : null;
|
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) {
|
private boolean doHandshake(EngineResult r, int caller) {
|
||||||
// unconditionally sets the HANDSHAKING bit, while preserving DOING_TASKS
|
// unconditionally sets the HANDSHAKING bit, while preserving task bits
|
||||||
handshakeState.getAndAccumulate(HANDSHAKING, (current, update) -> update | (current & DOING_TASKS));
|
handshakeState.getAndAccumulate(0, (current, unused) -> HANDSHAKING | (current & TASK_BITS));
|
||||||
if (stateList != null && debug.on()) {
|
if (stateList != null && debug.on()) {
|
||||||
stateList.add(r.handshakeStatus().toString());
|
stateList.add(r.handshakeStatus().toString());
|
||||||
stateList.add(Integer.toString(caller));
|
stateList.add(Integer.toString(caller));
|
||||||
}
|
}
|
||||||
switch (r.handshakeStatus()) {
|
switch (r.handshakeStatus()) {
|
||||||
case NEED_TASK:
|
case NEED_TASK:
|
||||||
int s = handshakeState.getAndUpdate((current) -> current | DOING_TASKS);
|
int s = handshakeState.accumulateAndGet(0, REQUEST_OR_DO_TASKS);
|
||||||
if ((s & DOING_TASKS) > 0) // someone else was doing tasks
|
if ((s & REQUESTING_TASKS) > 0) { // someone else is or will do tasks
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (debug.on()) debug.log("obtaining and initiating task execution");
|
if (debug.on()) debug.log("obtaining and initiating task execution");
|
||||||
List<Runnable> tasks = obtainTasks();
|
List<Runnable> tasks = obtainTasks();
|
||||||
@ -878,20 +971,25 @@ public class SSLFlowDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void executeTasks(List<Runnable> tasks) {
|
private void executeTasks(List<Runnable> tasks) {
|
||||||
if (tasks.isEmpty())
|
|
||||||
return;
|
|
||||||
exec.execute(() -> {
|
exec.execute(() -> {
|
||||||
try {
|
try {
|
||||||
List<Runnable> nextTasks = tasks;
|
List<Runnable> nextTasks = tasks;
|
||||||
|
if (debug.on()) debug.log("#tasks to execute: " + Integer.toString(nextTasks.size()));
|
||||||
do {
|
do {
|
||||||
nextTasks.forEach(Runnable::run);
|
nextTasks.forEach(Runnable::run);
|
||||||
if (engine.getHandshakeStatus() == HandshakeStatus.NEED_TASK) {
|
if (engine.getHandshakeStatus() == HandshakeStatus.NEED_TASK) {
|
||||||
nextTasks = obtainTasks();
|
nextTasks = obtainTasks();
|
||||||
} else {
|
} 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;
|
break;
|
||||||
}
|
}
|
||||||
} while (true);
|
} while (true);
|
||||||
handshakeState.getAndUpdate((current) -> current & ~DOING_TASKS);
|
if (debug.on()) debug.log("finished task execution");
|
||||||
resumeActivity();
|
resumeActivity();
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
handleError(t);
|
handleError(t);
|
||||||
@ -997,6 +1095,8 @@ public class SSLFlowDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The maximum network buffer size negotiated during
|
||||||
|
// the handshake. Usually 16K.
|
||||||
volatile int packetBufferSize;
|
volatile int packetBufferSize;
|
||||||
final ByteBuffer getNetBuffer() {
|
final ByteBuffer getNetBuffer() {
|
||||||
int netSize = packetBufferSize;
|
int netSize = packetBufferSize;
|
||||||
@ -1006,13 +1106,32 @@ public class SSLFlowDelegate {
|
|||||||
return ByteBuffer.allocate(netSize);
|
return ByteBuffer.allocate(netSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The maximum application buffer size negotiated during
|
||||||
|
// the handshake. Usually close to 16K.
|
||||||
volatile int applicationBufferSize;
|
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() {
|
final ByteBuffer getAppBuffer() {
|
||||||
int appSize = applicationBufferSize;
|
int appSize = applicationBufferSize;
|
||||||
if (appSize <= 0) {
|
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() {
|
final String dbgString() {
|
||||||
|
@ -309,7 +309,7 @@ public class SSLTube implements FlowTube {
|
|||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
previous = pendingDelegate.getAndSet(delegateWrapper);
|
previous = pendingDelegate.getAndSet(delegateWrapper);
|
||||||
subscription = readSubscription;
|
subscription = readSubscription;
|
||||||
handleNow = this.errorRef.get() != null || finished;
|
handleNow = this.errorRef.get() != null || onCompleteReceived;
|
||||||
}
|
}
|
||||||
if (previous != null) {
|
if (previous != null) {
|
||||||
previous.dropSubscription();
|
previous.dropSubscription();
|
||||||
@ -424,12 +424,20 @@ public class SSLTube implements FlowTube {
|
|||||||
// if onError is invoked concurrently with setDelegate.
|
// if onError is invoked concurrently with setDelegate.
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
failed = this.errorRef.get();
|
failed = this.errorRef.get();
|
||||||
completed = finished;
|
completed = onCompleteReceived;
|
||||||
subscribed = subscriberImpl;
|
subscribed = subscriberImpl;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (failed != null) {
|
if (failed != null) {
|
||||||
|
if (debug.on())
|
||||||
|
debug.log("onNewSubscription: subscriberImpl:%s, invoking onError:%s",
|
||||||
|
subscriberImpl, failed);
|
||||||
subscriberImpl.onError(failed);
|
subscriberImpl.onError(failed);
|
||||||
} else if (completed) {
|
} else if (completed) {
|
||||||
|
if (debug.on())
|
||||||
|
debug.log("onNewSubscription: subscriberImpl:%s, invoking onCompleted",
|
||||||
|
subscriberImpl);
|
||||||
|
finished = true;
|
||||||
subscriberImpl.onComplete();
|
subscriberImpl.onComplete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -490,7 +498,6 @@ public class SSLTube implements FlowTube {
|
|||||||
@Override
|
@Override
|
||||||
public void onComplete() {
|
public void onComplete() {
|
||||||
assert !finished && !onCompleteReceived;
|
assert !finished && !onCompleteReceived;
|
||||||
onCompleteReceived = true;
|
|
||||||
DelegateWrapper subscriberImpl;
|
DelegateWrapper subscriberImpl;
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
subscriberImpl = subscribed;
|
subscriberImpl = subscribed;
|
||||||
@ -505,8 +512,10 @@ public class SSLTube implements FlowTube {
|
|||||||
onErrorImpl(new SSLHandshakeException(
|
onErrorImpl(new SSLHandshakeException(
|
||||||
"Remote host terminated the handshake"));
|
"Remote host terminated the handshake"));
|
||||||
} else if (subscriberImpl != null) {
|
} else if (subscriberImpl != null) {
|
||||||
finished = true;
|
onCompleteReceived = finished = true;
|
||||||
subscriberImpl.onComplete();
|
subscriberImpl.onComplete();
|
||||||
|
} else {
|
||||||
|
onCompleteReceived = true;
|
||||||
}
|
}
|
||||||
// now if we have any pending subscriber, we should complete
|
// now if we have any pending subscriber, we should complete
|
||||||
// them immediately as the read scheduler will already be stopped.
|
// them immediately as the read scheduler will already be stopped.
|
||||||
@ -528,12 +537,17 @@ public class SSLTube implements FlowTube {
|
|||||||
final class SSLSubscriptionWrapper implements Flow.Subscription {
|
final class SSLSubscriptionWrapper implements Flow.Subscription {
|
||||||
|
|
||||||
volatile Flow.Subscription delegate;
|
volatile Flow.Subscription delegate;
|
||||||
|
private volatile boolean cancelled;
|
||||||
|
|
||||||
void setSubscription(Flow.Subscription sub) {
|
void setSubscription(Flow.Subscription sub) {
|
||||||
long demand = writeDemand.get(); // FIXME: isn't it a racy way of passing the demand?
|
long demand = writeDemand.get(); // FIXME: isn't it a racy way of passing the demand?
|
||||||
delegate = sub;
|
delegate = sub;
|
||||||
if (debug.on()) debug.log("setSubscription: demand=%d", demand);
|
if (debug.on())
|
||||||
if (demand > 0)
|
debug.log("setSubscription: demand=%d, cancelled:%s", demand, cancelled);
|
||||||
|
|
||||||
|
if (cancelled)
|
||||||
|
delegate.cancel();
|
||||||
|
else if (demand > 0)
|
||||||
sub.request(demand);
|
sub.request(demand);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -549,7 +563,9 @@ public class SSLTube implements FlowTube {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void cancel() {
|
public void cancel() {
|
||||||
// TODO: no-op or error?
|
cancelled = true;
|
||||||
|
if (delegate != null)
|
||||||
|
delegate.cancel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -260,6 +260,8 @@ public abstract class SubscriberWrapper
|
|||||||
try {
|
try {
|
||||||
run1();
|
run1();
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
|
if (debug.on())
|
||||||
|
debug.log("DownstreamPusher threw: " + t);
|
||||||
errorCommon(t);
|
errorCommon(t);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -292,6 +294,7 @@ public abstract class SubscriberWrapper
|
|||||||
pushScheduler.stop();
|
pushScheduler.stop();
|
||||||
outputQ.clear();
|
outputQ.clear();
|
||||||
downstreamSubscriber.onError(error);
|
downstreamSubscriber.onError(error);
|
||||||
|
cf.completeExceptionally(error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -383,9 +386,8 @@ public abstract class SubscriberWrapper
|
|||||||
(throwable = new AssertionError("null throwable")) != null;
|
(throwable = new AssertionError("null throwable")) != null;
|
||||||
if (errorRef.compareAndSet(null, throwable)) {
|
if (errorRef.compareAndSet(null, throwable)) {
|
||||||
if (debug.on()) debug.log("error", throwable);
|
if (debug.on()) debug.log("error", throwable);
|
||||||
pushScheduler.runOrSchedule();
|
|
||||||
upstreamCompleted = true;
|
upstreamCompleted = true;
|
||||||
cf.completeExceptionally(throwable);
|
pushScheduler.runOrSchedule();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -30,19 +30,23 @@ import sun.net.util.IPAddressUtil;
|
|||||||
import sun.net.www.HeaderParser;
|
import sun.net.www.HeaderParser;
|
||||||
|
|
||||||
import javax.net.ssl.ExtendedSSLSession;
|
import javax.net.ssl.ExtendedSSLSession;
|
||||||
|
import javax.net.ssl.SSLException;
|
||||||
import javax.net.ssl.SSLParameters;
|
import javax.net.ssl.SSLParameters;
|
||||||
import javax.net.ssl.SSLSession;
|
import javax.net.ssl.SSLSession;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
|
import java.io.EOFException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.PrintStream;
|
import java.io.PrintStream;
|
||||||
import java.io.UncheckedIOException;
|
import java.io.UncheckedIOException;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.lang.System.Logger.Level;
|
import java.lang.System.Logger.Level;
|
||||||
|
import java.net.ConnectException;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URLPermission;
|
import java.net.URLPermission;
|
||||||
import java.net.http.HttpHeaders;
|
import java.net.http.HttpHeaders;
|
||||||
|
import java.net.http.HttpTimeoutException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.CharBuffer;
|
import java.nio.CharBuffer;
|
||||||
import java.nio.charset.CharacterCodingException;
|
import java.nio.charset.CharacterCodingException;
|
||||||
@ -58,9 +62,11 @@ import java.util.Collections;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.CompletionException;
|
import java.util.concurrent.CompletionException;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.function.BiPredicate;
|
import java.util.function.BiPredicate;
|
||||||
|
import java.util.function.Function;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@ -119,6 +125,8 @@ public final class Utils {
|
|||||||
"jdk.httpclient.bufsize", DEFAULT_BUFSIZE
|
"jdk.httpclient.bufsize", DEFAULT_BUFSIZE
|
||||||
);
|
);
|
||||||
|
|
||||||
|
public static final BiPredicate<String,String> ACCEPT_ALL = (x,y) -> true;
|
||||||
|
|
||||||
private static final Set<String> DISALLOWED_HEADERS_SET;
|
private static final Set<String> DISALLOWED_HEADERS_SET;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
@ -131,25 +139,22 @@ public final class Utils {
|
|||||||
DISALLOWED_HEADERS_SET = Collections.unmodifiableSet(treeSet);
|
DISALLOWED_HEADERS_SET = Collections.unmodifiableSet(treeSet);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final Predicate<String>
|
public static final BiPredicate<String, String>
|
||||||
ALLOWED_HEADERS = header -> !DISALLOWED_HEADERS_SET.contains(header);
|
ALLOWED_HEADERS = (header, unused) -> !DISALLOWED_HEADERS_SET.contains(header);
|
||||||
|
|
||||||
public static final BiPredicate<String, List<String>> VALIDATE_USER_HEADER =
|
public static final BiPredicate<String, String> VALIDATE_USER_HEADER =
|
||||||
(name, lv) -> {
|
(name, value) -> {
|
||||||
requireNonNull(name, "header name");
|
assert name != null : "null header name";
|
||||||
requireNonNull(lv, "header values");
|
assert value != null : "null header value";
|
||||||
if (!isValidName(name)) {
|
if (!isValidName(name)) {
|
||||||
throw newIAE("invalid header name: \"%s\"", 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);
|
throw newIAE("restricted header name: \"%s\"", name);
|
||||||
}
|
}
|
||||||
for (String value : lv) {
|
|
||||||
requireNonNull(value, "header value");
|
|
||||||
if (!isValidValue(value)) {
|
if (!isValidValue(value)) {
|
||||||
throw newIAE("invalid header value for %s: \"%s\"", name, value);
|
throw newIAE("invalid header value for %s: \"%s\"", name, value);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -180,9 +185,20 @@ public final class Utils {
|
|||||||
.collect(Collectors.toUnmodifiableSet());
|
.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 String WSPACES = " \t\r\n";
|
||||||
private static final boolean isAllowedForProxy(String name,
|
private static final boolean isAllowedForProxy(String name,
|
||||||
List<String> value,
|
String value,
|
||||||
Set<String> disabledSchemes,
|
Set<String> disabledSchemes,
|
||||||
Predicate<String> allowedKeys) {
|
Predicate<String> allowedKeys) {
|
||||||
if (!allowedKeys.test(name)) return false;
|
if (!allowedKeys.test(name)) return false;
|
||||||
@ -191,15 +207,14 @@ public final class Utils {
|
|||||||
if (value.isEmpty()) return false;
|
if (value.isEmpty()) return false;
|
||||||
for (String scheme : disabledSchemes) {
|
for (String scheme : disabledSchemes) {
|
||||||
int slen = scheme.length();
|
int slen = scheme.length();
|
||||||
for (String v : value) {
|
int vlen = value.length();
|
||||||
int vlen = v.length();
|
|
||||||
if (vlen == slen) {
|
if (vlen == slen) {
|
||||||
if (v.equalsIgnoreCase(scheme)) {
|
if (value.equalsIgnoreCase(scheme)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else if (vlen > slen) {
|
} else if (vlen > slen) {
|
||||||
if (v.substring(0,slen).equalsIgnoreCase(scheme)) {
|
if (value.substring(0,slen).equalsIgnoreCase(scheme)) {
|
||||||
int c = v.codePointAt(slen);
|
int c = value.codePointAt(slen);
|
||||||
if (WSPACES.indexOf(c) > -1
|
if (WSPACES.indexOf(c) > -1
|
||||||
|| Character.isSpaceChar(c)
|
|| Character.isSpaceChar(c)
|
||||||
|| Character.isWhitespace(c)) {
|
|| Character.isWhitespace(c)) {
|
||||||
@ -209,17 +224,16 @@ public final class Utils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return true;
|
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,
|
(s,v) -> isAllowedForProxy(s, v, PROXY_AUTH_TUNNEL_DISABLED_SCHEMES,
|
||||||
IS_PROXY_HEADER);
|
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,
|
(s,v) -> isAllowedForProxy(s, v, PROXY_AUTH_DISABLED_SCHEMES,
|
||||||
ALL_HEADERS);
|
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);
|
(n,v) -> Utils.NO_PROXY_HEADER.test(n);
|
||||||
|
|
||||||
|
|
||||||
@ -256,6 +270,35 @@ public final class Utils {
|
|||||||
return new IOException(t);
|
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() { }
|
private Utils() { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -534,6 +577,16 @@ public final class Utils {
|
|||||||
return dst;
|
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) {
|
public static String dump(Object... objects) {
|
||||||
return Arrays.toString(objects);
|
return Arrays.toString(objects);
|
||||||
}
|
}
|
||||||
@ -901,6 +954,20 @@ public final class Utils {
|
|||||||
return address;
|
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
|
* Returns the smallest (closest to zero) positive number {@code m} (which
|
||||||
* is also a power of 2) such that {@code n <= m}.
|
* is also a power of 2) such that {@code n <= m}.
|
||||||
|
@ -110,14 +110,14 @@ public class SettingsFrame extends Http2Frame {
|
|||||||
return TYPE;
|
return TYPE;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getParameter(int paramID) {
|
public synchronized int getParameter(int paramID) {
|
||||||
if (paramID > MAX_PARAM) {
|
if (paramID > MAX_PARAM) {
|
||||||
throw new IllegalArgumentException("illegal parameter");
|
throw new IllegalArgumentException("illegal parameter");
|
||||||
}
|
}
|
||||||
return parameters[paramID - 1];
|
return parameters[paramID - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
public SettingsFrame setParameter(int paramID, int value) {
|
public synchronized SettingsFrame setParameter(int paramID, int value) {
|
||||||
if (paramID > MAX_PARAM) {
|
if (paramID > MAX_PARAM) {
|
||||||
throw new IllegalArgumentException("illegal parameter");
|
throw new IllegalArgumentException("illegal parameter");
|
||||||
}
|
}
|
||||||
@ -166,7 +166,7 @@ public class SettingsFrame extends Http2Frame {
|
|||||||
// TODO: check these values
|
// TODO: check these values
|
||||||
f.setParameter(ENABLE_PUSH, 1);
|
f.setParameter(ENABLE_PUSH, 1);
|
||||||
f.setParameter(HEADER_TABLE_SIZE, 4 * K);
|
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(INITIAL_WINDOW_SIZE, 64 * K - 1);
|
||||||
f.setParameter(MAX_FRAME_SIZE, 16 * K);
|
f.setParameter(MAX_FRAME_SIZE, 16 * K);
|
||||||
return f;
|
return f;
|
||||||
|
@ -28,6 +28,7 @@ import jdk.internal.net.http.hpack.HPACK.Logger;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
import static jdk.internal.net.http.hpack.HPACK.Logger.Level.EXTRA;
|
import static jdk.internal.net.http.hpack.HPACK.Logger.Level.EXTRA;
|
||||||
@ -66,7 +67,8 @@ public final class Decoder {
|
|||||||
private final Logger logger;
|
private final Logger logger;
|
||||||
private static final AtomicLong DECODERS_IDS = new AtomicLong();
|
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 {
|
static {
|
||||||
// To be able to do a quick lookup, each of 256 possibilities are mapped
|
// To be able to do a quick lookup, each of 256 possibilities are mapped
|
||||||
@ -78,21 +80,23 @@ public final class Decoder {
|
|||||||
// I do it mainly for better debugging (to not go each time step by step
|
// 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
|
// through if...else tree). As for performance win for the decoding, I
|
||||||
// believe is negligible.
|
// 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) {
|
if ((i & 0b1000_0000) == 0b1000_0000) {
|
||||||
states[i] = State.INDEXED;
|
s[i] = State.INDEXED;
|
||||||
} else if ((i & 0b1100_0000) == 0b0100_0000) {
|
} 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) {
|
} else if ((i & 0b1110_0000) == 0b0010_0000) {
|
||||||
states[i] = State.SIZE_UPDATE;
|
s[i] = State.SIZE_UPDATE;
|
||||||
} else if ((i & 0b1111_0000) == 0b0001_0000) {
|
} 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) {
|
} else if ((i & 0b1111_0000) == 0b0000_0000) {
|
||||||
states[i] = State.LITERAL;
|
s[i] = State.LITERAL;
|
||||||
} else {
|
} else {
|
||||||
throw new InternalError(String.valueOf(i));
|
throw new InternalError(String.valueOf(i));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
states = List.of(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
private final long id;
|
private final long id;
|
||||||
@ -278,7 +282,7 @@ public final class Decoder {
|
|||||||
|
|
||||||
private void resumeReady(ByteBuffer input) {
|
private void resumeReady(ByteBuffer input) {
|
||||||
int b = input.get(input.position()) & 0xff; // absolute read
|
int b = input.get(input.position()) & 0xff; // absolute read
|
||||||
State s = states[b];
|
State s = states.get(b);
|
||||||
if (logger.isLoggable(EXTRA)) {
|
if (logger.isLoggable(EXTRA)) {
|
||||||
logger.log(EXTRA, () -> format("next binary representation %s (first byte 0x%02x)",
|
logger.log(EXTRA, () -> format("next binary representation %s (first byte 0x%02x)",
|
||||||
s, b));
|
s, b));
|
||||||
@ -388,15 +392,17 @@ public final class Decoder {
|
|||||||
try {
|
try {
|
||||||
if (firstValueIndex) {
|
if (firstValueIndex) {
|
||||||
if (logger.isLoggable(NORMAL)) {
|
if (logger.isLoggable(NORMAL)) {
|
||||||
logger.log(NORMAL, () -> format("literal without indexing ('%s', '%s')",
|
logger.log(NORMAL, () -> format(
|
||||||
intValue, value));
|
"literal without indexing (%s, '%s', huffman=%b)",
|
||||||
|
intValue, value, valueHuffmanEncoded));
|
||||||
}
|
}
|
||||||
SimpleHeaderTable.HeaderField f = getHeaderFieldAt(intValue);
|
SimpleHeaderTable.HeaderField f = getHeaderFieldAt(intValue);
|
||||||
action.onLiteral(intValue, f.name, value, valueHuffmanEncoded);
|
action.onLiteral(intValue, f.name, value, valueHuffmanEncoded);
|
||||||
} else {
|
} else {
|
||||||
if (logger.isLoggable(NORMAL)) {
|
if (logger.isLoggable(NORMAL)) {
|
||||||
logger.log(NORMAL, () -> format("literal without indexing ('%s', '%s')",
|
logger.log(NORMAL, () -> format(
|
||||||
name, value));
|
"literal without indexing ('%s', huffman=%b, '%s', huffman=%b)",
|
||||||
|
name, nameHuffmanEncoded, value, valueHuffmanEncoded));
|
||||||
}
|
}
|
||||||
action.onLiteral(name, nameHuffmanEncoded, value, valueHuffmanEncoded);
|
action.onLiteral(name, nameHuffmanEncoded, value, valueHuffmanEncoded);
|
||||||
}
|
}
|
||||||
@ -445,8 +451,9 @@ public final class Decoder {
|
|||||||
String v = value.toString();
|
String v = value.toString();
|
||||||
if (firstValueIndex) {
|
if (firstValueIndex) {
|
||||||
if (logger.isLoggable(NORMAL)) {
|
if (logger.isLoggable(NORMAL)) {
|
||||||
logger.log(NORMAL, () -> format("literal with incremental indexing ('%s', '%s')",
|
logger.log(NORMAL, () -> format(
|
||||||
intValue, value));
|
"literal with incremental indexing (%s, '%s', huffman=%b)",
|
||||||
|
intValue, value, valueHuffmanEncoded));
|
||||||
}
|
}
|
||||||
SimpleHeaderTable.HeaderField f = getHeaderFieldAt(intValue);
|
SimpleHeaderTable.HeaderField f = getHeaderFieldAt(intValue);
|
||||||
n = f.name;
|
n = f.name;
|
||||||
@ -454,8 +461,9 @@ public final class Decoder {
|
|||||||
} else {
|
} else {
|
||||||
n = name.toString();
|
n = name.toString();
|
||||||
if (logger.isLoggable(NORMAL)) {
|
if (logger.isLoggable(NORMAL)) {
|
||||||
logger.log(NORMAL, () -> format("literal with incremental indexing ('%s', '%s')",
|
logger.log(NORMAL, () -> format(
|
||||||
n, value));
|
"literal with incremental indexing ('%s', huffman=%b, '%s', huffman=%b)",
|
||||||
|
n, nameHuffmanEncoded, value, valueHuffmanEncoded));
|
||||||
}
|
}
|
||||||
action.onLiteralWithIndexing(n, nameHuffmanEncoded, v, valueHuffmanEncoded);
|
action.onLiteralWithIndexing(n, nameHuffmanEncoded, v, valueHuffmanEncoded);
|
||||||
}
|
}
|
||||||
@ -496,15 +504,17 @@ public final class Decoder {
|
|||||||
try {
|
try {
|
||||||
if (firstValueIndex) {
|
if (firstValueIndex) {
|
||||||
if (logger.isLoggable(NORMAL)) {
|
if (logger.isLoggable(NORMAL)) {
|
||||||
logger.log(NORMAL, () -> format("literal never indexed ('%s', '%s')",
|
logger.log(NORMAL, () -> format(
|
||||||
intValue, value));
|
"literal never indexed (%s, '%s', huffman=%b)",
|
||||||
|
intValue, value, valueHuffmanEncoded));
|
||||||
}
|
}
|
||||||
SimpleHeaderTable.HeaderField f = getHeaderFieldAt(intValue);
|
SimpleHeaderTable.HeaderField f = getHeaderFieldAt(intValue);
|
||||||
action.onLiteralNeverIndexed(intValue, f.name, value, valueHuffmanEncoded);
|
action.onLiteralNeverIndexed(intValue, f.name, value, valueHuffmanEncoded);
|
||||||
} else {
|
} else {
|
||||||
if (logger.isLoggable(NORMAL)) {
|
if (logger.isLoggable(NORMAL)) {
|
||||||
logger.log(NORMAL, () -> format("literal never indexed ('%s', '%s')",
|
logger.log(NORMAL, () -> format(
|
||||||
name, value));
|
"literal never indexed ('%s', huffman=%b, '%s', huffman=%b)",
|
||||||
|
name, nameHuffmanEncoded, value, valueHuffmanEncoded));
|
||||||
}
|
}
|
||||||
action.onLiteralNeverIndexed(name, nameHuffmanEncoded, value, valueHuffmanEncoded);
|
action.onLiteralNeverIndexed(name, nameHuffmanEncoded, value, valueHuffmanEncoded);
|
||||||
}
|
}
|
||||||
|
@ -105,8 +105,8 @@ public class Encoder {
|
|||||||
|
|
||||||
private static final AtomicLong ENCODERS_IDS = new AtomicLong();
|
private static final AtomicLong ENCODERS_IDS = new AtomicLong();
|
||||||
|
|
||||||
// TODO: enum: no huffman/smart huffman/always huffman
|
/* Used to calculate the number of bytes required for Huffman encoding */
|
||||||
private static final boolean DEFAULT_HUFFMAN = true;
|
private final QuickHuffman.Writer huffmanWriter = new QuickHuffman.Writer();
|
||||||
|
|
||||||
private final Logger logger;
|
private final Logger logger;
|
||||||
private final long id;
|
private final long id;
|
||||||
@ -241,20 +241,29 @@ public class Encoder {
|
|||||||
int index = t.indexOf(name, value);
|
int index = t.indexOf(name, value);
|
||||||
if (index > 0) {
|
if (index > 0) {
|
||||||
indexed(index);
|
indexed(index);
|
||||||
} else if (index < 0) {
|
} else {
|
||||||
|
boolean huffmanValue = isHuffmanBetterFor(value);
|
||||||
|
if (index < 0) {
|
||||||
if (sensitive) {
|
if (sensitive) {
|
||||||
literalNeverIndexed(-index, value, DEFAULT_HUFFMAN);
|
literalNeverIndexed(-index, value, huffmanValue);
|
||||||
} else {
|
} else {
|
||||||
literal(-index, value, DEFAULT_HUFFMAN);
|
literal(-index, value, huffmanValue);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
boolean huffmanName = isHuffmanBetterFor(name);
|
||||||
if (sensitive) {
|
if (sensitive) {
|
||||||
literalNeverIndexed(name, DEFAULT_HUFFMAN, value, DEFAULT_HUFFMAN);
|
literalNeverIndexed(name, huffmanName, value, huffmanValue);
|
||||||
} else {
|
} else {
|
||||||
literal(name, DEFAULT_HUFFMAN, value, DEFAULT_HUFFMAN);
|
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.
|
* Sets a maximum capacity of the header table.
|
||||||
@ -416,8 +425,9 @@ public class Encoder {
|
|||||||
boolean useHuffman)
|
boolean useHuffman)
|
||||||
throws IndexOutOfBoundsException {
|
throws IndexOutOfBoundsException {
|
||||||
if (logger.isLoggable(EXTRA)) {
|
if (logger.isLoggable(EXTRA)) {
|
||||||
logger.log(EXTRA, () -> format("literal without indexing ('%s', '%s')",
|
logger.log(EXTRA, () -> format(
|
||||||
index, value));
|
"literal without indexing (%s, '%s', huffman=%b)",
|
||||||
|
index, value, useHuffman));
|
||||||
}
|
}
|
||||||
checkEncoding();
|
checkEncoding();
|
||||||
encoding = true;
|
encoding = true;
|
||||||
@ -428,10 +438,12 @@ public class Encoder {
|
|||||||
protected final void literal(CharSequence name,
|
protected final void literal(CharSequence name,
|
||||||
boolean nameHuffman,
|
boolean nameHuffman,
|
||||||
CharSequence value,
|
CharSequence value,
|
||||||
boolean valueHuffman) {
|
boolean valueHuffman)
|
||||||
|
{
|
||||||
if (logger.isLoggable(EXTRA)) {
|
if (logger.isLoggable(EXTRA)) {
|
||||||
logger.log(EXTRA, () -> format("literal without indexing ('%s', '%s')",
|
logger.log(EXTRA, () -> format(
|
||||||
name, value));
|
"literal without indexing ('%s', huffman=%b, '%s', huffman=%b)",
|
||||||
|
name, nameHuffman, value, valueHuffman));
|
||||||
}
|
}
|
||||||
checkEncoding();
|
checkEncoding();
|
||||||
encoding = true;
|
encoding = true;
|
||||||
@ -442,10 +454,12 @@ public class Encoder {
|
|||||||
protected final void literalNeverIndexed(int index,
|
protected final void literalNeverIndexed(int index,
|
||||||
CharSequence value,
|
CharSequence value,
|
||||||
boolean valueHuffman)
|
boolean valueHuffman)
|
||||||
throws IndexOutOfBoundsException {
|
throws IndexOutOfBoundsException
|
||||||
|
{
|
||||||
if (logger.isLoggable(EXTRA)) {
|
if (logger.isLoggable(EXTRA)) {
|
||||||
logger.log(EXTRA, () -> format("literal never indexed ('%s', '%s')",
|
logger.log(EXTRA, () -> format(
|
||||||
index, value));
|
"literal never indexed (%s, '%s', huffman=%b)",
|
||||||
|
index, value, valueHuffman));
|
||||||
}
|
}
|
||||||
checkEncoding();
|
checkEncoding();
|
||||||
encoding = true;
|
encoding = true;
|
||||||
@ -458,8 +472,9 @@ public class Encoder {
|
|||||||
CharSequence value,
|
CharSequence value,
|
||||||
boolean valueHuffman) {
|
boolean valueHuffman) {
|
||||||
if (logger.isLoggable(EXTRA)) {
|
if (logger.isLoggable(EXTRA)) {
|
||||||
logger.log(EXTRA, () -> format("literal never indexed ('%s', '%s')",
|
logger.log(EXTRA, () -> format(
|
||||||
name, value));
|
"literal never indexed ('%s', huffman=%b, '%s', huffman=%b)",
|
||||||
|
name, nameHuffman, value, valueHuffman));
|
||||||
}
|
}
|
||||||
checkEncoding();
|
checkEncoding();
|
||||||
encoding = true;
|
encoding = true;
|
||||||
@ -472,8 +487,9 @@ public class Encoder {
|
|||||||
boolean valueHuffman)
|
boolean valueHuffman)
|
||||||
throws IndexOutOfBoundsException {
|
throws IndexOutOfBoundsException {
|
||||||
if (logger.isLoggable(EXTRA)) {
|
if (logger.isLoggable(EXTRA)) {
|
||||||
logger.log(EXTRA, () -> format("literal with incremental indexing ('%s', '%s')",
|
logger.log(EXTRA, () -> format(
|
||||||
index, value));
|
"literal with incremental indexing (%s, '%s', huffman=%b)",
|
||||||
|
index, value, valueHuffman));
|
||||||
}
|
}
|
||||||
checkEncoding();
|
checkEncoding();
|
||||||
encoding = true;
|
encoding = true;
|
||||||
@ -485,9 +501,10 @@ public class Encoder {
|
|||||||
boolean nameHuffman,
|
boolean nameHuffman,
|
||||||
CharSequence value,
|
CharSequence value,
|
||||||
boolean valueHuffman) {
|
boolean valueHuffman) {
|
||||||
if (logger.isLoggable(EXTRA)) { // TODO: include huffman info?
|
if (logger.isLoggable(EXTRA)) {
|
||||||
logger.log(EXTRA, () -> format("literal with incremental indexing ('%s', '%s')",
|
logger.log(EXTRA, () -> format(
|
||||||
name, value));
|
"literal with incremental indexing ('%s', huffman=%b, '%s', huffman=%b)",
|
||||||
|
name, nameHuffman, value, valueHuffman));
|
||||||
}
|
}
|
||||||
checkEncoding();
|
checkEncoding();
|
||||||
encoding = true;
|
encoding = true;
|
||||||
|
@ -27,6 +27,7 @@ package jdk.internal.net.http.hpack;
|
|||||||
import jdk.internal.net.http.common.Utils;
|
import jdk.internal.net.http.common.Utils;
|
||||||
import jdk.internal.net.http.hpack.HPACK.Logger.Level;
|
import jdk.internal.net.http.hpack.HPACK.Logger.Level;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
import java.security.AccessController;
|
import java.security.AccessController;
|
||||||
import java.security.PrivilegedAction;
|
import java.security.PrivilegedAction;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@ -171,4 +172,124 @@ public final class HPACK {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -- 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,6 @@ import jdk.internal.net.http.hpack.HPACK.Logger;
|
|||||||
|
|
||||||
import java.util.Deque;
|
import java.util.Deque;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@ -104,16 +103,24 @@ final class HeaderTable extends SimpleHeaderTable {
|
|||||||
// Long.MAX_VALUE :-)
|
// 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 {
|
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++) {
|
for (int i = 1; i <= STATIC_TABLE_LENGTH; i++) {
|
||||||
HeaderField f = staticTable[i];
|
HeaderField f = staticTable.get(i);
|
||||||
Map<String, Integer> values = staticIndexes
|
Map<String, Integer> values
|
||||||
.computeIfAbsent(f.name, k -> new LinkedHashMap<>());
|
= map.computeIfAbsent(f.name, k -> new HashMap<>());
|
||||||
values.put(f.value, i);
|
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])
|
// name -> (value -> [index])
|
||||||
|
@ -46,29 +46,58 @@ final class ISO_8859_1 {
|
|||||||
|
|
||||||
public static final class Reader {
|
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)
|
public void read(ByteBuffer source, Appendable destination)
|
||||||
throws IOException {
|
throws IOException
|
||||||
for (int i = 0, len = source.remaining(); i < len; i++) {
|
{
|
||||||
char c = (char) (source.get() & 0xff);
|
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 {
|
try {
|
||||||
destination.append(c);
|
destination.append(c);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new IOException(
|
throw new IOException(
|
||||||
"Error appending to the destination", e);
|
"Error appending to the destination", e);
|
||||||
}
|
}
|
||||||
|
buffer <<= 8;
|
||||||
|
bufferLen -= 8;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Reader reset() {
|
public Reader reset() {
|
||||||
|
buffer = 0;
|
||||||
|
bufferLen = 0;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final class Writer {
|
public static final class Writer {
|
||||||
|
|
||||||
|
private final HPACK.BufferUpdateConsumer UPDATER =
|
||||||
|
(buf, bufLen) -> {
|
||||||
|
buffer = buf;
|
||||||
|
bufferLen = bufLen;
|
||||||
|
};
|
||||||
|
|
||||||
private CharSequence source;
|
private CharSequence source;
|
||||||
private int pos;
|
private int pos;
|
||||||
private int end;
|
private int end;
|
||||||
|
private long buffer;
|
||||||
|
private int bufferLen;
|
||||||
|
|
||||||
public Writer configure(CharSequence source, int start, int end) {
|
public Writer configure(CharSequence source, int start, int end) {
|
||||||
this.source = source;
|
this.source = source;
|
||||||
@ -78,25 +107,39 @@ final class ISO_8859_1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean write(ByteBuffer destination) {
|
public boolean write(ByteBuffer destination) {
|
||||||
for (; pos < end; pos++) {
|
while (true) {
|
||||||
char c = source.charAt(pos);
|
while (true) { // stuff codes into long
|
||||||
if (c > '\u00FF') {
|
if (pos >= end) {
|
||||||
throw new IllegalArgumentException(
|
break;
|
||||||
"Illegal ISO-8859-1 char: " + (int) c);
|
|
||||||
}
|
}
|
||||||
if (destination.hasRemaining()) {
|
char c = source.charAt(pos);
|
||||||
destination.put((byte) c);
|
if (c > 255) {
|
||||||
|
throw new IllegalArgumentException(Integer.toString((int) c));
|
||||||
|
}
|
||||||
|
if (bufferLen <= 56) {
|
||||||
|
buffer |= (((long) c) << (56 - bufferLen)); // append
|
||||||
|
bufferLen += 8;
|
||||||
|
pos++;
|
||||||
} else {
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bufferLen == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
int nBytes = HPACK.write(buffer, bufferLen, UPDATER, destination);
|
||||||
|
if (nBytes == 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Writer reset() {
|
public Writer reset() {
|
||||||
source = null;
|
source = null;
|
||||||
pos = -1;
|
pos = -1;
|
||||||
end = -1;
|
end = -1;
|
||||||
|
buffer = 0;
|
||||||
|
bufferLen = 0;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,19 +25,21 @@
|
|||||||
|
|
||||||
package jdk.internal.net.http.hpack;
|
package jdk.internal.net.http.hpack;
|
||||||
|
|
||||||
|
import jdk.internal.net.http.hpack.HPACK.BufferUpdateConsumer;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import static jdk.internal.net.http.hpack.HPACK.bytesForBits;
|
||||||
|
|
||||||
public final class QuickHuffman {
|
public final class QuickHuffman {
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Huffman codes for encoding.
|
* Mapping of characters to their code information. Code information
|
||||||
*
|
* consists of a code value and a code length. Both components are packed in
|
||||||
* EOS will never be matched, since there is no symbol for it, thus no need
|
* a single long as follows:
|
||||||
* 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:
|
|
||||||
*
|
*
|
||||||
* MSB LSB
|
* MSB LSB
|
||||||
* +----------------+----------------+
|
* +----------------+----------------+
|
||||||
@ -46,9 +48,14 @@ public final class QuickHuffman {
|
|||||||
* |<----- 32 ----->|<----- 32 ----->|
|
* |<----- 32 ----->|<----- 32 ----->|
|
||||||
* |<------------- 64 -------------->|
|
* |<------------- 64 -------------->|
|
||||||
*
|
*
|
||||||
* The leftmost 32 bits hold the code value. This value is aligned left
|
* The leftmost 32 bits hold the code value. This value is aligned left (or
|
||||||
* (or MSB). The rightmost 32 bits hold the length of the code value.
|
* MSB). The rightmost 32 bits hold the code length. This length is aligned
|
||||||
* This length is aligned right (or LSB).
|
* 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];
|
private static final long[] codes = new long[256];
|
||||||
|
|
||||||
@ -62,16 +69,19 @@ public final class QuickHuffman {
|
|||||||
|
|
||||||
private static final int EOS_LENGTH = 30;
|
private static final int EOS_LENGTH = 30;
|
||||||
private static final int EOS_LSB = 0x3fffffff;
|
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();
|
TemporaryNode tmpRoot = new TemporaryNode();
|
||||||
addChar(tmpRoot, 0, 0x1ff8, 13);
|
addChar(tmpRoot, 0, 0x1ff8, 13);
|
||||||
addChar(tmpRoot, 1, 0x7fffd8, 23);
|
addChar(tmpRoot, 1, 0x7fffd8, 23);
|
||||||
@ -333,8 +343,8 @@ public final class QuickHuffman {
|
|||||||
|
|
||||||
// The difference in performance can always be checked by not using
|
// The difference in performance can always be checked by not using
|
||||||
// the immutable trie:
|
// the immutable trie:
|
||||||
// root = tmpRoot;
|
// return tmpRoot;
|
||||||
root = ImmutableNode.copyOf(tmpRoot);
|
return ImmutableNode.copyOf(tmpRoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
private QuickHuffman() { }
|
private QuickHuffman() { }
|
||||||
@ -605,6 +615,12 @@ public final class QuickHuffman {
|
|||||||
|
|
||||||
static final class Reader implements Huffman.Reader {
|
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 Node curr = root; // current position in the trie
|
||||||
private long buffer; // bits left from the previous match (aligned to the left, or MSB)
|
private long buffer; // bits left from the previous match (aligned to the left, or MSB)
|
||||||
private int bufferLen; // number of bits in the buffer
|
private int bufferLen; // number of bits in the buffer
|
||||||
@ -615,54 +631,10 @@ public final class QuickHuffman {
|
|||||||
public void read(ByteBuffer source,
|
public void read(ByteBuffer source,
|
||||||
Appendable destination,
|
Appendable destination,
|
||||||
boolean isLast) throws IOException
|
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) {
|
while (!done) {
|
||||||
// read as much as possible (up to 8 bytes)
|
|
||||||
int remaining = source.remaining();
|
int remaining = source.remaining();
|
||||||
int nBytes = Math.min((64 - bufferLen) >> 3, remaining);
|
int nBytes = HPACK.read(source, buffer, bufferLen, UPDATER);
|
||||||
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));
|
|
||||||
}
|
|
||||||
// write as much as possible
|
// write as much as possible
|
||||||
while (true) {
|
while (true) {
|
||||||
if (bufferLen < 8) {
|
if (bufferLen < 8) {
|
||||||
@ -692,7 +664,7 @@ public final class QuickHuffman {
|
|||||||
throw new IOException(
|
throw new IOException(
|
||||||
"Not a EOS prefix padding or unexpected end of data");
|
"Not a EOS prefix padding or unexpected end of data");
|
||||||
}
|
}
|
||||||
if (reportEOS && node.isEOSPath()) {
|
if (node.isEOSPath()) {
|
||||||
throw new IOException("Encountered EOS");
|
throw new IOException("Encountered EOS");
|
||||||
}
|
}
|
||||||
destination.append(node.getSymbol());
|
destination.append(node.getSymbol());
|
||||||
@ -715,27 +687,24 @@ public final class QuickHuffman {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void readLong(ByteBuffer source) {
|
@Override
|
||||||
buffer = source.getLong();
|
public void reset() {
|
||||||
bufferLen = 64;
|
curr = root;
|
||||||
}
|
len = 0;
|
||||||
|
buffer = 0;
|
||||||
private void readInt(ByteBuffer source) {
|
bufferLen = 0;
|
||||||
long b;
|
done = false;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static final class Writer implements Huffman.Writer {
|
static final class Writer implements Huffman.Writer {
|
||||||
|
|
||||||
|
private final BufferUpdateConsumer UPDATER =
|
||||||
|
(buf, bufLen) -> {
|
||||||
|
buffer = buf;
|
||||||
|
bufferLen = bufLen;
|
||||||
|
};
|
||||||
|
|
||||||
private CharSequence source;
|
private CharSequence source;
|
||||||
private boolean padded;
|
private boolean padded;
|
||||||
private int pos;
|
private int pos;
|
||||||
@ -752,43 +721,44 @@ public final class QuickHuffman {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("fallthrough")
|
|
||||||
@Override
|
@Override
|
||||||
public boolean write(ByteBuffer destination) {
|
public boolean write(ByteBuffer destination) {
|
||||||
while (true) {
|
while (true) {
|
||||||
while (bufferLen < 32 && pos < end) {
|
while (true) { // stuff codes into long
|
||||||
char c = source.charAt(pos++);
|
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
|
buffer |= (codeValueOf(c) >>> bufferLen); // append
|
||||||
bufferLen += codeLengthOf(c);
|
bufferLen += len;
|
||||||
|
pos++;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (bufferLen == 0) {
|
if (bufferLen == 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (pos >= end && !padded) { // no more chars, pad
|
if (pos >= end && !padded) { // no more input chars are expected, pad
|
||||||
padded = true;
|
padded = true;
|
||||||
|
// 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);
|
buffer |= (EOS_MSB >>> bufferLen);
|
||||||
bufferLen = bytesForBits(bufferLen) << 3;
|
bufferLen = bytesForBits(bufferLen) << 3;
|
||||||
}
|
}
|
||||||
// The number of bytes that can be written at once
|
}
|
||||||
// (calculating in bytes, not bits, since
|
int nBytes = HPACK.write(buffer, bufferLen, UPDATER, destination);
|
||||||
// destination.remaining() * 8 might overflow)
|
if (nBytes == 0) {
|
||||||
|
|
||||||
int nBytes = Math.min(bytesForBits(bufferLen), destination.remaining()); // ceil?
|
|
||||||
switch (nBytes) {
|
|
||||||
case 0:
|
|
||||||
return false;
|
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -814,13 +784,4 @@ public final class QuickHuffman {
|
|||||||
return bytesForBits(len);
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,7 @@ package jdk.internal.net.http.hpack;
|
|||||||
|
|
||||||
import jdk.internal.net.http.hpack.HPACK.Logger;
|
import jdk.internal.net.http.hpack.HPACK.Logger;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.NoSuchElementException;
|
import java.util.NoSuchElementException;
|
||||||
|
|
||||||
import static jdk.internal.net.http.common.Utils.pow2Size;
|
import static jdk.internal.net.http.common.Utils.pow2Size;
|
||||||
@ -41,8 +42,9 @@ import static java.lang.String.format;
|
|||||||
*/
|
*/
|
||||||
class SimpleHeaderTable {
|
class SimpleHeaderTable {
|
||||||
|
|
||||||
protected static final HeaderField[] staticTable = {
|
/* An immutable list of static header fields */
|
||||||
null, // To make index 1-based, instead of 0-based
|
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(":authority"),
|
||||||
new HeaderField(":method", "GET"),
|
new HeaderField(":method", "GET"),
|
||||||
new HeaderField(":method", "POST"),
|
new HeaderField(":method", "POST"),
|
||||||
@ -103,10 +105,9 @@ class SimpleHeaderTable {
|
|||||||
new HeaderField("user-agent"),
|
new HeaderField("user-agent"),
|
||||||
new HeaderField("vary"),
|
new HeaderField("vary"),
|
||||||
new HeaderField("via"),
|
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;
|
protected static final int ENTRY_SIZE = 32;
|
||||||
|
|
||||||
private final Logger logger;
|
private final Logger logger;
|
||||||
@ -134,7 +135,7 @@ class SimpleHeaderTable {
|
|||||||
HeaderField get(int index) {
|
HeaderField get(int index) {
|
||||||
checkIndex(index);
|
checkIndex(index);
|
||||||
if (index <= STATIC_TABLE_LENGTH) {
|
if (index <= STATIC_TABLE_LENGTH) {
|
||||||
return staticTable[index];
|
return staticTable.get(index);
|
||||||
} else {
|
} else {
|
||||||
return buffer.get(index - STATIC_TABLE_LENGTH - 1);
|
return buffer.get(index - STATIC_TABLE_LENGTH - 1);
|
||||||
}
|
}
|
||||||
@ -263,23 +264,6 @@ class SimpleHeaderTable {
|
|||||||
public String toString() {
|
public String toString() {
|
||||||
return value.isEmpty() ? name : name + ": " + value;
|
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);
|
private final CircularBuffer<HeaderField> buffer = new CircularBuffer<>(0);
|
||||||
|
@ -32,6 +32,9 @@ import java.net.http.HttpRequest;
|
|||||||
import java.net.http.HttpResponse;
|
import java.net.http.HttpResponse;
|
||||||
import java.net.http.HttpResponse.BodyHandlers;
|
import java.net.http.HttpResponse.BodyHandlers;
|
||||||
import java.net.http.WebSocketHandshakeException;
|
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.MinimalFuture;
|
||||||
import jdk.internal.net.http.common.Pair;
|
import jdk.internal.net.http.common.Pair;
|
||||||
import jdk.internal.net.http.common.Utils;
|
import jdk.internal.net.http.common.Utils;
|
||||||
@ -104,7 +107,7 @@ public class OpeningHandshake {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final HttpRequest request;
|
private final HttpRequestImpl request;
|
||||||
private final Collection<String> subprotocols;
|
private final Collection<String> subprotocols;
|
||||||
private final String nonce;
|
private final String nonce;
|
||||||
|
|
||||||
@ -114,7 +117,7 @@ public class OpeningHandshake {
|
|||||||
checkPermissions(b, proxy);
|
checkPermissions(b, proxy);
|
||||||
this.client = b.getClient();
|
this.client = b.getClient();
|
||||||
URI httpURI = createRequestURI(b.getUri());
|
URI httpURI = createRequestURI(b.getUri());
|
||||||
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(httpURI);
|
HttpRequestBuilderImpl requestBuilder = new HttpRequestBuilderImpl(httpURI);
|
||||||
Duration connectTimeout = b.getConnectTimeout();
|
Duration connectTimeout = b.getConnectTimeout();
|
||||||
if (connectTimeout != null) {
|
if (connectTimeout != null) {
|
||||||
requestBuilder.timeout(connectTimeout);
|
requestBuilder.timeout(connectTimeout);
|
||||||
@ -127,7 +130,7 @@ public class OpeningHandshake {
|
|||||||
}
|
}
|
||||||
this.subprotocols = createRequestSubprotocols(b.getSubprotocols());
|
this.subprotocols = createRequestSubprotocols(b.getSubprotocols());
|
||||||
if (!this.subprotocols.isEmpty()) {
|
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_PROTOCOL, p);
|
||||||
}
|
}
|
||||||
requestBuilder.header(HEADER_VERSION, VERSION);
|
requestBuilder.header(HEADER_VERSION, VERSION);
|
||||||
@ -137,12 +140,12 @@ public class OpeningHandshake {
|
|||||||
// to upgrade from HTTP/2 to WebSocket (as of August 2016):
|
// to upgrade from HTTP/2 to WebSocket (as of August 2016):
|
||||||
//
|
//
|
||||||
// https://tools.ietf.org/html/draft-hirano-httpbis-websocket-over-http2-00
|
// https://tools.ietf.org/html/draft-hirano-httpbis-websocket-over-http2-00
|
||||||
this.request = requestBuilder.version(Version.HTTP_1_1).GET().build();
|
requestBuilder.version(Version.HTTP_1_1).GET();
|
||||||
WebSocketRequest r = (WebSocketRequest) this.request;
|
request = requestBuilder.buildForWebSocket();
|
||||||
r.isWebSocket(true);
|
request.isWebSocket(true);
|
||||||
r.setSystemHeader(HEADER_UPGRADE, "websocket");
|
request.setSystemHeader(HEADER_UPGRADE, "websocket");
|
||||||
r.setSystemHeader(HEADER_CONNECTION, "Upgrade");
|
request.setSystemHeader(HEADER_CONNECTION, "Upgrade");
|
||||||
r.setProxy(proxy);
|
request.setProxy(proxy);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Collection<String> createRequestSubprotocols(
|
private static Collection<String> createRequestSubprotocols(
|
||||||
|
@ -34,7 +34,6 @@ import jdk.internal.net.http.common.Utils;
|
|||||||
import jdk.internal.net.http.websocket.OpeningHandshake.Result;
|
import jdk.internal.net.http.websocket.OpeningHandshake.Result;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.System.Logger.Level;
|
|
||||||
import java.lang.ref.Reference;
|
import java.lang.ref.Reference;
|
||||||
import java.net.ProtocolException;
|
import java.net.ProtocolException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
@ -88,7 +87,7 @@ public final class WebSocketImpl implements WebSocket {
|
|||||||
PING,
|
PING,
|
||||||
PONG,
|
PONG,
|
||||||
CLOSE,
|
CLOSE,
|
||||||
ERROR;
|
ERROR
|
||||||
}
|
}
|
||||||
|
|
||||||
private final AtomicReference<ByteBuffer> lastAutomaticPong = new AtomicReference<>();
|
private final AtomicReference<ByteBuffer> lastAutomaticPong = new AtomicReference<>();
|
||||||
|
@ -227,8 +227,8 @@ class ExchangeImpl {
|
|||||||
contentLen = -1;
|
contentLen = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isHeadRequest()) {
|
if (isHeadRequest() || rCode == 304) {
|
||||||
/* HEAD requests should not set a content length by passing it
|
/* HEAD requests or 304 responses should not set a content length by passing it
|
||||||
* through this API, but should instead manually set the required
|
* through this API, but should instead manually set the required
|
||||||
* headers.*/
|
* headers.*/
|
||||||
if (contentLen >= 0) {
|
if (contentLen >= 0) {
|
||||||
@ -239,7 +239,7 @@ class ExchangeImpl {
|
|||||||
}
|
}
|
||||||
noContentToSend = true;
|
noContentToSend = true;
|
||||||
contentLen = 0;
|
contentLen = 0;
|
||||||
} else { /* not a HEAD request */
|
} else { /* not a HEAD request or 304 response */
|
||||||
if (contentLen == 0) {
|
if (contentLen == 0) {
|
||||||
if (http10) {
|
if (http10) {
|
||||||
o.setWrappedStream (new UndefLengthOutputStream (this, ros));
|
o.setWrappedStream (new UndefLengthOutputStream (this, ros));
|
||||||
|
@ -57,7 +57,7 @@ public abstract class AbstractNoBody {
|
|||||||
String https2URI_chunk;
|
String https2URI_chunk;
|
||||||
|
|
||||||
static final String SIMPLE_STRING = "Hello world. Goodbye world";
|
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
|
// a shared executor helps reduce the amount of threads created by the test
|
||||||
static final Executor executor = Executors.newFixedThreadPool(ITERATION_COUNT * 2);
|
static final Executor executor = Executors.newFixedThreadPool(ITERATION_COUNT * 2);
|
||||||
static final ExecutorService serverExecutor = Executors.newFixedThreadPool(ITERATION_COUNT * 4);
|
static final ExecutorService serverExecutor = Executors.newFixedThreadPool(ITERATION_COUNT * 4);
|
||||||
|
@ -21,22 +21,6 @@
|
|||||||
* questions.
|
* 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.HttpServer;
|
||||||
import com.sun.net.httpserver.HttpsConfigurator;
|
import com.sun.net.httpserver.HttpsConfigurator;
|
||||||
import com.sun.net.httpserver.HttpsServer;
|
import com.sun.net.httpserver.HttpsServer;
|
||||||
@ -89,7 +73,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
|
|||||||
import static org.testng.Assert.assertEquals;
|
import static org.testng.Assert.assertEquals;
|
||||||
import static org.testng.Assert.assertTrue;
|
import static org.testng.Assert.assertTrue;
|
||||||
|
|
||||||
public class ThrowingPublishers implements HttpServerAdapters {
|
public abstract class AbstractThrowingPublishers implements HttpServerAdapters {
|
||||||
|
|
||||||
SSLContext sslContext;
|
SSLContext sslContext;
|
||||||
HttpTestServer httpTestServer; // HTTP/1.1 [ 4 servers ]
|
HttpTestServer httpTestServer; // HTTP/1.1 [ 4 servers ]
|
||||||
@ -181,8 +165,8 @@ public class ThrowingPublishers implements HttpServerAdapters {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@DataProvider(name = "noThrows")
|
@DataProvider(name = "sanity")
|
||||||
public Object[][] noThrows() {
|
public Object[][] sanity() {
|
||||||
String[] uris = uris();
|
String[] uris = uris();
|
||||||
Object[][] result = new Object[uris.length * 2][];
|
Object[][] result = new Object[uris.length * 2][];
|
||||||
//Object[][] result = new Object[uris.length][];
|
//Object[][] result = new Object[uris.length][];
|
||||||
@ -190,7 +174,7 @@ public class ThrowingPublishers implements HttpServerAdapters {
|
|||||||
for (boolean sameClient : List.of(false, true)) {
|
for (boolean sameClient : List.of(false, true)) {
|
||||||
//if (!sameClient) continue;
|
//if (!sameClient) continue;
|
||||||
for (String uri: uris()) {
|
for (String uri: uris()) {
|
||||||
result[i++] = new Object[] {uri + "/noThrows", sameClient};
|
result[i++] = new Object[] {uri + "/sanity", sameClient};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert i == uris.length * 2;
|
assert i == uris.length * 2;
|
||||||
@ -198,28 +182,92 @@ public class ThrowingPublishers implements HttpServerAdapters {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@DataProvider(name = "variants")
|
enum Where {
|
||||||
public Object[][] variants() {
|
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();
|
String[] uris = uris();
|
||||||
Object[][] result = new Object[uris.length * 2 * 2][];
|
Object[][] result = new Object[uris.length * 2 * throwers.size()][];
|
||||||
//Object[][] result = new Object[(uris.length/2) * 2 * 2][];
|
//Object[][] result = new Object[(uris.length/2) * 2 * 2][];
|
||||||
int i = 0;
|
int i = 0;
|
||||||
for (Thrower thrower : List.of(
|
for (Thrower thrower : throwers) {
|
||||||
new UncheckedIOExceptionThrower(),
|
|
||||||
new UncheckedCustomExceptionThrower())) {
|
|
||||||
for (boolean sameClient : List.of(false, true)) {
|
for (boolean sameClient : List.of(false, true)) {
|
||||||
for (String uri : uris()) {
|
for (String uri : uris()) {
|
||||||
// if (uri.contains("http2") || uri.contains("https2")) continue;
|
// if (uri.contains("http2") || uri.contains("https2")) continue;
|
||||||
// if (!sameClient) continue;
|
// if (!sameClient) continue;
|
||||||
result[i++] = new Object[]{uri, sameClient, thrower};
|
result[i++] = new Object[]{uri, sameClient, thrower, whereValues};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert i == uris.length * 2 * 2;
|
assert i == uris.length * 2 * throwers.size();
|
||||||
//assert Stream.of(result).filter(o -> o != null).count() == result.length;
|
//assert Stream.of(result).filter(o -> o != null).count() == result.length;
|
||||||
return result;
|
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() {
|
private HttpClient makeNewClient() {
|
||||||
clientCount.incrementAndGet();
|
clientCount.incrementAndGet();
|
||||||
return TRACKER.track(HttpClient.newBuilder()
|
return TRACKER.track(HttpClient.newBuilder()
|
||||||
@ -244,11 +292,11 @@ public class ThrowingPublishers implements HttpServerAdapters {
|
|||||||
|
|
||||||
final String BODY = "Some string | that ? can | be split ? several | ways.";
|
final String BODY = "Some string | that ? can | be split ? several | ways.";
|
||||||
|
|
||||||
@Test(dataProvider = "noThrows")
|
//@Test(dataProvider = "sanity")
|
||||||
public void testNoThrows(String uri, boolean sameClient)
|
protected void testSanityImpl(String uri, boolean sameClient)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
HttpClient client = null;
|
HttpClient client = null;
|
||||||
out.printf("%n%s testNoThrows(%s, %b)%n", now(), uri, sameClient);
|
out.printf("%n%s testSanity(%s, %b)%n", now(), uri, sameClient);
|
||||||
for (int i=0; i< ITERATION_COUNT; i++) {
|
for (int i=0; i< ITERATION_COUNT; i++) {
|
||||||
if (!sameClient || client == null)
|
if (!sameClient || client == null)
|
||||||
client = newHttpClient(sameClient);
|
client = newHttpClient(sameClient);
|
||||||
@ -281,29 +329,31 @@ public class ThrowingPublishers implements HttpServerAdapters {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(dataProvider = "variants")
|
// @Test(dataProvider = "variants")
|
||||||
public void testThrowingAsString(String uri,
|
protected void testThrowingAsStringImpl(String uri,
|
||||||
boolean sameClient,
|
boolean sameClient,
|
||||||
Thrower thrower)
|
Thrower thrower,
|
||||||
|
Set<Where> whereValues)
|
||||||
throws Exception
|
throws Exception
|
||||||
{
|
{
|
||||||
String test = format("testThrowingAsString(%s, %b, %s)",
|
String test = format("testThrowingAsString(%s, %b, %s, %s)",
|
||||||
uri, sameClient, thrower);
|
uri, sameClient, thrower, whereValues);
|
||||||
List<byte[]> bytes = Stream.of(BODY.split("|"))
|
List<byte[]> bytes = Stream.of(BODY.split("|"))
|
||||||
.map(s -> s.getBytes(UTF_8))
|
.map(s -> s.getBytes(UTF_8))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
testThrowing(test, uri, sameClient, () -> BodyPublishers.ofByteArrays(bytes),
|
testThrowing(test, uri, sameClient, () -> BodyPublishers.ofByteArrays(bytes),
|
||||||
this::shouldNotThrowInCancel, thrower,false);
|
this::shouldNotThrowInCancel, thrower,false, whereValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
private <T,U> void testThrowing(String name, String uri, boolean sameClient,
|
private <T,U> void testThrowing(String name, String uri, boolean sameClient,
|
||||||
Supplier<BodyPublisher> publishers,
|
Supplier<BodyPublisher> publishers,
|
||||||
Finisher finisher, Thrower thrower, boolean async)
|
Finisher finisher, Thrower thrower,
|
||||||
|
boolean async, Set<Where> whereValues)
|
||||||
throws Exception
|
throws Exception
|
||||||
{
|
{
|
||||||
out.printf("%n%s%s%n", now(), name);
|
out.printf("%n%s%s%n", now(), name);
|
||||||
try {
|
try {
|
||||||
testThrowing(uri, sameClient, publishers, finisher, thrower, async);
|
testThrowing(uri, sameClient, publishers, finisher, thrower, async, whereValues);
|
||||||
} catch (Error | Exception x) {
|
} catch (Error | Exception x) {
|
||||||
FAILURES.putIfAbsent(name, x);
|
FAILURES.putIfAbsent(name, x);
|
||||||
throw x;
|
throw x;
|
||||||
@ -313,11 +363,11 @@ public class ThrowingPublishers implements HttpServerAdapters {
|
|||||||
private void testThrowing(String uri, boolean sameClient,
|
private void testThrowing(String uri, boolean sameClient,
|
||||||
Supplier<BodyPublisher> publishers,
|
Supplier<BodyPublisher> publishers,
|
||||||
Finisher finisher, Thrower thrower,
|
Finisher finisher, Thrower thrower,
|
||||||
boolean async)
|
boolean async, Set<Where> whereValues)
|
||||||
throws Exception
|
throws Exception
|
||||||
{
|
{
|
||||||
HttpClient client = null;
|
HttpClient client = null;
|
||||||
for (Where where : whereValues()) {
|
for (Where where : whereValues) {
|
||||||
//if (where == Where.ON_SUBSCRIBE) continue;
|
//if (where == Where.ON_SUBSCRIBE) continue;
|
||||||
//if (where == Where.ON_ERROR) continue;
|
//if (where == Where.ON_ERROR) continue;
|
||||||
if (!sameClient || client == null)
|
if (!sameClient || client == null)
|
||||||
@ -345,8 +395,12 @@ public class ThrowingPublishers implements HttpServerAdapters {
|
|||||||
try {
|
try {
|
||||||
response = client.send(req, handler);
|
response = client.send(req, handler);
|
||||||
} catch (Error | Exception t) {
|
} catch (Error | Exception t) {
|
||||||
if (thrower.test(where, t)) {
|
// synchronous send will rethrow exceptions
|
||||||
System.out.println(now() + "Got expected exception: " + t);
|
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);
|
} else throw causeNotFound(where, t);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -356,21 +410,6 @@ public class ThrowingPublishers implements HttpServerAdapters {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
// can be used to reduce the surface of the test when diagnosing
|
||||||
// some failure
|
// some failure
|
||||||
Set<Where> whereValues() {
|
Set<Where> whereValues() {
|
||||||
@ -617,7 +656,7 @@ public class ThrowingPublishers implements HttpServerAdapters {
|
|||||||
http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed/x";
|
http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed/x";
|
||||||
http2URI_chunk = "http://" + http2TestServer.serverAuthority() + "/http2/chunk/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_fixedLengthHandler, "/https2/fixed");
|
||||||
https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk");
|
https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk");
|
||||||
https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed/x";
|
https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed/x";
|
@ -27,15 +27,14 @@
|
|||||||
* response body handlers and subscribers throw unexpected exceptions.
|
* response body handlers and subscribers throw unexpected exceptions.
|
||||||
* @library /lib/testlibrary http2/server
|
* @library /lib/testlibrary http2/server
|
||||||
* @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters
|
* @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters
|
||||||
* ReferenceTracker ThrowingPushPromises
|
* ReferenceTracker AbstractThrowingPushPromises
|
||||||
* @modules java.base/sun.net.www.http
|
* @modules java.base/sun.net.www.http
|
||||||
* java.net.http/jdk.internal.net.http.common
|
* java.net.http/jdk.internal.net.http.common
|
||||||
* java.net.http/jdk.internal.net.http.frame
|
* java.net.http/jdk.internal.net.http.frame
|
||||||
* java.net.http/jdk.internal.net.http.hpack
|
* java.net.http/jdk.internal.net.http.hpack
|
||||||
* @run testng/othervm -Djdk.internal.httpclient.debug=true ThrowingPushPromises
|
* @run testng/othervm -Djdk.internal.httpclient.debug=true AbstractThrowingPushPromises
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import jdk.internal.net.http.common.HttpHeadersImpl;
|
|
||||||
import jdk.testlibrary.SimpleSSLContext;
|
import jdk.testlibrary.SimpleSSLContext;
|
||||||
import org.testng.annotations.AfterTest;
|
import org.testng.annotations.AfterTest;
|
||||||
import org.testng.annotations.AfterClass;
|
import org.testng.annotations.AfterClass;
|
||||||
@ -53,6 +52,7 @@ import java.io.UncheckedIOException;
|
|||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpHeaders;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
import java.net.http.HttpResponse;
|
import java.net.http.HttpResponse;
|
||||||
import java.net.http.HttpResponse.BodyHandler;
|
import java.net.http.HttpResponse.BodyHandler;
|
||||||
@ -72,6 +72,7 @@ import java.util.concurrent.Executor;
|
|||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.Flow;
|
import java.util.concurrent.Flow;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
import java.util.function.BiPredicate;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
@ -86,7 +87,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
|
|||||||
import static org.testng.Assert.assertEquals;
|
import static org.testng.Assert.assertEquals;
|
||||||
import static org.testng.Assert.assertTrue;
|
import static org.testng.Assert.assertTrue;
|
||||||
|
|
||||||
public class ThrowingPushPromises implements HttpServerAdapters {
|
public abstract class AbstractThrowingPushPromises implements HttpServerAdapters {
|
||||||
|
|
||||||
SSLContext sslContext;
|
SSLContext sslContext;
|
||||||
HttpTestServer http2TestServer; // HTTP/2 ( h2c )
|
HttpTestServer http2TestServer; // HTTP/2 ( h2c )
|
||||||
@ -169,8 +170,8 @@ public class ThrowingPushPromises implements HttpServerAdapters {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@DataProvider(name = "noThrows")
|
@DataProvider(name = "sanity")
|
||||||
public Object[][] noThrows() {
|
public Object[][] sanity() {
|
||||||
String[] uris = uris();
|
String[] uris = uris();
|
||||||
Object[][] result = new Object[uris.length * 2][];
|
Object[][] result = new Object[uris.length * 2][];
|
||||||
|
|
||||||
@ -184,24 +185,48 @@ public class ThrowingPushPromises implements HttpServerAdapters {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@DataProvider(name = "variants")
|
enum Where {
|
||||||
public Object[][] variants() {
|
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();
|
String[] uris = uris();
|
||||||
Object[][] result = new Object[uris.length * 2 * 2][];
|
Object[][] result = new Object[uris.length * 2 * throwers.size()][];
|
||||||
int i = 0;
|
int i = 0;
|
||||||
for (Thrower thrower : List.of(
|
for (Thrower thrower : throwers) {
|
||||||
new UncheckedIOExceptionThrower(),
|
|
||||||
new UncheckedCustomExceptionThrower())) {
|
|
||||||
for (boolean sameClient : List.of(false, true)) {
|
for (boolean sameClient : List.of(false, true)) {
|
||||||
for (String uri : uris()) {
|
for (String uri : uris()) {
|
||||||
result[i++] = new Object[]{uri, sameClient, thrower};
|
result[i++] = new Object[]{uri, sameClient, thrower};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert i == uris.length * 2 * 2;
|
assert i == uris.length * 2 * throwers.size();
|
||||||
return result;
|
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() {
|
private HttpClient makeNewClient() {
|
||||||
clientCount.incrementAndGet();
|
clientCount.incrementAndGet();
|
||||||
return TRACKER.track(HttpClient.newBuilder()
|
return TRACKER.track(HttpClient.newBuilder()
|
||||||
@ -224,8 +249,8 @@ public class ThrowingPushPromises implements HttpServerAdapters {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(dataProvider = "noThrows")
|
// @Test(dataProvider = "sanity")
|
||||||
public void testNoThrows(String uri, boolean sameClient)
|
protected void testSanityImpl(String uri, boolean sameClient)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
HttpClient client = null;
|
HttpClient client = null;
|
||||||
out.printf("%ntestNoThrows(%s, %b)%n", uri, sameClient);
|
out.printf("%ntestNoThrows(%s, %b)%n", uri, sameClient);
|
||||||
@ -265,8 +290,8 @@ public class ThrowingPushPromises implements HttpServerAdapters {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(dataProvider = "variants")
|
// @Test(dataProvider = "variants")
|
||||||
public void testThrowingAsString(String uri,
|
protected void testThrowingAsStringImpl(String uri,
|
||||||
boolean sameClient,
|
boolean sameClient,
|
||||||
Thrower thrower)
|
Thrower thrower)
|
||||||
throws Exception
|
throws Exception
|
||||||
@ -277,8 +302,8 @@ public class ThrowingPushPromises implements HttpServerAdapters {
|
|||||||
this::checkAsString, thrower);
|
this::checkAsString, thrower);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(dataProvider = "variants")
|
//@Test(dataProvider = "variants")
|
||||||
public void testThrowingAsLines(String uri,
|
protected void testThrowingAsLinesImpl(String uri,
|
||||||
boolean sameClient,
|
boolean sameClient,
|
||||||
Thrower thrower)
|
Thrower thrower)
|
||||||
throws Exception
|
throws Exception
|
||||||
@ -289,8 +314,8 @@ public class ThrowingPushPromises implements HttpServerAdapters {
|
|||||||
this::checkAsLines, thrower);
|
this::checkAsLines, thrower);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(dataProvider = "variants")
|
//@Test(dataProvider = "variants")
|
||||||
public void testThrowingAsInputStream(String uri,
|
protected void testThrowingAsInputStreamImpl(String uri,
|
||||||
boolean sameClient,
|
boolean sameClient,
|
||||||
Thrower thrower)
|
Thrower thrower)
|
||||||
throws Exception
|
throws Exception
|
||||||
@ -349,21 +374,6 @@ public class ThrowingPushPromises implements HttpServerAdapters {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 Thrower extends Consumer<Where>, Predicate<Throwable> {
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -648,7 +658,7 @@ public class ThrowingPushPromises implements HttpServerAdapters {
|
|||||||
http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed/x";
|
http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed/x";
|
||||||
http2URI_chunk = "http://" + http2TestServer.serverAuthority() + "/http2/chunk/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_fixedLengthHandler, "/https2/fixed");
|
||||||
https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk");
|
https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk");
|
||||||
https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed/x";
|
https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed/x";
|
||||||
@ -679,7 +689,12 @@ public class ThrowingPushPromises implements HttpServerAdapters {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void pushPromiseFor(HttpTestExchange t, URI requestURI, String pushPath, boolean fixed)
|
static final BiPredicate<String,String> ACCEPT_ALL = (x, y) -> true;
|
||||||
|
|
||||||
|
private static void pushPromiseFor(HttpTestExchange t,
|
||||||
|
URI requestURI,
|
||||||
|
String pushPath,
|
||||||
|
boolean fixed)
|
||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
@ -689,9 +704,13 @@ public class ThrowingPushPromises implements HttpServerAdapters {
|
|||||||
byte[] promiseBytes = promise.toASCIIString().getBytes(UTF_8);
|
byte[] promiseBytes = promise.toASCIIString().getBytes(UTF_8);
|
||||||
out.printf("TestServer: %s Pushing promise: %s%n", now(), promise);
|
out.printf("TestServer: %s Pushing promise: %s%n", now(), promise);
|
||||||
err.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) {
|
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);
|
t.serverPush(promise, headers, promiseBytes);
|
||||||
} catch (URISyntaxException x) {
|
} catch (URISyntaxException x) {
|
@ -21,20 +21,6 @@
|
|||||||
* questions.
|
* 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.HttpServer;
|
||||||
import com.sun.net.httpserver.HttpsConfigurator;
|
import com.sun.net.httpserver.HttpsConfigurator;
|
||||||
import com.sun.net.httpserver.HttpsServer;
|
import com.sun.net.httpserver.HttpsServer;
|
||||||
@ -86,7 +72,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
|
|||||||
import static org.testng.Assert.assertEquals;
|
import static org.testng.Assert.assertEquals;
|
||||||
import static org.testng.Assert.assertTrue;
|
import static org.testng.Assert.assertTrue;
|
||||||
|
|
||||||
public class ThrowingSubscribers implements HttpServerAdapters {
|
public abstract class AbstractThrowingSubscribers implements HttpServerAdapters {
|
||||||
|
|
||||||
SSLContext sslContext;
|
SSLContext sslContext;
|
||||||
HttpTestServer httpTestServer; // HTTP/1.1 [ 4 servers ]
|
HttpTestServer httpTestServer; // HTTP/1.1 [ 4 servers ]
|
||||||
@ -179,8 +165,10 @@ public class ThrowingSubscribers implements HttpServerAdapters {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@DataProvider(name = "noThrows")
|
static AtomicLong URICOUNT = new AtomicLong();
|
||||||
public Object[][] noThrows() {
|
|
||||||
|
@DataProvider(name = "sanity")
|
||||||
|
public Object[][] sanity() {
|
||||||
String[] uris = uris();
|
String[] uris = uris();
|
||||||
Object[][] result = new Object[uris.length * 2][];
|
Object[][] result = new Object[uris.length * 2][];
|
||||||
int i = 0;
|
int i = 0;
|
||||||
@ -264,32 +252,34 @@ public class ThrowingSubscribers implements HttpServerAdapters {
|
|||||||
return set;
|
return set;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(dataProvider = "noThrows")
|
//@Test(dataProvider = "sanity")
|
||||||
public void testNoThrows(String uri, boolean sameClient)
|
protected void testSanityImpl(String uri, boolean sameClient)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
HttpClient client = null;
|
HttpClient client = null;
|
||||||
out.printf("%ntestNoThrows(%s, %b)%n", uri, sameClient);
|
String uri2 = uri + "-" + URICOUNT.incrementAndGet() + "/sanity";
|
||||||
|
out.printf("%ntestSanity(%s, %b)%n", uri2, sameClient);
|
||||||
for (int i=0; i< ITERATION_COUNT; i++) {
|
for (int i=0; i< ITERATION_COUNT; i++) {
|
||||||
if (!sameClient || client == null)
|
if (!sameClient || client == null)
|
||||||
client = newHttpClient(sameClient);
|
client = newHttpClient(sameClient);
|
||||||
|
|
||||||
HttpRequest req = HttpRequest.newBuilder(URI.create(uri))
|
HttpRequest req = HttpRequest.newBuilder(URI.create(uri2))
|
||||||
.build();
|
.build();
|
||||||
BodyHandler<String> handler =
|
BodyHandler<String> handler =
|
||||||
new ThrowingBodyHandler((w) -> {},
|
new ThrowingBodyHandler((w) -> {},
|
||||||
BodyHandlers.ofString());
|
BodyHandlers.ofString());
|
||||||
HttpResponse<String> response = client.send(req, handler);
|
HttpResponse<String> response = client.send(req, handler);
|
||||||
String body = response.body();
|
String body = response.body();
|
||||||
assertEquals(URI.create(body).getPath(), URI.create(uri).getPath());
|
assertEquals(URI.create(body).getPath(), URI.create(uri2).getPath());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(dataProvider = "variants")
|
//@Test(dataProvider = "variants")
|
||||||
public void testThrowingAsString(String uri,
|
protected void testThrowingAsStringImpl(String uri,
|
||||||
boolean sameClient,
|
boolean sameClient,
|
||||||
Thrower thrower)
|
Thrower thrower)
|
||||||
throws Exception
|
throws Exception
|
||||||
{
|
{
|
||||||
|
uri = uri + "-" + URICOUNT.incrementAndGet();
|
||||||
String test = format("testThrowingAsString(%s, %b, %s)",
|
String test = format("testThrowingAsString(%s, %b, %s)",
|
||||||
uri, sameClient, thrower);
|
uri, sameClient, thrower);
|
||||||
testThrowing(test, uri, sameClient, BodyHandlers::ofString,
|
testThrowing(test, uri, sameClient, BodyHandlers::ofString,
|
||||||
@ -297,12 +287,13 @@ public class ThrowingSubscribers implements HttpServerAdapters {
|
|||||||
excludes(SubscriberType.INLINE));
|
excludes(SubscriberType.INLINE));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(dataProvider = "variants")
|
//@Test(dataProvider = "variants")
|
||||||
public void testThrowingAsLines(String uri,
|
protected void testThrowingAsLinesImpl(String uri,
|
||||||
boolean sameClient,
|
boolean sameClient,
|
||||||
Thrower thrower)
|
Thrower thrower)
|
||||||
throws Exception
|
throws Exception
|
||||||
{
|
{
|
||||||
|
uri = uri + "-" + URICOUNT.incrementAndGet();
|
||||||
String test = format("testThrowingAsLines(%s, %b, %s)",
|
String test = format("testThrowingAsLines(%s, %b, %s)",
|
||||||
uri, sameClient, thrower);
|
uri, sameClient, thrower);
|
||||||
testThrowing(test, uri, sameClient, BodyHandlers::ofLines,
|
testThrowing(test, uri, sameClient, BodyHandlers::ofLines,
|
||||||
@ -310,12 +301,13 @@ public class ThrowingSubscribers implements HttpServerAdapters {
|
|||||||
excludes(SubscriberType.OFFLINE));
|
excludes(SubscriberType.OFFLINE));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(dataProvider = "variants")
|
//@Test(dataProvider = "variants")
|
||||||
public void testThrowingAsInputStream(String uri,
|
protected void testThrowingAsInputStreamImpl(String uri,
|
||||||
boolean sameClient,
|
boolean sameClient,
|
||||||
Thrower thrower)
|
Thrower thrower)
|
||||||
throws Exception
|
throws Exception
|
||||||
{
|
{
|
||||||
|
uri = uri + "-" + URICOUNT.incrementAndGet();
|
||||||
String test = format("testThrowingAsInputStream(%s, %b, %s)",
|
String test = format("testThrowingAsInputStream(%s, %b, %s)",
|
||||||
uri, sameClient, thrower);
|
uri, sameClient, thrower);
|
||||||
testThrowing(test, uri, sameClient, BodyHandlers::ofInputStream,
|
testThrowing(test, uri, sameClient, BodyHandlers::ofInputStream,
|
||||||
@ -323,12 +315,13 @@ public class ThrowingSubscribers implements HttpServerAdapters {
|
|||||||
excludes(SubscriberType.OFFLINE));
|
excludes(SubscriberType.OFFLINE));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(dataProvider = "variants")
|
//@Test(dataProvider = "variants")
|
||||||
public void testThrowingAsStringAsync(String uri,
|
protected void testThrowingAsStringAsyncImpl(String uri,
|
||||||
boolean sameClient,
|
boolean sameClient,
|
||||||
Thrower thrower)
|
Thrower thrower)
|
||||||
throws Exception
|
throws Exception
|
||||||
{
|
{
|
||||||
|
uri = uri + "-" + URICOUNT.incrementAndGet();
|
||||||
String test = format("testThrowingAsStringAsync(%s, %b, %s)",
|
String test = format("testThrowingAsStringAsync(%s, %b, %s)",
|
||||||
uri, sameClient, thrower);
|
uri, sameClient, thrower);
|
||||||
testThrowing(test, uri, sameClient, BodyHandlers::ofString,
|
testThrowing(test, uri, sameClient, BodyHandlers::ofString,
|
||||||
@ -336,12 +329,13 @@ public class ThrowingSubscribers implements HttpServerAdapters {
|
|||||||
excludes(SubscriberType.INLINE));
|
excludes(SubscriberType.INLINE));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(dataProvider = "variants")
|
//@Test(dataProvider = "variants")
|
||||||
public void testThrowingAsLinesAsync(String uri,
|
protected void testThrowingAsLinesAsyncImpl(String uri,
|
||||||
boolean sameClient,
|
boolean sameClient,
|
||||||
Thrower thrower)
|
Thrower thrower)
|
||||||
throws Exception
|
throws Exception
|
||||||
{
|
{
|
||||||
|
uri = uri + "-" + URICOUNT.incrementAndGet();
|
||||||
String test = format("testThrowingAsLinesAsync(%s, %b, %s)",
|
String test = format("testThrowingAsLinesAsync(%s, %b, %s)",
|
||||||
uri, sameClient, thrower);
|
uri, sameClient, thrower);
|
||||||
testThrowing(test, uri, sameClient, BodyHandlers::ofLines,
|
testThrowing(test, uri, sameClient, BodyHandlers::ofLines,
|
||||||
@ -349,12 +343,13 @@ public class ThrowingSubscribers implements HttpServerAdapters {
|
|||||||
excludes(SubscriberType.OFFLINE));
|
excludes(SubscriberType.OFFLINE));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(dataProvider = "variants")
|
//@Test(dataProvider = "variants")
|
||||||
public void testThrowingAsInputStreamAsync(String uri,
|
protected void testThrowingAsInputStreamAsyncImpl(String uri,
|
||||||
boolean sameClient,
|
boolean sameClient,
|
||||||
Thrower thrower)
|
Thrower thrower)
|
||||||
throws Exception
|
throws Exception
|
||||||
{
|
{
|
||||||
|
uri = uri + "-" + URICOUNT.incrementAndGet();
|
||||||
String test = format("testThrowingAsInputStreamAsync(%s, %b, %s)",
|
String test = format("testThrowingAsInputStreamAsync(%s, %b, %s)",
|
||||||
uri, sameClient, thrower);
|
uri, sameClient, thrower);
|
||||||
testThrowing(test, uri, sameClient, BodyHandlers::ofInputStream,
|
testThrowing(test, uri, sameClient, BodyHandlers::ofInputStream,
|
||||||
@ -389,9 +384,9 @@ public class ThrowingSubscribers implements HttpServerAdapters {
|
|||||||
|
|
||||||
if (!sameClient || client == null)
|
if (!sameClient || client == null)
|
||||||
client = newHttpClient(sameClient);
|
client = newHttpClient(sameClient);
|
||||||
|
String uri2 = uri + "-" + where;
|
||||||
HttpRequest req = HttpRequest.
|
HttpRequest req = HttpRequest.
|
||||||
newBuilder(URI.create(uri))
|
newBuilder(URI.create(uri2))
|
||||||
.build();
|
.build();
|
||||||
BodyHandler<T> handler =
|
BodyHandler<T> handler =
|
||||||
new ThrowingBodyHandler(where.select(thrower), handlers.get());
|
new ThrowingBodyHandler(where.select(thrower), handlers.get());
|
||||||
@ -409,8 +404,12 @@ public class ThrowingSubscribers implements HttpServerAdapters {
|
|||||||
try {
|
try {
|
||||||
response = client.send(req, handler);
|
response = client.send(req, handler);
|
||||||
} catch (Error | Exception t) {
|
} catch (Error | Exception t) {
|
||||||
if (thrower.test(t)) {
|
// synchronous send will rethrow exceptions
|
||||||
System.out.println(now() + "Got expected exception: " + t);
|
Throwable throwable = t.getCause();
|
||||||
|
assert throwable != null;
|
||||||
|
|
||||||
|
if (thrower.test(throwable)) {
|
||||||
|
System.out.println(now() + "Got expected exception: " + throwable);
|
||||||
} else throw causeNotFound(where, t);
|
} else throw causeNotFound(where, t);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -666,7 +665,7 @@ public class ThrowingSubscribers implements HttpServerAdapters {
|
|||||||
http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed/x";
|
http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed/x";
|
||||||
http2URI_chunk = "http://" + http2TestServer.serverAuthority() + "/http2/chunk/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_fixedLengthHandler, "/https2/fixed");
|
||||||
https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk");
|
https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk");
|
||||||
https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed/x";
|
https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed/x";
|
@ -53,6 +53,7 @@ import java.net.InetAddress;
|
|||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpHeaders;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
import java.net.http.HttpRequest.BodyPublishers;
|
import java.net.http.HttpRequest.BodyPublishers;
|
||||||
import java.net.http.HttpResponse;
|
import java.net.http.HttpResponse;
|
||||||
@ -63,9 +64,8 @@ import java.nio.file.Paths;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.logging.ConsoleHandler;
|
import java.util.Locale;
|
||||||
import java.util.logging.Level;
|
import java.util.Map;
|
||||||
import java.util.logging.Logger;
|
|
||||||
import javax.net.ssl.SSLContext;
|
import javax.net.ssl.SSLContext;
|
||||||
import jdk.testlibrary.SimpleSSLContext;
|
import jdk.testlibrary.SimpleSSLContext;
|
||||||
import jdk.test.lib.util.FileUtils;
|
import jdk.test.lib.util.FileUtils;
|
||||||
@ -188,6 +188,10 @@ public class AsFileDownloadTest {
|
|||||||
assertEquals(response.headers().firstValue("Content-Disposition").get(),
|
assertEquals(response.headers().firstValue("Content-Disposition").get(),
|
||||||
contentDispositionValue);
|
contentDispositionValue);
|
||||||
assertEquals(fileContents, "May the luck of the Irish be with you!");
|
assertEquals(fileContents, "May the luck of the Irish be with you!");
|
||||||
|
|
||||||
|
// additional checks unrelated to file download
|
||||||
|
caseInsensitivityOfHeaders(request.headers());
|
||||||
|
caseInsensitivityOfHeaders(response.headers());
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Negative
|
// --- Negative
|
||||||
@ -299,7 +303,7 @@ public class AsFileDownloadTest {
|
|||||||
http2TestServer.addHandler(new Http2FileDispoHandler(), "/http2/afdt");
|
http2TestServer.addHandler(new Http2FileDispoHandler(), "/http2/afdt");
|
||||||
http2URI = "http://" + http2TestServer.serverAuthority() + "/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");
|
https2TestServer.addHandler(new Http2FileDispoHandler(), "/https2/afdt");
|
||||||
https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/afdt";
|
https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/afdt";
|
||||||
|
|
||||||
@ -372,4 +376,30 @@ public class AsFileDownloadTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -228,7 +228,7 @@ public class BasicRedirectTest implements HttpServerAdapters {
|
|||||||
http2TestServer = HttpTestServer.of(new Http2TestServer("localhost", false, 0));
|
http2TestServer = HttpTestServer.of(new Http2TestServer("localhost", false, 0));
|
||||||
http2TestServer.addHandler(new BasicHttpRedirectHandler(), "/http2/same/");
|
http2TestServer.addHandler(new BasicHttpRedirectHandler(), "/http2/same/");
|
||||||
http2URI = "http://" + http2TestServer.serverAuthority() + "/http2/same/redirect";
|
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/");
|
https2TestServer.addHandler(new BasicHttpRedirectHandler(), "/https2/same/");
|
||||||
https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/same/redirect";
|
https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/same/redirect";
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ import static java.lang.System.err;
|
|||||||
* @test
|
* @test
|
||||||
* @bug 8187503
|
* @bug 8187503
|
||||||
* @summary An example on how to read a response body with InputStream.
|
* @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
|
* @author daniel fuchs
|
||||||
*/
|
*/
|
||||||
public class BodyProcessorInputStreamTest {
|
public class BodyProcessorInputStreamTest {
|
||||||
|
@ -35,7 +35,9 @@
|
|||||||
* @library /lib/testlibrary http2/server
|
* @library /lib/testlibrary http2/server
|
||||||
* @build Http2TestServer
|
* @build Http2TestServer
|
||||||
* @build jdk.testlibrary.SimpleSSLContext
|
* @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;
|
import java.io.IOException;
|
||||||
@ -280,7 +282,7 @@ public class ConcurrentResponses {
|
|||||||
http2TestServer.addHandler(new Http2VariableHandler(), "/http2/variable");
|
http2TestServer.addHandler(new Http2VariableHandler(), "/http2/variable");
|
||||||
http2VariableURI = "http://" + http2TestServer.serverAuthority() + "/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");
|
https2TestServer.addHandler(new Http2FixedHandler(), "/https2/fixed");
|
||||||
https2FixedURI = "https://" + https2TestServer.serverAuthority() + "/https2/fixed";
|
https2FixedURI = "https://" + https2TestServer.serverAuthority() + "/https2/fixed";
|
||||||
https2TestServer.addHandler(new Http2VariableHandler(), "/https2/variable");
|
https2TestServer.addHandler(new Http2VariableHandler(), "/https2/variable");
|
||||||
|
167
test/jdk/java/net/httpclient/ConnectExceptionTest.java
Normal file
167
test/jdk/java/net/httpclient/ConnectExceptionTest.java
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -205,7 +205,7 @@ public class CookieHeaderTest implements HttpServerAdapters {
|
|||||||
http2TestServer = HttpTestServer.of(new Http2TestServer("localhost", false, 0));
|
http2TestServer = HttpTestServer.of(new Http2TestServer("localhost", false, 0));
|
||||||
http2TestServer.addHandler(new CookieValidationHandler(), "/http2/cookie/");
|
http2TestServer.addHandler(new CookieValidationHandler(), "/http2/cookie/");
|
||||||
http2URI = "http://" + http2TestServer.serverAuthority() + "/http2/cookie/retry";
|
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/");
|
https2TestServer.addHandler(new CookieValidationHandler(), "/https2/cookie/");
|
||||||
https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/cookie/retry";
|
https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/cookie/retry";
|
||||||
|
|
||||||
|
@ -340,7 +340,7 @@ public class CustomRequestPublisher {
|
|||||||
http2TestServer.addHandler(new Http2EchoHandler(), "/http2/echo");
|
http2TestServer.addHandler(new Http2EchoHandler(), "/http2/echo");
|
||||||
http2URI = "http://" + http2TestServer.serverAuthority() + "/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");
|
https2TestServer.addHandler(new Http2EchoHandler(), "/https2/echo");
|
||||||
https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/echo";
|
https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/echo";
|
||||||
|
|
||||||
|
@ -217,7 +217,7 @@ public class CustomResponseSubscriber {
|
|||||||
http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed";
|
http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed";
|
||||||
http2URI_chunk = "http://" + http2TestServer.serverAuthority() + "/http2/chunk";
|
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_fixedLengthHandler, "/https2/fixed");
|
||||||
https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk");
|
https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk");
|
||||||
https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed";
|
https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed";
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
* completes are executed either asynchronously in an executor when the
|
* completes are executed either asynchronously in an executor when the
|
||||||
* CF later completes, or in the user thread that joins.
|
* CF later completes, or in the user thread that joins.
|
||||||
* @library /lib/testlibrary http2/server
|
* @library /lib/testlibrary http2/server
|
||||||
* @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters ThrowingPublishers
|
* @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters DependentActionsTest
|
||||||
* @modules java.base/sun.net.www.http
|
* @modules java.base/sun.net.www.http
|
||||||
* java.net.http/jdk.internal.net.http.common
|
* java.net.http/jdk.internal.net.http.common
|
||||||
* java.net.http/jdk.internal.net.http.frame
|
* java.net.http/jdk.internal.net.http.frame
|
||||||
@ -82,12 +82,14 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Predicate;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import static java.lang.System.out;
|
import static java.lang.System.out;
|
||||||
import static java.lang.String.format;
|
import static java.lang.String.format;
|
||||||
|
import static java.util.stream.Collectors.toList;
|
||||||
import static org.testng.Assert.assertEquals;
|
import static org.testng.Assert.assertEquals;
|
||||||
import static org.testng.Assert.assertTrue;
|
import static org.testng.Assert.assertTrue;
|
||||||
|
|
||||||
@ -374,13 +376,13 @@ public class DependentActionsTest implements HttpServerAdapters {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final List<String> extractStream(HttpResponse<Stream<String>> resp) {
|
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) {
|
final List<String> extractInputStream(HttpResponse<InputStream> resp) {
|
||||||
try (InputStream is = resp.body()) {
|
try (InputStream is = resp.body()) {
|
||||||
return new BufferedReader(new InputStreamReader(is))
|
return new BufferedReader(new InputStreamReader(is))
|
||||||
.lines().collect(Collectors.toList());
|
.lines().collect(toList());
|
||||||
} catch (IOException x) {
|
} catch (IOException x) {
|
||||||
throw new CompletionException(x);
|
throw new CompletionException(x);
|
||||||
}
|
}
|
||||||
@ -399,44 +401,28 @@ public class DependentActionsTest implements HttpServerAdapters {
|
|||||||
.findFirst();
|
.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,
|
<T> void checkThreadAndStack(Thread thread,
|
||||||
AtomicReference<RuntimeException> failed,
|
AtomicReference<RuntimeException> failed,
|
||||||
T result,
|
T result,
|
||||||
Throwable error) {
|
Throwable error) {
|
||||||
if (Thread.currentThread() == thread) {
|
|
||||||
//failed.set(new RuntimeException("Dependant action was executed in " + thread));
|
//failed.set(new RuntimeException("Dependant action was executed in " + thread));
|
||||||
List<StackFrame> httpStack = WALKER.walk(s -> s.filter(f -> f.getDeclaringClass()
|
List<StackFrame> otherFrames = WALKER.walk(s -> s.filter(NotDATorJUCorJLT).collect(toList()));
|
||||||
.getModule().equals(HttpClient.class.getModule()))
|
if (!otherFrames.isEmpty()) {
|
||||||
.collect(Collectors.toList()));
|
|
||||||
if (!httpStack.isEmpty()) {
|
|
||||||
System.out.println("Found unexpected trace: ");
|
System.out.println("Found unexpected trace: ");
|
||||||
httpStack.forEach(f -> System.out.printf("\t%s%n", f));
|
otherFrames.forEach(f -> System.out.printf("\t%s%n", f));
|
||||||
failed.set(new RuntimeException("Dependant action has unexpected frame in " +
|
failed.set(new RuntimeException("Dependant action has unexpected frame in " +
|
||||||
Thread.currentThread() + ": " + httpStack.get(0)));
|
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)));
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<T> void finish(Where w, CompletableFuture<HttpResponse<T>> cf,
|
<T> void finish(Where w, CompletableFuture<HttpResponse<T>> cf,
|
||||||
@ -620,7 +606,7 @@ public class DependentActionsTest implements HttpServerAdapters {
|
|||||||
http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed/x";
|
http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed/x";
|
||||||
http2URI_chunk = "http://" + http2TestServer.serverAuthority() + "/http2/chunk/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_fixedLengthHandler, "/https2/fixed");
|
||||||
https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk");
|
https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk");
|
||||||
https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed/x";
|
https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed/x";
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
* completes are executed either asynchronously in an executor when the
|
* completes are executed either asynchronously in an executor when the
|
||||||
* CF later completes, or in the user thread that joins.
|
* CF later completes, or in the user thread that joins.
|
||||||
* @library /lib/testlibrary http2/server
|
* @library /lib/testlibrary http2/server
|
||||||
* @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters ThrowingPublishers
|
* @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters DependentPromiseActionsTest
|
||||||
* @modules java.base/sun.net.www.http
|
* @modules java.base/sun.net.www.http
|
||||||
* java.net.http/jdk.internal.net.http.common
|
* java.net.http/jdk.internal.net.http.common
|
||||||
* java.net.http/jdk.internal.net.http.frame
|
* java.net.http/jdk.internal.net.http.frame
|
||||||
@ -40,7 +40,6 @@
|
|||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.lang.StackWalker.StackFrame;
|
import java.lang.StackWalker.StackFrame;
|
||||||
import jdk.internal.net.http.common.HttpHeadersImpl;
|
|
||||||
import jdk.testlibrary.SimpleSSLContext;
|
import jdk.testlibrary.SimpleSSLContext;
|
||||||
import org.testng.annotations.AfterTest;
|
import org.testng.annotations.AfterTest;
|
||||||
import org.testng.annotations.AfterClass;
|
import org.testng.annotations.AfterClass;
|
||||||
@ -79,6 +78,7 @@ import java.util.concurrent.Flow;
|
|||||||
import java.util.concurrent.Semaphore;
|
import java.util.concurrent.Semaphore;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.function.BiPredicate;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
@ -672,7 +672,7 @@ public class DependentPromiseActionsTest implements HttpServerAdapters {
|
|||||||
http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed/y";
|
http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed/y";
|
||||||
http2URI_chunk = "http://" + http2TestServer.serverAuthority() + "/http2/chunk/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_fixedLengthHandler, "/https2/fixed");
|
||||||
https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk");
|
https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk");
|
||||||
https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed/y";
|
https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed/y";
|
||||||
@ -690,7 +690,12 @@ public class DependentPromiseActionsTest implements HttpServerAdapters {
|
|||||||
https2TestServer.stop();
|
https2TestServer.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void pushPromiseFor(HttpTestExchange t, URI requestURI, String pushPath, boolean fixed)
|
static final BiPredicate<String,String> ACCEPT_ALL = (x, y) -> true;
|
||||||
|
|
||||||
|
private static void pushPromiseFor(HttpTestExchange t,
|
||||||
|
URI requestURI,
|
||||||
|
String pushPath,
|
||||||
|
boolean fixed)
|
||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
@ -700,9 +705,13 @@ public class DependentPromiseActionsTest implements HttpServerAdapters {
|
|||||||
byte[] promiseBytes = promise.toASCIIString().getBytes(UTF_8);
|
byte[] promiseBytes = promise.toASCIIString().getBytes(UTF_8);
|
||||||
out.printf("TestServer: %s Pushing promise: %s%n", now(), promise);
|
out.printf("TestServer: %s Pushing promise: %s%n", now(), promise);
|
||||||
err.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) {
|
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);
|
t.serverPush(promise, headers, promiseBytes);
|
||||||
} catch (URISyntaxException x) {
|
} catch (URISyntaxException x) {
|
||||||
|
@ -170,7 +170,7 @@ public abstract class DigestEchoServer implements HttpServerAdapters {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String toString(HttpTestHeaders headers) {
|
private static String toString(HttpTestRequestHeaders headers) {
|
||||||
return headers.entrySet().stream()
|
return headers.entrySet().stream()
|
||||||
.map((e) -> e.getKey() + ": " + e.getValue())
|
.map((e) -> e.getKey() + ": " + e.getValue())
|
||||||
.collect(Collectors.joining("\n"));
|
.collect(Collectors.joining("\n"));
|
||||||
@ -970,11 +970,13 @@ public abstract class DigestEchoServer implements HttpServerAdapters {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void requestAuthentication(HttpTestExchange he)
|
protected void requestAuthentication(HttpTestExchange he)
|
||||||
throws IOException {
|
throws IOException
|
||||||
he.getResponseHeaders().addHeader(getAuthenticate(),
|
{
|
||||||
"Basic realm=\"" + auth.getRealm() + "\"");
|
String headerName = getAuthenticate();
|
||||||
System.out.println(type + ": Requesting Basic Authentication "
|
String headerValue = "Basic realm=\"" + auth.getRealm() + "\"";
|
||||||
+ he.getResponseHeaders().firstValue(getAuthenticate()));
|
he.getResponseHeaders().addHeader(headerName, headerValue);
|
||||||
|
System.out.println(type + ": Requesting Basic Authentication, "
|
||||||
|
+ headerName + " : "+ headerValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -1061,14 +1063,13 @@ public abstract class DigestEchoServer implements HttpServerAdapters {
|
|||||||
} else {
|
} else {
|
||||||
throw new InternalError(String.valueOf(v));
|
throw new InternalError(String.valueOf(v));
|
||||||
}
|
}
|
||||||
he.getResponseHeaders().addHeader(getAuthenticate(),
|
String headerName = getAuthenticate();
|
||||||
"Digest realm=\"" + auth.getRealm() + "\","
|
String headerValue = "Digest realm=\"" + auth.getRealm() + "\","
|
||||||
+ separator + "qop=\"auth\","
|
+ separator + "qop=\"auth\","
|
||||||
+ separator + "nonce=\"" + ns +"\"");
|
+ separator + "nonce=\"" + ns +"\"";
|
||||||
System.out.println(type + ": Requesting Digest Authentication "
|
he.getResponseHeaders().addHeader(headerName, headerValue);
|
||||||
+ he.getResponseHeaders()
|
System.out.println(type + ": Requesting Digest Authentication, "
|
||||||
.firstValue(getAuthenticate())
|
+ headerName + " : " + headerValue);
|
||||||
.orElse("null"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -33,7 +33,8 @@
|
|||||||
* java.net.http/jdk.internal.net.http.frame
|
* java.net.http/jdk.internal.net.http.frame
|
||||||
* java.net.http/jdk.internal.net.http.hpack
|
* java.net.http/jdk.internal.net.http.hpack
|
||||||
* @run testng/othervm
|
* @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
|
//* -Djdk.internal.httpclient.debug=true
|
||||||
|
|
||||||
@ -48,21 +49,14 @@ import org.testng.annotations.DataProvider;
|
|||||||
import org.testng.annotations.Test;
|
import org.testng.annotations.Test;
|
||||||
|
|
||||||
import javax.net.ServerSocketFactory;
|
import javax.net.ServerSocketFactory;
|
||||||
import javax.net.SocketFactory;
|
|
||||||
import javax.net.ssl.SSLContext;
|
import javax.net.ssl.SSLContext;
|
||||||
import javax.net.ssl.SSLServerSocket;
|
|
||||||
import javax.net.ssl.SSLServerSocketFactory;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.OutputStreamWriter;
|
|
||||||
import java.io.PrintWriter;
|
|
||||||
import java.io.Writer;
|
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.ServerSocket;
|
import java.net.ServerSocket;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.net.SocketAddress;
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
@ -71,8 +65,6 @@ import java.net.http.HttpRequest.BodyPublishers;
|
|||||||
import java.net.http.HttpResponse;
|
import java.net.http.HttpResponse;
|
||||||
import java.net.http.HttpResponse.BodyHandler;
|
import java.net.http.HttpResponse.BodyHandler;
|
||||||
import java.net.http.HttpResponse.BodyHandlers;
|
import java.net.http.HttpResponse.BodyHandlers;
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.StringTokenizer;
|
import java.util.StringTokenizer;
|
||||||
@ -87,6 +79,7 @@ import java.util.concurrent.atomic.AtomicLong;
|
|||||||
import static java.lang.String.format;
|
import static java.lang.String.format;
|
||||||
import static java.lang.System.in;
|
import static java.lang.System.in;
|
||||||
import static java.lang.System.out;
|
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.nio.charset.StandardCharsets.UTF_8;
|
||||||
import static java.net.http.HttpClient.Builder.NO_PROXY;
|
import static java.net.http.HttpClient.Builder.NO_PROXY;
|
||||||
import static org.testng.Assert.assertEquals;
|
import static org.testng.Assert.assertEquals;
|
||||||
@ -292,7 +285,7 @@ public class EncodedCharsInURI implements HttpServerAdapters {
|
|||||||
http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed/x";
|
http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed/x";
|
||||||
http2URI_chunk = "http://" + http2TestServer.serverAuthority() + "/http2/chunk/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_fixedLengthHandler, "/https2/fixed");
|
||||||
https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk");
|
https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk");
|
||||||
https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed/x";
|
https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed/x";
|
||||||
@ -402,9 +395,6 @@ public class EncodedCharsInURI implements HttpServerAdapters {
|
|||||||
Socket targetConnection = null;
|
Socket targetConnection = null;
|
||||||
InputStream ccis = clientConnection.getInputStream();
|
InputStream ccis = clientConnection.getInputStream();
|
||||||
OutputStream ccos = clientConnection.getOutputStream();
|
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");
|
System.out.println(now() + getName() + ": Reading request line");
|
||||||
String requestLine = readLine(ccis);
|
String requestLine = readLine(ccis);
|
||||||
System.out.println(now() + getName() + ": Request line: " + requestLine);
|
System.out.println(now() + getName() + ": Request line: " + requestLine);
|
||||||
@ -459,11 +449,13 @@ public class EncodedCharsInURI implements HttpServerAdapters {
|
|||||||
// Then send the 200 OK response to the client
|
// Then send the 200 OK response to the client
|
||||||
System.out.println(now() + getName() + ": Sending "
|
System.out.println(now() + getName() + ": Sending "
|
||||||
+ response);
|
+ response);
|
||||||
pw.print(response);
|
ccos.write(response.toString().getBytes(UTF_8));
|
||||||
pw.flush();
|
ccos.flush();
|
||||||
|
System.out.println(now() + getName() + ": sent response headers");
|
||||||
ccos.write(b);
|
ccos.write(b);
|
||||||
ccos.flush();
|
ccos.flush();
|
||||||
ccos.close();
|
ccos.close();
|
||||||
|
System.out.println(now() + getName() + ": sent " + b.length + " body bytes");
|
||||||
connections.remove(clientConnection);
|
connections.remove(clientConnection);
|
||||||
clientConnection.close();
|
clientConnection.close();
|
||||||
}
|
}
|
||||||
|
@ -201,7 +201,7 @@ public class EscapedOctetsInURI {
|
|||||||
http2TestServer.addHandler(new HttpASCIIUriStringHandler(), "/http2");
|
http2TestServer.addHandler(new HttpASCIIUriStringHandler(), "/http2");
|
||||||
http2URI = "http://" + http2TestServer.serverAuthority() + "/http2";
|
http2URI = "http://" + http2TestServer.serverAuthority() + "/http2";
|
||||||
|
|
||||||
https2TestServer = new Http2TestServer("localhost", true, 0);
|
https2TestServer = new Http2TestServer("localhost", true, sslContext);
|
||||||
https2TestServer.addHandler(new HttpASCIIUriStringHandler(), "/https2");
|
https2TestServer.addHandler(new HttpASCIIUriStringHandler(), "/https2");
|
||||||
https2URI = "https://" + https2TestServer.serverAuthority() + "/https2";
|
https2URI = "https://" + https2TestServer.serverAuthority() + "/https2";
|
||||||
|
|
||||||
|
@ -359,7 +359,7 @@ public class FlowAdapterPublisherTest {
|
|||||||
http2TestServer.addHandler(new Http2EchoHandler(), "/http2/echo");
|
http2TestServer.addHandler(new Http2EchoHandler(), "/http2/echo");
|
||||||
http2URI = "http://" + http2TestServer.serverAuthority() + "/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");
|
https2TestServer.addHandler(new Http2EchoHandler(), "/https2/echo");
|
||||||
https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/echo";
|
https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/echo";
|
||||||
|
|
||||||
|
@ -534,7 +534,7 @@ public class FlowAdapterSubscriberTest {
|
|||||||
http2TestServer.addHandler(new Http2EchoHandler(), "/http2/echo");
|
http2TestServer.addHandler(new Http2EchoHandler(), "/http2/echo");
|
||||||
http2URI = "http://" + http2TestServer.serverAuthority() + "/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");
|
https2TestServer.addHandler(new Http2EchoHandler(), "/https2/echo");
|
||||||
https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/echo";
|
https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/echo";
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
|
|
||||||
import javax.net.ServerSocketFactory;
|
import javax.net.ServerSocketFactory;
|
||||||
import javax.net.ssl.SSLContext;
|
import javax.net.ssl.SSLContext;
|
||||||
|
import javax.net.ssl.SSLParameters;
|
||||||
import javax.net.ssl.SSLHandshakeException;
|
import javax.net.ssl.SSLHandshakeException;
|
||||||
import javax.net.ssl.SSLSocket;
|
import javax.net.ssl.SSLSocket;
|
||||||
import java.io.DataInputStream;
|
import java.io.DataInputStream;
|
||||||
@ -45,7 +46,7 @@ import static java.net.http.HttpResponse.BodyHandlers.discarding;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @test
|
* @test
|
||||||
* @run main/othervm HandshakeFailureTest
|
* @run main/othervm -Djdk.internal.httpclient.debug=true HandshakeFailureTest
|
||||||
* @summary Verify SSLHandshakeException is received when the handshake fails,
|
* @summary Verify SSLHandshakeException is received when the handshake fails,
|
||||||
* either because the server closes ( EOF ) the connection during handshaking
|
* either because the server closes ( EOF ) the connection during handshaking
|
||||||
* or no cipher suite ( or similar ) can be negotiated.
|
* or no cipher suite ( or similar ) can be negotiated.
|
||||||
@ -80,9 +81,17 @@ public class HandshakeFailureTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
void testSyncSameClient(URI uri, Version version) throws Exception {
|
||||||
out.printf("%n--- testSyncSameClient %s ---%n", version);
|
out.printf("%n--- testSyncSameClient %s ---%n", version);
|
||||||
HttpClient client = HttpClient.newHttpClient();
|
HttpClient client = getClient();
|
||||||
for (int i = 0; i < TIMES; i++) {
|
for (int i = 0; i < TIMES; i++) {
|
||||||
out.printf("iteration %d%n", i);
|
out.printf("iteration %d%n", i);
|
||||||
HttpRequest request = HttpRequest.newBuilder(uri)
|
HttpRequest request = HttpRequest.newBuilder(uri)
|
||||||
@ -92,8 +101,9 @@ public class HandshakeFailureTest {
|
|||||||
HttpResponse<Void> response = client.send(request, discarding());
|
HttpResponse<Void> response = client.send(request, discarding());
|
||||||
String msg = String.format("UNEXPECTED response=%s%n", response);
|
String msg = String.format("UNEXPECTED response=%s%n", response);
|
||||||
throw new RuntimeException(msg);
|
throw new RuntimeException(msg);
|
||||||
} catch (SSLHandshakeException expected) {
|
} catch (IOException expected) {
|
||||||
out.printf("Client: caught expected exception: %s%n", expected);
|
out.printf("Client: caught expected exception: %s%n", expected);
|
||||||
|
checkExceptionOrCause(SSLHandshakeException.class, expected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -103,7 +113,7 @@ public class HandshakeFailureTest {
|
|||||||
for (int i = 0; i < TIMES; i++) {
|
for (int i = 0; i < TIMES; i++) {
|
||||||
out.printf("iteration %d%n", i);
|
out.printf("iteration %d%n", i);
|
||||||
// a new client each time
|
// a new client each time
|
||||||
HttpClient client = HttpClient.newHttpClient();
|
HttpClient client = getClient();
|
||||||
HttpRequest request = HttpRequest.newBuilder(uri)
|
HttpRequest request = HttpRequest.newBuilder(uri)
|
||||||
.version(version)
|
.version(version)
|
||||||
.build();
|
.build();
|
||||||
@ -111,15 +121,16 @@ public class HandshakeFailureTest {
|
|||||||
HttpResponse<Void> response = client.send(request, discarding());
|
HttpResponse<Void> response = client.send(request, discarding());
|
||||||
String msg = String.format("UNEXPECTED response=%s%n", response);
|
String msg = String.format("UNEXPECTED response=%s%n", response);
|
||||||
throw new RuntimeException(msg);
|
throw new RuntimeException(msg);
|
||||||
} catch (SSLHandshakeException expected) {
|
} catch (IOException expected) {
|
||||||
out.printf("Client: caught expected exception: %s%n", expected);
|
out.printf("Client: caught expected exception: %s%n", expected);
|
||||||
|
checkExceptionOrCause(SSLHandshakeException.class, expected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void testAsyncSameClient(URI uri, Version version) throws Exception {
|
void testAsyncSameClient(URI uri, Version version) throws Exception {
|
||||||
out.printf("%n--- testAsyncSameClient %s ---%n", version);
|
out.printf("%n--- testAsyncSameClient %s ---%n", version);
|
||||||
HttpClient client = HttpClient.newHttpClient();
|
HttpClient client = getClient();
|
||||||
for (int i = 0; i < TIMES; i++) {
|
for (int i = 0; i < TIMES; i++) {
|
||||||
out.printf("iteration %d%n", i);
|
out.printf("iteration %d%n", i);
|
||||||
HttpRequest request = HttpRequest.newBuilder(uri)
|
HttpRequest request = HttpRequest.newBuilder(uri)
|
||||||
@ -132,12 +143,9 @@ public class HandshakeFailureTest {
|
|||||||
String msg = String.format("UNEXPECTED response=%s%n", response);
|
String msg = String.format("UNEXPECTED response=%s%n", response);
|
||||||
throw new RuntimeException(msg);
|
throw new RuntimeException(msg);
|
||||||
} catch (CompletionException ce) {
|
} catch (CompletionException ce) {
|
||||||
if (ce.getCause() instanceof SSLHandshakeException) {
|
Throwable expected = ce.getCause();
|
||||||
out.printf("Client: caught expected exception: %s%n", ce.getCause());
|
out.printf("Client: caught expected exception: %s%n", expected);
|
||||||
} else {
|
checkExceptionOrCause(SSLHandshakeException.class, expected);
|
||||||
out.printf("Client: caught UNEXPECTED exception: %s%n", ce.getCause());
|
|
||||||
throw ce;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -147,7 +155,7 @@ public class HandshakeFailureTest {
|
|||||||
for (int i = 0; i < TIMES; i++) {
|
for (int i = 0; i < TIMES; i++) {
|
||||||
out.printf("iteration %d%n", i);
|
out.printf("iteration %d%n", i);
|
||||||
// a new client each time
|
// a new client each time
|
||||||
HttpClient client = HttpClient.newHttpClient();
|
HttpClient client = getClient();
|
||||||
HttpRequest request = HttpRequest.newBuilder(uri)
|
HttpRequest request = HttpRequest.newBuilder(uri)
|
||||||
.version(version)
|
.version(version)
|
||||||
.build();
|
.build();
|
||||||
@ -158,14 +166,24 @@ public class HandshakeFailureTest {
|
|||||||
String msg = String.format("UNEXPECTED response=%s%n", response);
|
String msg = String.format("UNEXPECTED response=%s%n", response);
|
||||||
throw new RuntimeException(msg);
|
throw new RuntimeException(msg);
|
||||||
} catch (CompletionException ce) {
|
} catch (CompletionException ce) {
|
||||||
if (ce.getCause() instanceof SSLHandshakeException) {
|
ce.printStackTrace(out);
|
||||||
out.printf("Client: caught expected exception: %s%n", ce.getCause());
|
Throwable expected = ce.getCause();
|
||||||
} else {
|
out.printf("Client: caught expected exception: %s%n", expected);
|
||||||
out.printf("Client: caught UNEXPECTED exception: %s%n", ce.getCause());
|
checkExceptionOrCause(SSLHandshakeException.class, expected);
|
||||||
throw ce;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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. */
|
/** Common supertype for PlainServer and SSLServer. */
|
||||||
|
234
test/jdk/java/net/httpclient/HeadTest.java
Normal file
234
test/jdk/java/net/httpclient/HeadTest.java
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -32,6 +32,7 @@ import java.util.HashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.function.BiPredicate;
|
||||||
import static java.net.http.HttpClient.Builder.NO_PROXY;
|
import static java.net.http.HttpClient.Builder.NO_PROXY;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -41,20 +42,11 @@ import static java.net.http.HttpClient.Builder.NO_PROXY;
|
|||||||
*/
|
*/
|
||||||
public class HeadersTest {
|
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 URI TEST_URI = URI.create("http://www.foo.com/");
|
||||||
static final HttpClient client = HttpClient.newBuilder().proxy(NO_PROXY).build();
|
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 {
|
static void bad(String name) throws Exception {
|
||||||
HttpRequest.Builder builder = HttpRequest.newBuilder(TEST_URI);
|
HttpRequest.Builder builder = HttpRequest.newBuilder(TEST_URI);
|
||||||
try {
|
try {
|
||||||
@ -85,7 +77,7 @@ public class HeadersTest {
|
|||||||
}
|
}
|
||||||
@Override public HttpHeaders headers() {
|
@Override public HttpHeaders headers() {
|
||||||
Map<String, List<String>> map = Map.of(name, List.of("foo"));
|
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());
|
client.send(req, HttpResponse.BodyHandlers.ofString());
|
||||||
@ -127,7 +119,7 @@ public class HeadersTest {
|
|||||||
}
|
}
|
||||||
@Override public HttpHeaders headers() {
|
@Override public HttpHeaders headers() {
|
||||||
Map<String, List<String>> map = Map.of("x-bad", List.of(value));
|
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());
|
client.send(req, HttpResponse.BodyHandlers.ofString());
|
||||||
@ -170,7 +162,7 @@ public class HeadersTest {
|
|||||||
@Override public HttpHeaders headers() {
|
@Override public HttpHeaders headers() {
|
||||||
Map<String, List<String>> map = new HashMap<>();
|
Map<String, List<String>> map = new HashMap<>();
|
||||||
map.put(null, List.of("foo"));
|
map.put(null, List.of("foo"));
|
||||||
return new HttpHeadersStub(map);
|
return HttpHeaders.of(map, ACCEPT_ALL);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
client.send(req, HttpResponse.BodyHandlers.ofString());
|
client.send(req, HttpResponse.BodyHandlers.ofString());
|
||||||
@ -211,7 +203,7 @@ public class HeadersTest {
|
|||||||
@Override public HttpHeaders headers() {
|
@Override public HttpHeaders headers() {
|
||||||
Map<String, List<String>> map = new HashMap<>();
|
Map<String, List<String>> map = new HashMap<>();
|
||||||
map.put("x-bar", null);
|
map.put("x-bar", null);
|
||||||
return new HttpHeadersStub(map);
|
return HttpHeaders.of(map, ACCEPT_ALL);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
client.send(req, HttpResponse.BodyHandlers.ofString());
|
client.send(req, HttpResponse.BodyHandlers.ofString());
|
||||||
@ -243,11 +235,10 @@ public class HeadersTest {
|
|||||||
List<String> values = new ArrayList<>();
|
List<String> values = new ArrayList<>();
|
||||||
values.add("foo");
|
values.add("foo");
|
||||||
values.add(null);
|
values.add(null);
|
||||||
return new HttpHeadersStub(Map.of("x-bar", values));
|
return HttpHeaders.of(Map.of("x-bar", values), ACCEPT_ALL);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
client
|
client.send(req, HttpResponse.BodyHandlers.ofString());
|
||||||
.send(req, HttpResponse.BodyHandlers.ofString());
|
|
||||||
throw new RuntimeException("Expected NPE for null header value");
|
throw new RuntimeException("Expected NPE for null header value");
|
||||||
} catch (NullPointerException expected) {
|
} catch (NullPointerException expected) {
|
||||||
System.out.println("Got expected NPE: " + expected);
|
System.out.println("Got expected NPE: " + expected);
|
||||||
@ -276,7 +267,7 @@ public class HeadersTest {
|
|||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
@Override public HttpHeaders headers() {
|
@Override public HttpHeaders headers() {
|
||||||
return new HttpHeadersStub(null);
|
return HttpHeaders.of(null, ACCEPT_ALL);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
client.send(req, HttpResponse.BodyHandlers.ofString());
|
client.send(req, HttpResponse.BodyHandlers.ofString());
|
||||||
@ -370,7 +361,7 @@ public class HeadersTest {
|
|||||||
}
|
}
|
||||||
@Override public HttpHeaders headers() {
|
@Override public HttpHeaders headers() {
|
||||||
Map<String, List<String>> map = Map.of("x-good", List.of("foo"));
|
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());
|
client.send(req, HttpResponse.BodyHandlers.ofString());
|
||||||
@ -416,7 +407,7 @@ public class HeadersTest {
|
|||||||
}
|
}
|
||||||
@Override public HttpHeaders headers() {
|
@Override public HttpHeaders headers() {
|
||||||
Map<String, List<String>> map = Map.of("x-good", List.of("foo"));
|
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());
|
client.send(req, HttpResponse.BodyHandlers.ofString());
|
||||||
@ -472,7 +463,7 @@ public class HeadersTest {
|
|||||||
@Override
|
@Override
|
||||||
public HttpHeaders headers() {
|
public HttpHeaders headers() {
|
||||||
Map<String, List<String>> map = Map.of("x-good", List.of("foo"));
|
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());
|
client.send(req, HttpResponse.BodyHandlers.ofString());
|
||||||
@ -526,7 +517,7 @@ public class HeadersTest {
|
|||||||
@Override
|
@Override
|
||||||
public HttpHeaders headers() {
|
public HttpHeaders headers() {
|
||||||
Map<String, List<String>> map = Map.of("x-good", List.of("foo"));
|
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());
|
client.send(req, HttpResponse.BodyHandlers.ofString());
|
||||||
@ -538,12 +529,10 @@ public class HeadersTest {
|
|||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
public static void main(String[] args) throws Exception {
|
||||||
bad("bad:header");
|
bad("bad:header");
|
||||||
bad("Foo\n");
|
|
||||||
good("X-Foo!");
|
good("X-Foo!");
|
||||||
good("Bar~");
|
good("Bar~");
|
||||||
good("x");
|
good("x");
|
||||||
bad(" ");
|
bad(" ");
|
||||||
bad("Bar\r\n");
|
|
||||||
good("Hello#world");
|
good("Hello#world");
|
||||||
good("Qwer#ert");
|
good("Qwer#ert");
|
||||||
badValue("blah\r\n blah");
|
badValue("blah\r\n blah");
|
||||||
|
@ -275,16 +275,7 @@ public class HttpClientBuilderTest {
|
|||||||
@Override public boolean expectContinue() { return false; }
|
@Override public boolean expectContinue() { return false; }
|
||||||
@Override public URI uri() { return URI.create("http://foo.com/"); }
|
@Override public URI uri() { return URI.create("http://foo.com/"); }
|
||||||
@Override public Optional<Version> version() { return Optional.empty(); }
|
@Override public Optional<Version> version() { return Optional.empty(); }
|
||||||
private final FixedHttpHeaders headers = new FixedHttpHeaders();
|
@Override public HttpHeaders headers() { return HttpHeaders.of(Map.of(), (x, y) -> true); }
|
||||||
@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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---
|
// ---
|
||||||
|
422
test/jdk/java/net/httpclient/HttpHeadersOf.java
Normal file
422
test/jdk/java/net/httpclient/HttpHeadersOf.java
Normal file
@ -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());
|
||||||
|
}
|
||||||
|
}
|
@ -47,7 +47,7 @@ import static java.lang.System.err;
|
|||||||
/*
|
/*
|
||||||
* @test
|
* @test
|
||||||
* @summary An example on how to read a response body with InputStream.
|
* @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
|
* @author daniel fuchs
|
||||||
*/
|
*/
|
||||||
public class HttpInputStreamTest {
|
public class HttpInputStreamTest {
|
||||||
|
@ -27,11 +27,11 @@ import com.sun.net.httpserver.HttpContext;
|
|||||||
import com.sun.net.httpserver.HttpExchange;
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
import com.sun.net.httpserver.HttpHandler;
|
import com.sun.net.httpserver.HttpHandler;
|
||||||
import com.sun.net.httpserver.HttpServer;
|
import com.sun.net.httpserver.HttpServer;
|
||||||
|
import jdk.internal.net.http.common.HttpHeadersBuilder;
|
||||||
|
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.net.http.HttpClient.Version;
|
import java.net.http.HttpClient.Version;
|
||||||
import jdk.internal.net.http.common.HttpHeadersImpl;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
@ -41,6 +41,7 @@ import java.io.UncheckedIOException;
|
|||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpHeaders;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.ListIterator;
|
import java.util.ListIterator;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@ -95,26 +96,26 @@ public interface HttpServerAdapters {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 Optional<String> firstValue(String name);
|
||||||
public abstract void addHeader(String name, String value);
|
|
||||||
public abstract Set<String> keySet();
|
public abstract Set<String> keySet();
|
||||||
public abstract Set<Map.Entry<String, List<String>>> entrySet();
|
public abstract Set<Map.Entry<String, List<String>>> entrySet();
|
||||||
public abstract List<String> get(String name);
|
public abstract List<String> get(String name);
|
||||||
public abstract boolean containsKey(String name);
|
public abstract boolean containsKey(String name);
|
||||||
|
|
||||||
public static HttpTestHeaders of(Headers headers) {
|
public static HttpTestRequestHeaders of(Headers headers) {
|
||||||
return new Http1TestHeaders(headers);
|
return new Http1TestRequestHeaders(headers);
|
||||||
}
|
|
||||||
public static HttpTestHeaders of(HttpHeadersImpl headers) {
|
|
||||||
return new Http2TestHeaders(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;
|
private final Headers headers;
|
||||||
Http1TestHeaders(Headers h) { this.headers = h; }
|
Http1TestRequestHeaders(Headers h) { this.headers = h; }
|
||||||
@Override
|
@Override
|
||||||
public Optional<String> firstValue(String name) {
|
public Optional<String> firstValue(String name) {
|
||||||
if (headers.containsKey(name)) {
|
if (headers.containsKey(name)) {
|
||||||
@ -122,11 +123,6 @@ public interface HttpServerAdapters {
|
|||||||
}
|
}
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
@Override
|
|
||||||
public void addHeader(String name, String value) {
|
|
||||||
headers.add(name, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Set<String> keySet() { return headers.keySet(); }
|
public Set<String> keySet() { return headers.keySet(); }
|
||||||
@Override
|
@Override
|
||||||
@ -142,17 +138,14 @@ public interface HttpServerAdapters {
|
|||||||
return headers.containsKey(name);
|
return headers.containsKey(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private final static class Http2TestHeaders extends HttpTestHeaders {
|
private static final class Http2TestRequestHeaders extends HttpTestRequestHeaders {
|
||||||
private final HttpHeadersImpl headers;
|
private final HttpHeaders headers;
|
||||||
Http2TestHeaders(HttpHeadersImpl h) { this.headers = h; }
|
Http2TestRequestHeaders(HttpHeaders h) { this.headers = h; }
|
||||||
@Override
|
@Override
|
||||||
public Optional<String> firstValue(String name) {
|
public Optional<String> firstValue(String name) {
|
||||||
return headers.firstValue(name);
|
return headers.firstValue(name);
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
public void addHeader(String name, String value) {
|
|
||||||
headers.addHeader(name, value);
|
|
||||||
}
|
|
||||||
public Set<String> keySet() { return headers.map().keySet(); }
|
public Set<String> keySet() { return headers.map().keySet(); }
|
||||||
@Override
|
@Override
|
||||||
public Set<Map.Entry<String, List<String>>> entrySet() {
|
public Set<Map.Entry<String, List<String>>> entrySet() {
|
||||||
@ -169,6 +162,37 @@ public interface HttpServerAdapters {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
* A version agnostic adapter class for HTTP Server Exchange.
|
||||||
*/
|
*/
|
||||||
@ -177,17 +201,17 @@ public interface HttpServerAdapters {
|
|||||||
public abstract Version getExchangeVersion();
|
public abstract Version getExchangeVersion();
|
||||||
public abstract InputStream getRequestBody();
|
public abstract InputStream getRequestBody();
|
||||||
public abstract OutputStream getResponseBody();
|
public abstract OutputStream getResponseBody();
|
||||||
public abstract HttpTestHeaders getRequestHeaders();
|
public abstract HttpTestRequestHeaders getRequestHeaders();
|
||||||
public abstract HttpTestHeaders getResponseHeaders();
|
public abstract HttpTestResponseHeaders getResponseHeaders();
|
||||||
public abstract void sendResponseHeaders(int code, int contentLength) throws IOException;
|
public abstract void sendResponseHeaders(int code, int contentLength) throws IOException;
|
||||||
public abstract URI getRequestURI();
|
public abstract URI getRequestURI();
|
||||||
public abstract String getRequestMethod();
|
public abstract String getRequestMethod();
|
||||||
public abstract void close();
|
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);
|
ByteArrayInputStream bais = new ByteArrayInputStream(body);
|
||||||
serverPush(uri, headers, bais);
|
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());
|
throw new UnsupportedOperationException("serverPush with " + getExchangeVersion());
|
||||||
}
|
}
|
||||||
public boolean serverPushAllowed() {
|
public boolean serverPushAllowed() {
|
||||||
@ -221,12 +245,12 @@ public interface HttpServerAdapters {
|
|||||||
return exchange.getResponseBody();
|
return exchange.getResponseBody();
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
public HttpTestHeaders getRequestHeaders() {
|
public HttpTestRequestHeaders getRequestHeaders() {
|
||||||
return HttpTestHeaders.of(exchange.getRequestHeaders());
|
return HttpTestRequestHeaders.of(exchange.getRequestHeaders());
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
public HttpTestHeaders getResponseHeaders() {
|
public HttpTestResponseHeaders getResponseHeaders() {
|
||||||
return HttpTestHeaders.of(exchange.getResponseHeaders());
|
return HttpTestResponseHeaders.of(exchange.getResponseHeaders());
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
public void sendResponseHeaders(int code, int contentLength) throws IOException {
|
public void sendResponseHeaders(int code, int contentLength) throws IOException {
|
||||||
@ -268,12 +292,13 @@ public interface HttpServerAdapters {
|
|||||||
return exchange.getResponseBody();
|
return exchange.getResponseBody();
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
public HttpTestHeaders getRequestHeaders() {
|
public HttpTestRequestHeaders getRequestHeaders() {
|
||||||
return HttpTestHeaders.of(exchange.getRequestHeaders());
|
return HttpTestRequestHeaders.of(exchange.getRequestHeaders());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HttpTestHeaders getResponseHeaders() {
|
public HttpTestResponseHeaders getResponseHeaders() {
|
||||||
return HttpTestHeaders.of(exchange.getResponseHeaders());
|
return HttpTestResponseHeaders.of(exchange.getResponseHeaders());
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
public void sendResponseHeaders(int code, int contentLength) throws IOException {
|
public void sendResponseHeaders(int code, int contentLength) throws IOException {
|
||||||
@ -286,20 +311,8 @@ public interface HttpServerAdapters {
|
|||||||
return exchange.serverPushAllowed();
|
return exchange.serverPushAllowed();
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
public void serverPush(URI uri, HttpTestHeaders headers, InputStream body) {
|
public void serverPush(URI uri, HttpHeaders headers, InputStream body) {
|
||||||
HttpHeadersImpl headersImpl;
|
exchange.serverPush(uri, headers, body);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
void doFilter(Filter.Chain filter) throws IOException {
|
void doFilter(Filter.Chain filter) throws IOException {
|
||||||
throw new IOException("cannot use HTTP/1.1 filter with HTTP/2 server");
|
throw new IOException("cannot use HTTP/1.1 filter with HTTP/2 server");
|
||||||
@ -363,7 +376,7 @@ public interface HttpServerAdapters {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static boolean expectException(HttpTestExchange e) {
|
public static boolean expectException(HttpTestExchange e) {
|
||||||
HttpTestHeaders h = e.getRequestHeaders();
|
HttpTestRequestHeaders h = e.getRequestHeaders();
|
||||||
Optional<String> expectException = h.firstValue("X-expect-exception");
|
Optional<String> expectException = h.firstValue("X-expect-exception");
|
||||||
if (expectException.isPresent()) {
|
if (expectException.isPresent()) {
|
||||||
return expectException.get().equalsIgnoreCase("true");
|
return expectException.get().equalsIgnoreCase("true");
|
||||||
|
@ -206,7 +206,7 @@ public class ImmutableFlowItems {
|
|||||||
http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed";
|
http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed";
|
||||||
http2URI_chunk = "http://" + http2TestServer.serverAuthority() + "/http2/chunk";
|
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_fixedLengthHandler, "/https2/fixed");
|
||||||
https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk");
|
https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk");
|
||||||
https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed";
|
https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed";
|
||||||
|
@ -472,7 +472,7 @@ public class InvalidInputStreamSubscriptionRequest implements HttpServerAdapters
|
|||||||
http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed";
|
http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed";
|
||||||
http2URI_chunk = "http://" + http2TestServer.serverAuthority() + "/http2/chunk";
|
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_fixedLengthHandler, "/https2/fixed");
|
||||||
https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk");
|
https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk");
|
||||||
https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed";
|
https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed";
|
||||||
|
@ -53,6 +53,8 @@ import org.testng.annotations.AfterTest;
|
|||||||
import org.testng.annotations.BeforeTest;
|
import org.testng.annotations.BeforeTest;
|
||||||
import org.testng.annotations.DataProvider;
|
import org.testng.annotations.DataProvider;
|
||||||
import org.testng.annotations.Test;
|
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_1_1;
|
||||||
import static java.net.http.HttpClient.Version.HTTP_2;
|
import static java.net.http.HttpClient.Version.HTTP_2;
|
||||||
|
|
||||||
@ -75,6 +77,7 @@ public class InvalidSSLContextTest {
|
|||||||
public void testSync(Version version) throws Exception {
|
public void testSync(Version version) throws Exception {
|
||||||
// client-side uses a different context to that of the server-side
|
// client-side uses a different context to that of the server-side
|
||||||
HttpClient client = HttpClient.newBuilder()
|
HttpClient client = HttpClient.newBuilder()
|
||||||
|
.proxy(NO_PROXY)
|
||||||
.sslContext(SSLContext.getDefault())
|
.sslContext(SSLContext.getDefault())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@ -85,8 +88,9 @@ public class InvalidSSLContextTest {
|
|||||||
try {
|
try {
|
||||||
HttpResponse<?> response = client.send(request, BodyHandlers.discarding());
|
HttpResponse<?> response = client.send(request, BodyHandlers.discarding());
|
||||||
Assert.fail("UNEXPECTED response" + response);
|
Assert.fail("UNEXPECTED response" + response);
|
||||||
} catch (SSLException sslex) {
|
} catch (IOException ex) {
|
||||||
System.out.println("Caught expected: " + sslex);
|
System.out.println("Caught expected: " + ex);
|
||||||
|
assertExceptionOrCause(SSLException.class, ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,6 +98,7 @@ public class InvalidSSLContextTest {
|
|||||||
public void testAsync(Version version) throws Exception {
|
public void testAsync(Version version) throws Exception {
|
||||||
// client-side uses a different context to that of the server-side
|
// client-side uses a different context to that of the server-side
|
||||||
HttpClient client = HttpClient.newBuilder()
|
HttpClient client = HttpClient.newBuilder()
|
||||||
|
.proxy(NO_PROXY)
|
||||||
.sslContext(SSLContext.getDefault())
|
.sslContext(SSLContext.getDefault())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@ -117,21 +122,26 @@ public class InvalidSSLContextTest {
|
|||||||
if (cause == null) {
|
if (cause == null) {
|
||||||
Assert.fail("Unexpected null cause: " + error);
|
Assert.fail("Unexpected null cause: " + error);
|
||||||
}
|
}
|
||||||
assertException(clazz, cause);
|
assertExceptionOrCause(clazz, cause);
|
||||||
} else {
|
} else {
|
||||||
assertException(clazz, error);
|
assertExceptionOrCause(clazz, error);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}).join();
|
}).join();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void assertException(Class<? extends Throwable> clazz, Throwable t) {
|
static void assertExceptionOrCause(Class<? extends Throwable> clazz, Throwable t) {
|
||||||
if (t == null) {
|
if (t == null) {
|
||||||
Assert.fail("Expected " + clazz + ", caught nothing");
|
Assert.fail("Expected " + clazz + ", caught nothing");
|
||||||
}
|
}
|
||||||
if (!clazz.isInstance(t)) {
|
final Throwable original = t;
|
||||||
Assert.fail("Expected " + clazz + ", caught " + t);
|
do {
|
||||||
|
if (clazz.isInstance(t)) {
|
||||||
|
return; // found
|
||||||
}
|
}
|
||||||
|
} while ((t = t.getCause()) != null);
|
||||||
|
original.printStackTrace(System.out);
|
||||||
|
Assert.fail("Expected " + clazz + "in " + original);
|
||||||
}
|
}
|
||||||
|
|
||||||
@BeforeTest
|
@BeforeTest
|
||||||
@ -163,7 +173,7 @@ public class InvalidSSLContextTest {
|
|||||||
s.startHandshake();
|
s.startHandshake();
|
||||||
s.close();
|
s.close();
|
||||||
Assert.fail("SERVER: UNEXPECTED ");
|
Assert.fail("SERVER: UNEXPECTED ");
|
||||||
} catch (SSLHandshakeException he) {
|
} catch (SSLException he) {
|
||||||
System.out.println("SERVER: caught expected " + he);
|
System.out.println("SERVER: caught expected " + he);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
System.out.println("SERVER: caught: " + e);
|
System.out.println("SERVER: caught: " + e);
|
||||||
|
@ -404,7 +404,7 @@ public class InvalidSubscriptionRequest implements HttpServerAdapters {
|
|||||||
http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed";
|
http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed";
|
||||||
http2URI_chunk = "http://" + http2TestServer.serverAuthority() + "/http2/chunk";
|
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_fixedLengthHandler, "/https2/fixed");
|
||||||
https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk");
|
https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk");
|
||||||
https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed";
|
https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed";
|
||||||
|
@ -661,7 +661,7 @@ public class LineBodyHandlerTest implements HttpServerAdapters {
|
|||||||
http2TestServer.addHandler(new HttpTestEchoHandler(), "/http2/echo");
|
http2TestServer.addHandler(new HttpTestEchoHandler(), "/http2/echo");
|
||||||
http2URI = "http://" + http2TestServer.serverAuthority() + "/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");
|
https2TestServer.addHandler(new HttpTestEchoHandler(), "/https2/echo");
|
||||||
https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/echo";
|
https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/echo";
|
||||||
|
|
||||||
|
@ -88,7 +88,7 @@ public class MappingResponseSubscriber {
|
|||||||
String https2URI_fixed;
|
String https2URI_fixed;
|
||||||
String https2URI_chunk;
|
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
|
// a shared executor helps reduce the amount of threads created by the test
|
||||||
static final Executor executor = Executors.newCachedThreadPool();
|
static final Executor executor = Executors.newCachedThreadPool();
|
||||||
|
|
||||||
@ -224,7 +224,7 @@ public class MappingResponseSubscriber {
|
|||||||
http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed";
|
http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed";
|
||||||
http2URI_chunk = "http://" + http2TestServer.serverAuthority() + "/http2/chunk";
|
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_fixedLengthHandler, "/https2/fixed");
|
||||||
https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk");
|
https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk");
|
||||||
https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed";
|
https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed";
|
||||||
|
239
test/jdk/java/net/httpclient/MaxStreams.java
Normal file
239
test/jdk/java/net/httpclient/MaxStreams.java
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -21,8 +21,6 @@
|
|||||||
* questions.
|
* questions.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import jdk.internal.net.http.common.HttpHeadersImpl;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
import java.net.http.HttpHeaders;
|
import java.net.http.HttpHeaders;
|
||||||
@ -30,10 +28,10 @@ import java.net.http.HttpRequest;
|
|||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.http.HttpResponse;
|
import java.net.http.HttpResponse;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import static java.net.http.HttpClient.Builder.NO_PROXY;
|
import static java.net.http.HttpClient.Builder.NO_PROXY;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @test
|
* @test
|
||||||
* @bug 8199135
|
* @bug 8199135
|
||||||
@ -75,7 +73,7 @@ public class MethodsTest {
|
|||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
@Override public HttpHeaders headers() {
|
@Override public HttpHeaders headers() {
|
||||||
return new HttpHeadersImpl();
|
return HttpHeaders.of(Map.of(), (x, y) -> true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
client.send(req, HttpResponse.BodyHandlers.ofString());
|
client.send(req, HttpResponse.BodyHandlers.ofString());
|
||||||
|
@ -216,7 +216,7 @@ public class NonAsciiCharsInURI implements HttpServerAdapters {
|
|||||||
http2TestServer.addHandler(handler, "/http2");
|
http2TestServer.addHandler(handler, "/http2");
|
||||||
http2URI = "http://" + http2TestServer.serverAuthority() + "/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");
|
https2TestServer.addHandler(handler, "/https2");
|
||||||
https2URI = "https://" + https2TestServer.serverAuthority() + "/https2";
|
https2URI = "https://" + https2TestServer.serverAuthority() + "/https2";
|
||||||
|
|
||||||
|
@ -212,7 +212,7 @@ public class RedirectMethodChange implements HttpServerAdapters {
|
|||||||
http2TestServer.addHandler(handler, "/http2/");
|
http2TestServer.addHandler(handler, "/http2/");
|
||||||
http2URI = "http://" + http2TestServer.serverAuthority() + "/http2/test/rmt";
|
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";
|
targetURI = "https://" + https2TestServer.serverAuthority() + "/https2/redirect/rmt";
|
||||||
handler = new RedirMethodChgeHandler(targetURI);
|
handler = new RedirMethodChgeHandler(targetURI);
|
||||||
https2TestServer.addHandler(handler, "/https2/");
|
https2TestServer.addHandler(handler, "/https2/");
|
||||||
@ -283,14 +283,14 @@ public class RedirectMethodChange implements HttpServerAdapters {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (newtest) {
|
if (newtest) {
|
||||||
HttpTestHeaders hdrs = he.getRequestHeaders();
|
HttpTestRequestHeaders hdrs = he.getRequestHeaders();
|
||||||
String value = hdrs.firstValue("X-Redirect-Code").get();
|
String value = hdrs.firstValue("X-Redirect-Code").get();
|
||||||
int redirectCode = Integer.parseInt(value);
|
int redirectCode = Integer.parseInt(value);
|
||||||
expectedMethod = hdrs.firstValue("X-Expect-Method").get();
|
expectedMethod = hdrs.firstValue("X-Expect-Method").get();
|
||||||
if (!readAndCheckBody(he))
|
if (!readAndCheckBody(he))
|
||||||
return;
|
return;
|
||||||
hdrs = he.getResponseHeaders();
|
HttpTestResponseHeaders headersbuilder = he.getResponseHeaders();
|
||||||
hdrs.addHeader("Location", targetURL);
|
headersbuilder.addHeader("Location", targetURL);
|
||||||
he.sendResponseHeaders(redirectCode, 0);
|
he.sendResponseHeaders(redirectCode, 0);
|
||||||
inTest = true;
|
inTest = true;
|
||||||
} else {
|
} else {
|
||||||
|
@ -167,7 +167,7 @@ public class RedirectWithCookie implements HttpServerAdapters {
|
|||||||
http2TestServer = HttpTestServer.of(new Http2TestServer("localhost", false, 0));
|
http2TestServer = HttpTestServer.of(new Http2TestServer("localhost", false, 0));
|
||||||
http2TestServer.addHandler(new CookieRedirectHandler(), "/http2/cookie/");
|
http2TestServer.addHandler(new CookieRedirectHandler(), "/http2/cookie/");
|
||||||
http2URI = "http://" + http2TestServer.serverAuthority() + "/http2/cookie/redirect";
|
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/");
|
https2TestServer.addHandler(new CookieRedirectHandler(), "/https2/cookie/");
|
||||||
https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/cookie/redirect";
|
https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/cookie/redirect";
|
||||||
|
|
||||||
|
@ -209,6 +209,7 @@ public class RequestBuilderTest {
|
|||||||
for (HttpRequest r : requests) {
|
for (HttpRequest r : requests) {
|
||||||
assertEquals(r.headers().map().size(), 1);
|
assertEquals(r.headers().map().size(), 1);
|
||||||
assertTrue(r.headers().firstValue("A").isPresent());
|
assertTrue(r.headers().firstValue("A").isPresent());
|
||||||
|
assertTrue(r.headers().firstValue("a").isPresent());
|
||||||
assertEquals(r.headers().firstValue("A").get(), "B");
|
assertEquals(r.headers().firstValue("A").get(), "B");
|
||||||
assertEquals(r.headers().allValues("A"), List.of("B"));
|
assertEquals(r.headers().allValues("A"), List.of("B"));
|
||||||
assertEquals(r.headers().allValues("C").size(), 0);
|
assertEquals(r.headers().allValues("C").size(), 0);
|
||||||
@ -310,6 +311,30 @@ public class RequestBuilderTest {
|
|||||||
assertThrows(UOE, () -> r.headers().allValues("A").addAll(List.of("Z")));
|
assertThrows(UOE, () -> r.headers().allValues("A").addAll(List.of("Z")));
|
||||||
assertThrows(UOE, () -> r.headers().allValues("A").add(1, "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",
|
private static final Set<String> RESTRICTED = Set.of("connection", "content-length",
|
||||||
|
530
test/jdk/java/net/httpclient/ResponseBodyBeforeError.java
Normal file
530
test/jdk/java/net/httpclient/ResponseBodyBeforeError.java
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -426,7 +426,7 @@ public class ResponsePublisher implements HttpServerAdapters {
|
|||||||
http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed";
|
http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed";
|
||||||
http2URI_chunk = "http://" + http2TestServer.serverAuthority() + "/http2/chunk";
|
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_fixedLengthHandler, "/https2/fixed");
|
||||||
https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk");
|
https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk");
|
||||||
https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed";
|
https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed";
|
||||||
|
237
test/jdk/java/net/httpclient/RetryPost.java
Normal file
237
test/jdk/java/net/httpclient/RetryPost.java
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -165,7 +165,7 @@ public class RetryWithCookie implements HttpServerAdapters {
|
|||||||
http2TestServer = HttpTestServer.of(new Http2TestServer("localhost", false, 0));
|
http2TestServer = HttpTestServer.of(new Http2TestServer("localhost", false, 0));
|
||||||
http2TestServer.addHandler(new CookieRetryHandler(), "/http2/cookie/");
|
http2TestServer.addHandler(new CookieRetryHandler(), "/http2/cookie/");
|
||||||
http2URI = "http://" + http2TestServer.serverAuthority() + "/http2/cookie/retry";
|
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/");
|
https2TestServer.addHandler(new CookieRetryHandler(), "/https2/cookie/");
|
||||||
https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/cookie/retry";
|
https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/cookie/retry";
|
||||||
|
|
||||||
|
668
test/jdk/java/net/httpclient/ShortResponseBody.java
Normal file
668
test/jdk/java/net/httpclient/ShortResponseBody.java
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
34
test/jdk/java/net/httpclient/ShortResponseBodyWithRetry.java
Normal file
34
test/jdk/java/net/httpclient/ShortResponseBodyWithRetry.java
Normal file
@ -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
|
||||||
|
*/
|
287
test/jdk/java/net/httpclient/SpecialHeadersTest.java
Normal file
287
test/jdk/java/net/httpclient/SpecialHeadersTest.java
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -24,6 +24,11 @@
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.SocketException;
|
import java.net.SocketException;
|
||||||
import java.net.URI;
|
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 java.util.concurrent.CompletableFuture;
|
||||||
import javax.net.ssl.SSLContext;
|
import javax.net.ssl.SSLContext;
|
||||||
import javax.net.ServerSocketFactory;
|
import javax.net.ServerSocketFactory;
|
||||||
@ -32,6 +37,8 @@ import java.net.http.HttpClient;
|
|||||||
import java.net.http.HttpClient.Version;
|
import java.net.http.HttpClient.Version;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
import java.net.http.HttpResponse;
|
import java.net.http.HttpResponse;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import jdk.testlibrary.SimpleSSLContext;
|
import jdk.testlibrary.SimpleSSLContext;
|
||||||
import static java.lang.System.out;
|
import static java.lang.System.out;
|
||||||
import static java.lang.String.format;
|
import static java.lang.String.format;
|
||||||
@ -44,7 +51,10 @@ import static java.net.http.HttpResponse.BodyHandlers.ofString;
|
|||||||
* @library /lib/testlibrary
|
* @library /lib/testlibrary
|
||||||
* @build jdk.testlibrary.SimpleSSLContext
|
* @build jdk.testlibrary.SimpleSSLContext
|
||||||
* @build MockServer
|
* @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 @@ public class SplitResponse {
|
|||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum Protocol {
|
||||||
|
HTTP, HTTPS
|
||||||
|
}
|
||||||
|
enum Connection {
|
||||||
|
KEEP_ALIVE,
|
||||||
|
CLOSE
|
||||||
|
}
|
||||||
|
enum Mode {
|
||||||
|
SYNC, ASYNC
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
public static void main(String[] args) throws Exception {
|
||||||
boolean useSSL = false;
|
boolean useSSL = false;
|
||||||
if (args != null && args.length == 1) {
|
if (args != null && args.length >= 1) {
|
||||||
useSSL = "SSL".equals(args[0]);
|
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);
|
SplitResponse sp = new SplitResponse(useSSL);
|
||||||
|
|
||||||
for (Version version : Version.values()) {
|
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
|
// Note: the mock server doesn't support Keep-Alive, but
|
||||||
// pretending that it might exercises code paths in and out of
|
// pretending that it might exercises code paths in and out of
|
||||||
// the connection pool, and retry logic
|
// the connection pool, and retry logic
|
||||||
for (boolean async : new boolean[]{ true, false }) {
|
for (Mode mode : modes) {
|
||||||
sp.test(version, serverKeepalive, async);
|
sp.test(version,serverKeepalive == Connection.KEEP_ALIVE,mode == Mode.ASYNC);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
34
test/jdk/java/net/httpclient/SplitResponseAsync.java
Normal file
34
test/jdk/java/net/httpclient/SplitResponseAsync.java
Normal file
@ -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
|
||||||
|
*/
|
34
test/jdk/java/net/httpclient/SplitResponseKeepAlive.java
Normal file
34
test/jdk/java/net/httpclient/SplitResponseKeepAlive.java
Normal file
@ -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
|
||||||
|
*/
|
@ -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
|
||||||
|
*/
|
@ -30,10 +30,5 @@
|
|||||||
* @run main/othervm
|
* @run main/othervm
|
||||||
* -Djdk.internal.httpclient.debug=true
|
* -Djdk.internal.httpclient.debug=true
|
||||||
* -Djdk.httpclient.HttpClient.log=all
|
* -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user