Repository: quarkusio/quarkus
Issue: RESTEasy ResponseBuilder.location Inadvertently Decodes Path Segments in Relative URI

### Describe the bug

The `ResponseBuilder.location(URI)` method accepts relative URIs and will build an absolute URI relative to the base URI to put in the `Location` header. When the relative URI contains URL-encoded path segments, `ResponseBuilder.location` erroneously decodes those segments when building the new absolute URI.

For example, the given JAX-RS resource method should build an HTTP response with a `Location` header that has the URI path `/items/foo%2Fbar`, but it instead returns `/items/foo/bar`.

```java
public Response create() { 
  var uri = UriBuilder.newInstance().path("{id}").build("foo/bar");
  return ResponseBuilder.status(202).location(uri).build(); 
}
```

### Expected behavior

`ResponseBuilder.location` creates an absolute URI without changing the semantics of the URI path.

### Actual behavior

`ResponseBuilder.location` creates an absolute URI with a URI path that has different semantics.

### How to Reproduce?

[quarkus-resteasy-reactive-uri-decode-issue.tar.gz](https://github.com/quarkusio/quarkus/files/11490428/quarkus-resteasy-reactive-uri-decode-issue.tar.gz)

The attached reproducer includes a test that demonstrates the issue through a failure. The test fails with a message like

```
2023-05-16 13:29:47,195 INFO  [io.quarkus] (main) quarkus-resteasy-reactive-uri-decode-issue 1.0.0-SNAPSHOT on JVM (powered by Quarkus 3.0.3.Final) started in 0.940s. Listening on: http://localhost:63035
2023-05-16 13:29:47,214 INFO  [io.quarkus] (main) Profile test activated. 
2023-05-16 13:29:47,215 INFO  [io.quarkus] (main) Installed features: [cdi, resteasy-reactive, smallrye-context-propagation, vertx]
HTTP/1.1 201 Created
Location: http://localhost:63035/greeting/en/us
content-length: 0

java.lang.AssertionError: 1 expectation failed.
Expected header "Location" was not a string matching the pattern 'http://localhost:\\d+/greeting/en-us', was "http://localhost:63035/greeting/en/us". Headers are:
Location=http://localhost:63035/greeting/en/us
content-length=0
```

because the URI path should have been `/greeting/en%2Fus`

### Output of `uname -a` or `ver`

Darwin pixee-mbp-gilday.localdomain 22.4.0 Darwin Kernel Version 22.4.0: Mon Mar  6 20:59:28 PST 2023; root:xnu-8796.101.5~3/RELEASE_ARM64_T6000 arm64

### Output of `java -version`

﻿openjdk version "17.0.7" 2023-04-18 OpenJDK Runtime Environment Temurin-17.0.7+7 (build 17.0.7+7) OpenJDK 64-Bit Server VM Temurin-17.0.7+7 (build 17.0.7+7, mixed mode)

### GraalVM version (if different from Java)

_No response_

### Quarkus version or git rev

3.0.3.Final

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

Apache Maven 3.8.8 (4c87b05d9aedce574290d1acc98575ed5eb6cd39) Maven home: /Users/jgilday/.m2/wrapper/dists/apache-maven-3.8.8-bin/67c30f74/apache-maven-3.8.8 Java version: 17.0.7, vendor: Eclipse Adoptium, runtime: /Library/Java/JavaVirtualMachines/temurin-17.jdk/Contents/Home Default locale: en_US, platform encoding: UTF-8 OS name: "mac os x", version: "13.3.1", arch: "aarch64", family: "mac"

### Additional information

_No response_

Changed files:
independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/jaxrs/RestResponseBuilderImpl.java
independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/response/RestResponseTest.java
independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/jaxrs/ResponseBuilderImpl.java
independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/response/RestResponseResource.java
independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/response/ResponseTest.java

diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/jaxrs/ResponseBuilderImpl.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/jaxrs/ResponseBuilderImpl.java
index c4c2847152c..208c1093d4f 100644
--- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/jaxrs/ResponseBuilderImpl.java
+++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/jaxrs/ResponseBuilderImpl.java
@@ -40,10 +40,12 @@ public Response.ResponseBuilder location(URI location) {
                         prefix = deployment.getPrefix();
                     }
                     // Spec says relative to request, but TCK tests relative to Base URI, so we do that
-                    location = new URI(req.getRequestScheme(), null, host, port,
-                            prefix +
-                                    (location.getPath().startsWith("/") ? location.getPath() : "/" + location.getPath()),
-                            location.getQuery(), null);
+                    String path = location.toString();
+                    if (!path.startsWith("/")) {
+                        path = "/" + path;
+                    }
+                    URI baseUri = new URI(req.getRequestScheme(), null, host, port, null, null, null);
+                    location = baseUri.resolve(prefix + path);
                 } catch (URISyntaxException e) {
                     throw new RuntimeException(e);
                 }
@@ -72,9 +74,12 @@ public Response.ResponseBuilder contentLocation(URI location) {
                         port = Integer.parseInt(host.substring(index + 1));
                         host = host.substring(0, index);
                     }
-                    location = new URI(req.getRequestScheme(), null, host, port,
-                            location.getPath().startsWith("/") ? location.getPath() : "/" + location.getPath(),
-                            location.getQuery(), null);
+                    String path = location.toString();
+                    if (!path.startsWith("/")) {
+                        path = "/" + path;
+                    }
+                    location = new URI(req.getRequestScheme(), null, host, port, null, null, null)
+                            .resolve(path);
                 } catch (URISyntaxException e) {
                     throw new RuntimeException(e);
                 }
diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/jaxrs/RestResponseBuilderImpl.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/jaxrs/RestResponseBuilderImpl.java
index 21d74d4b9f8..a62003cbef6 100644
--- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/jaxrs/RestResponseBuilderImpl.java
+++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/jaxrs/RestResponseBuilderImpl.java
@@ -40,10 +40,12 @@ public RestResponse.ResponseBuilder<T> location(URI location) {
                         prefix = deployment.getPrefix();
                     }
                     // Spec says relative to request, but TCK tests relative to Base URI, so we do that
-                    location = new URI(req.getRequestScheme(), null, host, port,
-                            prefix +
-                                    (location.getPath().startsWith("/") ? location.getPath() : "/" + location.getPath()),
-                            location.getQuery(), null);
+                    String path = location.toString();
+                    if (!path.startsWith("/")) {
+                        path = "/" + path;
+                    }
+                    URI baseUri = new URI(req.getRequestScheme(), null, host, port, null, null, null);
+                    location = baseUri.resolve(prefix + path);
                 } catch (URISyntaxException e) {
                     throw new RuntimeException(e);
                 }
@@ -72,9 +74,12 @@ public RestResponse.ResponseBuilder<T> contentLocation(URI location) {
                         port = Integer.parseInt(host.substring(index + 1));
                         host = host.substring(0, index);
                     }
-                    location = new URI(req.getRequestScheme(), null, host, port,
-                            location.getPath().startsWith("/") ? location.getPath() : "/" + location.getPath(),
-                            location.getQuery(), null);
+                    String path = location.toString();
+                    if (!path.startsWith("/")) {
+                        path = "/" + path;
+                    }
+                    location = new URI(req.getRequestScheme(), null, host, port, null, null, null)
+                            .resolve(path);
                 } catch (URISyntaxException e) {
                     throw new RuntimeException(e);
                 }
diff --git a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/response/ResponseTest.java b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/response/ResponseTest.java
index 40af3653860..849646f9eb3 100644
--- a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/response/ResponseTest.java
+++ b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/response/ResponseTest.java
@@ -2,6 +2,7 @@
 
 import jakarta.ws.rs.core.HttpHeaders;
 import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.UriBuilder;
 
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
@@ -16,4 +17,20 @@ public void testCaseInsensitivity() {
         Assertions.assertEquals("HEAD", response.getHeaders().getFirst("allow"));
         Assertions.assertEquals("HEAD", response.getHeaders().getFirst(HttpHeaders.ALLOW));
     }
+
+    @Test
+    public void testLocation() {
+        final var location = UriBuilder.fromUri("http://localhost:8080").path("{language}")
+                .build("en/us");
+        Response response = Response.ok("Hello").location(location).build();
+        Assertions.assertEquals("http://localhost:8080/en%2Fus", response.getLocation().toString());
+    }
+
+    @Test
+    public void testContentLocation() {
+        final var location = UriBuilder.fromUri("http://localhost:8080").path("{language}")
+                .build("en/us");
+        Response response = Response.ok("Hello").contentLocation(location).build();
+        Assertions.assertEquals("http://localhost:8080/en%2Fus", response.getHeaderString("Content-Location"));
+    }
 }
diff --git a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/response/RestResponseResource.java b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/response/RestResponseResource.java
index 1c2f8f1e5a8..d72fdf301ac 100644
--- a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/response/RestResponseResource.java
+++ b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/response/RestResponseResource.java
@@ -13,6 +13,7 @@
 import jakarta.ws.rs.core.CacheControl;
 import jakarta.ws.rs.core.NewCookie;
 import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.UriBuilder;
 import jakarta.ws.rs.core.Variant;
 
 import org.jboss.resteasy.reactive.RestResponse;
@@ -47,6 +48,24 @@ public RestResponse<?> wildcard() {
         return RestResponse.ResponseBuilder.ok("Hello").header("content-type", "text/plain").build();
     }
 
+    @GET
+    @Path("rest-response-location")
+    public RestResponse<?> location() {
+        final var location = UriBuilder.fromResource(RestResponseResource.class).path("{language}")
+                .queryParam("user", "John")
+                .build("en/us");
+        return RestResponse.ResponseBuilder.ok("Hello").location(location).build();
+    }
+
+    @GET
+    @Path("rest-response-content-location")
+    public RestResponse<?> contentLocation() {
+        final var location = UriBuilder.fromResource(RestResponseResource.class).path("{language}")
+                .queryParam("user", "John")
+                .build("en/us");
+        return RestResponse.ResponseBuilder.ok("Hello").contentLocation(location).build();
+    }
+
     @GET
     @Path("rest-response-full")
     @SuppressWarnings("deprecation")
diff --git a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/response/RestResponseTest.java b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/response/RestResponseTest.java
index 03d83f208b2..2563091c4f0 100644
--- a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/response/RestResponseTest.java
+++ b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/response/RestResponseTest.java
@@ -1,5 +1,7 @@
 package org.jboss.resteasy.reactive.server.vertx.test.response;
 
+import static org.hamcrest.CoreMatchers.endsWith;
+
 import java.util.function.Supplier;
 
 import org.hamcrest.Matchers;
@@ -107,5 +109,11 @@ public void test() {
                 .then().statusCode(200)
                 .and().body(Matchers.equalTo("Uni<RestResponse> request filter"))
                 .and().contentType("text/plain");
+        RestAssured.get("/rest-response-location")
+                .then().statusCode(200)
+                .header("Location", endsWith("/en%2Fus?user=John"));
+        RestAssured.get("/rest-response-content-location")
+                .then().statusCode(200)
+                .header("Content-Location", endsWith("/en%2Fus?user=John"));
     }
 }