Why does spray-can server not respond to a http request? - scala

I'm trying to set a very basic HTTP server using spray-can. If I call the endpoint I've set a mapping for, I'm getting a timeout (although using a debugger I can see that the actor receives the message).
My source looks like this:
import akka.actor.{Actor, ActorRef, ActorSystem, Props}
import akka.io.IO
import spray.can.Http
import spray.http.{HttpMethods, HttpRequest, HttpResponse, Uri}
class App extends Actor {
implicit val system = context.system
override def receive = {
case "start" =>
val listener: ActorRef = system.actorOf(Props[HttpListener])
IO(Http) ! Http.Bind(listener, interface = "localhost", port = 8080)
}
}
class HttpListener extends Actor {
def receive = {
case _: Http.Connected =>
sender() ! Http.Register(self)
case HttpRequest(HttpMethods.GET, Uri.Path("/ping"), _, _, _) =>
HttpResponse(entity = "PONG")
}
}
object Main {
def main(args: Array[String]) {
val system = ActorSystem("my-actor-system")
val app: ActorRef = system.actorOf(Props[App], "app")
app ! "start"
}
}
Executing run shows:
> run
[info] Running Main
[INFO] [09/10/2014 21:33:38.839] [my-actor-system-akka.actor.default-dispatcher-3] [akka://my-actor-system/user/IO-HTTP/listener-0] Bound to localhost/127.0.0.1:8080
The following HTTP/1.1 500 Internal Server Error shows up when I hit http://localhost:8080/ping:
➜ ~ curl --include http://localhost:8080/ping
HTTP/1.1 500 Internal Server Error
Server: spray-can/1.3.1
Date: Wed, 10 Sep 2014 19:34:08 GMT
Content-Type: text/plain; charset=UTF-8
Connection: close
Content-Length: 111
Ooops! The server was not able to produce a timely response to your request.
Please try again in a short while!
My build.sbt looks like:
scalaVersion := "2.11.2"
resolvers += "spray repo" at "http://repo.spray.io"
libraryDependencies ++= Seq(
"io.spray" %% "spray-can" % "1.3.1",
"io.spray" %% "spray-routing" % "1.3.1",
"com.typesafe.akka" %% "akka-actor" % "2.3.5"
)
Any ideas on what I'm doing wrong?

case HttpRequest(HttpMethods.GET, Uri.Path("/ping"), _, _, _) =>
HttpResponse(entity = "PONG")
should be
case HttpRequest(HttpMethods.GET, Uri.Path("/ping"), _, _, _) =>
sender ! HttpResponse(entity = "PONG")
You are returning HttpResponse instead of sending a message to the sender.

Related

Akkatype with classic actors giving me an error: Unsupported access to ActorContext from outside of Actor

I am getting the following runtime error in my akka application (using both akka typed and classic)
java.lang.UnsupportedOperationException: Unsupported access to
ActorContext from the outside of
Actor[akka://my-classic-actor-system/user/ChatServer#1583147696]. No
message is currently processed by the actor, but ActorContext was
called from
Thread[my-classic-actor-system-akka.actor.default-dispatcher-5,5,run-main-group-0].
def main(args: Array[String]): Unit = {
val logger = LoggerFactory.getLogger(getClass)
logger.debug("Server starting yo...")
implicit val system = akka.actor.ActorSystem("my-classic-actor-system")
implicit val typedGuardian: ActorRef[Any] =
system.spawn(HttpWebsocketServer(), "ChatServer")
val client = system.actorOf(Client.props(remote), "clientActor")
//..
}
The source of the error is inside of HttpWebsocketServer which is taken from (
https://github.com/johanandren/chat-with-akka-http-websockets/blob/akka-2.6/src/main/scala/chat/Server.scala#L101):
object HttpWebsocketServer {
def apply(): Behavior[Any] = {
Behaviors.setup[Any] { context =>
implicit val ec: ExecutionContext = context.executionContext
val system = context.system
val chatRoom = context.spawn(ChatRoom(), "chat")
val userParent = context.spawn(SpawnProtocol(), "users")
val maker =
context.spawn(websock.Maker(), "mmaker")
val route =
path("chat") {
get {
handleWebSocketMessages(
newUserFlow(system, chatRoom, userParent)
)
}
}
// needed until Akka HTTP has a 2.6 only release
implicit val materializer: Materializer =
SystemMaterializer(context.system).materializer
implicit val classicSystem: akka.actor.ActorSystem =
context.system.toClassic
Http()
.bindAndHandle(route, "0.0.0.0", 9000)
// future callback, be careful not to touch actor state from in here
.onComplete {
case Success(binding) =>
context.log.debug(
s"Started server at ${binding.localAddress.getHostString}:${binding.localAddress.getPort}"
)
case Failure(ex) =>
ex.printStackTrace()
context.log.error("Server failed to start, terminating")
context.system.terminate()
}
Behaviors.empty
}
}
}
build.sbt
val AkkaVersion = "2.6.18"
val AkkaHttpVersion = "10.2.7"
libraryDependencies ++= Seq(
"io.netty" % "netty-all" % "4.1.68.Final",
"com.typesafe.akka" %% "akka-actor" % AkkaVersion,
"com.typesafe.akka" %% "akka-actor-typed" % AkkaVersion,
"com.typesafe.akka" %% "akka-stream" % AkkaVersion,
"com.typesafe.akka" %% "akka-stream-typed" % AkkaVersion,
"com.typesafe.akka" %% "akka-http" % AkkaHttpVersion,
"com.typesafe.akka" %% "akka-http-spray-json" % AkkaHttpVersion,
"ch.qos.logback" % "logback-classic" % "1.2.10",
scalaTest % Test
)
If I comment out below in my code I don't get the runtime error, so it looks like that is the source of the issue:
implicit val materializer: Materializer =
SystemMaterializer(context.system).materializer
implicit val classicSystem: akka.actor.ActorSystem =
context.system.toClassic
Http()
.bindAndHandle(route, "0.0.0.0", 9000)
// future callback, be careful not to touch actor state from in here
.onComplete {
case Success(binding) =>
context.log.debug(
s"Started server at ${binding.localAddress.getHostString}:${binding.localAddress.getPort}"
)
case Failure(ex) =>
ex.printStackTrace()
context.log.error("Server failed to start, terminating")
context.system.terminate()
}
I'm not sure what I can do to solve this issue, I am creating actors from either the system or child actors.
You are not allowed to use context outside of an actor. And you do it in callback of your future.
case Success(binding) =>
context.log.debug(
s"Started server at ${binding.localAddress.getHostString}:${binding.localAddress.getPort}"
)
case Failure(ex) =>
ex.printStackTrace()
context.log.error("Server failed to start, terminating")
context.system.terminate()
The problem is that actor context is treated as internal state of an actor and must not be accessed outside of the scope. Even your comment says that
// future callback, be careful not to touch actor state from in here
The solution is to pre-reference your log and system (you have this already) in a val outside of the callback. The callback can look like
case Success(binding) =>
log.debug(
s"Started server at ${binding.localAddress.getHostString}:${binding.localAddress.getPort}"
)
case Failure(ex) =>
ex.printStackTrace()
log.error("Server failed to start, terminating")
system.terminate()

[Akka]: Spawn an Actor at runtime with Scala Reflection and monitor its behavior using Grafana and Prometheus

I set up Grafana and Prometheus in Akka to monitor the behaviour of my system.
If I spawn Actors at compile time it works and I can see them on the dashboard.
Now I'd like to compile an Actor at runtime and monitor its behaviour.
In order to achieve that, I run
val toolbox = currentMirror.mkToolBox()
// Class instance
val actorCode = q"""
import akka.actor._
object HelloActor {
def props(name : String) = Props(new HelloActor(name))
}
class HelloActor(myName: String) extends Actor {
def receive = {
case "hello" => println("hello from %s".format(myName))
case _ => println("'huh?', said %s".format(myName))
}
}
return HelloActor.props("Jane")
"""
Then I compile the Actor, I get the Props and send a message to it in this way
val compiledCode = toolbox.compile(actorCode)()
val actorSystem = ActorSystem("firstActorSystem")
val myProps = compiledCode.asInstanceOf[Props]
val helloActor = actorSystem.actorOf(myProps)
helloActor ! "hello"
Everything works fine, but if I go to the Prometheus dashboard I can not see the Actor instance and the messages that have been sent.
Any tips to solve this issue ?
Actually, I guess with current versions (Scala 2.13.8, Akka 2.6.20, Cinnamon 2.17.0, Prometheus 2.38.0, Grafana 9.1.6) the actors are displayed.
src/main/scala/Main.scala
import scala.reflect.runtime.universe._
import scala.reflect.runtime
import scala.tools.reflect.ToolBox
import akka.actor._
object Main extends App {
object HelloActor {
def props(name: String) = Props(new HelloActor(name))
}
class HelloActor(myName: String) extends Actor {
def receive = {
case "hello" => println("hello from %s".format(myName))
case _ => println("'huh?', said %s".format(myName))
}
}
val myProps = HelloActor.props("Jane")
val actorSystem = ActorSystem("firstActorSystem")
val helloActor = actorSystem.actorOf(myProps)
helloActor ! "hello"
val rm = runtime.currentMirror
val tb = rm.mkToolBox()
val actorCode =
q"""
import akka.actor._
object HelloActor1 {
def props(name : String) = Props(new HelloActor1(name))
}
class HelloActor1(myName: String) extends Actor {
def receive = {
case "hello" => println("hello from %s".format(myName))
case _ => println("'huh?', said %s".format(myName))
}
}
HelloActor1.props("Jane1")
"""
val compiledCode = tb.compile(actorCode)()
val myProps1 = compiledCode.asInstanceOf[Props]
val helloActor1 = actorSystem.actorOf(myProps1)
helloActor1 ! "hello"
}
src/main/resources/application.conf
cinnamon.application = "telemetry"
cinnamon.akka {
actors {
"/user/*" {
report-by = instance
}
}
}
cinnamon.prometheus {
exporters += http-server
}
project/build.properties
sbt.version = 1.7.1
project/plugins.sbt
addSbtPlugin("com.lightbend.cinnamon" % "sbt-cinnamon" % "2.17.0")
build.sbt
lazy val root = (project in file(".")).
settings(
inThisBuild(List(
organization := "com.example",
scalaVersion := "2.13.8"
)),
name := "telemetry",
libraryDependencies ++=
Seq(
"com.typesafe.akka" %% "akka-actor" % "2.6.20",
Cinnamon.library.cinnamonAkka,
Cinnamon.library.cinnamonPrometheus,
Cinnamon.library.cinnamonPrometheusHttpServer,
scalaOrganization.value % "scala-reflect" % scalaVersion.value,
scalaOrganization.value % "scala-compiler" % scalaVersion.value,
),
mainClass := Some("Main"),
) enablePlugins Cinnamon
run / cinnamon := true
test / cinnamon := true
lightbend.sbt (mykey is from https://www.lightbend.com/account/lightbend-platform/credentials)
lazy val mykey = "..."
ThisBuild / resolvers += "lightbend-commercial-mvn" at
s"https://repo.lightbend.com/pass/$mykey/commercial-releases"
ThisBuild / resolvers += Resolver.url(
"lightbend-commercial-ivy",
url(s"https://repo.lightbend.com/pass/$mykey/commercial-releases")
)(Resolver.ivyStylePatterns)
I downloaded Prometheus from https://prometheus.io/download/, Grafana from https://grafana.com/grafana/download (standalone binaries).
prometheus-2.38.0.linux-amd64/prometheus.yml
# Scrape configuration for default Cinnamon Prometheus HTTP Server on localhost
scrape_configs:
- job_name: 'cinnamon'
scrape_interval: 10s
static_configs:
- targets: ['localhost:9001']
I run
/prometheus-2.38.0.linux-amd64$ ./prometheus --config.file=prometheus.yml
/grafana-9.1.6/bin$ ./grafana-server
sbt clean compile run
(or you can run in IDE with vm option -javaagent:/home/dmitin/.cache/coursier/v1/https/repo.lightbend.com/pass/[mykey]/commercial-releases/com/lightbend/cinnamon/cinnamon-agent/2.17.0/cinnamon-agent-2.17.0.jar).
I can see Cinnamon Prometheus Http Server (datasource) metrics at http://localhost:9001/metrics (http://localhost:9001), Prometheus Server at http://localhost:9090, http://localhost:9090/metrics, Grafana at http://localhost:3000.
I login at Grafana (admin:admin), create datasource Cinnamon Prometheus (How to add the Kafka Exporter as a data source to Grafana?)
(plugin Prometheus was already installed), enable the datasource, import dashboard Akka Actors from here, go to the dashboard and see
Please notice actor __wrapper$1$10f8024d0bba48c08a394d97fd56b2f0.__wrapper$1$10f8024d0bba48c08a394d97fd56b2f0$HelloActor1$3. It is HelloActor1 created by the Tolbox.
Links:
https://developer.lightbend.com/docs/telemetry/current/sandbox/prometheus-sandbox.html
https://medium.com/akka-scala/akka-monitor-your-applications-with-lightbend-telemetry-prometheus-and-grafana-dashboard-1b7353e281c1
https://developer.lightbend.com/docs/telemetry/current/visualizations/grafana.html
https://developer.lightbend.com/docs/akka-platform-guide/telemetry/prometheus-backend.html
https://developer.lightbend.com/docs/telemetry/current/plugins/prometheus/prometheus.html
https://developer.lightbend.com/docs/telemetry/current/setup/cinnamon-agent-sbt.html

Running http4s server with ZIO Env

Trying to learn using ZIO library, so I decided to create a basic web service app. Idea pretty basic, use http4s lib for server and route endpoints, print "hello world" on endpoint call.
With the help of docs and examples I found, produces code:
object Main extends ManagedApp {
type AppEnvironment = Clock with Console with HelloRepository
type AppTask[A] = RIO[AppEnvironment, A]
override def run(args: List[String]): ZManaged[ZEnv, Nothing, Int] = {
val httpApp: HttpApp[AppTask] = Router[AppTask]("/" -> helloWorldService).orNotFound
val server = ZIO.runtime[AppEnvironment].flatMap { implicit rts =>
BlazeServerBuilder[AppTask]
.bindHttp(8080, "0.0.0.0")
.withHttpApp(CORS(httpApp))
.serve
.compile[AppTask, AppTask, ExitCode]
.drain
}
(for {
_ <- ZManaged.environment[ZEnv] >>> server.toManaged_
} yield ())
.foldM(err => putStrLn(s"Execution failed with: $err").as(1).toManaged_, _ => ZManaged.succeed(0))
}
val dsl: Http4sDsl[AppTask] = Http4sDsl[AppTask]
import dsl._
val helloWorldService: HttpRoutes[AppTask] = HttpRoutes.of[AppTask] {
case GET -> Root / "hello" / name => Ok(Repo.getHello(name))
}
}
trait HelloRepository extends Serializable {
val helloRepository: HelloRepository.Service[Any]
}
object HelloRepository extends Serializable {
trait Service[R] extends Serializable {
def getHello(name: String): ZIO[R, Nothing, String]
}
}
object Repo extends HelloRepository.Service[HelloRepository] {
override def getHello(name: String): ZIO[HelloRepository, Nothing, String] = ZIO.succeed(s"Hello $name")
}
I create router: Router[AppTask]("/" ...
I create server: ZIO.runtime[AppEnvironment].flatMap ...
Then trying to start server with ZIO enviroment,
but something I am missing as this line:
_ <- ZManaged.environment[ZEnv] >>> server.toManaged_
is incorected, and throws error on build:
Error:(34, 39) inferred type arguments [touch.Main.AppEnvironment,Throwable,Unit] do not conform to method >>>'s type parameter bounds [R1 >: zio.ZEnv,E1,B]
_ <- ZManaged.environment[ZEnv] >>> server.toManaged_
Error:(34, 39) inferred type arguments [touch.Main.AppEnvironment,Throwable,Unit] do not conform to method >>>'s type parameter bounds [R1 >: zio.ZEnv,E1,B]
Error:(34, 50) type mismatch;
found : zio.ZManaged[touch.Main.AppEnvironment,Throwable,Unit]
(which expands to) zio.ZManaged[zio.clock.Clock with zio.console.Console with touch.HelloRepository,Throwable,Unit]
required: zio.ZManaged[R1,E1,B]
maybe someone can help me with the correct syntax?
also would appriacete some explanation, or link to docs, where this is explained.
I would like to explain more but I don't know where you got your code sample or what your build.sbt looks like but I happen to have some http4s code lying around so I took the liberty of adding some import statements and simplifying it a bit. You can always add back the complexity I took out.
Here's what worked for me.
/tmp/http4s/test.scala
import org.http4s.implicits._
import org.http4s.server.blaze._
import org.http4s.server.Router
import org.http4s.server.middleware.CORS
import org.http4s._
import org.http4s.dsl.Http4sDsl
import zio._
import zio.clock._
import zio.console._
import zio.interop.catz._
trait HelloRepository
{
def getHello(name: String): ZIO[AppEnvironment, Nothing, String]
}
trait AppEnvironment extends Console with Clock
{
val helloRepository: HelloRepository
}
object Main extends App {
type AppTask[A] = RIO[AppEnvironment, A]
val dsl: Http4sDsl[AppTask] = Http4sDsl[AppTask]
import dsl._
val httpApp: HttpApp[AppTask] = Router[AppTask](
"/" -> HttpRoutes.of[AppTask] {
case GET -> Root / "hello" / name => Ok( ZIO.accessM[AppEnvironment](_.helloRepository.getHello(name)) )
}
).orNotFound
val program = for {
server <- ZIO.runtime[AppEnvironment]
.flatMap {
implicit rts =>
BlazeServerBuilder[AppTask]
.bindHttp(8080, "0.0.0.0")
.withHttpApp(CORS(httpApp))
.serve
.compile
.drain
}
} yield server
val runEnv = new AppEnvironment with Console.Live with Clock.Live
{
val helloRepository = new HelloRepository
{
def getHello(name: String): ZIO[AppEnvironment, Nothing, String] = ZIO.succeed(s"Hello $name")
}
}
def run(args: List[String]) =
program
.provide(runEnv)
.foldM(err => putStrLn(s"Execution failed with: $err") *> ZIO.succeed(1), _ => ZIO.succeed(0))
}
/tmp/http4s/build.sbt
val Http4sVersion = "0.20.0"
val CatsVersion = "2.0.0"
val ZioCatsVersion = "2.0.0.0-RC3"
val ZioVersion = "1.0.0-RC13"
val LogbackVersion = "1.2.3"
lazy val root = (project in file("."))
.settings(
organization := "example",
name := "example",
version := "0.0.1-SNAPSHOT",
scalaVersion := "2.12.8",
scalacOptions ++= Seq("-Ypartial-unification"),
libraryDependencies ++= Seq(
"org.typelevel" %% "cats-effect" % CatsVersion,
"dev.zio" %% "zio" % ZioVersion,
"dev.zio" %% "zio-interop-cats" % ZioCatsVersion,
"org.http4s" %% "http4s-blaze-server" % Http4sVersion,
"org.http4s" %% "http4s-dsl" % Http4sVersion,
"ch.qos.logback" % "logback-classic" % LogbackVersion,
),
addCompilerPlugin("org.spire-math" %% "kind-projector" % "0.9.6"),
addCompilerPlugin("com.olegpy" %% "better-monadic-for" % "0.2.4")
)
scalacOptions ++= Seq(
"-deprecation", // Emit warning and location for usages of deprecated APIs.
"-encoding", "UTF-8", // Specify character encoding used by source files.
"-language:higherKinds", // Allow higher-kinded types
"-language:postfixOps", // Allows operator syntax in postfix position (deprecated since Scala 2.10)
"-feature", // Emit warning and location for usages of features that should be imported explicitly.
"-Ypartial-unification", // Enable partial unification in type constructor inference
"-Xfatal-warnings", // Fail the compilation if there are any warnings
)
sample execution
bash-3.2$ cd /tmp/http4s
bash-3.2$ sbt
...
sbt:example> compile
...
[info] Done compiling.
[success] Total time: 5 s, completed Oct 24, 2019 11:20:53 PM
sbt:example> run
...
[info] Running Main
23:21:03.720 [zio-default-async-1-163838348] INFO org.http4s.blaze.channel.nio1.NIO1SocketServerGroup - Service bound to address /0:0:0:0:0:0:0:0:8080
23:21:03.725 [blaze-selector-0] DEBUG org.http4s.blaze.channel.nio1.SelectorLoop - Channel initialized.
23:21:03.732 [zio-default-async-1-163838348] INFO org.http4s.server.blaze.BlazeServerBuilder -
_ _ _ _ _
| |_| |_| |_ _ __| | | ___
| ' \ _| _| '_ \_ _(_-<
|_||_\__|\__| .__/ |_|/__/
|_|
23:21:03.796 [zio-default-async-1-163838348] INFO org.http4s.server.blaze.BlazeServerBuilder - http4s v0.20.0 on blaze v0.14.0 started at http://[0:0:0:0:0:0:0:0]:8080/
23:21:11.070 [blaze-selector-1] DEBUG org.http4s.blaze.channel.nio1.SelectorLoop - Channel initialized.
23:21:11.070 [blaze-selector-1] DEBUG org.http4s.blaze.channel.nio1.NIO1HeadStage - Starting up.
23:21:11.070 [blaze-selector-1] DEBUG org.http4s.blaze.channel.nio1.NIO1HeadStage - Stage NIO1HeadStage sending inbound command: Connected
23:21:11.070 [blaze-selector-1] DEBUG org.http4s.server.blaze.Http1ServerStage$$anon$1 - Starting HTTP pipeline
23:21:11.072 [blaze-selector-1] DEBUG org.http4s.blazecore.IdleTimeoutStage - Starting idle timeout stage with timeout of 30000 ms
At this point after opening http://localhost:8080/hello/there I observed the expected output in the browser.
Hope this helps.

how to start server/client grpc using scalapb on spark?

i have a problem about running server/client using ScalaPB on spark.
its totally work fine while I running my code using "sbt run". i want running this code using spark coz next ill import my spark model to predict some label. but while I submit my jar to spark, they give me error like this.
Exception in thread "main" io.grpc.ManagedChannelProvider$ProviderNotFoundException:
No functional server found. Try adding a dependency on the grpc-netty artifact
this is my build.sbt
scalaVersion := "2.11.7"
PB.targets in Compile := Seq(
scalapb.gen() -> (sourceManaged in Compile).value
)
val scalapbVersion =
scalapb.compiler.Version.scalapbVersion
val grpcJavaVersion =
scalapb.compiler.Version.grpcJavaVersion
libraryDependencies ++= Seq(
// protobuf
"com.thesamet.scalapb" %% "scalapb-runtime" % scalapbVersion % "protobuf",
//for grpc
"io.grpc" % "grpc-netty" % grpcJavaVersion ,
"com.thesamet.scalapb" %% "scalapb-runtime-grpc" % scalapbVersion
)
assemblyMergeStrategy in assembly := {
case PathList("META-INF", xs # _*) => MergeStrategy.discard
case x => MergeStrategy.first
}
using shade still not work
assemblyShadeRules in assembly := Seq(ShadeRule.rename("com.google.**" -> "shadegoogle.#1").inAll)
and this my main
import java.util.logging.Logger
import io.grpc.{Server, ServerBuilder}
import org.apache.spark.ml.tuning.CrossValidatorModel
import org.apache.spark.sql.SparkSession
import testproto.test.{Email, EmailLabel, RouteGuideGrpc}
import scala.concurrent.{ExecutionContext, Future}
object HelloWorldServer {
private val logger = Logger.getLogger(classOf[HelloWorldServer].getName)
def main(args: Array[String]): Unit = {
val server = new HelloWorldServer(ExecutionContext.global)
server.start()
server.blockUntilShutdown()
}
private val port = 50051
}
class HelloWorldServer(executionContext: ExecutionContext) {
self =>
private[this] var server: Server = null
private def start(): Unit = {
server = ServerBuilder.forPort(HelloWorldServer.port).addService(RouteGuideGrpc.bindService(new RouteGuideImpl, executionContext)).build.start
HelloWorldServer.logger.info("Server started, listening on " + HelloWorldServer.port)
sys.addShutdownHook {
System.err.println("*** shutting down gRPC server since JVM is shutting down")
self.stop()
System.err.println("*** server shut down")
}
}
private def stop(): Unit = {
if (server != null) {
server.shutdown()
}
}
private def blockUntilShutdown(): Unit = {
if (server != null) {
server.awaitTermination()
}
}
private class RouteGuideImpl extends RouteGuideGrpc.RouteGuide {
override def getLabel(request: Email): Future[EmailLabel] = {
val replay = EmailLabel(emailId = request.emailId, label = "aaaaa")
Future.successful(replay)
}
}
}
thanks
It looks like grpc-netty is not found when an uber jar is made. Instead of using ServerBuilder, change your code to use io.grpc.netty.NettyServerBuilder.

Obtaining the client IP in Akka-http

I am trying to write an Akka HTTP microservice (akka version 2.4.11, Scala version 2.11.8, both latest versions at time of writing) which is aware of the client service's IP (i.e., remote address), and I cannot get this to work.
I can create and run a service which says 'Hello!' using a route like this:
val routeHello: Route = path("SayHello") {
get {
entity(as[String]) {
body => complete {
HttpResponse(entity = HttpEntity("Hello!"))
}
}
}
}
I have constructed a similar route to the one above, which is extended so that it is aware of the client's IP address.
I noted that I need to edit the application.conf file and set 'remote-address-header = on' to enable the addition of a Remote-Address header holding the clients (remote) IP address. I have done this in case it is required.
Here is the route:
val routeHelloIp: Route = path("SayHelloIp") {
get {
// extractClientIp appears to be working as a filter
// instead of an extractor - why?
extractClientIp {
clientIp => {
entity(as[String]) {
body => complete {
HttpResponse(entity = HttpEntity("Hello!"))
}
}
}
}
}
}
However when I run this route, I get a message 'The requested resource could not be found.'.
It looks like I have got the Akka-http DSL syntactic sugar wrong in the example above. I would be grateful if you could put me on the right path!
EDIT:
I have tried the following program in response to Ramon's helpful answer. Unfortunately it does not compile and I cannot see what I need to do to make it compile.
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.Http.IncomingConnection
import java.net.InetSocketAddress
import akka.stream.ActorMaterializer
import akka.stream.scaladsl.Sink
import akka.http.scaladsl.server.Directives._
import java.net.InetSocketAddress
object TestHttp {
def main(args: Array[String]) {
implicit val system = ActorSystem("my-system")
implicit val materializer = ActorMaterializer()
// allow connections from any IP
val interface = "0.0.0.0"
//from the question
def createRoute(address: InetSocketAddress) = path("SayHelloIp") {
get {
extractRequestEntity { entity =>
entity(as[String]) { body =>
complete(entity = s"Hello ${address.getAddress().getHostAddress()}")
}
}
}
}
Http().bind(interface).runWith(Sink foreach { conn =>
val address = conn.remoteAddress
conn.handleWithAsyncHandler(createRoute(address))
})
}
}
I have the following build.sbt to ensure that the latest version of Scala and akka-http are used:
import sbt.Keys._
name := "Find my IP"
version := "1.0"
scalaVersion := "2.11.8"
resolvers ++= Seq(
"Typesafe Repository" at "http://repo.typesafe.com/typesafe/releases/"
)
libraryDependencies ++= {
Seq(
"com.typesafe.akka" %% "akka-actor" % "2.4.11",
"com.typesafe.akka" %% "akka-stream" % "2.4.11",
"com.typesafe.akka" %% "akka-http-experimental" % "2.4.11",
"com.typesafe.akka" %% "akka-http-core" % "2.4.11"
)
}
I get the following compile-time errors:
[error] /Users/tilopa/temp/akka-test/src/main/scala/Test.scala:24: akka.http.scaladsl.model.RequestEntity does not take parameters
[error] entity(as[String]) { body =>
[error] ^
[error] /Users/tilopa/temp/akka-test/src/main/scala/Test.scala:25: reassignment to val
[error] complete(entity = s"Hello ${address.getAddress().getHostAddress()}")
[error] ^
[error] two errors found
[error] (compile:compileIncremental) Compilation failed
Using extractClientIp
extractClientIp is not working for you because the sender has not specified one of the required header fields. From the documentation:
Provides the value of X-Forwarded-For, Remote-Address, or X-Real-IP
headers as an instance of RemoteAddress.
You just have to turn on the right setting in your sender:
The akka-http server engine adds the Remote-Address header to every
request automatically if the respective setting
akka.http.server.remote-address-header is set to on. Per default it is
set to off.
generic solution
If you want this to work for any HttpRequest, not just the ones with the correct header settings, then you have to use the bind method on an HttpExt instead of bindAndHandle:
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.Http.IncomingConnection
import java.net.InetSocketAddress
implicit val actorSystem : ActorSystem = ???
implicit val actorMat = ActorMaterializer()
//alow connections from any IP
val interface = "0.0.0.0"
//from the question
def createRoute(address : InetSocketAddress) = path("SayHelloIp") {
get {
extractRequestEntity { entity =>
entity(as[String]) { body =>
complete(entity = s"Hello ${address.getAddress().getHostAddress()}")
}
}
}
}
Http().bind(interface).runWith(Sink foreach { conn =>
val address = conn.remoteAddress
conn.handleWithAsyncHandler(createRoute(address))
})
Edit
As noted in the comments: since akka 10.0.13 use conn.handleWith.
HttpExt.bind() has been depreciated. Here's a workaround with connectionSource():
val https: HttpsConnectionContext = /*...*/
Http(actorSystem).
newServerAt(interface,httpsPort).
enableHttps(https).
connectionSource().
to(Sink.foreach { connection =>
println("New Connection from remote address :: ${connection.remoteAddress}")
// handle connection here
}).run().onComplete {
case Success(binding)=>
binding.addToCoordinatedShutdown(hardTerminationDeadline = 10.seconds)
log.info("{} HTTPS Server running {}",errorContext,binding)
case Failure(ex)=>
log.error("{} HTTPS Server failed {}",errorContext,ex.toString)
sys.exit(10)
}