I have a problem that i cannot solve for some time already, plus i'm new to apache camel and it does not help.
My simple app exposes SOAP web service using CXF (with jetty as http engine) then soap request are passed to akka actors using camel.
I want to validate SOAP request on the road to actor and check if it contains certain headers and content values. I do not want to use CXF interceptor. Problem is, that what ever happens in camel (exception, fault message return) is not propagate to cxf. I always get SUCCESS 202 as a response and information about validation exception in logs.
This is my simple app:
class RequestActor extends Actor with WithLogger {
def receive = {
case CamelMessage(body: Request, headers) =>
logger.info(s"Received Request $body [$headers]")
case msg: CamelMessage =>
logger.error(s"unknown message ${msg.body}")
}
}
class CustomRouteBuilder(endpointUrl: String, serviceClassPath: String, system: ActorSystem)
extends RouteBuilder {
def configure {
val requestActor = system.actorOf(Props[RequestActor])
from(s"cxf:${endpointUrl}?serviceClass=${serviceClassPath}")
.onException(classOf[PredicateValidationException])
.handled(true)
.process(new Processor {
override def process(exchange: Exchange): Unit = {
val message = MessageFactory.newInstance().createMessage();
val envelope = message.getSOAPPart().getEnvelope();
val body = message.getSOAPBody();
val fault = body.addFault();
fault.setFaultCode("Server");
fault.setFaultString("Unexpected server error.");
val detail = fault.addDetail();
val entryName = envelope.createName("message");
val entry = detail.addDetailEntry(entryName);
entry.addTextNode("The server is not able to complete the request. Internal error.");
log.info(s"Returning $message")
exchange.getOut.setFault(true)
exchange.getOut.setBody(message)
}
})
.end()
.validate(header("attribute").isEqualTo("for_sure_not_defined"))
.to(genericActor)
}
}
object Init extends App {
implicit val system = ActorSystem("superman")
val camel = CamelExtension(system)
val camelContext = camel.context
val producerTemplate = camel.template
val endpointClassPath = classOf[Service].getName
val endpointUrl = "http://localhost:1234/endpoint"
camel.context.addRoutes(new CustomRouteBuilder(endpointUrl, endpointClassPath, system))
}
When i run app i see log from log.info(s"Returning $message") so i'm sure route invokes processor, also actor is not invoked therefore lines:
exchange.getOut.setFault(true)
exchange.getOut.setBody(message)
do their job. But still my SOAP service returns 202 SUCCESS instead of fault information.
I'm not sure is what you are looking for, but I processed Exceptions for CXF endpoint differently. I had to return HTTP-500 with custom details in the SOAPFault (like validation error messages etc.), so...
Keep exception unhandled by Camel to pass it to CXF .onException(classOf[PredicateValidationException]).handled(false)
Create org.apache.cxf.interceptor.Fault object with all needed details out of Exception. (Not SOAP Fault). It allows to set custom detail element, custom FaultCode element, message.
finally replace Exchange.EXCEPTION_CAUGHT property with that cxfFault exchange.setProperty(Exchange.EXCEPTION_CAUGHT, cxfFault)
Resulting message from CXF Endpoint is a HTTP-500 with SOAPFault in the body with details I set in cxfFault
Camel is only looking at the in-portion of the exchange but you are modifying the out-portion.
Try changing
exchange.getOut.setFault(true)
exchange.getOut.setBody(message)
to
exchange.getIn.setFault(true)
exchange.getIn.setBody(message)
Related
I have a custom Jetty redirect Handler:
object RedirectHandler extends HandlerWrapper {
override def handle(
target: String,
baseRequest: Request,
request: HttpServletRequest,
response: HttpServletResponse
): Unit = {
require(!response.isCommitted)
val redirectURIOpt = rewrite(request.getRequestURI)
redirectURIOpt.foreach { v =>
response.sendRedirect(v)
require(response.isCommitted)
}
}
def rewrite(target: String): Option[String] = {
if (target.endsWith(".html")) None
else {
var _target = target.stripSuffix("/")
if (_target.nonEmpty) {
_target += ".html"
Some(_target)
} else {
None
}
}
}
}
It is configured to work with an ordinary ResourceHandler to set up a web server:
...
val server = new Server(10092)
val resourceHandler = new ResourceHandler
resourceHandler.setDirectoriesListed(true)
// resource_handler.setWelcomeFiles(Array[String]("test-sites.html"))
resourceHandler.setResourceBase(CommonConst.USER_DIR \\ "test-sites")
val handlers = new HandlerList
handlers.setHandlers(Array(RedirectHandler, resourceHandler))
server.setHandler(handlers)
Every time a redirect is triggered, I found a strange warning message:
WARN HttpChannel: /test-sites/e-commerce/static
java.lang.IllegalStateException: Committed
at org.sparkproject.jetty.server.HttpChannel.resetBuffer(HttpChannel.java:994)
at org.sparkproject.jetty.server.HttpOutput.resetBuffer(HttpOutput.java:1459)
at org.sparkproject.jetty.server.Response.resetBuffer(Response.java:1217)
at org.sparkproject.jetty.server.Response.sendRedirect(Response.java:569)
at org.sparkproject.jetty.server.Response.sendRedirect(Response.java:503)
at org.sparkproject.jetty.server.Response.sendRedirect(Response.java:578)
at org.sparkproject.jetty.server.ResourceService.sendWelcome(ResourceService.java:398)
at org.sparkproject.jetty.server.ResourceService.doGet(ResourceService.java:257)
at org.sparkproject.jetty.server.handler.ResourceHandler.handle(ResourceHandler.java:262)
at org.sparkproject.jetty.server.handler.HandlerList.handle(HandlerList.java:59)
at org.sparkproject.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127)
at org.sparkproject.jetty.server.Server.handle(Server.java:516)
at org.sparkproject.jetty.server.HttpChannel.lambda$handle$1(HttpChannel.java:388)
at org.sparkproject.jetty.server.HttpChannel.dispatch(HttpChannel.java:633)
at org.sparkproject.jetty.server.HttpChannel.handle(HttpChannel.java:380)
at org.sparkproject.jetty.server.HttpConnection.onFillable(HttpConnection.java:277)
at org.sparkproject.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:311)
at org.sparkproject.jetty.io.FillInterest.fillable(FillInterest.java:105)
at org.sparkproject.jetty.io.ChannelEndPoint$1.run(ChannelEndPoint.java:104)
at org.sparkproject.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:883)
at org.sparkproject.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:1034)
at java.lang.Thread.run(Thread.java:748)
According to this post:
jetty webSocket : java.lang.IllegalStateException: Committed
The problem is triggered if the response is further tampered after sendRedirect. However, given that resourceHandler is a very mature component, it is very unlikely to make that low level mistake.
So what are the possible causes, and more importantly, what can be done to fix it by declaring the redirect response to be final?
You missed the baseRequest.setHandled(true) to tell Jetty to stop processing further Handlers. You use this when your Handler produces a response (or handles the request).
A later handler (ResourceHandler according to your stacktrace) attempted to write a response and was smacked down because the response was already committed.
Web Socket connections in Akka Http are treated as an Akka Streams Flow. This seems like it works great for basic request-reply, but it gets more complex when messages should also be pushed out over the websocket. The core of my server looks kind of like:
lazy val authSuccessMessage = Source.fromFuture(someApiCall)
lazy val messageFlow = requestResponseFlow
.merge(updateBroadcastEventSource)
lazy val handler = codec
.atop(authGate(authSuccessMessage))
.join(messageFlow)
handleWebSocketMessages {
handler
}
Here, codec is a (de)serialization BidiFlow and authGate is a BidiFlow that processes an authorization message and prevents outflow of any messages until authorization succeeds. Upon success, it sends authSuccessMessage as a reply. requestResponseFlow is the standard request-reply pattern, and updateBroadcastEventSource mixes in async push messages.
I want to be able to send an error message and terminate the connection gracefully in certain situations, such as bad authorization, someApiCall failing, or a bad request processed by requestResponseFlow. So basically, basically it seems like I want to be able to asynchronously complete messageFlow with one final message, even though its other constituent flows are still alive.
Figured out how to do this using a KillSwitch.
Updated version
The old version had the problem that it didn't seem to work when triggered by a BidiFlow stage higher up in the stack (such as my authGate). I'm not sure exactly why, but modeling the shutoff as a BidiFlow itself, placed further up the stack, resolved the issue.
val shutoffPromise = Promise[Option[OutgoingWebsocketEvent]]()
/**
* Shutoff valve for the connection. It is triggered when `shutoffPromise`
* completes, and sends a final optional termination message if that
* promise resolves with one.
*/
val shutoffBidi = {
val terminationMessageSource = Source
.maybe[OutgoingWebsocketEvent]
.mapMaterializedValue(_.completeWith(shutoffPromise.future))
val terminationMessageBidi = BidiFlow.fromFlows(
Flow[IncomingWebsocketEventOrAuthorize],
Flow[OutgoingWebsocketEvent].merge(terminationMessageSource)
)
val terminator = BidiFlow
.fromGraph(KillSwitches.singleBidi[IncomingWebsocketEventOrAuthorize, OutgoingWebsocketEvent])
.mapMaterializedValue { killSwitch =>
shutoffPromise.future.foreach { _ => println("Shutting down connection"); killSwitch.shutdown() }
}
terminationMessageBidi.atop(terminator)
}
Then I apply it just inside the codec:
val handler = codec
.atop(shutoffBidi)
.atop(authGate(authSuccessMessage))
.join(messageFlow)
Old version
val shutoffPromise = Promise[Option[OutgoingWebsocketEvent]]()
/**
* Shutoff valve for the flow of outgoing messages. It is triggered when
* `shutoffPromise` completes, and sends a final optional termination
* message if that promise resolves with one.
*/
val shutoffFlow = {
val terminationMessageSource = Source
.maybe[OutgoingWebsocketEvent]
.mapMaterializedValue(_.completeWith(shutoffPromise.future))
Flow
.fromGraph(KillSwitches.single[OutgoingWebsocketEvent])
.mapMaterializedValue { killSwitch =>
shutoffPromise.future.foreach(_ => killSwitch.shutdown())
}
.merge(terminationMessageSource)
}
Then handler looks like:
val handler = codec
.atop(authGate(authSuccessMessage))
.join(messageFlow via shutoffFlow)
I want to implement an client app that first send an request to server then wait for its reply(similar to http)
My client process may be
val topic = async.topic[ByteVector]
val client = topic.subscribe
Here is the api
trait Client {
val incoming = tcp.connect(...)(client)
val reqBus = topic.pubsh()
def ask(req: ByteVector): Task[Throwable \/ ByteVector] = {
(tcp.writes(req).flatMap(_ => tcp.reads(1024))).to(reqBus)
???
}
}
Then, how to implement the remain part of ask ?
Usually, the implementation is done with publishing the message via sink and then awaiting some sort of reply on some source, like your topic.
Actually we have a lot of idioms of this in our code :
def reqRply[I,O,O2](src:Process[Task,I],sink:Sink[Task,I],reply:Process[Task,O])(pf: PartialFunction[O,O2]):Process[Task,O2] = {
merge.mergeN(Process(reply, (src to sink).drain)).collectFirst(pf)
}
Essentially this first hooks to reply stream to await any resulting O confirming our request sent. Then we publish message I and consult pf for any incoming O to be eventually translated to O2 and then terminate.
I am trying to create a jetty consumer. I am able to get it successfully running using the endpoint uri:
jetty:http://0.0.0.0:8080
However, when I modify the endpoint uri for https:
jetty:https://0.0.0.0:8443
The page times out trying to load. This seems odd because the camel documentation states it should function right out of the box.
I have since loaded a signed SSL into java's default keystore, with my attempted implementation to load it below:http://camel.apache.org/jetty.html
I have a basic Jetty instance using the akka-camel library with akka and scala. ex:
class RestActor extends Actor with Consumer {
val ksp: KeyStoreParameters = new KeyStoreParameters();
ksp.setPassword("...");
val kmp: KeyManagersParameters = new KeyManagersParameters();
kmp.setKeyStore(ksp);
val scp: SSLContextParameters = new SSLContextParameters();
scp.setKeyManagers(kmp);
val jettyComponent: JettyHttpComponent = CamelExtension(context.system).context.getComponent("jetty", classOf[JettyHttpComponent])
jettyComponent.setSslContextParameters(scp);
def endpointUri = "jetty:https://0.0.0.0:8443/"
def receive = {
case msg: CamelMessage => {
...
}
...
}
...
}
This resulted in some progress, because the page does not timeout anymore, but instead gives a "The connection was interrupted" error. I am not sure where to go from here because camel is not throwing any Exceptions, but rather failing silently somewhere (apparently).
Does anybody know what would cause this behavior?
When using java's "keytool" I did not specify an output file. It didn't throw back an error, so it probably went somewhere. I created a new keystore and explicitly imported my crt into the keyfile. I then explicitly added the filepath to that keystore I created, and everything works now!
If I had to speculate, it is possible things failed silently because I was adding the certs to jetty's general bank of certs to use if eligible, instead of explicitly binding it as the SSL for the endpoint.
class RestActor extends Actor with Consumer {
val ksp: KeyStoreParameters = new KeyStoreParameters();
ksp.setResource("/path/to/keystore");
ksp.setPassword("...");
val kmp: KeyManagersParameters = new KeyManagersParameters();
kmp.setKeyStore(ksp);
val scp: SSLContextParameters = new SSLContextParameters();
scp.setKeyManagers(kmp);
val jettyComponent: JettyHttpComponent = CamelExtension(context.system).context.getComponent("jetty", classOf[JettyHttpComponent])
jettyComponent.setSslContextParameters(scp);
def endpointUri = "jetty:https://0.0.0.0:8443/"
def receive = {
case msg: CamelMessage => {
...
}
...
}
...
}
Hopefully somebody in the future can find use for this code as a template in implementing Jetty over SSL with akka-camel (surprisingly no examples seem to exist)
I am learning Spray and Akka. Have built a simple Spray routing App with one of the route stubs as -
path("blog" / LongNumber) {
blogId =>
respondWithMediaType(MediaTypes.`application/json`) {
get {
request => BlogFetchActor ! Get(blogId)
}
}
}
In the above code I dispatch a message in my spray route definition (in a class implementing HttpService) to another Actor with an ID (blogId) in the request. The BlogFetchActor is supposed to fetch the data from database and respond to the sender. This I have coded as below -
def receive: Receive = LoggingReceive {
case Get(id: Long) => {
log.debug("Retrieving blog with id %d".format(id))
sender ! ReturnBlog(get(id))
}
}
The Actor message from the route is getting to my BlogFetchActor. My BlogFetchActor completes the job of getting the data from the database too. But when I try to send the response back to the sender HttpService, the ReturnBlog message, it does not work. The message ends up in DeadLetters and I see the below in my logs -
Message [in.bharathwrites.BlogFetchActor$ReturnBlog] from Actor[akka://on-spray-can/user/blog-service/blog#1301907662] to Actor[akka://on-spray-can/deadLetters] was not delivered.
Why does the response back to the sender not work? What am I doing wrong? How else can I achieve this? I tried reading the Spray docs and ScalaDoc but could not figure out the problem. Am not proficient enough to read the Spray code and fathom the reasons... Thanks in advance
If you want to ask another actor before completing a request, you should use Akka's ask pattern (? in the example below).
You may also consider returning an Option[ReturnBlog] instead of ReturnBlog to allow Spray to generate 404 status response automatically if such a blog does not exists.
path("blog" / LongNumber) {
blogId =>
respondWithMediaType(MediaTypes.`application/json`) {
get {
complete { (BlogFetchActor ? Get(blogId)).mapTo[ReturnBlog] }
}
}
}