javamicronautmicronaut-clientmicronaut-rest

Required Body [file] not specified Micronaut Multipart upload


Trying to upload the file using declarative HTTP client as below

Http CLient

@Client("http://localhost:8080/product")
public interface IHttpClient {
    @Post(consumes = MediaType.MULTIPART_FORM_DATA, produces = MediaType.MULTIPART_FORM_DATA)
    public String post(@Body MultipartBody file);
}

Dependency injection to Client

@Controller("/productManager")
public class ProductManagerController implements IProductOperation{
    private final IHttpClient iProduct;

    public ProductManagerController(IHttpClient iProduct) {
        this.iProduct = iProduct;
    }

    @Override
    public String post(CompletedFileUpload file) throws IOException {
        MultipartBody requestBody = MultipartBody.builder().addPart("file", file.getFilename(), MediaType.MULTIPART_FORM_DATA_TYPE, file.getBytes()).build();
        return this.iProduct.post(requestBody);
    }
}

Product controller

@Controller("/product")
public class ProductController implements IProductOperation {
    @Post(consumes = MediaType.MULTIPART_FORM_DATA, produces = MediaType.MULTIPART_FORM_DATA)
    public String post(@Body MultipartBody file)  {
        return null;
    }
}

CURL

curl --location --request POST 'http://localhost:8080/productManager' \
--form 'file=@"/Users/macbook/Downloads/anand 001.jpg"'

An exception I am facing

02:32:24.519 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 7442ms. Server Running: http://localhost:8080
02:32:33.985 [default-nioEventLoopGroup-1-4] DEBUG i.m.h.client.netty.DefaultHttpClient - Sending HTTP POST to http://localhost:8080/product
02:32:33.990 [default-nioEventLoopGroup-1-4] TRACE i.m.h.client.netty.DefaultHttpClient - Accept: multipart/form-data
02:32:33.993 [default-nioEventLoopGroup-1-4] TRACE i.m.h.client.netty.DefaultHttpClient - content-type: multipart/form-data; boundary=ac4442578cac3c2
02:32:33.994 [default-nioEventLoopGroup-1-4] TRACE i.m.h.client.netty.DefaultHttpClient - transfer-encoding: chunked
02:32:33.994 [default-nioEventLoopGroup-1-4] TRACE i.m.h.client.netty.DefaultHttpClient - host: localhost:8080
02:32:33.995 [default-nioEventLoopGroup-1-4] TRACE i.m.h.client.netty.DefaultHttpClient - connection: close
02:32:35.260 [default-nioEventLoopGroup-1-4] DEBUG i.m.h.client.netty.DefaultHttpClient - Received response 400 from http://localhost:8080/product
02:32:35.260 [default-nioEventLoopGroup-1-4] TRACE i.m.h.client.netty.DefaultHttpClient - Content-Type: application/json
02:32:35.260 [default-nioEventLoopGroup-1-4] TRACE i.m.h.client.netty.DefaultHttpClient - content-length: 119
02:32:35.260 [default-nioEventLoopGroup-1-4] TRACE i.m.h.client.netty.DefaultHttpClient - connection: close
02:32:35.260 [default-nioEventLoopGroup-1-4] TRACE i.m.h.client.netty.DefaultHttpClient - Response Body
02:32:35.261 [default-nioEventLoopGroup-1-4] TRACE i.m.h.client.netty.DefaultHttpClient - ----
02:32:35.261 [default-nioEventLoopGroup-1-4] TRACE i.m.h.client.netty.DefaultHttpClient - {"message":"Required Body [file] not specified","path":"/file","_links":{"self":{"href":"/product","templated":false}}}
02:32:35.261 [default-nioEventLoopGroup-1-4] TRACE i.m.h.client.netty.DefaultHttpClient - ----
02:32:35.364 [default-nioEventLoopGroup-1-3] ERROR i.m.r.intercept.RecoveryInterceptor - Type [com.example.IProduct$Intercepted] executed with error: Required Body [file] not specified
io.micronaut.http.client.exceptions.HttpClientResponseException: Required Body [file] not specified
    at io.micronaut.http.client.netty.DefaultHttpClient$12.channelRead0(DefaultHttpClient.java:2140)
    at io.micronaut.http.client.netty.DefaultHttpClient$12.channelRead0(DefaultHttpClient.java:2055)
    at io.netty.channel.SimpleChannelInboundHandler.channelRead(SimpleChannelInboundHandler.java:99)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
    at io.micronaut.http.netty.stream.HttpStreamsHandler.channelRead(HttpStreamsHandler.java:193)
    at io.micronaut.http.netty.stream.HttpStreamsClientHandler.channelRead(HttpStreamsClientHandler.java:183)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
    at io.netty.handler.codec.MessageToMessageDecoder.channelRead(MessageToMessageDecoder.java:103)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
    at io.netty.handler.codec.MessageToMessageDecoder.channelRead(MessageToMessageDecoder.java:103)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
    at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:436)
    at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:324)
    at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:296)
    at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:251)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
    at io.netty.handler.timeout.IdleStateHandler.channelRead(IdleStateHandler.java:286)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
    at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
    at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
    at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166)
    at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:719)
    at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:655)
    at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:581)
    at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493)
    at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)
    at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
    at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
    at java.base/java.lang.Thread.run(Thread.java:832)

Solution

  • There seem to be more than one issue within your implementation, and here down the details.

    Client and Server MultipartBody

    The post misses the most important part to diagnose the error, which is class imports.

    When programmatically building multipart requests, Micronaut affords the io.micronaut.http.client.multipart.MultipartBody type to build multipart/form-data requests. Note the FQN (Fully-Qualified-Name) for the MultipartBody which is part of the io.micronaut.http.client.* API.

    On the other hand, the @Controller endpoint should use the respective server type which is: io.micronaut.http.server.multipart.MultipartBody. Note the FQN as well which is part of the io.micronaut.http.server.* API.

    Client HTTP Headers

    Under the hood, the generated Micronaut client will translate your @Client methods annotations to appropriate HTTP headers (the Content-Type HTTP header is the one of interest here). Those headers should obviously match your controller endpoint expected ones.

    While the trivial approach would be to have the same endpoint annotation for the client and controller, this is unfortunately wrong: @Client and @Controller respective methods must have reversed produces and consumes annotation fields so that:

    MIME Types

    In both your client and controller implementation, you are returning a String. The endpoint annotation fields for content types specification should match the returned type which is MediaTye.TEXT_PLAIN for java.lang.String and not MediaType.MULTIPART_FORM_DATA

    Fixed implementation

    The @Client declarative interface then should look like below (with full imports, fixed input and result MIME types):

    import io.micronaut.http.MediaType;
    import io.micronaut.http.annotation.Body;
    import io.micronaut.http.annotation.Post;
    import io.micronaut.http.client.annotation.Client;
    import io.micronaut.http.client.multipart.MultipartBody;
    
    @Client("http://localhost:8080/product")
    public interface IHttpClient {
        @Post(consumes = MediaType.TEXT_PLAIN, produces = MediaType.MULTIPART_FORM_DATA)
        public String post(@Body MultipartBody file);
    }
    

    And here what the @Controller endpoint implementation would be (with appropriate MultipartBody and fixed result MIME type):

    import io.micronaut.http.MediaType;
    import io.micronaut.http.annotation.Body;
    import io.micronaut.http.annotation.Controller;
    import io.micronaut.http.annotation.Post;
    import io.micronaut.http.server.multipart.MultipartBody;
    
    @Controller("/product")
    public class ProductController implements IProductOperation {
        @Post(consumes = MediaType.MULTIPART_FORM_DATA, produces = MediaType.TEXT_PLAIN)
        public String post(@Body MultipartBody file)  {
            return null;
        }
    }