micronaut-projects / micronaut-core

Micronaut Application Framework
http://micronaut.io
Apache License 2.0
6.04k stars 1.06k forks source link

Controller method with both @Body and HttpRequest arguments fail #9694

Open catatafishen opened 1 year ago

catatafishen commented 1 year ago

Expected Behavior

In micronaut 3 I was using controller methods with both @body and HttpRequest as arguments. For example the following code worked fine:

@Controller("/echo")
public class EchoController {
    @Post
    @Produces(MediaType.TEXT_PLAIN)
    public String echo(HttpRequest<String> request, @Body String body) {
        return body;
    }
}

Actual Behaviour

After upgrading to Micronaut 4 requesting an endpoint that defines both HttpRequest and @Body as arguments throws Java.lang.IllegalStateException: Already claimed.

Either argument alone works fine. Therefor, as a workaround, I can instead of the @body argument use HttpRequest.getBody().

Steps To Reproduce

No response

Environment Information

No response

Example Application

https://github.com/catatafishen/controller.arguments.example/blob/master/src/main/java/example/micronaut/EchoController.java

Version

4.0.2

runenielsen commented 1 year ago

We have the same problem - any news on this?

yawkat commented 1 year ago

you can also pass a HttpRequest<?> as a workaround

runenielsen commented 1 year ago

@yawkat : Thanks a lot 🙂

Will this workaround have an effect on performance?

It seems a bit odd, that it works differently for <?> than specifying an actual type for the generic...

yawkat commented 1 year ago

HttpRequest<?> should technically be faster. but i doubt it matters.

rafaeljimenezgodoy commented 12 months ago

We have the same issue, version 4.1.1

koebbingd commented 11 months ago

Same with version 4.1.2, but as a workaround exists, it's ok for now.

gad2103 commented 11 months ago

same here.

gad2103 commented 11 months ago

Also, it looks like @QueryValue is no longer working with form data like it was in 3.x

wade-taylor commented 11 months ago

I'm seeing the same Already claimed error when using @Body & Authentication like:

@Controller
public class ExampleController {
    @Post("/example")
    @Produces(MediaType.TEXT_PLAIN)
    public String examplePost(@Nullable Authentication auth, @Body String body) {
        return body;
    }
}
loicmathieu commented 9 months ago

Same error here, we're using as params for the same method @Body, @Path and @QueryVaue annotated parameters and we're hitting the same Already claimed error.

    @Post(uri = "/trigger/{namespace}/{id}", consumes = MediaType.MULTIPART_FORM_DATA)
    public Execution trigger(
        @PathVariable String namespace,
        @PathVariable String id,
        @Nullable @Body Map<String, Object> inputs,
         @Nullable @QueryValue List<String> labels,
        @Nullable @Part Publisher<StreamingFileUpload> files,
        @QueryValue(defaultValue = "false") Boolean wait,
         @QueryValue Optional<Integer> revision
    ) {
    }
graemerocher commented 9 months ago

the issue is probably the present of both @Part and @Body in your example.

loicmathieu commented 5 months ago

Hi, Back to this issue, I used the trick to bind the body to Publisher and HttpRequest<?> but it caused a memory issue.

First, let me explain what we do. We have a multipart which can contain plain string or files, the files can have multiple parts with different filenames. In Micronaut 3, we bind the body to a @Body HashMap<String, Object> and the files part using an @Part, and it works fine. Now that we change the body to an HttpRequest<?>, I discover that when I do request.getBody(Map.class) I have all the parts including the files, so when there is a huge file it is loaded in memory.

So my plan will be to bind the body once in a @Body MultipartBody but looking at it quickly I cannot access the filename nor have an easy way to know if the part is a file or not (I need to instanceof on internal classes). I'll open an issue for proposing improvements to the MultiPart body to offer in the API the filename and a way to differentiate between a file and an attribute.

CezaryBD commented 4 months ago

I'm seeing the same Already claimed error when using @Body & Authentication like:

@Controller
public class ExampleController {
    @Post("/example")
    @Produces(MediaType.TEXT_PLAIN)
    public String examplePost(@Nullable Authentication auth, @Body String body) {
        return body;
    }
}

I have exact same issue at the moment. I cannot bind both @Body and Authentication, the application runtime works fine, but I am not able to test it at all.

yawkat commented 4 months ago

@CezaryBD This sounds like you're missing something to bind Authentication in your tests

KazimirDobrzhinsky commented 3 months ago

@yawkat I face the same issue as @CezaryBD with both Authentication and Body in the test

For Get requests without the body (query or path params and Authentication as parameters for controller method) everything works good, but for requests with body I observe this error

So binding of Authentication is not a problem, cause it works in other cases (especially since in my code and in @CezaryBD Authentication example marked as @Nullable)

Same is for case when I have HttpRequest and Authentication as parameters

graemerocher commented 3 months ago

@KazimirDobrzhinsky provide an example that reproduces it

KazimirDobrzhinsky commented 3 months ago

@KazimirDobrzhinsky provide an example that reproduces it

I couldn't provide a full example repository, but here is my dependencies configuration and controller similar to the one, that I am using

`

io.micronaut.platform
    <artifactId>micronaut-parent</artifactId>
    <version>4.5.0</version>
</parent>

<properties>
    <dir.interface.in>${project.basedir}/src/main/resources/interfaces/in</dir.interface.in>
    <dir.interface.out>${project.basedir}/src/main/resources/interfaces/out</dir.interface.out>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <packaging>jar</packaging>
    <jdk.version>21</jdk.version>
    <release.version>21</release.version>
    <micronaut.version>4.5.0</micronaut.version>
    <micronaut.runtime>netty</micronaut.runtime>
    <micronaut.aot.enabled>false</micronaut.aot.enabled>
    <micronaut.aot.packageName>de.telekom.aot.generated</micronaut.aot.packageName>
    <exec.mainClass>"deleted"</exec.mainClass>
    <org.mapstruct.version>1.5.5.Final</org.mapstruct.version>
    <allure.version>2.25.0</allure.version>
    <lombok.version>1.18.32</lombok.version>
    <jacoco.version>0.8.12</jacoco.version>
    <awaitility.version>4.2.1</awaitility.version>
    <logstash-logback-encoder.version>7.4</logstash-logback-encoder.version>
    <elk.apm-agent-attach.version>1.50.0</elk.apm-agent-attach.version>
    <oracle.driver-non-reactive.version>23.4.0.24.05</oracle.driver-non-reactive.version>
    <wiremock.version>3.6.0</wiremock.version>
</properties>

<dependencies>
    <!-- general micronaut dependencies -->
    <dependency>
        <groupId>io.micronaut</groupId>
        <artifactId>micronaut-core-processor</artifactId>
    </dependency>
    <dependency>
        <groupId>io.micronaut</groupId>
        <artifactId>micronaut-http-server-netty</artifactId>
        <scope>compile</scope>
    </dependency>
    <dependency>
        <groupId>io.micronaut.reactor</groupId>
        <artifactId>micronaut-reactor</artifactId>
        <scope>compile</scope>
    </dependency>
    <dependency>
        <groupId>io.micronaut.reactor</groupId>
        <artifactId>micronaut-reactor-http-client</artifactId>
        <scope>compile</scope>
    </dependency>
    <dependency>
        <groupId>io.micronaut.serde</groupId>
        <artifactId>micronaut-serde-jackson</artifactId> <!-- serialisation library -->
        <scope>compile</scope>
    </dependency>
    <!-- security related -->
    <dependency>
        <groupId>io.micronaut.security</groupId>
        <artifactId>micronaut-security-jwt</artifactId> <!-- incoming security -->
        <scope>compile</scope>
    </dependency>
    <dependency>
        <groupId>io.micronaut.security</groupId>
        <artifactId>micronaut-security-oauth2</artifactId> <!-- outgoing security -->
        <scope>compile</scope>
    </dependency>
    <!-- DB related -->
    <dependency>
        <groupId>io.micronaut.data</groupId>
        <artifactId>micronaut-data-hibernate-reactive</artifactId>
    </dependency>
    <dependency>
        <groupId>io.micronaut.data</groupId>
        <artifactId>micronaut-data-hibernate-jpa</artifactId>
    </dependency>

    <dependency>
        <groupId>io.micronaut.beanvalidation</groupId>
        <artifactId>micronaut-hibernate-validator</artifactId>
        <scope>compile</scope>
    </dependency>
    <!-- Flyway related -->
    <dependency>
        <groupId>io.vertx</groupId>
        <artifactId>vertx-oracle-client</artifactId>
    </dependency>
    <dependency>
        <groupId>io.micronaut.flyway</groupId>
        <artifactId>micronaut-flyway</artifactId>
        <scope>compile</scope>
    </dependency>
    <dependency>
        <groupId>org.flywaydb</groupId>
        <artifactId>flyway-database-oracle</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>com.oracle.database.jdbc</groupId>
        <artifactId>ojdbc11</artifactId>
        <version>${oracle.driver-non-reactive.version}</version>
    </dependency>
    <dependency>
        <groupId>io.micronaut.sql</groupId>
        <artifactId>micronaut-jdbc-hikari</artifactId>
        <scope>compile</scope>
    </dependency>
    <!-- miscellaneous -->
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.openapitools</groupId>
        <artifactId>openapi-generator-maven-plugin</artifactId>
        <version>${openapi.version}</version>
        <scope>provided</scope>
        <exclusions>
            <exclusion>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-simple</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>${org.mapstruct.version}</version>
    </dependency>
    <dependency>
        <groupId>org.yaml</groupId>
        <artifactId>snakeyaml</artifactId> <!-- snakeyaml is required to support yaml configuration files -->
        <scope>runtime</scope>
    </dependency>
    <!-- ELK related -->
    <dependency>
        <groupId>co.elastic.apm</groupId>
        <artifactId>apm-agent-attach</artifactId>
        <version>${elk.apm-agent-attach.version}</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>net.logstash.logback</groupId>
        <artifactId>logstash-logback-encoder</artifactId>
        <version>${logstash-logback-encoder.version}</version>
    </dependency>
    <!-- test related -->
    <dependency>
        <groupId>io.micronaut.test</groupId>
        <artifactId>micronaut-test-rest-assured</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>io.qameta.allure</groupId>
        <artifactId>allure-rest-assured</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>io.rest-assured</groupId>
                <artifactId>rest-assured</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>io.qameta.allure</groupId>
        <artifactId>allure-junit5</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.awaitility</groupId>
        <artifactId>awaitility</artifactId>
        <version>${awaitility.version}</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>io.micronaut.test</groupId>
        <artifactId>micronaut-test-junit5</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-engine</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>junit-jupiter</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>oracle-xe</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>testcontainers</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.wiremock</groupId>
        <artifactId>wiremock-standalone</artifactId>
        <version>${wiremock.version}</version>
        <scope>test</scope>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>io.qameta.allure</groupId>
            <artifactId>allure-bom</artifactId>
            <version>${allure.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<build>
    <plugins>
        <plugin>
            <groupId>io.micronaut.maven</groupId>
            <artifactId>micronaut-maven-plugin</artifactId>
            <configuration>
                <configFile>aot-${packaging}.properties</configFile>
            </configuration>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-enforcer-plugin</artifactId>
            <version>${maven-enforcer-plugin.version}</version>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <source>${jdk.version}</source>
                <target>${jdk.version}</target>
                <!-- Uncomment to enable incremental compilation -->
                <!-- <useIncrementalCompilation>false</useIncrementalCompilation> -->

                <annotationProcessorPaths combine.self="override">
                    <path>
                        <groupId>org.projectlombok</groupId>
                        <artifactId>lombok</artifactId>
                        <version>${lombok.version}</version>
                    </path>
                    <path>
                        <groupId>io.micronaut</groupId>
                        <artifactId>micronaut-inject-java</artifactId>
                        <version>${micronaut.core.version}</version>
                    </path>

                    <path>
                        <groupId>io.micronaut.data</groupId>
                        <artifactId>micronaut-data-processor</artifactId>
                        <version>${micronaut.data.version}</version>
                    </path>
                    <path>
                        <groupId>io.micronaut</groupId>
                        <artifactId>micronaut-graal</artifactId>
                        <version>${micronaut.core.version}</version>
                    </path>
                    <path>
                        <groupId>io.micronaut</groupId>
                        <artifactId>micronaut-http-validation</artifactId>
                        <version>${micronaut.core.version}</version>
                    </path>
                    <path>
                        <groupId>io.micronaut</groupId>
                        <artifactId>micronaut-http-validation</artifactId>
                        <version>${micronaut.core.version}</version>
                    </path>
                    <path>
                        <groupId>io.micronaut.serde</groupId>
                        <artifactId>micronaut-serde-processor</artifactId>
                        <version>${micronaut.serialization.version}</version>
                        <exclusions>
                            <exclusion>
                                <groupId>io.micronaut</groupId>
                                <artifactId>micronaut-inject</artifactId>
                            </exclusion>
                        </exclusions>
                    </path>
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>${org.mapstruct.version}</version>
                    </path>
                </annotationProcessorPaths>
                <compilerArgs>
                    <arg>-Amicronaut.processing.group=de.telekom</arg>
                    <arg>-Amicronaut.processing.module=assurancesubscribers</arg>
                </compilerArgs>
            </configuration>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>${maven-surefire-plugin.version}</version>
            <executions>
                <execution>
                    <id>sonar</id>
                    <configuration>
                        <argLine>@{argLine} -Dfile.encoding=UTF-8</argLine>
                    </configuration>
                </execution>
                <execution>
                    <id>AllureReport-SystemTests</id>
                    <configuration>
                        <testFailureIgnore>false</testFailureIgnore>
                        <argLine>
                            -javaagent:"${settings.localRepository}${file.separator}org${file.separator}aspectj${file.separator}aspectjweaver${file.separator}${aspectj.version}${file.separator}aspectjweaver-${aspectj.version}.jar"
                        </argLine>
                    </configuration>
                </execution>
            </executions>
        </plugin>
        <plugin>
            <groupId>org.jacoco</groupId>
            <artifactId>jacoco-maven-plugin</artifactId>
            <version>${jacoco.version}</version>
            <executions>
                <execution>
                    <id>prepare-agent</id>
                    <goals>
                        <goal>prepare-agent</goal>
                    </goals>
                </execution>
                <execution>
                    <id>report</id>
                    <phase>prepare-package</phase>
                    <goals>
                        <goal>report</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>`

Controller: `import io.micronaut.http.HttpRequest; import io.micronaut.http.annotation.; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.convert.format.Format; import io.micronaut.security.authentication.Authentication; import io.micronaut.security.annotation.Secured; import io.micronaut.security.rules.SecurityRule; import reactor.core.publisher.Mono; import io.micronaut.http.HttpResponse; import io.micronaut.http.HttpStatus; import io.micronaut.http.exceptions.HttpStatusException; import jakarta.annotation.Generated; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import jakarta.validation.Valid; import jakarta.validation.constraints.; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.security.SecurityRequirement;

@Controller("/v2") @Tag(name = "", description = "") public class ControllerClass { @Operation( operationId = "", summary = "", responses = { @ApiResponse(responseCode = "201", description = "Created successfully", content = { @Content(mediaType = "application/json", schema = @Schema(implementation = "deleted".class)) }), @ApiResponse(responseCode = "400", description = "Bad request or business logic error occurred."), @ApiResponse(responseCode = "401", description = "User is not logged in i.e. unauthorized"), @ApiResponse(responseCode = "403", description = "The user is not authorized to access the service ."), @ApiResponse(responseCode = "404", description = " was not found."), @ApiResponse(responseCode = "409", description = "A conflict occurred, because two clients tried to create the same resource."), @ApiResponse(responseCode = "415", description = "Unknown media-type received."), @ApiResponse(responseCode = "500", description = "Internal Server Error"), @ApiResponse(responseCode = "501", description = "Not yet implemented"), @ApiResponse(responseCode = "503", description = "Temporarily Unavailable") }, parameters = { @Parameter(name = "_body", description = ".") } ) @Post(uri="/create") @Produces(value = {"application/json"}) @Consumes(value = {"application/json"}) @Secured({SecurityRule.IS_ANONYMOUS}) public Mono<HttpResponse<"deleted">> create( HttpRequest< "deleted"> _body, @Nullable Authentication authentication ) { // imolementation here } @Operation( operationId = "", summary = "", responses = { @ApiResponse(responseCode = "200", description = "OK", content = { @Content(mediaType = "application/json", schema = @Schema(implementation = "deleted".class)) }), @ApiResponse(responseCode = "400", description = "Bad request or business logic error occurred."), @ApiResponse(responseCode = "401", description = "User is not logged in i.e. unauthorized"), @ApiResponse(responseCode = "403", description = "The user is not authorized to access the service ."), @ApiResponse(responseCode = "404", description = " was not found."), @ApiResponse(responseCode = "415", description = "Unknown media-type received."), @ApiResponse(responseCode = "500", description = "Internal Server Error"), @ApiResponse(responseCode = "503", description = "Temporarily Unavailable") }, parameters = { @Parameter(name = "deleted", description = ".", required = true) } ) @Get(uri="/get}") @Produces(value = {"application/json"}) @Secured({SecurityRule.IS_ANONYMOUS}) public Mono<HttpResponse<"deleted">> get( @PathVariable(value="deleted") @NotNull @Min(1L) Long assuranceSubscriptionId, @Nullable Authentication authentication ) { // Timplementation here }

}`