micronaut-core icon indicating copy to clipboard operation
micronaut-core copied to clipboard

Controller method with both @Body and HttpRequest arguments fail

Open catatafishen opened this issue 1 year ago • 17 comments

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

catatafishen avatar Aug 08 '23 10:08 catatafishen

We have the same problem - any news on this?

runenielsen avatar Aug 31 '23 12:08 runenielsen

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

yawkat avatar Sep 01 '23 07:09 yawkat

@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...

runenielsen avatar Sep 01 '23 08:09 runenielsen

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

yawkat avatar Sep 01 '23 10:09 yawkat

We have the same issue, version 4.1.1

rafaeljimenezgodoy avatar Sep 21 '23 13:09 rafaeljimenezgodoy

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

koebbingd avatar Sep 28 '23 05:09 koebbingd

same here.

gad2103 avatar Oct 02 '23 17:10 gad2103

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

gad2103 avatar Oct 02 '23 21:10 gad2103

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;
    }
}

wade-taylor avatar Oct 09 '23 02:10 wade-taylor

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
    ) {
    }

loicmathieu avatar Dec 14 '23 14:12 loicmathieu

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

graemerocher avatar Dec 14 '23 16:12 graemerocher

Hi, Back to this issue, I used the trick to bind the body to Publisher<StreamingFileUpload> 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.

loicmathieu avatar Mar 29 '24 12:03 loicmathieu

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.

CezaryBD avatar Apr 26 '24 06:04 CezaryBD

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

yawkat avatar Apr 26 '24 06:04 yawkat

@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

KazimirDobrzhinsky avatar Jun 13 '24 09:06 KazimirDobrzhinsky

@KazimirDobrzhinsky provide an example that reproduces it

graemerocher avatar Jun 13 '24 10:06 graemerocher

@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

` <groupId>io.micronaut.platform</groupId> <artifactId>micronaut-parent</artifactId> 4.5.0

<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 }

}`

KazimirDobrzhinsky avatar Jun 14 '24 11:06 KazimirDobrzhinsky