Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Smart bypass of the dev server SPA #593

Merged
merged 2 commits into from
Dec 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,19 @@ public interface DevServerConfig {
@ConfigDocDefault("auto-detected falling back to the quinoa.index-page")
Optional<String> indexPage();

/**
* Quinoa deals with SPA routing by itself (see quarkus.quinoa.enable-spa-routing), some dev-server have this feature
* enabled by default.
* This is a problem for proxying as it prevents other Quarkus resources (REST, ...) to answer.
* By default, Quinoa will try to detect when the dev server is answering with a html page for non-existing resources
* (SPA-Routing)
* in which case it will instead allow other Quarkus resources (REST, ...) to answer.
* Set this to true (direct) when the other Quarkus resources use a specific path prefix (and marked as ignored by Quinoa)
* or if the dev-server is configured without SPA routing.
*/
@WithDefault("false")
boolean directForwarding();

static boolean isEqual(DevServerConfig d1, DevServerConfig d2) {
if (!Objects.equals(d1.enabled(), d2.enabled())) {
return false;
Expand Down Expand Up @@ -102,6 +115,9 @@ static boolean isEqual(DevServerConfig d1, DevServerConfig d2) {
if (!Objects.equals(d1.indexPage(), d2.indexPage())) {
return false;
}
if (!Objects.equals(d1.directForwarding(), d2.directForwarding())) {
return false;
}
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ static QuinoaHandlerConfig toHandlerConfig(QuinoaConfig config, boolean prodMode
final String indexPage = !isDevServerMode(config) ? config.indexPage()
: config.devServer().indexPage().orElse(config.indexPage());
return new QuinoaHandlerConfig(getNormalizedIgnoredPathPrefixes(config), indexPage, prodMode,
httpBuildTimeConfig.enableCompression, compressMediaTypes);
httpBuildTimeConfig.enableCompression, compressMediaTypes, config.devServer().directForwarding());
}

private static Optional<String> readExternalConfigPath(Config config, String key) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,9 @@ public boolean logs() {
public Optional<String> indexPage() {
return delegate.indexPage();
}

@Override
public boolean directForwarding() {
return delegate.directForwarding();
}
}
17 changes: 17 additions & 0 deletions docs/modules/ROOT/pages/includes/quarkus-quinoa.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,23 @@ endif::add-copy-button-to-env-var[]
|`auto-detected falling back to the quinoa.index-page`


a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.dev-server.direct-forwarding]]`link:#quarkus-quinoa_quarkus.quinoa.dev-server.direct-forwarding[quarkus.quinoa.dev-server.direct-forwarding]`


[.description]
--
Quinoa deals with SPA routing by itself (see quarkus.quinoa.enable-spa-routing), some dev-server have this feature enabled by default. This is a problem for proxying as it prevents other Quarkus resources (REST, ...) to answer. By default, Quinoa will try to detect when the dev server is answering with a html page for non-existing resources (SPA-Routing) in which case it will instead allow other Quarkus resources (REST, ...) to answer. Set this to true (direct) when the other Quarkus resources use a specific path prefix (and marked as ignored by Quinoa) or if the dev-server is configured without SPA routing.

ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_DEV_SERVER_DIRECT_FORWARDING+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++QUARKUS_QUINOA_DEV_SERVER_DIRECT_FORWARDING+++`
endif::add-copy-button-to-env-var[]
--|boolean
|`false`


a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.package-manager-command.install-env-install-env]]`link:#quarkus-quinoa_quarkus.quinoa.package-manager-command.install-env-install-env[quarkus.quinoa.package-manager-command.install-env]`


Expand Down
15 changes: 9 additions & 6 deletions integration-tests/src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
%root-path.quarkus.http.root-path=/foo/bar
%root-path.quarkus.quinoa.enable-spa-routing=true
%lit-root-path.quarkus.http.root-path=/foo/bar
%lit-root-path.quarkus.quinoa.enable-spa-routing=true
%lit-root-path.quarkus.quinoa.package-manager-command.build-env.ROOT_PATH=/foo/bar/

%react.quarkus.quinoa.ui-dir=src/main/ui-react
%vue.quarkus.quinoa.ui-dir=src/main/ui-vue
Expand All @@ -11,12 +12,14 @@
%angular.quarkus.quinoa.package-manager-install.install-dir=target/
%angular.quarkus.quinoa.package-manager-install.node-version=20.9.0
%angular.quarkus.quinoa.package-manager-install.yarn-version=1.22.19
%lit.quarkus.quinoa.ui-dir=src/main/ui-lit
%lit.quarkus.quinoa.build-dir=dist
%lit.quarkus.quinoa.index-page=app.html
%lit.quarkus.quinoa.package-manager-command.build=run build-per-env
%lit,lit-root-path.quarkus.quinoa.ui-dir=src/main/ui-lit
%lit,lit-root-path.quarkus.quinoa.build-dir=dist
%lit,lit-root-path.quarkus.quinoa.index-page=app.html
%lit,lit-root-path.quarkus.quinoa.package-manager-command.build=run build-per-env
%lit-root-path.quarkus.quinoa.package-manager-command.build-env.FOO=bar
%lit.quarkus.quinoa.package-manager-command.build-env.FOO=bar
%lit.quarkus.quinoa.package-manager-command.build-env.ROOT_PATH=/

%yarn.quarkus.quinoa.package-manager=yarn
%just-build.quarkus.quinoa.just-build=true

Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.junit.jupiter.api.Test;

import com.microsoft.playwright.BrowserContext;
import com.microsoft.playwright.ElementHandle;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.Response;

Expand Down Expand Up @@ -39,6 +40,11 @@ public void testUIIndex() {

String title = page.title();
Assertions.assertEquals("Quinoa Lit App", title);

// Make sure the component loaded and hits the backend
final ElementHandle quinoaEl = page.waitForSelector(".greeting");
String greeting = quinoaEl.innerText();
Assertions.assertEquals("Hello Quinoa and World and bar", greeting);
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public String getConfigProfile() {
public static class RootPathTests extends QuinoaTestProfiles.Enable {
@Override
public String getConfigProfile() {
return "root-path,lit";
return "lit-root-path";
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,6 @@ private void handleHttpRequest(final RoutingContext ctx, final String resourcePa
final MultiMap headers = request.headers();
final String uri = computeResourceURI(resourcePath, request);

// 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
headers.remove("Accept");
// Disable compression in the forwarded request
headers.remove("Accept-Encoding");
client.request(request.method(), port, host, uri)
Expand All @@ -99,7 +94,12 @@ private void handleHttpRequest(final RoutingContext ctx, final String resourcePa
final int statusCode = event.result().statusCode();
switch (statusCode) {
case 200:
forwardResponse(event, request, ctx, resourcePath);
if (config.devServerDirectForwarding || shouldForward(ctx, event.result())) {
forwardResponse(event, request, ctx, resourcePath);
} else {
next(currentClassLoader, ctx);
}

break;
case 404:
next(currentClassLoader, ctx);
Expand All @@ -113,6 +113,18 @@ private void handleHttpRequest(final RoutingContext ctx, final String resourcePa
});
}

private boolean shouldForward(RoutingContext ctx, HttpResponse<Buffer> result) {
final List<String> contentType = result.headers().getAll(HttpHeaders.CONTENT_TYPE);
if (contentType != null && contentType.stream().anyMatch(s -> s.contains("text/html"))) {
final String path = QuinoaRecorder.resolvePath(ctx);
// We forward if the server returns a html, and it was intended:
// - if the path ends with .html
// - if the path is empty (root)
return path.endsWith(".html") || path.equals("/") || path.isEmpty();
}
return true;
}

private String computeResourceURI(String path, HttpServerRequest request) {
String uri = path;
final String query = request.query();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,17 @@ public class QuinoaHandlerConfig {
public final boolean enableCompression;
public final Set<String> compressMediaTypes;

public final boolean devServerDirectForwarding;

@RecordableConstructor
public QuinoaHandlerConfig(List<String> ignoredPathPrefixes, String indexPage, boolean prodMode, boolean enableCompression,
Set<String> compressMediaTypes) {
Set<String> compressMediaTypes, boolean devServerDirectForwarding) {
this.ignoredPathPrefixes = ignoredPathPrefixes;
this.indexPage = "/".equals(indexPage) ? "" : indexPage;
this.prodMode = prodMode;
this.enableCompression = enableCompression;
this.compressMediaTypes = compressMediaTypes;
this.devServerDirectForwarding = devServerDirectForwarding;
}

@Override
Expand All @@ -30,11 +33,15 @@ public boolean equals(Object o) {
if (o == null || getClass() != o.getClass())
return false;
QuinoaHandlerConfig that = (QuinoaHandlerConfig) o;
return Objects.equals(ignoredPathPrefixes, that.ignoredPathPrefixes) && Objects.equals(indexPage, that.indexPage);
return prodMode == that.prodMode && enableCompression == that.enableCompression
&& devServerDirectForwarding == that.devServerDirectForwarding
&& Objects.equals(ignoredPathPrefixes, that.ignoredPathPrefixes) && Objects.equals(indexPage, that.indexPage)
&& Objects.equals(compressMediaTypes, that.compressMediaTypes);
}

@Override
public int hashCode() {
return Objects.hash(ignoredPathPrefixes, indexPage);
return Objects.hash(ignoredPathPrefixes, indexPage, prodMode, enableCompression, compressMediaTypes,
devServerDirectForwarding);
}
}