Skip to content

Commit

Permalink
[java] A draft of a Netty-based HTTP client
Browse files Browse the repository at this point in the history
The default is still OkHttp. Pass -Dwebdriver.http.factory=netty to use the new one. Its stability is below my expectations yet. Everything works well if I run tests from IDEA in a single process, but there are random TimeoutException-s during a Bazel run.
  • Loading branch information
barancev committed Oct 7, 2019
1 parent b2e1adc commit 69f6699
Show file tree
Hide file tree
Showing 23 changed files with 451 additions and 0 deletions.
13 changes: 13 additions & 0 deletions .idea/libraries/async_http_client.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions .idea/libraries/netty_reactive_streams.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions .idea/libraries/reactive_streams.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions java/client/client.iml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
<orderEntry type="library" scope="TEST" name="littleproxy" level="project" />
<orderEntry type="library" name="mockito-core" level="project" />
<orderEntry type="library" name="netty" level="project" />
<orderEntry type="library" name="async-http-client" level="project" />
<orderEntry type="library" name="netty-reactive-streams" level="project" />
<orderEntry type="library" name="reactive-streams" level="project" />
<orderEntry type="library" name="objenesis" level="project" />
<orderEntry type="library" name="okhttp" level="project" />
<orderEntry type="library" name="opentracing" level="project" />
Expand Down
1 change: 1 addition & 0 deletions java/client/src/org/openqa/selenium/remote/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ java_export(
"//java/client/src/org/openqa/selenium/io",
"//java/client/src/org/openqa/selenium/os",
"//java/client/src/org/openqa/selenium/remote/http/okhttp",
"//java/client/src/org/openqa/selenium/remote/http/netty",
"//third_party/java/bytebuddy:byte-buddy",
"//third_party/java/guava",
],
Expand Down
10 changes: 10 additions & 0 deletions java/client/src/org/openqa/selenium/remote/http/HttpClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ interface Factory {
static Factory createDefault() {
String defaultFactory = System.getProperty("webdriver.http.factory", "okhttp");
switch (defaultFactory) {
case "netty":
try {
Class<? extends Factory> clazz =
Class.forName("org.openqa.selenium.remote.http.netty.NettyClient$Factory")
.asSubclass(Factory.class);
return clazz.getConstructor().newInstance();
} catch (ReflectiveOperationException e) {
throw new UnsupportedOperationException("Unable to create HTTP client factory", e);
}

case "okhttp":
default:
try {
Expand Down
15 changes: 15 additions & 0 deletions java/client/src/org/openqa/selenium/remote/http/netty/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
java_library(
name = "netty",
srcs = glob(["*.java"]),
visibility = [
"//java/client/src/org/openqa/selenium/remote:__pkg__",
"//java/client/test/org/openqa/selenium/remote/http/netty:__pkg__",
],
deps = [
"//java/client/src/org/openqa/selenium/remote/http",
"//third_party/java/guava",
"//third_party/java/netty:netty-all",
"//third_party/java/async-http-client",
"//third_party/java/reactive-streams",
],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package org.openqa.selenium.remote.http.netty;

import static org.asynchttpclient.Dsl.asyncHttpClient;

import org.asynchttpclient.AsyncHttpClient;
import org.openqa.selenium.remote.http.ClientConfig;

import java.util.Objects;
import java.util.function.Function;

class CreateNettyClient implements Function<ClientConfig, AsyncHttpClient> {

@Override
public AsyncHttpClient apply(ClientConfig config) {
Objects.requireNonNull(config, "Client config to use must be set.");

return asyncHttpClient();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package org.openqa.selenium.remote.http.netty;

import org.openqa.selenium.remote.http.ClientConfig;
import org.openqa.selenium.remote.http.Filter;
import org.openqa.selenium.remote.http.HttpClient;
import org.openqa.selenium.remote.http.HttpHandler;
import org.openqa.selenium.remote.http.HttpRequest;
import org.openqa.selenium.remote.http.HttpResponse;
import org.openqa.selenium.remote.http.WebSocket;

import java.io.UncheckedIOException;
import java.util.Objects;
import java.util.function.BiFunction;

public class NettyClient implements HttpClient {

private final HttpHandler handler;
private BiFunction<HttpRequest, WebSocket.Listener, WebSocket> toWebSocket;

private NettyClient(HttpHandler handler, BiFunction<HttpRequest, WebSocket.Listener, WebSocket> toWebSocket) {
this.handler = Objects.requireNonNull(handler);
this.toWebSocket = Objects.requireNonNull(toWebSocket);
}

@Override
public HttpResponse execute(HttpRequest request) throws UncheckedIOException {
return handler.execute(request);
}

@Override
public WebSocket openSocket(HttpRequest request, WebSocket.Listener listener) {
Objects.requireNonNull(request, "Request to send must be set.");
Objects.requireNonNull(listener, "WebSocket listener must be set.");

return toWebSocket.apply(request, listener);
}

@Override
public HttpClient with(Filter filter) {
Objects.requireNonNull(filter, "Filter to use must be set.");

// TODO: We should probably ensure that websocket requests are run through the filter.
return new NettyClient(handler.with(filter), toWebSocket);
}

public static class Factory implements HttpClient.Factory {

@Override
public HttpClient createClient(ClientConfig config) {
Objects.requireNonNull(config, "Client config to use must be set.");

return new NettyClient(new NettyHttpHandler(config).with(config.filter()), NettyWebSocket.create(config));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package org.openqa.selenium.remote.http.netty;

import org.asynchttpclient.AsyncHttpClient;
import org.asynchttpclient.Response;
import org.openqa.selenium.remote.http.ClientConfig;
import org.openqa.selenium.remote.http.HttpHandler;
import org.openqa.selenium.remote.http.HttpRequest;
import org.openqa.selenium.remote.http.HttpResponse;
import org.openqa.selenium.remote.http.RemoteCall;

import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

public class NettyHttpHandler extends RemoteCall {

private final AsyncHttpClient client;
private final HttpHandler handler;

public NettyHttpHandler(ClientConfig config) {
super(config);
this.client = new CreateNettyClient().apply(config);
this.handler = config.filter().andFinally(this::makeCall);
}

@Override
public HttpResponse execute(HttpRequest request) {
return handler.execute(request);
}

private HttpResponse makeCall(HttpRequest request) {
Objects.requireNonNull(request, "Request must be set.");

Future<Response> whenResponse = client.executeRequest(
NettyMessages.toNettyRequest(getConfig().baseUri(), request));

try {
Response response = whenResponse.get();
return NettyMessages.toSeleniumResponse(response);
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package org.openqa.selenium.remote.http.netty;

import static org.openqa.selenium.remote.http.Contents.empty;

import org.asynchttpclient.Request;
import org.asynchttpclient.RequestBuilder;
import org.asynchttpclient.Response;
import org.openqa.selenium.remote.http.Contents;
import org.openqa.selenium.remote.http.HttpMethod;
import org.openqa.selenium.remote.http.HttpRequest;
import org.openqa.selenium.remote.http.HttpResponse;

import java.net.URI;

import static org.asynchttpclient.Dsl.request;

class NettyMessages {

private NettyMessages() {
// Utility classes.
}

protected static Request toNettyRequest(URI baseUrl, HttpRequest request) {
String rawUrl;

if (request.getUri().startsWith("ws://")) {
rawUrl = "http://" + request.getUri().substring("ws://".length());
} else if (request.getUri().startsWith("wss://")) {
rawUrl = "https://" + request.getUri().substring("wss://".length());
} else if (request.getUri().startsWith("http://") || request.getUri().startsWith("https://")) {
rawUrl = request.getUri();
} else {
rawUrl = baseUrl.toString().replaceAll("/$", "") + request.getUri();
}

RequestBuilder builder = request(request.getMethod().toString(), rawUrl);

for (String name : request.getQueryParameterNames()) {
for (String value : request.getQueryParameters(name)) {
builder.addQueryParam(name, value);
}
}

for (String name : request.getHeaderNames()) {
for (String value : request.getHeaders(name)) {
builder.addHeader(name, value);
}
}

if (request.getMethod().equals(HttpMethod.POST)) {
builder.setBody(request.getContent().get());
}

return builder.build();
}

public static HttpResponse toSeleniumResponse(Response response) {
HttpResponse toReturn = new HttpResponse();

toReturn.setStatus(response.getStatusCode());

toReturn.setContent(! response.hasResponseBody()
? empty()
: Contents.memoize(response::getResponseBodyAsStream));

response.getHeaders().names().forEach(
name -> response.getHeaders(name).forEach(value -> toReturn.addHeader(name, value)));

return toReturn;
}
}
Loading

0 comments on commit 69f6699

Please sign in to comment.