Skip to content

Commit

Permalink
Fix quarkiverse#268: HMR support for web sockets
Browse files Browse the repository at this point in the history
  • Loading branch information
melloware committed Mar 11, 2023
1 parent 32459c6 commit a7f581d
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 8 deletions.
31 changes: 24 additions & 7 deletions docs/modules/ROOT/pages/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,29 @@ Edit the karma.conf.js:
},
----

[#nextjs]
=== Next.js
Any app created with Next.js (https://nextjs.org/) should work with Quinoa after the following changes:

In application.properties add:
[source,properties]
----
%dev.quarkus.quinoa.index-page=/
quarkus.quinoa.build-dir=out
----

In Dev mode Next.js serves everything out of root "/" but in PRD mode its the normal "/index.html".

Add these scripts to package.json
[source,json]
----
"scripts": {
...
"start": "next dev",
"build": "next build && next export",
}
----

[#vite]
=== Vite
Any app created with Vite (https://vitejs.dev/guide/) should work with Quinoa after the following changes:
Expand All @@ -312,16 +335,10 @@ To make hot module replacement work, add the following to the config object in v
----
server: {
port: 5173,
host: '127.0.0.1',
hmr: {
port: 5173,
host: '127.0.0.1',
}
host: '127.0.0.1'
}
----

You do need the host and port in both places to ensure the websocket is constructed and upgraded properly.

[#spa-routing]
=== Single Page application routing

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,25 @@
import static io.quarkiverse.quinoa.QuinoaRecorder.resolvePath;
import static io.quarkiverse.quinoa.QuinoaRecorder.shouldHandleMethod;

import java.util.ArrayList;
import java.util.List;

import org.jboss.logging.Logger;

import io.quarkus.runtime.util.StringUtil;
import io.vertx.core.AsyncResult;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.MultiMap;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpClient;
import io.vertx.core.http.HttpHeaders;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.core.http.ServerWebSocket;
import io.vertx.core.http.WebSocket;
import io.vertx.core.http.WebSocketConnectOptions;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.client.HttpResponse;
import io.vertx.ext.web.client.WebClient;
Expand All @@ -31,12 +38,14 @@ class QuinoaDevProxyHandler implements Handler<RoutingContext> {
HttpHeaders.CONTENT_TYPE.toString());
private final int port;
private final WebClient client;
private final HttpClient httpClient;
private final ClassLoader currentClassLoader;
private final QuinoaHandlerConfig config;

QuinoaDevProxyHandler(final QuinoaHandlerConfig config, final Vertx vertx, int port) {
this.port = port;
this.client = WebClient.create(vertx);
this.httpClient = vertx.createHttpClient();
this.config = config;
currentClassLoader = Thread.currentThread().getContextClassLoader();
}
Expand All @@ -58,8 +67,74 @@ public void handle(final RoutingContext ctx) {
next(currentClassLoader, ctx);
return;
}
final String uri = computeURI(resourcePath, request);

final MultiMap headers = request.headers();
final String uri = computeURI(resourcePath, request);

// WebSocket connection for Hot Module Reloading (HMR)
if (!config.prodMode && headers.get("Connection").contains("Upgrade")) {
// Vite uses the root URL "/" which is special
final String forwardUri = request.uri().equals("/") ? "/" : uri;
// some servers use sub-protocols like Vite which must be forwarded
final String subProtocol = headers.get("Sec-WebSocket-Protocol");
final List<String> subProtocols = new ArrayList<>(1);
MultiMap socketHeaders;
if (StringUtil.isNullOrEmpty(subProtocol)) {
socketHeaders = null;
} else {
socketHeaders = headers;
subProtocols.add(headers.get("Sec-WebSocket-Protocol"));
LOG.infof("Sec-WebSocket-Protocol: %s", subProtocol);
}
final String host = request.localAddress().host();
LOG.infof("Quinoa is forwarding web socket: '%s'", forwardUri);
final Future<ServerWebSocket> fut = request.toWebSocket();
fut.onSuccess(serverWs -> {
final WebSocketConnectOptions options = new WebSocketConnectOptions()
.setHost(host)
.setPort(port)
.setURI(forwardUri)
.setHeaders(socketHeaders)
.setSubProtocols(subProtocols)
.setAllowOriginHeader(false);

LOG.infof("Quinoa client websocket: %s:%s%s", host, port, forwardUri);
httpClient.webSocket(options, clientContext -> {
if (clientContext.succeeded()) {
final WebSocket clientWs = clientContext.result();

try {
// messages from browser forwarded to NodeJS
serverWs.textMessageHandler((msg) -> {
LOG.debugf("Server %s", msg);
clientWs.writeTextMessage(msg);
}).exceptionHandler((e) -> {
LOG.warnf("Server websocket closed with error: %s", e.getMessage());
}).closeHandler((__) -> {
LOG.warnf("Server websocket is closed");
});

// messages from NodeJS forwarded back to browser
clientWs.textMessageHandler((msg) -> {
LOG.debugf("Client %s", msg);
serverWs.writeTextMessage(msg);
}).exceptionHandler((e) -> {
LOG.warnf("Client websocket closed with error: %s", e.getMessage());
}).closeHandler((__) -> {
LOG.warnf("Client websocket is closed");
});
} catch (Exception ise) {
LOG.warnf("Server websocket is not ready: %s", ise.getMessage());
}
LOG.infof("Quinoa has forwarded web socket: '%s'", request.uri());
} else {
LOG.error("Connection Failed", clientContext.cause());
}
});
});
return;
}

// Workaround for issue https://github.com/quarkiverse/quarkus-quinoa/issues/91
// See https://www.npmjs.com/package/connect-history-api-fallback#htmlacceptheaders
// When no Accept header is provided, the historyApiFallback is disabled
Expand Down

0 comments on commit a7f581d

Please sign in to comment.