I'm trying to create a client which I use to test my controller
The controller
#Secured(SecurityRule.IS_AUTHENTICATED)
#Controller
class InjuryController(private val userService: UserService, private val injuryService: InjuryService) {
...
#Consumes(MediaType.MULTIPART_FORM_DATA)
#Post("/injuries/{id}/images")
fun postImage(id: Long, file: CompletedFileUpload, principal: Principal): HttpResponse<*>? {
...
return HttpResponse.ok(imageReference)
}
...
}
The client
#Client("/")
interface InjuryClient {
#Post("/injuries/{id}/images", produces = [MULTIPART_FORM_DATA])
fun postImage(id: Long, body: MultipartBody, #Header authorization: String): ImageReference
}
The test
#Test
fun `Post an image an injury`() {
// Given
val description = "description"
val occurredAt = LocalDateTime.now()
val id = createInjury(description, occurredAt).id
val toWrite = "test file"
val file = File.createTempFile("data", ".txt")
FileWriter(file).apply {
write(toWrite)
close()
}
val requestBody = MultipartBody.builder()
.addPart("data",
file.name,
MediaType.TEXT_PLAIN_TYPE,
file
).build()
// When
val response = injuryClient.postImage(id, requestBody, authorization)
// Then
assertEquals("$id:${file.name}", response.key)
}
The error
The type java.util.LinkedHashMap is not a supported type for a multipart request body
io.micronaut.http.multipart.MultipartException: The type java.util.LinkedHashMap is not a supported type for a multipart request body
at io.micronaut.http.client.DefaultHttpClient.buildMultipartRequest(DefaultHttpClient.java:2063)
at io.micronaut.http.client.DefaultHttpClient.buildNettyRequest(DefaultHttpClient.java:1480)
at io.micronaut.http.client.DefaultHttpClient.sendRequestThroughChannel(DefaultHttpClient.java:1599)
at io.micronaut.http.client.DefaultHttpClient.lambda$null$27(DefaultHttpClient.java:1035)
at io.netty.util.concurrent.DefaultPromise.notifyListener0(DefaultPromise.java:577)
at io.netty.util.concurrent.DefaultPromise.notifyListeners0(DefaultPromise.java:570)
at io.netty.util.concurrent.DefaultPromise.notifyListenersNow(DefaultPromise.java:549)
at io.netty.util.concurrent.DefaultPromise.notifyListeners(DefaultPromise.java:490)
at io.netty.util.concurrent.DefaultPromise.setValue0(DefaultPromise.java:615)
at io.netty.util.concurrent.DefaultPromise.setSuccess0(DefaultPromise.java:604)
at io.netty.util.concurrent.DefaultPromise.trySuccess(DefaultPromise.java:104)
at io.netty.channel.DefaultChannelPromise.trySuccess(DefaultChannelPromise.java:84)
at io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe.fulfillConnectPromise(AbstractNioChannel.java:300)
at io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe.finishConnect(AbstractNioChannel.java:335)
at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:688)
at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:635)
at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:552)
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:514)
at io.netty.util.concurrent.SingleThreadEventExecutor$6.run(SingleThreadEventExecutor.java:1044)
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:834)
Any clue about how to get rid of that error?
The client assumes method arguments are members of the body instead of each one representing the entire body. You can achieve the desired behavior by annotating the body argument in the client with #Body
Related
I have a POST Gateway filter that I want to modify the response body with the response of a separate webclient request within the gateway filter. I am able to get as far as sending the WebClient().create().post().exchange() within my custom filter. I can see this in my logs
onStateChange(POST{uri=/test, connection=PooledConnection{channel=[id: 0x38eb2b4f, L:/127.0.0.1:51643 - R:localhost/127.0.0.1:9000]}}, [request_prepared])
and
onStateChange(POST{uri=/test, connection=PooledConnection{channel=[id: 0x38eb2b4f, L:/127.0.0.1:51643 - R:localhost/127.0.0.1:9000]}}, [request_sent])
the connection hangs here and doesn't complete.
here is my custom filter code
class PostProxyGatewayFilterFactory : AbstractGatewayFilterFactory<PostProxyGatewayFilterFactory.Params>(Params::class.java) {
override fun apply(params: Params): GatewayFilter {
val cachedBody = StringBuilder()
return GatewayFilter { exchange: ServerWebExchange, chain: GatewayFilterChain ->
chain.filter(exchange).then(
executeRequest(cachedBody,exchange, params)
.map {
val mr = ResponseHandler(exchange)
mr.mutateResponse(it.body.toString())
}.flatMap{
it
}
) }
}
data class Params(
val urlPath: String = "",
)
private fun cache(cachedBody: StringBuilder, buffer: DataBuffer) {
cachedBody.append(Charsets.UTF_8.decode(buffer.asByteBuffer())
.toString())
}
private fun executeRequest(cachedBody: StringBuilder, exchange: ServerWebExchange, params: PostProxyGatewayFilterFactory.Params): Mono<ResponseEntity<JsonNode>>{
val request = when(exchange.request.method){
HttpMethod.PUT -> WebClient.create().put().uri(params.urlPath).body(BodyInserters.fromDataBuffers(exchange.request.body.doOnNext{ cache(cachedBody, it)}))
HttpMethod.POST -> WebClient.create().post().uri(params.urlPath).body(BodyInserters.fromDataBuffers(exchange.request.body.doOnNext{ cache(cachedBody, it)}))
HttpMethod.GET -> WebClient.create().get().uri(params.urlPath)
HttpMethod.DELETE -> WebClient.create().delete().uri(params.urlPath)
else -> throw Exception("Invalid request method passed in to the proxy filter")
}
return request.headers { it.addAll(exchange.request.headers) }
.exchange()
.flatMap{
it.toEntity(JsonNode::class.java)
}
}
}
here is my ResponseHandler class
class ResponseHandler(val delegate: ServerWebExchange) {
fun mutateResponse(body: String): Mono<Void> {
val bytes: ByteArray = body.toByteArray(StandardCharsets.UTF_8)
val buffer: DataBuffer = delegate.response.bufferFactory().wrap(bytes)
return delegate.response.writeWith(Flux.just(buffer))
}
}
here is application.yml
- id: proxy
uri: http://${HOST:localhost}:${PORT:9000}
predicates:
- Path=/proxy/**
filters:
- RewritePath=/test(?<segment>/?.*), $\{segment}
- name: PostProxy
args:
proxyBasePath: http://localhost:9000/thisSecond
So the idea is to send request to localhost:9001/test/thisFirst (proxied to localhost:9000/thisFirst which does happen successfully),get this response back and do nothing with this response, make WebClient request to localhost:9000/thisSecond via executeRequest() ,return this response, and then use that response as the new exchange.response body. I am also not sure if ResponseHandler is correct as executeRequest() never finishes. This would be part 2 of the question once I can resolve why executeRequest() never finishes.
I'm trying to create endpoint for a post request:
#Singleton
#Controller("/v1")
class Addr() {
#Post("/setAddress")
fun set(#Body body: RpcRequest<Address>) {
println(body.params.newAddress)
}
}
Data:
data class Address(val newAddress: String)
#JsonIgnoreProperties(ignoreUnknown = true)
data class RpcRequest<T>(
val method: String,
val params: T
)
But unfortunately, error occured:
java.util.LinkedHashMap cannot be cast to com.project.location.data.Address
Seems like Micronaut couldn't resolve my Address data class inside RpcRequest ?
I'm a newbie about Kotlin. My first project is to consume a rest api. I already made that using retrofit. But I have a problem when I'm logging the response, my data class is null. I don't know where is the error.
My Rerofit Client
object RetrofitClient {
var retrofit: Retrofit? = null
fun getClient(baseUrl: String): Retrofit? {
if (retrofit == null) {
//TODO While release in Google Play Change the Level to NONE
val interceptor = HttpLoggingInterceptor()
interceptor.level = HttpLoggingInterceptor.Level.BODY
val client = OkHttpClient.Builder()
.addInterceptor(interceptor)
.connectTimeout(100, TimeUnit.SECONDS)
.readTimeout(100, TimeUnit.SECONDS)
.build()
retrofit = Retrofit.Builder()
.client(client)
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
return retrofit
}
}
My Interface
public interface ApiLoginService {
#POST("UserManagementCoreAPI/api/v1/users")
fun loginService(#Body request: RequestBody): Call<DataLogin>
}
object ApiUtils {
val BASE_URL = "------"
val apiLoginService: ApiLoginService
get() = RetrofitClient.getClient(BASE_URL)!!.create(ApiLoginService::class.java)
}
My class data
data class DataLogin (
#SerializedName("employeeId") val employeeId : String,
#SerializedName("fullName") val fullName : String,
#SerializedName("loginConfins") val loginConfins : String,
#SerializedName("branchId") val branchId : String,
#SerializedName("isActive") val isActive : String
)
Main Activity
mApiLoginService!!.loginService(requestBody).enqueue(object : Callback<DataLogin>{
override fun onResponse(call: Call<DataLogin>, response: Response<DataLogin>) {
if(response.isSuccessful()){
if(response.body().toString() == null){
Log.d(tag,"Null")
}else{
Log.d(tag,"Logging In " + response.body()!!)
progressBar.visibility = View.GONE
btn_submit.visibility = View.VISIBLE
startActivity(Intent(this#MainActivity, HomeActivity::class.java))
}
}else{
Toast.makeText(applicationContext, "Invalid username or password", Toast.LENGTH_LONG).show()
Log.d(tag,"Error " + response.errorBody().toString())
progressBar.visibility = View.GONE
btn_submit.visibility = View.VISIBLE
}
}
override fun onFailure(call: Call<DataLogin>, t: Throwable) {
progressBar.visibility = View.GONE
btn_submit.visibility = View.VISIBLE
}
})
My Respone Log
message: Logging In DataLogin(employeeId=null, fullName=null, loginConfins=null, branchId=null, isActive=null)
I don't know where is the error and why my data is null. If the response succeeds is still gives me null.
This is a postman example
You have an issue with your schema , Your DataLogin class is different of your postman schema , Retrofit is waiting for : fullName, isActive ...., and the response is : header , data .. , you have to create class that contains header as variable of type Header(errors:List<AnotherClass>), data as variable of type Data(data(List<DataLogin>),totalRecord:Int), i would suggest if you use helper website like JSON to Class , parse your postman response there , and he will give you your correct response class but it's will be in java , you have to rewrite the code yourself of just copy paste in android studio and he will convert the code to Kotlin for you. (in the web site , check Source type: JSON)
You have to match the json structure with your data classes if you do not provide a custom adapter to Gson. So if you want to have a result, maybe something like this will work:
data class Response(
#SerializedName("headers") val headers: List<Any>,
#SerializedName("data") val data: Data
)
data class Data(
#SerializedName("data") val data: List<DataLogin>,
#SerializedName("totalRecord") val totalRecord: Int
)
data class DataLogin (
#SerializedName("employeeId") val employeeId : String,
#SerializedName("fullName") val fullName : String,
#SerializedName("loginConfins") val loginConfins : String,
#SerializedName("branchId") val branchId : String,
#SerializedName("isActive") val isActive : String
)
You need to return a Response object from your retrofit call.
Also a few tips about kotlin, Gson works well for Java, but it has some issues with kotlin (about null safety). I use Moshi when the project language is kotlin and it works well with it.
Try to avoid using !! in kotlin because it will cause RuntimeException. There are other ways of checking null and to protect your code from RuntimeExceptions.
networkCall = NetworkCall(context)
val responceCall = networkCall!!.getRetrofit(true).callReadMeService()
responceCall.clone().enqueue(object : Callback<BaseResponse<*>?> {
override fun onResponse(call: Call<BaseResponse<*>?>, response: Response<BaseResponse<*>?>) {
networkCall!!.dismissLoading()
web_view!!.loadData((response.body()?.data as LinkedTreeMap<*, *>)["description"] as String, "text/html", "UTF-8")
}
override fun onFailure(call: Call<BaseResponse<*>?>, t: Throwable) {
networkCall!!.dismissLoading()
}
})
here is api method
#POST("stories/readme")
fun callReadMeService(): Call<BaseResponse<*>?>
now i am getting this exception
here is my BaseResponse class
class BaseResponse<T> {
var message: String? = null
var status: Boolean = false
var errors: Array<String>? = null
var code: String? = null
var data: T? = null
}
Method return type must not include a type variable or wildcard: retrofit2.Call>
for method IApi.callReadMeService
now i can't remove generic data variable from BaseResponse class, cause i am using this class as a common Api parser
any solution for this
You cannot do this, because the type info needs to be fully specified, otherwise retrofit cannot correctly generate the Service. See this discussion.
You need to create a different API method for each type.
I'm building a very simple application in Kotlin with Vertx and RxJava 2 (RxKotlin), using Kovert REST framework and Retrofit. I have retrofit-vertx adapter and the RxJava2 Retrofit adapter. I can return an arbitrary list from my listUndergroundStations() method, but whenever I try to load from the remote API I get the following error:
Jun 23, 2017 2:16:29 PM uk.amb85.rxweb.api.UndergroundRestController
SEVERE: HTTP CODE 500 - /api/underground/stations - java.io.IOException: java.lang.IllegalStateException: message == null
java.lang.RuntimeException: java.io.IOException: java.lang.IllegalStateException: message == null
at io.reactivex.internal.util.ExceptionHelper.wrapOrThrow(ExceptionHelper.java:45)
at io.reactivex.internal.observers.BlockingMultiObserver.blockingGet(BlockingMultiObserver.java:91)
at io.reactivex.Single.blockingGet(Single.java:2148)
at uk.amb85.rxweb.api.UndergroundRestController$listUndergroundStations$1.invoke(UndergroundRestController.kt:35)
at uk.amb85.rxweb.api.UndergroundRestController$listUndergroundStations$1.invoke(UndergroundRestController.kt:13)
at nl.komponents.kovenant.TaskPromise$wrapper$1.invoke(promises-jvm.kt:138)
at nl.komponents.kovenant.TaskPromise$wrapper$1.invoke(promises-jvm.kt:130)
at nl.komponents.kovenant.NonBlockingDispatcher$ThreadContext.run(dispatcher-jvm.kt:327)
at java.lang.Thread.run(Thread.java:748)
Caused by: java.io.IOException: java.lang.IllegalStateException: message == null
at com.julienviet.retrofit.vertx.VertxCallFactory$VertxCall.lambda$enqueue$0(VertxCallFactory.java:90)
at io.vertx.core.impl.FutureImpl.tryFail(FutureImpl.java:170)
at io.vertx.core.http.impl.HttpClientResponseImpl.handleException(HttpClientResponseImpl.java:270)
at io.vertx.core.http.impl.HttpClientResponseImpl.handleEnd(HttpClientResponseImpl.java:259)
at io.vertx.core.http.impl.ClientConnection.handleResponseEnd(ClientConnection.java:361)
at io.vertx.core.http.impl.ClientHandler.doMessageReceived(ClientHandler.java:80)
at io.vertx.core.http.impl.ClientHandler.doMessageReceived(ClientHandler.java:38)
at io.vertx.core.http.impl.VertxHttpHandler.lambda$channelRead$0(VertxHttpHandler.java:71)
at io.vertx.core.impl.ContextImpl.lambda$wrapTask$2(ContextImpl.java:335)
at io.vertx.core.impl.ContextImpl.executeFromIO(ContextImpl.java:193)
at io.vertx.core.http.impl.VertxHttpHandler.channelRead(VertxHttpHandler.java:71)
at io.vertx.core.net.impl.VertxHandler.channelRead(VertxHandler.java:122)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:363)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:349)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:341)
at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:435)
at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:293)
at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:267)
at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:250)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:363)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:349)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:341)
at io.netty.handler.ssl.SslHandler.unwrap(SslHandler.java:1228)
at io.netty.handler.ssl.SslHandler.decode(SslHandler.java:1039)
at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:411)
at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:248)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:363)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:349)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:341)
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1334)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:363)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:349)
at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:926)
at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:129)
at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:642)
at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:565)
at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:479)
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:441)
at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:858)
... 1 more
Caused by: java.lang.IllegalStateException: message == null
at okhttp3.Response$Builder.build(Response.java:431)
at com.julienviet.retrofit.vertx.VertxCallFactory$VertxCall.lambda$null$1(VertxCallFactory.java:109)
at io.vertx.core.http.impl.HttpClientResponseImpl$BodyHandler.notifyHandler(HttpClientResponseImpl.java:301)
at io.vertx.core.http.impl.HttpClientResponseImpl.lambda$bodyHandler$0(HttpClientResponseImpl.java:193)
at io.vertx.core.http.impl.HttpClientResponseImpl.handleEnd(HttpClientResponseImpl.java:257)
... 36 more
I can't for the life of me work out what is causing the IllegalStateException and have googled it to death. I don't think it's Rx related because I get the same error if I make the method return Observable<List<UndergroundLine>> or even get rid of Rx entirely and return Call<List<UndergroundLine>> (adjusting the controller accordingly). However, beyond that, I'm beating my head against a wall! Is anyone able to point out the error of my ways (besides putting a cushion under my head)?
Main Verticle:
class ApiVerticle : AbstractVerticle() {
override fun start(startFuture: Future<Void>?) {
// Initialise injection.
configureKodein()
val apiRouter = configureRouter(vertx)
vertx.createHttpServer()
.requestHandler { apiRouter.accept(it) }
.listen(8080)
}
private fun configureKodein() {
Kodein.global.addImport(Kodein.Module {
import(TflUndergroundService.module)
})
}
private fun configureRouter(vertx: Vertx): Router {
val apiMountPoint = "api"
val routerInit = fun Router.() {
bindController(UndergroundRestController(), apiMountPoint)
}
val router = Router.router(vertx) initializedBy { router ->
router.routerInit()
}
return router
}
}
TflService:
interface TflService {
#GET("/Line/Mode/tube")
fun getAllUndergroundLines(): Observable<UndergroundLine>
#GET("/Line/{lineName}/StopPoints")
fun getStationsForUndergroundLine(
#Path("lineName") lineName: String
): Observable<UndergroundStation>
#GET("/Line/{lineName}/Arrivals?stopPointId={stationNaptanId")
fun getArrivalsFor(
#Path("lineName") lineName: String,
#Path("stationNaptanId") stationNaptanId: String
) : Observable<Arrival>
}
data class UndergroundLine(val id: String, val name: String)
data class UndergroundStation(val naptanId: String, val commonName: String)
data class Arrival(
val platformName: String,
val towards: String,
val currentLocation: String,
val expectedArrival: LocalDateTime)
object TflUndergroundService {
val module = Kodein.Module {
val vertx: Vertx = Vertx.currentContext().owner()
val client: HttpClient = vertx.createHttpClient()
val jacksonMapper: ObjectMapper = ObjectMapper()
jacksonMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl("https://api.tfl.gov.uk/")
.callFactory(VertxCallFactory(client))
.addCallAdapterFactory(RxJava2CallAdapterFactory.createAsync())
.addConverterFactory(JacksonConverterFactory.create(jacksonMapper))
.build()
val tflService: TflService = retrofit.create(TflService::class.java)
bind<TflService>() with instance(tflService)
}
}
ApiKeySecured (Just requires "appid" to be a parameter):
class ApiKeySecured(private val routingContext: RoutingContext) : KodeinGlobalAware {
val user: String = routingContext.request().getParam("appid") ?: throw HttpErrorUnauthorized()
}
The offending REST controller (in Kovert, Promise's are executed on Vertx worker thread):
class UndergroundRestController(val undergroundService: TflService = Kodein.global.instance()) {
fun ApiKeySecured.listUndergroundStations(): Promise<List<UndergroundLine>, Exception> {
//TODO: This is blocking, fix it!??
return task {
undergroundService
.getAllUndergroundLines()
.doOnError { println(it) }
.toList()
.blockingGet()
}
}
}
build.gradle:
mainClassName = "io.vertx.core.Launcher"
def mainVerticleName = "uk.amb85.rxweb.verticles.ApiVerticle"
def configurationFile = "conf/development.json"
run {
args = ["run",
mainVerticleName,
"--launcher-class=$mainClassName",
"-conf $configurationFile"
]
}
There's an issue with retrofit-vertx you are using. OkHttp3's ResponseBuilder requires message to be not null, but VertxCallFactory doesn't set it.
It's fixed in the latest version, but as it's still in development, you have to use snapshot:
repositories {
mavenCentral()
maven {
url "https://oss.sonatype.org/content/repositories/snapshots"
}
}
dependencies {
compile 'com.julienviet:retrofit-vertx:1.0.2-SNAPSHOT'
}
Switching to snapshot dependency fixes the issue you mention in your question, but there's an issue with json mapping, which can be easily fixed by switching code from:
#GET("/Line/Mode/tube")
fun getAllUndergroundLines(): Observable<UndergroundLine>
to:
#GET("/Line/Mode/tube")
fun getAllUndergroundLines(): Observable<List<UndergroundLine>>
And updating your data classes to have default empty constructor to let Jackson instantiate using reflection:
data class UndergroundLine(var id: String = "", var name: String = "")
More on emtpy default constructor for data classes.
But it's another question related to how to parse response from API you're using to Observable and should be asked if you don't find a workaround.