quarkusio / quarkus

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

Can't load static resource created by extension #28474

Open FroMage opened 1 year ago

FroMage commented 1 year ago

Describe the bug

In my extension, I created a static resource with:

    @BuildStep
    public void processModel(BuildProducer<GeneratedResourceBuildItem> output,
            BuildProducer<AdditionalStaticResourceBuildItem> staticResources) {

        String path = "/routes.js";
        output.produce(new GeneratedResourceBuildItem("META-INF/resources" + path,
                "//hello".getBytes(StandardCharsets.UTF_8)));
        staticResources.produce(new AdditionalStaticResourceBuildItem(path, false));
}

When I try to test this:

@QuarkusTest
public class MyTest {
    @Test
    public void testJsRouting() {
        given()
                .when().get("/routes.js")
                .then()
                .statusCode(200)
                .body(is("//hello"));
    }
}

I'm getting the following exception:

ERROR: HTTP Request to /routes.js failed, error id: 0c1267fb-df47-4c0c-b97f-f7943ca4ef19-1
java.lang.IllegalStateException: Invalid url protocol: quarkus
    at io.vertx.core.file.impl.FileResolverImpl.unpackUrlResource(FileResolverImpl.java:247)
    at io.vertx.core.file.impl.FileResolverImpl.resolveFile(FileResolverImpl.java:174)
    at io.vertx.core.impl.VertxImpl.resolveFile(VertxImpl.java:794)
    at io.vertx.core.file.impl.FileSystemImpl$20.perform(FileSystemImpl.java:1136)
    at io.vertx.core.file.impl.FileSystemImpl$20.perform(FileSystemImpl.java:1134)
    at io.vertx.core.file.impl.FileSystemImpl$BlockingAction.handle(FileSystemImpl.java:1175)
    at io.vertx.core.file.impl.FileSystemImpl$BlockingAction.handle(FileSystemImpl.java:1157)
    at io.vertx.core.impl.ContextBase.lambda$null$0(ContextBase.java:137)
    at io.vertx.core.impl.ContextInternal.dispatch(ContextInternal.java:264)
    at io.vertx.core.impl.ContextBase.lambda$executeBlocking$1(ContextBase.java:135)
    at io.vertx.core.impl.TaskQueue.run(TaskQueue.java:76)
    at org.jboss.threads.ContextHandler$1.runWith(ContextHandler.java:18)
    at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2449)
    at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1452)
    at org.jboss.threads.DelegatingRunnable.run(DelegatingRunnable.java:29)
    at org.jboss.threads.ThreadLocalResettingRunnable.run(ThreadLocalResettingRunnable.java:29)
    at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
    at java.base/java.lang.Thread.run(Thread.java:834)

This is because those are served via a Vert.x StaticHandler, which accesses them via the file system, and obtains the URL to the resource via the QuarkusClassLoader: https://github.com/eclipse-vertx/vert.x/blob/master/src/main/java/io/vertx/core/file/impl/FileResolverImpl.java#L173 and then attempts to open it in https://github.com/eclipse-vertx/vert.x/blob/bbb4ddc417a604697d2ff4f5a8eda2e74113697e/src/main/java/io/vertx/core/file/impl/FileResolverImpl.java#L225

The problem is that QuarkusClassLoader serves it using a MemoryClassPathElement https://github.com/quarkusio/quarkus/blob/main/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/MemoryClassPathElement.java#L86 that has a quarkus: URL scheme, which is registered via a UrlStreamHandler at https://github.com/quarkusio/quarkus/blob/main/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/MemoryClassPathElement.java#L131 but Vert.x doesn't know about it and doesn't know how to deal with it.

I'm not sure what to do to fix this, frankly.

Expected behavior

No response

Actual behavior

No response

How to Reproduce?

No response

Output of uname -a or ver

No response

Output of java -version

No response

GraalVM version (if different from Java)

No response

Quarkus version or git rev

No response

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

No response

Additional information

No response

geoand commented 1 year ago

I'll take a look tomorrow

geoand commented 1 year ago

@FroMage do you have your sample code anywhere?

FroMage commented 1 year ago

Not already, but I guess I could push it. You can't reproduce?

geoand commented 1 year ago

My point is that I don't want to try to reproduce and fail :)

FroMage commented 1 year ago

Well, meet me half way :) This code should be enough, and you have to add a test for this anyway, so giving you a full project won't make your test materialise. And if it doesn't fail to reproduce, I'll give you a full project? Come to think of it, how do we even write tests for extensions?

geoand commented 1 year ago

Come to think of it, how do we even write tests for extensions?

The deployment module tests are usually the answer

geoand commented 1 year ago

The easiest thing for me would be to just use your extension code instead of coming up with a new one

FroMage commented 1 year ago

Well, OK, so I found how to write an extension test to have a special build step today:

package io.quarkus.vertx.http;

import java.nio.charset.StandardCharsets;
import java.util.function.Consumer;

import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.builder.BuildChainBuilder;
import io.quarkus.builder.BuildContext;
import io.quarkus.builder.BuildStep;
import io.quarkus.deployment.builditem.GeneratedResourceBuildItem;
import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.vertx.http.deployment.spi.AdditionalStaticResourceBuildItem;
import io.restassured.RestAssured;

public class AdditionalStaticResourceTest {

    @RegisterExtension
    static final QuarkusUnitTest config = new QuarkusUnitTest()
            .addBuildChainCustomizer(buildCustomizer());

    static Consumer<BuildChainBuilder> buildCustomizer() {
        return new Consumer<BuildChainBuilder>() {
            @Override
            public void accept(BuildChainBuilder builder) {
                builder.addBuildStep(new BuildStep() {
                    @Override
                    public void execute(BuildContext context) {
                        String path = "/routes.js";

                        context.produce(new GeneratedResourceBuildItem("META-INF/resources" + path,
                                "//hello".getBytes(StandardCharsets.UTF_8)));
                        context.produce(new AdditionalStaticResourceBuildItem(path, false));
                    }
                }).produces(GeneratedResourceBuildItem.class)
                .produces(AdditionalStaticResourceBuildItem.class)
                .build();
            }
        };
    }

    @Test
    public void testNonApplicationEndpointDirect() {
        RestAssured.given()
        .when().get("/routes.js")
        .then()
        .statusCode(200)
        .body(Matchers.is("//hello"));
    }
}

Here's your reproducer ;)

geoand commented 1 year ago

Where would routes.js be found in this test?

FroMage commented 1 year ago

It's not found, it's generated by the extension:

context.produce(new GeneratedResourceBuildItem("META-INF/resources" + path,
                                "//hello".getBytes(StandardCharsets.UTF_8)));
geoand commented 1 year ago

Ah, okay, I misread that.

FroMage commented 1 year ago

You are on way too many issues at the same time, this tells me ;)

geoand commented 1 year ago

There are a few ways to deal with this:

1) The extension actually generates a file as well (that's what excludeFromDevCL is used for) - see this discussion 2) We could make Vert.x use the URLStreamHandler of a URL for protocols it does not know

FroMage commented 1 year ago

Huh, I thought GeneratedResourceBuildItem would generate the file. What does it do then? And how do I generate it if not using that?

geoand commented 1 year ago

It does generates it in prod mode, in the other modes it's just kept "in memory" backed by the QuarkusClassLoader.

FroMage commented 1 year ago

Oh. So it's not clear what you're suggesting I do for 1. then ;)

geoand commented 1 year ago

You would have to actually persist it on the disk yourself

FroMage commented 1 year ago

If I knew where to put it, it could be an option. I suppose this depends on the build system. Or we could stop the Quarkus CL from defining its own protocol perhaps? Is this even used by anything other than the vert.x static handler?

geoand commented 1 year ago

The protocol is not used AFAIK - but even if we do, I don't think Vert.x will work

FroMage commented 1 year ago

Well, if Quarkus itself creates the file and stops using this protocol, then Vert.x will find it on disk, no?

ia3andy commented 6 months ago

@FroMage you need to be cautious here, because when using the classloader instead of the FS, there is a cache preventing update of the file.

FYI, I am working on a very similar issue with the Web Bundler: https://github.com/quarkiverse/quarkus-web-bundler/issues/171