quarkusio / quarkus

Quarkus: Supersonic Subatomic Java.
https://quarkus.io
Apache License 2.0
13.36k stars 2.56k forks source link

Redirect to static content not working in production mode when using RESTEasy, quarkus.http.root-path and no trailing slash #35076

Open FrenkelS opened 11 months ago

FrenkelS commented 11 months ago

Describe the bug

Quarkus behaves differently in development mode compared to production mode when using RESTEasy, a custom quarkus.http.root-path and static content.

My application.properties contains

quarkus.http.root-path=/testing
quarkus.swagger-ui.always-include=true

and I have one REST service:

package my.problem;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@Path("")
public class ExampleResource {

    @GET
    @Path("/hello")
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "hello";
    }
}

I also have an index.html in src/main/resources/META-INF/resources that has a link to Swagger UI. When Quarkus runs in development mode (mvn quarkus:dev) localhost:8080/testing redirects to localhost:8080/testing/ and index.html is shown in my browser. But when Quarkus runs in production mode (java -jar target\quarkus-app\quarkus-run.jar), localhost:8080/testing returns HTTP status code 404.

The following urls behave the same in both modes: localhost:8080/testing/ localhost:8080/testing/q/swagger-ui localhost:8080/testing/q/swagger-ui/ localhost:8080/testing/hello localhost:8080/testing/hello/

Expected behavior

In production mode, curl localhost:8080/testing -i should show HTTP status code 301 and a redirect to /testing/.

Actual behavior

In production mode, curl localhost:8080/testing -i shows HTTP status code 404.

How to Reproduce?

my-redirect-problem.zip

Output of uname -a or ver

Microsoft Windows [Version 10.0.22621.1992]

Output of java -version

openjdk version "17" 2021-09-14 OpenJDK Runtime Environment (build 17+35-2724) OpenJDK 64-Bit Server VM (build 17+35-2724, mixed mode, sharing)

GraalVM version (if different from Java)

No response

Quarkus version or git rev

3.2.2.Final

Build tool (ie. output of mvnw --version or gradlew --version)

Apache Maven 3.9.3 (21122926829f1ead511c958d89bd2f672198ae9f) Maven home: C:\Progs\apache-maven-3.9.3 Java version: 17, vendor: Oracle Corporation, runtime: C:\Progs\Java\jdk-17 Default locale: nl_NL, platform encoding: Cp1252 OS name: "windows 10", version: "10.0", arch: "amd64", family: "windows"

Additional information

When quarkus-undertow is added as a dependency, production mode behaves the same as development mode. But curl localhost:8080/testing -i shows HTTP 302 instead of 301, and the redirect is to http://localhost:8080/testing/ instead of /testing/.

This problem is similar to https://github.com/quarkusio/quarkus/issues/3091, https://github.com/quarkusio/quarkus/issues/6760 and https://github.com/quarkusio/quarkus/issues/19492

geoand commented 11 months ago

I took a quick look at this and even if we apply the following patch to Quarkus:

diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesRecorder.java
index 7738d9bfe34..df476450956 100644
--- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesRecorder.java
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesRecorder.java
@@ -88,10 +88,17 @@ public void handle(RoutingContext ctx) {
             handlers.add(new Handler<>() {
                 @Override
                 public void handle(RoutingContext ctx) {
-                    String rel = ctx.mountPoint() == null ? ctx.normalizedPath()
-                            : ctx.normalizedPath().substring(
-                                    // let's be extra careful here in case Vert.x normalizes the mount points at some point
-                                    ctx.mountPoint().endsWith("/") ? ctx.mountPoint().length() - 1 : ctx.mountPoint().length());
+                    String rel;
+                    if (ctx.mountPoint() == null) {
+                        rel = ctx.normalizedPath();
+                    } else {
+                        rel = ctx.normalizedPath().substring(
+                                // let's be extra careful here in case Vert.x normalizes the mount points at some point

then Vert.x's StaticResourceHandler still does not return the file for curl localhost:8080/testing because of how the non-root path is handled.

What I think would also be needed to fix things would be to in Vert.x Web is:

diff --git a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/StaticHandlerImpl.java b/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/StaticHandlerImpl.java
--- a/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/StaticHandlerImpl.java  (revision 58678777c7607289a0b6b8cb87a514155017217f)
+++ b/vertx-web/src/main/java/io/vertx/ext/web/handler/impl/StaticHandlerImpl.java  (revision 12fd7ed21e85bc09ff5a5dfe92f1e62fb1e9843a)
@@ -195,9 +195,21 @@
           path,
           // only root is known for sure to be a directory. all other directories must be
           // identified as such.
-          !directoryListing && "/".equals(path));
+          !directoryListing && isPathRoot(path, context));
     }
   }
+
+  private static boolean isPathRoot(String path, RoutingContext context) {
+    if ("/".equals(path)) {
+      return true;
+    }
+
+    String mountPoint = context.mountPoint();
+    if (mountPoint == null) {
+      return false;
+    }
+    return mountPoint.equals(path.endsWith("/") ? path : path + "/");
+  }

WDYT @cescoffier @vietj?

FrenkelS commented 10 months ago

As a workaround I've added dependency quarkus-reactive-routes and do the redirect myself in a @RouteFilter:

package my.problem;

import java.net.HttpURLConnection;
import java.util.HashSet;
import java.util.Set;

import io.quarkus.vertx.web.RouteFilter;
import io.vertx.ext.web.RoutingContext;
import org.eclipse.microprofile.config.inject.ConfigProperty;

class RedirectBugFixFilter {

    private final Set<String> rootPaths = new HashSet<>();

    RedirectBugFixFilter(@ConfigProperty(name = "quarkus.http.root-path",         defaultValue = "/") String quarkusHttpRootPath,
                         @ConfigProperty(name = "quarkus.resteasy.path",          defaultValue = "/") String quarkusResteasyPath,
                         @ConfigProperty(name = "quarkus.resteasy-reactive.path", defaultValue = "/") String quarkusResteasyReactivePath) {
        addRootPath(quarkusHttpRootPath);
        addRootPath(quarkusResteasyPath);
        addRootPath(quarkusResteasyReactivePath);
    }

    private void addRootPath(String rootPath) {
        if (!"/".equals(rootPath)) {
            rootPaths.add(rootPath.endsWith("/") ? rootPath.substring(0, rootPath.length() - 1) : rootPath);
        }
    }

    @RouteFilter( 100 )
    void redirect(RoutingContext routingContext) {
        String uri = routingContext.request().uri();
        if (rootPaths.contains(uri)) {
            routingContext.response()
                          .setStatusCode(HttpURLConnection.HTTP_MOVED_PERM)
                          .putHeader("location", uri + '/')
                          .end();
        } else {
            routingContext.next();
        }
    }
}