Akka-http and handling an empty body during POST - scala

Suppose I am handling a post message.
The Content-Type may be application/xml or application/json. I can unmarshal XML and JSON documents just fine.
However, I would like to handle the case where the POST method is called with an empty body (the content-type may still be xml or json, but the document itself is an empty string). This is for various logging and analytics purposes. Right now, the method fails since the empty body cannot be processed by either one of my unmarshallers.
How do I write an unmarshaller for this case? Or can a unmarshaller handle a null body?
Somewhere in my code I have the following. Do I need more marshallers?
implicit def myUnmarshaller(implicit mat: Materializer): FromEntityUnmarshaller[MyClass] =
Unmarshaller.firstOf[HttpEntity, MyClass](
/* myNullBodyMarshaller, */ // do I need this?
myJsonUnmarshaller,
myXmlUnmarshaller
)
or can I modify an existing one? For example, this one?
def myJsonUnmarshaller(implicit mat: Materializer): FromEntityUnmarshaller[MyClass] =
Unmarshaller.byteStringUnmarshaller.forContentTypes(MediaTypes.`application/json`).mapWithCharset { (data, charset) ⇒
val input: String = if (charset == HttpCharsets.`UTF-8`) data.utf8String else data.decodeString(charset.nioCharset.name)
val tmp = input.parseJson.convertTo[MyClass]
MyClass(tmp.A, tmp.B)
}

Related

Http4s Client Encode Entity as x-www-form-urlencoded Recursively

I have a request like the following
val request =
Request[IO](
method = POST,
uri = Uri.uri("..."),
headers = Headers(
Authorization(BasicCredentials("...", "..."))
)
)
.withEntity(PaymentIntentRequest2(2000, "usd"))
I am looking at the source code and it looks like the withEntity inherits the headers from the nested EntityDecoder so the code above defaults to Content-Type: application/json. Where as if I explicitly pass in UrlForm everything is fine.
Unfortunately the API I am hitting expected the data as x-www-form-urlencoded and given the complexity of the target API with all the different endpoints/requests I would like to find a way to encode the given case class as a form. What is the best way of doing that?
I have tried:
Explicitly specifying the Content-Type but this doesn't work because the inherited type takes priority
Building an implicit generic conversion from Product to UrlForm (extension method for now)
implicit class UrlFormEncode[+B <: Product](val u: B) {
def asUrlForm: UrlForm =
u.productElementNames
.zip(u.productIterator)
.foldLeft(UrlForm()) { (a, b) =>
a.combine(UrlForm(b._1 -> b._2.toString))
}
}
The problem here is UrlForm expects a string in both sides of the mapping. And if I just convert things with .toString it doesn't work because of nested typed for example:
ChargeRequest(Amount(refInt), EUR, source = Some(SourceId("...."))
Results in the following json which is not valid
{
"currency": "EUR",
"amount": "2000",
"source": "Some(SourceId(....))",
"customer": "None"
}
I tried asJson instead of toString but circe can not decide on the proper KeyEncoder
What is the right way of approaching this so the given Product is encoded down the stream ?
I just faced the same issue and this is the way it worked for me.
From https://http4s.org/v0.20/client/
// This import will add the right `apply` to the POST.
import org.http4s.client.dsl.io._
val form = UrlForm(
OAuthAttribute.Code -> code,
OAuthAttribute.RedirectUri -> callbackUri,
OAuthAttribute.GrantType -> "authorization_code"
)
private def buildRequest(tokenUri: Uri, form: UrlForm, header: String): Request[IO] =
POST(
form,
tokenUri,
Header.Raw(CIString("Authorization"), header),
Header.Raw(CIString("Content-Type"), "application/x-www-form-urlencoded"),
Header.Raw(CIString("Accept"), "application/json")
)
And that's it. For some strange reason using .withHeaders didn't work for me, seems like they are overridden or so.

set header as accept and value as application/json in Requestbuilding in play framework and scala Akka-type

beans.scala - class contains connection to server
lazy val ConnectionFlow: Flow[HttpRequest, HttpResponse, Any] =
Http().outgoingConnection(config.getString("host"), config.getInt("port"))
lazy val AppService = new Service(config, ConnectionFlow)
Service.scala class
def Request(request: HttpRequest): Future[HttpResponse] =
Source.single(request).via(ConnectionFlow).runWith(Sink.head)
//building Json request
val reqJs = Json.obj("PARAMS" -> Json.obj("param1" -> value1))
Request(RequestBuilding.Post("/services/serviceName",reqJS).flatMap { response =>
// need response to be in JSobject format but the service returns application/xml format
If I understand correctly, you're asking how to modify the request s.t. it indicates to the server it expects a JSON response.
To do that, you can try
val builder = RequestBuilding.Post("/services/serviceName",reqJS)
builder.addHeader(Accept(MediaRange(`application/json`)))
// send out the request as you did
According to https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept, Accept Header only advertisers which content types the client is able to understand, while it is fully on the server's mercy to respect this request. That being said, the server could literally respond with anything they want, despite the header you send.

Scala, Hammock - retrieve http response headers and convert JSON to custom object

I have created a simple program which use Hammock(https://github.com/pepegar/hammock) and now I would like to get response from github API with reposne's headers. I created a code like this:
object GitHttpClient extends App {
implicit val decoder = jsonOf[IO, List[GitRepository]]
implicit val interpreter = ApacheInterpreter.instance[IO]
val response = Hammock
.request(Method.GET, uri"https://api.github.com/orgs/github/repos?per_page=3", Map())
.as[List[GitRepository]]
.exec[IO]
.unsafeRunSync()
println(response)
}
case class GitRepository(full_name: String, contributors_url: String)
And it works fine, I got Git data mapped to my object. But now I also want to get headers from response and I cannot do this by simple response.headers. Only when I remove .as[List[GitRepository]] line and have whole HttpResponse I could access headers. Is it possible to get headers without parsing whole HttpResponse?
I solved this problem by using Decoder after received reponse:
val response = Hammock
.request(Method.GET, uri"https://api.github.com/orgs/github/repos?per_page=3", Map())
.exec[IO]
.unsafeRunSync()
println(response.headers("Link") contains ("next"))
println(HammockDecoder[List[GitRepository]].decode(response.entity))

Cannot mock WSRequest.post() using scalamock

I am writing unit tests for Play application using Scalamock and Scalatest.
My original code looks like:
// Here ws is an injected WSClient
val req = Json.toJson(someRequestObject)
val resp: Future[WSResponse] = ws.url(remoteURL).post(Json.toJson(req))
In a part I have to mock external calls to a web service, which I am trying to do using scalamock:
ws = stub[WSClient]
wsReq = stub[WSRequest]
wsResp = stub[WSResponse]
ws.url _ when(*) returns wsReq
wsReq.withRequestTimeout _ when(*) returns wsReq
(wsReq.post (_: java.io.File)).when(*) returns Future(wsResp)
I am successfully able to mock post requests using a file, but I cannot mock post requests using JSON.
I tried putting stub function references separately like:
val f: StubFunction1[java.io.File, Future[WSResponse]] = wsReq.post (_: java.io.File)
val j: StubFunction1[JsValue, Future[WSResponse]] = wsReq.post(_: JsValue)
I get the compile error for second line: Unable to resolve overloaded method post
What am I missing here? Why cannot I mock one overloaded method but not the other one?
play.api.libs.ws.WSRequest has two post methods (https://www.playframework.com/documentation/2.4.x/api/scala/index.html#play.api.libs.ws.WSRequest), taking:
File
T (where T has an implicit bounds on Writeable)
The compiler is failing because you are trying to calling post with a single parameter, which only matches version 1. However, JsValue cannot be substituted with File.
You actually want to call the 2nd version, but this is a curried method that takes two sets of parameters (albeit the 2nd are implicit). Therefore you need to explicitly provide the mock value that you expect for the implicit, i.e.
val j: StubFunction1[JsValue, Future[WSResponse]] = wsReq.post(_: JsValue)(implicitly[Writeable[JsValue]])
Therefore a working solution would be:
(wsReq.post(_)(_)).when(*) returns Future(wsResp)
Old answer:
WSRequest provides 4 overloads of post method (https://www.playframework.com/documentation/2.5.8/api/java/play/libs/ws/WSRequest.html), taking:
String
JsonNode
InputStream
File
You can mock with a File because it matches overload 4, but JsValue does not match (this is part of the Play JSON model, whereas JsonNode is part of the Jackson JSON model). If you convert to a String or JsonNode, then it will resolve the correct overload and compile.
My best guess is that your WSRequest is actually a play.libs.ws.WSRequest which is part of the Java API, instead you should use play.api.libs.ws.WSRequest which is the Scala API.
The method WSRequest.post exists and BodyWritable[JsValue] is implicitly provided by WSBodyWritables in the Scala API but not in the Java API.
Another cause could be that your JsValue is not a play.api.libs.json.JsValue but something else (e.g. spray.json.JsValue).
I'll quote an example where I have successfully achieved what you are trying to do, the main difference is that I used mock instead of stub.
The important part is:
val ws = mock[WSClient]
val responseBody = "{...}"
...
"availableBooks" should {
"retrieve available books" in {
val expectedBooks = "BTC_DASH ETH_DASH USDT_LTC BNB_LTC".split(" ").map(Book.fromString).map(_.get).toList
val request = mock[WSRequest]
val response = mock[WSResponse]
val json = Json.parse(responseBody)
when(ws.url(anyString)).thenReturn(request)
when(response.status).thenReturn(200)
when(response.json).thenReturn(json)
when(request.get()).thenReturn(Future.successful(response))
whenReady(service.availableBooks()) { books =>
books.size mustEqual expectedBooks.size
books.sortBy(_.string) mustEqual expectedBooks.sortBy(_.string)
}
}
}
An you could see the complete test at: BinanceServiceSpec
I guess it should work fine, if you mock a response that is JsValue.
when(wsReq.post(Json.parse("""{...json request...}"""))).thenReturn(Future(wsResp))
Here Json.parse returns JsValue. Yo should pass the json string that you expect in the request body.

Play (Scala) handling different Content-Type within the same action

I am developing a web service which accepts JSON data. Sometimes input data comes with attachments like image or some PDF file. In that case this data comes as multi-part data.
I need to create action which accepts both content type. And depending on content type it should be able to parse the json and retrieve the attachment related metadata from json and then download the attachment.
I have two actions which handles things separately
def multiPartAction: Action[MultipartFormData[Array[Byte]]] = Action(multipartFormDataAsBytes)={request =>
...
}
Second action
def handleJSon: Action[JsValue] = Action.async(parse.json) {
request =>
...
}
How do I handle these two actions together in one action?
You can either specify your own body parser that is a combination of two, similarly to how it is done here, or leave the body parser out, sticking to default AnyContent body type. Then:
def action = Action { request =>
val body: AnyContent = request.body
val jsonBody: Option[JsValue] = body.asJson
val multipartBody: Option[MultipartFormData[TemporaryFile] =
body.asMultipartFormData
(jsonBody map getResponseForJson) orElse
(multipartBody map getResponseForAttachment) getOrElse
BadRequest("Unsupported request body")
}