How to get status code of HttpCall with Ktor and kotlinx serialization - ktor

I am trying to figure out how to check the http status code of a http request with Ktor
I have a simple GET request like this with a HttpResponseObject that holds the data the server returns and any errors server side that I control
val response:HttpResponseObject<MyObject> = client.get<HttpResponseObject<MyObject>>(url)
Now what I need to also be able to check are is if there are unhandled exceptions or Authentication exceptions that get thrown by the server. In these cases nothing would be returned by the server and a status code of 500 or 401 error would be returned.
I see the documentation has you can get the full http response with something like this
val response:HttpResponse client.get(url)
but then how do lose my serialized data coming back and I couldnt find any examples on how to serialize it from the HttpResponse object.
Does anyone have any suggestions? is there a way to get the http status code from my first example?

You can try getting the status code by using the following code:
val response = client.get<HttpResponse>(url) after that, to get the bytes from the response and serialize it you can try using val bytes: ByteArray = response.readBytes()
You can find full documentation here :
https://ktor.io/clients/http-client/quick-start/responses.html

What I ended up doing was using the HttpResponseValidator in the HttpClientConfig to catch the status codes then throw exceptions
HttpResponseValidator{
validateResponse { response: HttpResponse ->
val statusCode = response.status.value
when (statusCode) {
in 300..399 -> throw RedirectResponseException(response)
in 400..499 -> throw ClientRequestException(response)
in 500..599 -> throw ServerResponseException(response)
}
if (statusCode >= 600) {
throw ResponseException(response)
}
}
}
By doing so I was then able to pass the error through my custom object back up to the UI
private suspend fun getCurrentWeatherForUrl(url:String, callback: (HttpResponseObject<MyObject>?) -> Unit){
var response:HttpResponseObject<MyObject>? = null
response = try{
client.get<HttpResponseObject<MyObject>>(url){
header("Authorization", "Bearer $authKey")
}
}catch (e:Exception){
HttpResponseObject(null, e.toString())
}
callback(response)
}
Also you can use HttpResponse.receive() to get a serialized object AND the response data
val response:HttpResponse = client.get(url)
val myObject:MyObject = response.receive<MyObject>()

HttpResponse is deprecated, you need to use HttpStatement and then get the status after calling execute() on it.

Related

Ktor response streaming

I am trying to call a twitter endpoint that gives you a constant streams of json results back to the client
https://documenter.getpostman.com/view/9956214/T1LMiT5U#977c147d-0462-4553-adfa-d7a1fe59c3ec
I try to make a call to the endpoint like this
val url = "https://api.twitter.com/2/tweets/search/stream"
_streamChannel = _client.get<ByteReadChannel>(token, url) //Stops here
val byteBufferSize = 1024
val byteBuffer = ByteArray(byteBufferSize)
_streamChannel?.let {
while (_streamChannel!!.availableForRead > 0) {
_streamChannel!!.readAvailable(byteBuffer, 0, byteBufferSize)
val s = String(byteBuffer)
parseStreamResponseString(s).forEach {
emit(Response.Success(it))
}
}
}
my client.get code is this
suspend inline fun <reified T> get(authKey: String, url: String): T? {
val response = _client.get<HttpResponse>(url) {
header("Authorization", "Bearer $authKey")
}
when (response.status.value) {
in 300..399 -> throw RedirectResponseException(response)
in 400..499 -> throw ClientRequestException(response)
in 500..599 -> throw ServerResponseException(response)
}
if (response.status.value >= 600) {
throw ResponseException(response)
}
return response.receive<T>()
}
When I make the request it just sits there in what I am assuming is waiting for the full response to be returned before giving it to me.
Edit
I also tried using scoped streaming but it just sits at the line readAvailable I know there are messages coming through because when I run the request via cURL I am constantly getting data
_client.get<HttpStatement> {
header("Authorization", "Bearer $authKey")
url(urlString)
contentType(ContentType.Application.Json)
method = HttpMethod.Get
}.execute {
val streamChannel = it.receive<ByteReadChannel>()
val byteBufferSize = 1024
val byteBuffer = ByteArray(byteBufferSize)
streamChannel.readAvailable(byteBuffer, 0, byteBufferSize) // Stops here
val s = String(byteBuffer)
}
How do I process a constant stream of json data using Ktor?
As far as I am aware, the Ktor client does note expose access to the IO buffer of the request in the way that twitter's streaming API requires.
From the twitter documentation here:
Some HTTP client libraries only return the response body after the connection has been closed by the server. These clients will not work for accessing the Streaming API. You must use an HTTP client that will return response data incrementally. Most robust HTTP client libraries will provide this functionality. The Apache HttpClient will handle this use case, for example.
What you are doing is telling Ktor that the thing you are getting is a ByteReadChannel, and so, once the request closes (which will never happen with this twitter endpoint) the Ktor client would attempt to use whatever plugin (json for example) you were using to parse that data into a ByteReadChannel. It would also not be able to do that, because the data you are getting from twitter is not a ByteReadChannel, it is a new line seperated list of json objects.

How can I send POST request to other server with Ktor server?

I use IDEA to generate a template and notice that runBlocking in Application.module like:
runBlocking {
// Sample for making a HTTP Client request
val message = client.post<JsonSampleClass> {
url("http://127.0.0.1:8080/path/to/endpoint")
contentType(ContentType.Application.Json)
body = JsonSampleClass(hello = "world")
}
}
But when I write like that to send a Post request to another server (such as a server to get weather), I got:
java.io.IOException: Broken pipe
I don't know that if I write it in a wrong way or just in a wrong place.
for sure, the date class is worth JsonSampleClass, you need to change this class as in the overgrowth response or use HttpResponse.
Example:
runBlocking {
// Sample for making a HTTP Client request
val message = client.post<HttpResponse> { // or your data class
url("url")
contentType(ContentType.Application.Json)
body = your data class
}
}

How to set body of HttpServletResponse using ktor client

I have spring boot controller
#PostMapping(path = ["/download"])
fun getFile(#RequestBody myObjectRq: myObjectRq, httpServletResponse: HttpServletResponse): CompletableFuture<HttpServletResponse> {
return GlobalScope.async {
val response = webService.getFile(myObjectRq)
response?.let {
httpServletResponse.setHeader("Content-Type", response.headers.get("Content-Type"))
httpServletResponse.setHeader("Content-Disposition", response.headers.get("Content-Disposition"))
httpServletResponse.writer.write(String(response.content.toByteArray()))
httpServletResponse.writer.flush()
httpServletResponse.status = response.status.value
}
httpServletResponse
}.asCompletableFuture()
}
in which I use service which in turn uses ktor client to send post request to external server which should respond sending csv file. csv file content depends on values I send in myObjectRq.
Service:
suspend fun getFile(myObjectRq: myObjectRq): HttpResponse {
val response = ktorClient.post<HttpResponse> {
accept(ContentType.Application.OctetStream)
url(externalWebServerUrl)
body = myObjectRq
contentType(ContentType.Application.Json)
}
log.info(String(response.content.toByteArray()))
response
}
Headers in response are properly set, also log.info(String(response.content.toByteArray())) in the method prints out the content of received file, but I can't set it as a body of HttpServletResponse. I keep getting org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation.
Also I get Inappropriate blocking method call for httpServletResponse.writer which kind of breaks async qualities of ktor client.
What do I do wrong? How should I solve it?
So, I think SpringBoot is confused with your return type. It is trying to find a way to serialize your return CompletableFuture<HttpServletResponse> into the body of the HTTP response but failing. I believe you can achieve the same result by changing your implementation as follows:
#PostMapping(path = ["/download"])
fun getFile(#RequestBody myObjectRq: myObjectRq, httpServletResponse: HttpServletResponse): CompletableFuture<Void> {
return GlobalScope.async {
val response = webService.getFile(myObjectRq)
response?.let {
httpServletResponse.setHeader("Content-Type", response.headers.get("Content-Type"))
httpServletResponse.setHeader("Content-Disposition", response.headers.get("Content-Disposition"))
httpServletResponse.writer.write(String(response.content.toByteArray()))
httpServletResponse.writer.flush()
httpServletResponse.status = response.status.value
}
null
}.asCompletableFuture()
}
I actually managed to solve this using CompletableFuture<ResponseEntity<ByteArray>> as return type and setting body of the response this way:
ResponseEntity.ok().body(response.content.toByteArray())
This also removed Inappropriate blocking method call warnings.

How to "peep" if Flux has exception?

I use Spring Webflux with Tomcat servlet container (spring-boot-starter-web + spring-boot-starter-webflux) and I would like to get the following result:
If flux of fails immediately, I would like sent to client response code 400
Otherwise, I would like to sent response code 200 and stream the flux
I tried different solutions, but no one works. v1 and v2 does not sent expected response code if failure scenario, v3 does not stream output is happy scenario.
I would like to "peep" exception on failFlux and trigger the exception before response code 200 is sent
#RequestMapping(produces = MediaType.APPLICATION_STREAM_JSON_VALUE)
public class X {
Flux<String> happyFlux = Flux.generate(s -> s.next("x"));
Flux<String> failFlux = Flux.error(new ResponseStatusException(BAD_REQUEST));
//ok: flux is streamed
#RequestMapping("/v1/happy")
Flux<String> v1Happy() {
return happyFlux;
}
//nok: http status code is 200
#RequestMapping("/v1/fail")
Flux<String> v1Fail() {
return failFlux;
}
//ok: flux is streamed
#RequestMapping("/v2/happy")
Mono<ResponseEntity<Flux<String>>> v2Happy() {
return Mono.just(ResponseEntity.ok().body(happyFlux));
}
//nok: http status code is 200
#RequestMapping("/v2/fail")
Mono<ResponseEntity<Flux<String>>> v2Fail() {
return Mono.just(ResponseEntity.ok().body(failFlux));
}
//nok: flux is not streamed but collected on server side
#RequestMapping("/v3/happy")
Mono<ResponseEntity<List<String>>> v3Happy() {
return happyFlux.collectList().map(ResponseEntity::ok);
}
//ok: http status code is 400
#RequestMapping("/v3/fail")
Mono<ResponseEntity<List<String>>> v3Fail() {
return failFlux.collectList().map(ResponseEntity::ok);
}
PS. What is interesting, v1 and v2 works with netty (only spring-boot-starter-webflux).
Update
I think "peeping" Flux is impossible. What I really is better Flux handling in Spring for servlet stack: https://jira.spring.io/browse/SPR-17440
I would advise not to use collectList() as that defeats the entire purpose of producing a Stream.
I believe you should be getting a 500 in case of exception message.
For instance, check the below code.
public Mono<ServerResponse> listPeople(ServerRequest request) {
int error = 10/0;
Flux<Person> peopleFlux = this.repository.allPeople();
peopleFlux = withDelay(peopleFlux);
return ServerResponse.ok().contentType(MediaType.TEXT_EVENT_STREAM).body(peopleFlux, Person.class);
}
The statement
int error = 10/0;
causes a 500 exception and in the client I do get 500. If I comment out the error statement then I get a 200.
So, please share you code if you are not getting 500. Please note that if the error happens after the server has started returning individual events in the stream then it will not be a 500.
You should rather use HTTP 207. https://httpstatuses.com/207

How do I get the message from an API using Flurl?

I've created an API in .NET Core 2 using C#. It returns an ActionResult with a status code and string message. In another application, I call the API using Flurl. I can get the status code number, but I can't find a way to get the message. How do I get the message or what do I need to change in the API to put the message someway Flurl can get it?
Here's the code for the API. The "message" in this example is "Sorry!".
[HttpPost("{orderID}/SendEmail")]
[Produces("application/json", Type = typeof(string))]
public ActionResult Post(int orderID)
{
return StatusCode(500, "Sorry!");
}
Here's the code in another app calling the API. I can get the status code number (500) using (int)getRespParams.StatusCode and the status code text (InternalError) using getRespParams.StatusCode, but how do I get the "Sorry!" message?
var getRespParams = await $"http://localhost:1234/api/Orders/{orderID}/SendEmail".PostUrlEncodedAsync();
int statusCodeNumber = (int)getRespParams.StatusCode;
PostUrlEncodedAsync returns an HttpResponseMessage object. To get the body as a string, just do this:
var message = await getRespParams.Content.ReadAsStringAsync();
One thing to note is that Flurl throws an exception on non-2XX responses by default. (This is configurable). Often you only care about the status code if the call is unsuccessful, so a typical pattern is to use a try/catch block:
try {
var obj = await url
.PostAsync(...)
.ReceiveJson<MyResponseType>();
}
catch (FlurlHttpException ex) {
var status = ex.Call.HttpStatus;
var message = await ex.GetResponseStringAsync();
}
One advantage here is you can use Flurl's ReceiveJson to get the response body directly in successful cases, and get the error body (which is a different shape) separately in the catch block. That way you're not dealing with deserializing a "raw" HttpResponseMessage at all.