Debug/Log request for akka-http server - akka-http

I have the following test http, and I would like to get all the information from the request in the default route.
class HttpTestServer(port: Int = 6666)(implicit as: ActorSystem, ec: ExecutionContext, mat: ActorMaterializer)
extends JsonSupport
with LazyLogging{
var numRequest: Int = 0
def start(): Future[ServerBinding] = {
def routes = {
// print request information here
logRequest("test")
numRequest += 1
println(s"HttpServer request received: $numRequest")
complete(Map("key"-> 1.0))
}
Http().bindAndHandle(routes, "0.0.0.0", port)
}
}
And I have the following line at logback.xml, to enable the debug at akka-http:
<logger name="akka.http" level="DEBUG"/>
As a references, I am using akka-http DebuggingDirective
UPDATED: My guess is that there is a problem with the LoggingAdapter or incompatibility between LazyLogging and the akka LoggingAdapter. If I print to stdout works fine:
val logRequestPrintln = DebuggingDirectives.logRequest(LoggingMagnet(_ => println _))
....
logRequestPrintln(complete(Map("key"-> 1.0)))

Related

suspend function testing with spock

I have a simple function in kotlin like that :
suspend fun createTicket(#Valid request: CreateTicketRequest, authentication: Authentication): HttpResponse<Any> {
request.customerId = "customerId"
logger().info("Receive by the client $request")
return HttpResponse.created(service.create(request))
}
I've already Mock the request and the authentication.
So, I call it on Spock:
def 'It should create a ticket with success'() {
given:
def request = createRequest(
TICKET_ID,
TICKET_NAME,
TICKET_PHONE,
TICKET_CPF,
TICKET_EMAIL,
TICKET_COMMENT,
TICKET_SUBJECT,
TICKET_TAG
)
when:
response = controller.createTicket(
request,
authentication
)
then:
response != null
}
I'm getting the following error :
Suspend function 'create' should be called only from a coroutine or another suspend function.
Can anyone help me with this question ?
Best regards
Solved I created a Kotlin class code
class CallCreateTicket {
private lateinit var response: HttpResponse<Any>
private fun createTicket(
request: CreateTicketRequest,
authenticator: Authenticator,
controller: TicketController,
): HttpResponse<Any> {
runBlocking {
response = controller.createTicket(request, authenticator)
}
return response
}
}
and I called it on groovy ...
#Mockable(TicketCreateServiceImpl)
class TicketControllerTest extends Specification {
def mockUtil = new MockUtil()
def service = Mock(TicketCreateServiceImpl)
def authenticator = Mock(Authenticator)
def response = Mock(HttpResponse)
def controller = new TicketController(service)
def callCreateTicket = new CallCreateTicket()
def 'check if all instances are mocked'() {
mockUtil.isMock(authentication)
mockUtil.isMock(service)
}
def 'It should call the index function and return a valid String'() {
when:
response = controller.index()
then:
response == INDEX_RETURN
}
def 'It should call the index function and return a invalid String'() {
when:
response = controller.index()
then:
response != INVALID_INDEX_RETURN
}
def 'It should create a ticket with success'() {
given:
def request = createRequest(
TICKET_ID,
TICKET_NAME,
TICKET_PHONE,
TICKET_CPF,
TICKET_EMAIL,
TICKET_COMMENT,
TICKET_SUBJECT,
TICKET_TAG
)
when:
response = callCreateTicket.createTicket(
request,
authenticator,
controller
)
then:
response.status(HttpStatus.CREATED)
}
}

I want send a filepart image with Vertx, I want replicate this case in code like Postman

PostmanExample
fun sendFileToMatch(path:String){
val client = WebClient.create(vertx);
var form = MultipartForm.create()
.binaryFileUpload("image","imageName" , path, "image/jpeg")
client.post(8888, "localhost", "/search?")
.putHeader("content-type", "multipart/form-data")
.sendMultipartForm(form) { }
}
when I run the code show bad request I have put exactly key "image" and send filepart image
TL;DR - your client code seems fine.
The only suspicious part is the path itself, as you don't specify how exactly you get it in your code, and the fact that you didn't specify how you handle response from the server: you just do {} in your example
Here is a fully working example for you to refer to, though:
val vertx = Vertx.vertx()
val router = Router.router(vertx)
router.route().handler(BodyHandler.create());
router.post("/search").handler {
val uploads: Set<FileUpload> = it.fileUploads()
uploads.forEach { upload ->
println(upload.name()) // "image"
println(upload.fileName()) // "imageName"
println(upload.size()) // 42537
}
it.response().end("OK!")
}
vertx.createHttpServer().requestHandler(router)
.listen(8888)
// We read the PNG file from /resources
val path = object {}.javaClass.getResource("5EWx9.png").path
val form = MultipartForm.create()
.binaryFileUpload("image","imageName" , path, "image/png")
val client = WebClient.create(vertx);
client.post(8888, "localhost", "/search?")
.putHeader("content-type", "multipart/form-data")
.sendMultipartForm(form) {
if (it.succeeded()) {
println(it.result().bodyAsString()) // "OK!"
}
else {
println(it.cause())
}
}
As the file to upload, I used the PostmanExample you've provided, which is a PNG image, which I put in the /resources directory of my project.

How to call a service through a menu-driven application in Ballerina

I have a file main.bal which contains the prints the menu and deals with user input. gmail_service.bal file contains a hello service which has the ability to send emails.
main.bal
function main(string... args) {
int c = 0;
while ( c != 2) {
// print options menu to choose from
io:println("-------------------------------------------------------------------------");
io:println("1. Email");
io:println("2. Exit");
io:println("-------------------------------------------------------------------------");
// read user's choice
string choice = io:readln("Enter choice 1 - 2: ");
c = check <int>choice;
if (c == 1) {
//code to send email
}
if (c == 2) {
break;
}
}
}
gmail_service.bal
// A system package containing protocol access constructs
// Package objects referenced with 'http:' in code
import ballerina/http;
import ballerina/io;
import wso2/gmail;
import ballerina/config;
endpoint gmail:Client gmailEP {
clientConfig:{
auth:{
accessToken:config:getAsString("accessToken"),
clientId:config:getAsString("clientId"),
clientSecret:config:getAsString("clientSecret"),
refreshToken:config:getAsString("refreshToken")
}
}
};
documentation {
A service endpoint represents a listener.
}
endpoint http:Listener listener {
port:9090
};
documentation {
A service is a network-accessible API
Advertised on '/hello', port comes from listener endpoint
}
#http:ServiceConfig {
basePath: "/"
}
service<http:Service> hello bind listener {
#http:ResourceConfig {
methods: ["POST"],
path: "/"
}
documentation {
A resource is an invokable API method
Accessible at '/hello/sayHello
'caller' is the client invoking this resource
P{{caller}} Server Connector
P{{request}} Request
}
sayHello (endpoint caller, http:Request request) {
gmail:MessageRequest messageRequest;
messageRequest.recipient = "abc#gmail.com";
messageRequest.sender = "efg#gmail.com";
messageRequest.cc = "";
messageRequest.subject = "Email-Subject";
messageRequest.messageBody = "Email Message Body Text";
//Set the content type of the mail as TEXT_PLAIN or TEXT_HTML.
messageRequest.contentType = gmail:TEXT_PLAIN;
//Send the message.
var sendMessageResponse = gmailEP -> sendMessage("efg#gmail.com", messageRequest);
}
}
How can I invoke the gmail service when the user enters "1"?
In Ballerina, we interact with network-accessible points such as services through endpoints. For example, the your Gmail service source, you have used two endpoints: a listener endpoint and a client endpoint. The listener endpoint is used to bind your hello service to a port and the client endpoint is used to invoke a 3rd party API (the Gmail API).
Similarly, to invoke your hello service from your main() function, you need to create an HTTP client endpoint for the service. You will be interacting with your service through this endpoint. The modified source for main.bal would look something like below. Note that a payload hasn't been set to the POST request since the request body is not used anywhere in the hello service.
import ballerina/http;
import ballerina/io;
endpoint http:Client emailClient {
url: "http://localhost:9090"
};
function main(string... args) {
int c = 0;
while ( c != 2) {
// print options menu to choose from
io:println("-------------------------------------------------------------------------");
io:println("1. Email");
io:println("2. Exit");
io:println("-------------------------------------------------------------------------");
// read user's choice
string choice = io:readln("Enter choice 1 - 2: ");
c = check <int>choice;
if (c == 1) {
http:Response response = check emailClient->post("/", ());
// Do whatever you want with the response
}
if (c == 2) {
break;
}
}
}

RxAlamofire - how to get the response on error?

I need the response body when an error occurs in an RxAlamofire call. I've seen this hack but I wonder if there's a cleaner way.
Inspired by it, I created this RxAlamofire fork with a similar hack. With it, errors will usually be an instance of DataResponseError so you can do this:
RxAlamofire.data(method, url).subscribe(
onError: { error in
if let error = error as? DataResponseError<Data> {
// Get response body (in this case, convert it to a String)
if let data = error.response.data {
let message = String(data: data, encoding: String.Encoding.utf8)
print("Message: \(message)")
}
// Get status code
if let statusCode = error.response.response?.statusCode {
print("Status code: \(statusCode)")
}
}
}
)
Issue description. I'm using RxAlamofire to make network requests, and I needed to get information from the body of the error response.
I've made a hack in a folloing way:
Added a PError:
import UIKit
import Alamofire
import ObjectMapper
class PError: Error, Mappable {
var message: String?
var statusCode: Int?
required init?(map: Map) {
}
func mapping( map: Map) {
message <- map["error.message"]
statusCode <- map["error.statusCode"]
}
}
And now added such extensions to DataRequest:
import Alamofire
import ObjectMapper
extension DataRequest {
//Overriding several methods from Alamofire Validation
#discardableResult
public func validate<S: Sequence>(statusCode acceptableStatusCodes: S) -> Self where S.Iterator.Element == Int {
return validate { [unowned self] _, response, bodyData in
return self.validate(statusCode: acceptableStatusCodes, response: response, bodyData: bodyData)
}
}
//Overriding several methods from Alamofire Validataion
fileprivate func validate<S: Sequence>(
statusCode acceptableStatusCodes: S,
response: HTTPURLResponse, bodyData: Data?)
-> ValidationResult
where S.Iterator.Element == Int
{
if acceptableStatusCodes.contains(response.statusCode) {
return .success
} else {
var error: Error = AFError.responseValidationFailed(reason: AFError.ResponseValidationFailureReason.unacceptableStatusCode(code: response.statusCode))
if let bodyData = bodyData {
if let jsonString = String(data: bodyData, encoding: .utf8) {
if let errorNew = Mapper<PError>().map(JSONString: jsonString)
{
error = errorNew
}
}
}
return .failure(error)
}
}
}
Next, somewhere in the code you'll be able to work with this custom error object:
if let err = error as? PError {
status = err.message ?? "No message in the error description"
}
else

How to define SSLContext with Spray https client?

I want to post http requests to a secured server with a given ca cert.
I'm using Spray 1.3.1, the code looks something like this:
val is = this.getClass().getResourceAsStream("/cacert.crt")
val cf: CertificateFactory = CertificateFactory.getInstance("X.509")
val caCert: X509Certificate = cf.generateCertificate(is).asInstanceOf[X509Certificate];
val tmf: TrustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
val ks: KeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(null);
ks.setCertificateEntry("caCert", caCert);
tmf.init(ks);
implicit val sslContext: SSLContext = SSLContext.getInstance("TLS");
sslContext.init(null, tmf.getTrustManagers(), null);
implicit val timeout: Timeout = Timeout(15.seconds)
import spray.httpx.RequestBuilding._
val respFuture = (IO(Http) ? Post( uri=Uri(url), content="my content")).mapTo[HttpResponse]
The problem is that the defined implicit SSLContext is not taken and I'm getting: "unable to find valid certification path to requested target" on runtime.
How can I define a SSLContext to be used with spray client?
I use the following to define an SSLContext in spray. In my case, I'm using a very permissive context that does not validate the remote server's certificate. Based on the first solution in this post - works for me.
import java.security.SecureRandom
import java.security.cert.X509Certificate
import javax.net.ssl.{SSLContext, X509TrustManager, TrustManager}
import akka.actor.ActorRef
import akka.io.IO
import akka.util.Timeout
import spray.can.Http
import scala.concurrent.Future
trait HttpClient {
/** For the HostConnectorSetup ask operation. */
implicit val ImplicitPoolSetupTimeout: Timeout = 30 seconds
val hostName: String
val hostPort: Int
implicit val sslContext = {
/** Create a trust manager that does not validate certificate chains. */
val permissiveTrustManager: TrustManager = new X509TrustManager() {
override def checkClientTrusted(chain: Array[X509Certificate], authType: String): Unit = {
}
override def checkServerTrusted(chain: Array[X509Certificate], authType: String): Unit = {
}
override def getAcceptedIssuers(): Array[X509Certificate] = {
null
}
}
val initTrustManagers = Array(permissiveTrustManager)
val ctx = SSLContext.getInstance("TLS")
ctx.init(null, initTrustManagers, new SecureRandom())
ctx
}
def initClientPool(): Future[ActorRef] = {
val hostPoolFuture = for {
Http.HostConnectorInfo(connector, _) <- IO(Http) ? Http.HostConnectorSetup(hostName, port = hostPort,
sslEncryption = true)
} yield connector
}
}
I came up with this replacement for sendReceive which allows passing a custom SSLContext (as an implicit)
def mySendReceive( request: HttpRequest )( implicit uri: spray.http.Uri, ec: ExecutionContext, futureTimeout: Timeout = 60.seconds, sslContext: SSLContext = SSLContext.getDefault): Future[ HttpResponse ] = {
implicit val clientSSLEngineProvider = ClientSSLEngineProvider { _ =>
val engine = sslContext.createSSLEngine( )
engine.setUseClientMode( true )
engine
}
for {
Http.HostConnectorInfo( connector, _ ) <- IO( Http ) ? Http.HostConnectorSetup( uri.authority.host.address, port = uri.authority.port, sslEncryption = true )
response <- connector ? request
} yield response match {
case x: HttpResponse ⇒ x
case x: HttpResponsePart ⇒ sys.error( "sendReceive doesn't support chunked responses, try sendTo instead" )
case x: Http.ConnectionClosed ⇒ sys.error( "Connection closed before reception of response: " + x )
case x ⇒ sys.error( "Unexpected response from HTTP transport: " + x )
}
}
Then use it as "usual" (almost see below):
val pipeline: HttpRequest => Future[ HttpResponse ] = mySendReceive
pipeline( Get( uri ) ) map processResponse
There are a couple of things I really do not like though:
it is a hack. I would expect spray-client to allow support of a custom SSLContext natively. These are very useful during dev and test, to force custom TrustManagers typically
there is an implicit uri: spray.http.Uri parameter to avoid hard coding of the host and port on the connector. So uri must be declared implicit.
Any improvement to this code or even better, a patch to spray-client, is most welcome (externalization of the creation of the SSLEngine being an obvious one)
The shortest I've gotten to work is this:
IO(Http) ! HostConnectorSetup(host = Conf.base.getHost, port = 443, sslEncryption = true)
i.e. what's in #reed-sandberg's answer, but no ask pattern seems to be needed. I don't pass a connection parameter to sendReceive but instead:
// `host` is the host part of the service
//
def addHost = { req: HttpRequest => req.withEffectiveUri(true, Host(host, 443)) }
val pipeline: HttpRequest => Future[Seq[PartitionInfo]] = (
addHost
~> sendReceive
~> unmarshal[...]
)
This seems to work, but I would naturally be interested to hear if there are downsides to this approach.
I agree with all spray-client SSL support criticism. It's awkward that something like this is so hard. I probably spent 2 days on it, merging data from various sources (SO, spray documentation, mailing list).