Scala WSClient cannot connect to https with no httpProxy provided - scala

I'm facing a little issue with Scala WSClient.
I'm working on a VM with a squid proxy, and I want to only allow HTTPS proxying (blocking HTTP).
I tried a java HTTPS connection (using URLConnection), and it works fine with https proxy and https url. If I try mixing configs (proxy and url), I get this :
HTTP with http proxy : 302 answer (redirection to https)
HTTP with https proxy : UnknownHostException
HTTP with https proxy and ip resolution in /etc/hosts: SocketTimeoutException
HTTPS with https proxy : 200
HTTPS with http proxy : UnknownHostException
So we have dns issues when url protocol and proxy opened route are not matching (not sure why), but if I use HTTPS with https proxy only, I'm fine : expected result.
I tried the same with a Scala object (called from java), using also a URLConnection.
Same result, works fine (same errors when mixing proxy and protocols).
#throws(classOf[java.io.IOException])
#throws(classOf[java.net.SocketTimeoutException])
def doHttps(url: String,
connectTimeout: Int = 5000,
readTimeout: Int = 5000,
requestMethod: String = "GET") = {
import java.net.URL
import javax.net.ssl.HttpsURLConnection
val connection = (new URL("https://"+url)).openConnection.asInstanceOf[HttpsURLConnection]
connection.setConnectTimeout(connectTimeout)
connection.setReadTimeout(readTimeout)
connection.setRequestMethod(requestMethod)
connection.connect()
val headers = connection.getHeaderFields
for ((k,v) <- headers.asScala) {
if (k == null) {
println(v)
} else {
println(s"$k: $v")
}
}
}
But I need to use scala's WSClient, so I tested with it (tried both AhcWSClient and StandaloneAhcWsClient, and a problem appears. With this tool, I cannot connect to an https endpoint (here test.salesforce.com) if I don't have httpProxy allowed.
Here is the code (both ws clients) :
def wsClientTest(clientKey: String) {
implicit val ec: scala.concurrent.ExecutionContext = scala.concurrent.ExecutionContext.global
implicit val system: ActorSystem = ActorSystem("test", ConfigFactory.defaultReference())
implicit val materializer: ActorMaterializer = ActorMaterializer(ActorMaterializerSettings(system))
implicit val ws: AhcWSClient = AhcWSClient(AhcWSClientConfig())
val tokenEndpoint: String = s"https://test.salesforce.com/services/oauth2/token"
val query = ws.url(tokenEndpoint).withRequestTimeout(Duration.apply(5L, TimeUnit.SECONDS))
.withRequestFilter(AhcCurlRequestLogger())
.post( Map("response_type" -> "device_code", "client_id" -> clientKey));
val results = Await.result(query, Duration.apply(5L, TimeUnit.SECONDS))
println(Json.parse(results.body))
ws.close()
}
def standaloneWSClientTest(clientKey: String) {
implicit val ec: scala.concurrent.ExecutionContext = scala.concurrent.ExecutionContext.global
implicit val system: ActorSystem = ActorSystem("test", ConfigFactory.defaultReference())
implicit val materializer: ActorMaterializer = ActorMaterializer(ActorMaterializerSettings(system))
implicit val ws: StandaloneWSClient = StandaloneAhcWSClient()
val tokenEndpoint: String = s"https://test.salesforce.com/services/oauth2/token"
val query = ws.url(tokenEndpoint).withRequestTimeout(Duration.apply(5L, TimeUnit.SECONDS))
.withRequestFilter(AhcCurlRequestLogger())
.post( Map("response_type" -> "device_code", "client_id" -> clientKey));
val results = Await.result(query, Duration.apply(5L, TimeUnit.SECONDS))
println(Json.parse(results.body))
ws.close()
}
With that client, with only httpsProxy allowed, I get dns errors, and if I add the url/ip pair in /etc/hosts, I get a timeout.
Scala Using URLConnection (HTTPS with only httpsProxy defined)
--------------------------------------------------------------
Transfer-Encoding: [chunked]
[HTTP/1.1 200 OK]
X-Content-Type-Options: [nosniff]
X-FRAME-OPTIONS: [DENY]
Date: [Tue, 08 Feb 2022 10:03:19 GMT]
Strict-Transport-Security: [max-age=31536000; includeSubDomains]
Cache-Control: [no-cache,must-revalidate,max-age=0,no-store,private]
Content-Security-Policy: [frame-ancestors 'none', upgrade-insecure-requests]
Vary: [Accept-Encoding]
Set-Cookie: [QCQQ=bzfE6xRGxLR; path=/; secure, BrowserId=VzwVGIjGEey_0Qe3-91VQQ; domain=.salesforce.com; path=/; expires=Wed, 08-Feb-2023 10:03:19 GMT; Max-Age=31536000, LSKey-c$CookieConsentPolicy=0:0; domain=test.salesforce.com; path=/; expires=Wed, 08-Feb-2023 10:03:19 GMT; Max-Age=31536000, CookieConsentPolicy=0:0; domain=test.salesforce.com; path=/; expires=Wed, 08-Feb-2023 10:03:19 GMT; Max-Age=31536000]
Expires: [Thu, 01 Jan 1970 00:00:00 GMT]
X-XSS-Protection: [1; mode=block]
Content-Type: [text/html; charset=UTF-8]
Scala Using WSclient (HTTPS with only httpsProxy defined)
---------------------------------------------------------
INFO [main] AhcCurlRequestLogger - curl \
--verbose \
--request POST \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data 'response_type=device_code&client_id=test-client-id' \
'https://test.salesforce.com/services/oauth2/token'
DEBUG[main] AbstractByteBuf - -Dplay.shaded.ahc.io.netty.buffer.checkAccessible: true
DEBUG[main] AbstractByteBuf - -Dplay.shaded.ahc.io.netty.buffer.checkBounds: true
DEBUG[main] ResourceLeakDetectorFactory - Loaded default ResourceLeakDetector: play.shaded.ahc.io.netty.util.ResourceLeakDetector#3e7634b9
DEBUG[main] NettyRequestSender - Aborting Future NettyResponseFuture{currentRetry=0,
isDone=0,
isCancelled=0,
asyncHandler=play.api.libs.ws.ahc.StandaloneAhcWSClient$$anon$1#2b5825fa,
nettyRequest=play.shaded.ahc.org.asynchttpclient.netty.request.NettyRequest#53d1b9b3,
future=java.util.concurrent.CompletableFuture#2cae1042[Not completed],
uri=https://test.salesforce.com/services/oauth2/token,
keepAlive=true,
redirectCount=0,
timeoutsHolder=play.shaded.ahc.org.asynchttpclient.netty.timeout.TimeoutsHolder#163d04ff,
inAuth=0,
touch=1644314479717}
DEBUG[main] NettyRequestSender - test.salesforce.com: Name or service not known
java.net.UnknownHostException: test.salesforce.com: Name or service not known
at java.net.Inet4AddressImpl.lookupAllHostAddr(Native Method) ~[na:1.8.0_281]
at java.net.InetAddress$2.lookupAllHostAddr(InetAddress.java:929) ~[na:1.8.0_281]
at java.net.InetAddress.getAddressesFromNameService(InetAddress.java:1324) ~[na:1.8.0_281]
at java.net.InetAddress.getAllByName0(InetAddress.java:1277) ~[na:1.8.0_281]
at java.net.InetAddress.getAllByName(InetAddress.java:1193) ~[na:1.8.0_281]
at java.net.InetAddress.getAllByName(InetAddress.java:1127) ~[na:1.8.0_281]
at play.shaded.ahc.io.netty.util.internal.SocketUtils$9.run(SocketUtils.java:161) ~[scalatest-1.0-SNAPSHOT.jar:na]
at play.shaded.ahc.io.netty.util.internal.SocketUtils$9.run(SocketUtils.java:158) ~[scalatest-1.0-SNAPSHOT.jar:na]
at java.security.AccessController.doPrivileged(Native Method) ~[na:1.8.0_281]
at play.shaded.ahc.io.netty.util.internal.SocketUtils.allAddressesByName(SocketUtils.java:158) ~[scalatest-1.0-SNAPSHOT.jar:na]
at play.shaded.ahc.io.netty.resolver.DefaultNameResolver.doResolveAll(DefaultNameResolver.java:52) ~[scalatest-1.0-SNAPSHOT.jar:na]
at play.shaded.ahc.io.netty.resolver.SimpleNameResolver.resolveAll(SimpleNameResolver.java:81) ~[scalatest-1.0-SNAPSHOT.jar:na]
at play.shaded.ahc.io.netty.resolver.SimpleNameResolver.resolveAll(SimpleNameResolver.java:73) ~[scalatest-1.0-SNAPSHOT.jar:na]
at play.shaded.ahc.org.asynchttpclient.resolver.RequestHostnameResolver.resolve(RequestHostnameResolver.java:50) ~[scalatest-1.0-SNAPSHOT.jar:na]
at play.shaded.ahc.org.asynchttpclient.netty.request.NettyRequestSender.resolveAddresses(NettyRequestSender.java:357) [scalatest-1.0-SNAPSHOT.jar:na]
at play.shaded.ahc.org.asynchttpclient.netty.request.NettyRequestSender.sendRequestWithNewChannel(NettyRequestSender.java:300) [scalatest-1.0-SNAPSHOT.jar:na]
at play.shaded.ahc.org.asynchttpclient.netty.request.NettyRequestSender.sendRequestWithCertainForceConnect(NettyRequestSender.java:142) [scalatest-1.0-SNAPSHOT.jar:na]
at play.shaded.ahc.org.asynchttpclient.netty.request.NettyRequestSender.sendRequest(NettyRequestSender.java:113) [scalatest-1.0-SNAPSHOT.jar:na]
at play.shaded.ahc.org.asynchttpclient.DefaultAsyncHttpClient.execute(DefaultAsyncHttpClient.java:241) [scalatest-1.0-SNAPSHOT.jar:na]
at play.shaded.ahc.org.asynchttpclient.DefaultAsyncHttpClient.executeRequest(DefaultAsyncHttpClient.java:210) [scalatest-1.0-SNAPSHOT.jar:na]
at play.api.libs.ws.ahc.StandaloneAhcWSClient.execute(StandaloneAhcWSClient.scala:90) [scalatest-1.0-SNAPSHOT.jar:na]
at play.api.libs.ws.ahc.StandaloneAhcWSRequest.$anonfun$execute$1(StandaloneAhcWSRequest.scala:216) [scalatest-1.0-SNAPSHOT.jar:na]
Scala Using WSclient and DNS in /etc/hosts (HTTPS with only httpsProxy defined)
-------------------------------------------------------------------------------
DEBUG[AsyncHttpClient-timer-1-1] TimeoutTimerTask - Request timeout to test.salesforce.com:443 after 5000 ms for NettyResponseFuture{currentRetry=0,
isDone=0,
isCancelled=0,
asyncHandler=play.api.libs.ws.ahc.StandaloneAhcWSClient$$anon$1#156a2245,
nettyRequest=play.shaded.ahc.org.asynchttpclient.netty.request.NettyRequest#1badde98,
future=java.util.concurrent.CompletableFuture#1f924935[Not completed],
uri=https://test.salesforce.com/services/oauth2/token,
keepAlive=true,
redirectCount=0,
timeoutsHolder=play.shaded.ahc.org.asynchttpclient.netty.timeout.TimeoutsHolder#5a90d4a3,
inAuth=0,
touch=1644314381722} after 5042 ms
DEBUG[AsyncHttpClient-timer-1-1] NettyRequestSender - Aborting Future NettyResponseFuture{currentRetry=0,
isDone=0,
isCancelled=0,
asyncHandler=play.api.libs.ws.ahc.StandaloneAhcWSClient$$anon$1#156a2245,
nettyRequest=play.shaded.ahc.org.asynchttpclient.netty.request.NettyRequest#1badde98,
future=java.util.concurrent.CompletableFuture#1f924935[Not completed],
uri=https://test.salesforce.com/services/oauth2/token,
keepAlive=true,
redirectCount=0,
timeoutsHolder=play.shaded.ahc.org.asynchttpclient.netty.timeout.TimeoutsHolder#5a90d4a3,
inAuth=0,
touch=1644314381722}
Exception in thread "main" java.util.concurrent.TimeoutException: Future timed out after [5 seconds]
at scala.concurrent.impl.Promise$DefaultPromise.tryAwait0(Promise.scala:212)
at scala.concurrent.impl.Promise$DefaultPromise.result(Promise.scala:225)
at scala.concurrent.Await$.$anonfun$result$1(package.scala:200)
at scala.concurrent.BlockContext$DefaultBlockContext$.blockOn(BlockContext.scala:62)
at scala.concurrent.Await$.result(package.scala:124)
at DeviceVerificationClient$.standaloneWSClientTest(DeviceVerification.scala:59)
at DeviceVerificationClient.standaloneWSClientTest(DeviceVerification.scala)
at ScalaTest.main(ScalaTest.java:17)
DEBUG[AsyncHttpClient-timer-1-1] NettyRequestSender - Request timeout to test.salesforce.com:443 after 5000 ms
I tried disabling hostname verification (disableHostnameVerification = true), SNI, and commented inet-address dns resolver to use async-dns (provider-object = "akka.io.dns.internal.AsyncDnsProvider") but none of them work.
I'd be glad to know if I'm missing something here, a parameter to configure, an option to set...
Any help would be greatly appreciated !
Regards
Frederic Esnault

Related

Sse stream crashed io.gatling.http.action.sse.SseInvalidContentTypeException: Server returned http response with content-type null

I am trying to set up a load test scenario with Gatling;
package mypackage
import io.gatling.core.scenario.Simulation
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration.DurationInt
class My_LoadTest extends Simulation {
val httpProtocol = http
.baseUrl("https://my-base.url")
.header("API_KEY", "my-api-key")
val scn = scenario("MyTestScenario")
.exec(
sse("mySSE").connect("/my/end-point")
.await(10.seconds)(
sse.checkMessage("data").check(regex("""event: snapshot(.*)"""))
)
)
.pause(5)
.exec(sse("Close").close)
setUp(scn.inject(atOnceUsers(1))).protocols(httpProtocol)
}
but it's continuously throwing error:
> i.g.h.a.s.SseInvalidContentTypeException: Server returned http 1 (50.00%)
response with content-type null
> Close: Client issued close order but SSE stream was already cr 1 (50.00%)
ashed: i.g.h.a.s.SseInvalidContentTypeException: Server return...
Whereas, I have already tested with CURL command (and that works fine) as;
curl 'https://my-base.url/my/end-point' \
-H 'authority: xyz’ \
-H 'accept: text/event-stream' \
-H 'API_KEY: my’-api-key \
Now, even though, Gatling claims that Gatling automatically sets Accept header to text/event-stream and Cache-Control to no-cache., but I also tried with:
val sentHeaders = Map("Content-Type" -> "text/event-stream", "API_KEY" -> "my-api-key")
val httpProtocol = http
.baseUrl("https://my-base.url")
.headers(sentHeaders)
Whatever I have tried so far, the error remains the same; Server returned http response with content-type null.
Any clue/solution/suggestion?
Check the logs. A Server Sent Event stream must have a Content-Type header of text/event-stream, see specification. It looks like your stream is malformed.

Response 411 Length Required in Gatling

I'm starting in Gatling. I have 411 status and don't understand why.
Response DefaultHttpResponse(decodeResult: success, version: HTTP/1.1)
HTTP/1.1 411 Length Required
Connection: close
Date: Tue, 13 Feb 2018 16:07:51 GMT
Server: Kestrel
Content-Length: 0
19:07:53.083 [gatling-http-thread-1-2] DEBUG org.asynchttpclient.netty.channel.ChannelManager - Closing Channel [id: 0x5f14313e, L:/10.8.1.89:52767 - R:blabla.com:5000]
19:07:53.107 [gatling-http-thread-1-2] INFO io.gatling.commons.validation.package$ - Boon failed to parse into a valid AST: -1
java.lang.ArrayIndexOutOfBoundsException: -1
...
19:07:53.111 [gatling-http-thread-1-2] WARN io.gatling.http.ahc.ResponseProcessor - Request 'HTTP Request createCompany' failed: status.find.is(200), but actually found 411
19:07:53.116 [gatling-http-thread-1-2] DEBUG io.gatling.http.ahc.ResponseProcessor -
My code:
package load
import io.gatling.core.scenario.Simulation
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._
class LoadScript extends Simulation{
val httpConf = http
.baseURL("http://blabla.com:5000")
.authorizationHeader("Bearer 35dfd7a3c46f3f0bc7a2f06929399756029f47b9cc6d193ed638aeca1306d")
.acceptHeader("application/json, text/plain,")
.acceptEncodingHeader("gzip, deflate, br")
.acceptLanguageHeader("ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7")
.userAgentHeader("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36")
val basicLoad = scenario("BASIC_LOAD").exec(BasicLoad.start)
setUp(
basicLoad.inject(rampUsers(1) over (1 minutes))
.protocols(httpConf))
}
object BasicLoad {
val start =
exec(
http("HTTP Request createCompany")
.post("/Companies/CreateCompanyAndStartTransaction")
.queryParam("inn","7733897761")
.queryParam("ogrn","5147746205041")
.check(status is 200, jsonPath("$.id").saveAs("idCompany"))
)
}
When you are not sending message-body you need to add
.header("Content-Length", "0") as workaround.
I have similar issue. I'm running my tests on two environments and a difference is in application infrastructure.
Tests are passing on Amazon AWS but getting HTTP 411 on Azure. So looks like the issue is not in Gatling itself.
This issue has been also well answered by Gatling team at the and of this chat:
https://groups.google.com/forum/#!topic/gatling/mAGzjzoMr1I
I've just upgraded Gatling from 2.3 to 3.0.2. They wrote their own HTTP client and it sends now content-length: 0 except one case described in this bug:
https://github.com/gatling/gatling/issues/3648
so if you avoid using httpRequest() with method type passed as string e.g:
exec(http("empty POST test").httpRequest("POST","https://gatling.io/"))
and use post() as you do:
exec(
http("HTTP Request createCompany")
.post("/Companies/CreateCompanyAndStartTransaction")...
or
exec(
http("HTTP Request createCompany")
.httpRequest(HttpMethod.POST, "/Companies/CreateCompanyAndStartTransaction")
then upgrade Gatling to 3.0.2 is enough. Otherwise you need to wait for Gatling 3.0.3

Using Finagle Http client for https requests

I am trying to get some data from a REST web service. So far I can get the data correctly if I don't use HTTPS with this code working as expected -
val client = Http.client.newService(s"$host:80")
val r = http.Request(http.Method.Post, "/api/search/")
r.host(host)
r.content = queryBuf
r.headerMap.add(Fields.ContentLength, queryBuf.length.toString)
r.headerMap.add("Content-Type", "application/json;charset=UTF-8")
val response: Future[http.Response] = client(r)
But when I am trying to get the same data from https request (Following this link)
val client = Http.client.withTls(host).newService(s"$host:443")
val r = http.Request(http.Method.Post, "/api/search/")
r.headerMap.add("Cookie", s"_elfowl=${authToken.elfowlToken}; dc=$dc")
r.host(host)
r.content = queryBuf
r.headerMap.add(Fields.ContentLength, queryBuf.length.toString)
r.headerMap.add("Content-Type", "application/json;charset=UTF-8")
r.headerMap.add("User-Agent", authToken.userAgent)
val response: Future[http.Response] = client(r)
I get the error
Remote Info: Not Available at remote address: searchservice.com/10.59.201.29:443. Remote Info: Not Available, flags=0x08
I can curl the same endpoint with 443 port and it returns the right result. Can anyone please help me troubleshoot the issue ?
Few things to check:
withTls(host)
needs to be the host name that is in the certificate of server (as opposed to the the ip for instance)
you can try:
Http.client.withTlsWithoutValidation
to verify the above.
Also you might want to verify if the server checks that the host header is set, and if so, you might want to include it:
val withHeader = new SimpleFilter[http.Request, http.Response] {
override def apply(request: http.Request, service: HttpService): Future[http.Response] = {
request.host_=(host)
service(request)
}
}
withHeader.andThen(client)
more info on host header:
What is http host header?

Play Framework CORS Headers

I'm trying to set CORS Headers for my play framework app. Specifically I'm getting this error
cannot load http://127.0.0.1:9000/. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:9000' is therefore not allowed access.
I figured I could easily handle this by following these instructions:
https://www.playframework.com/documentation/2.5.x/CorsFilter
However, after doing this. nothing has changed.
curl -I localhost:9000/
HTTP/1.1 200 OK
Content-Length: 4540
Content-Type: text/html; charset=utf-8
Date: Mon, 11 Jul 2016 20:03:33 GMT
My conf is:
play.http.filters = "global.Filters"
play.filters.cors {
allowedOrigins = ["http://www.example.com", "*"]
allowedHttpMethods = ["GET", "POST"]
allowedHttpHeaders = ["Accept"]
}
and my Filters.scala file is:
package global
import javax.inject.Inject
import play.api.http.DefaultHttpFilters
import play.filters.cors.CORSFilter
class Filters #Inject() (corsFilter: CORSFilter)
extends DefaultHttpFilters(corsFilter)
If someone could tell me why the filters don't seem to be getting applied to the responses, that'd be great.
Play filters are enticing, but when they do not work as expected, as you noticed, the magic is not that easy to track down.
I prefer to use something like this:
implicit class RichResult (result: Result) {
def enableCors = result.withHeaders(
"Access-Control-Allow-Origin" -> "*"
, "Access-Control-Allow-Methods" -> "OPTIONS, GET, POST, PUT, DELETE, HEAD" // OPTIONS for pre-flight
, "Access-Control-Allow-Headers" -> "Accept, Content-Type, Origin, X-Json, X-Prototype-Version, X-Requested-With" //, "X-My-NonStd-Option"
, "Access-Control-Allow-Credentials" -> "true"
)
}
Then you can easily invoke it in your response like this:
Ok(Json.obj("ok" -> "1")).enableCors
It's easy to understand, can be placed only where you want to enable CORS, and very easy to debug!
I would not recommend writing/using any code to enable CORS which is basically a framework feature and only needs configuration.
The stuff you copied from the documentation is correct:
cors.conf where you modify the play.filters.cors settings. But you seem to have misconfigured something, e.g. the allowedOrigin = * should be configured as null in the config. (Have a look at the documentation page and the linked reference.conf)
# The allowed origins. If null, all origins are allowed.
play.filters.cors.allowedOrigins = null
You have correctly enabled the CORSFilter in your Filters.scala
Now test your configuration with a correct cURL CORS request:
curl -H "Origin: http://example.com" \
-H "Access-Control-Request-Method: GET" \
-H "Access-Control-Request-Headers: X-Requested-With" \
-X OPTIONS --verbose \
http://localhost:9000/
for me it worked after one day (maybe cash or other things)
application.conf:
play.http.filters = "filters.Filters"
play.filters.cors {
# allow all paths
pathPrefixes = ["/"]
# allow all origins (You can specify if you want)
allowedOrigins = null
allowedHttpMethods = ["GET", "POST"]
# allow all headers
allowedHttpHeaders = null
}
build.sbt :
val appDependencies = Seq(
filters,
....
)
in package filters.Filter :
package filters;
import javax.inject.Inject;
import play.mvc.EssentialFilter;
import play.filters.cors.CORSFilter;
import play.http.DefaultHttpFilters;
public class Filters extends DefaultHttpFilters {
CORSFilter corsFilter;
#Inject
public Filters(CORSFilter corsFilter) {
super(corsFilter);
this.corsFilter = corsFilter;
}
public EssentialFilter[] filters() {
return new EssentialFilter[] { corsFilter.asJava() };
}
}
and in my ajax call:
$.ajax({
method:'GET',
url: xxxxxxxx',
dataType: 'json',
headers: {'url': yyyyy,
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT',
'Access-Control-Allow-Headers': 'Content-Type'
},
success: function(data) {
....});
i have no more error, in prod and in local environment !! thank you all

spray-routing with spray-can - Hand off to actor only works sometimes in my application

Sorry, this is kind of long, because I need to include various files.
Problem
I am not sure what is going on in my setup [using spray 1.3.3]. I am trying to do file uploads using chunked requests which seemed to work as expected once or twice, but for some reason most of the time the actor never receives the chunks after the initial registration of the chunk handler is finished. It just disappears into oblivion and my request and logs just keep waiting the whole time. The 2 times out of 50 it did work were when I ran through the debugger. However even with the debugger it doesn't work mostly.
Based on various examples and discussions related to DemoService and FileUploadHandler, I am using spray-can to check the received HttpMessage for chunks, and at that point spawn off a separate route. I use curl with chunked encoding to test my output.
Please help! I have spent too many hours trying to get chunked requests mixed with routes working for my use case.
Code
Here is the code I have:
TestApp.scala
object TestApp extends App with GlobalConfig {
implicit val system = ActorSystem("TestApp")
implicit val ec = system.dispatcher
val healthActor = system.actorOf(Props[HealthStateActor])
val routes = new HealthcheckController(healthActor).route ~
new ResourceController().route
val requestRouter = system.actorOf(Props(new HttpRequestCustomHandler(routes)))
IO(Http) ! Http.Bind(requestRouter, "0.0.0.0", HttpBindPort)
}
FileUploadActor.scala
class FileUploadActor(client: ActorRef, requestMetadata: RequestMetadata, request: HttpRequest, ctx: RequestContext)
extends Actor with ActorLogging with GlobalConfig {
import request._
var bytesWritten = 0L
var bytes: Array[Byte] = "".getBytes
// client ! CommandWrapper(SetRequestTimeout(Duration.Inf)) // cancel timeout
def receive = {
case c: MessageChunk =>
log.info(s"Got ${c.data.length} bytes of chunked request $method $uri")
bytes ++= c.data.toByteArray
bytesWritten += c.data.length
case e: ChunkedMessageEnd =>
log.info(s"Got end of chunked request $method $uri. Writing $bytesWritten bytes for upload: $requestMetadata")
Try(saveFile(requestMetadata)) match {
case Success(_) => ctx.complete(HttpResponse(StatusCodes.Created, entity = "success"))
case Failure(f) => f.printStackTrace(); ctx.complete(HttpResponse(StatusCodes.InternalServerError, entity = "failure"))
}
// client ! CommandWrapper(SetRequestTimeout(UploadRequestTimeout.seconds)) // reset timeout to original value
context.stop(self)
}
}
FileUploadService.scala
The RegisterChunkHandler message is the last step where I see the debugger stop at break points, and where the logs go quiet. When it does work I can see MessageChunk messages being received by FileUploadActor.
trait FileUploadService extends Directives {
this: Actor with ActorLogging with GlobalConfig =>
def chunkedRoute() = {
path(resourceAPI / "upload" / "resource" / Segment) { resourceId =>
put {
detach() {
ctx => {
val request = ctx.request
val client = sender()
val handler = context.actorOf(Props(new FileUploadActor(client,
RequestMetadata(....),
request, ctx)))
sender ! RegisterChunkHandler(handler)
}
}
}
}
}
}
HttpRequestCustomHandler.scala
class HttpRequestCustomHandler(routes: Route, resourceProviderRef: ResourceProvider)
extends HttpServiceActor
with FileUploadService
with ActorLogging
with GlobalConfig {
val normal = routes
val chunked = chunkedRoute()
def resourceProvider = resourceProviderRef
val customReceive: Receive = {
// clients get connected to self (singleton handler)
case _: Http.Connected => sender ! Http.Register(self)
case r: HttpRequest =>
normal(RequestContext(r, sender(), r.uri.path).withDefaultSender(sender()))
case s#ChunkedRequestStart(HttpRequest(PUT, path, _, _, _)) =>
chunked(RequestContext(s.request, sender(), s.request.uri.path).withDefaultSender(sender()))
}
override def receive: Receive = customReceive
}
HttpRequestHandler.scala
abstract class HttpRequestHandler(routes: Route) extends HttpServiceActor{
override def receive: Receive = runRoute(routes)
}
application.conf:
spray.can.server {
request-timeout = 20 s
pipelining-limit = disabled
reaping-cycle = infinite
stats-support = off
request-chunk-aggregation-limit = 0
parsing.max-content-length = 100000000
parsing.incoming-auto-chunking-threshold-size = 15000000
chunkless-streaming = on
verbose-error-messages = on
verbose-error-logging = on
}
curl Success:
curl -vvv -X PUT -H "Content-Type: multipart/form-data" -d
'#/Users/abc/Documents/test.json'
'http://localhost:8180/upload/resource/02521081'
* Hostname was NOT found in DNS cache
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8100 (#0)
> PUT /upload/resource/02521081 HTTP/1.1
> User-Agent: curl/7.37.1
> Host: localhost:8100
> Accept: */*
> Content-Type: multipart/form-data
> Content-Length: 82129103
> Expect: 100-continue
>
< HTTP/1.1 100 Continue
< HTTP/1.1 201 Created
* Server spray-can/1.3.3 is not blacklisted
< Server: spray-can/1.3.3
< Date: Mon, 17 Aug 2015 07:45:58 GMT
< Content-Type: text/plain; charset=UTF-8
< Content-Length: 7
<
* Connection #0 to host localhost left intact
success
Failure with same curl (waits forever):
* Hostname was NOT found in DNS cache
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8100 (#0)
> PUT /upload/resource/02521081 HTTP/1.1
> User-Agent: curl/7.37.1
> Host: localhost:8100
> Accept: */*
> Transfer-Encoding: chunked
> Content-Type: multipart/form-data
> Expect: 100-continue
>
< HTTP/1.1 100 Continue
^C
Failure (waits forever):
resource 2015-08-17 01:33:09.374 [Resource] INFO akka.event.slf4j.Slf4jLogger - Slf4jLogger started
resource 2015-08-17 01:33:09.396 08:33:09.382UTC [Resource] DEBUG akka.event.EventStream main EventStream(akka://resource) - logger log1-Slf4jLogger started
resource 2015-08-17 01:33:09.404 08:33:09.383UTC [Resource] DEBUG akka.event.EventStream main EventStream(akka://resource) - Default Loggers started
resource 2015-08-17 01:33:10.160 08:33:10.159UTC [Resource] INFO spray.can.server.HttpListener Resource-akka.actor.default-dispatcher-4 akka://resource/user/IO-HTTP/listener-0 - Bound to /0.0.0.0:8100
Success (logs edited for clarity):
resource 2015-08-17 00:42:12.283 [Resource] INFO akka.event.slf4j.Slf4jLogger - Slf4jLogger started
resource 2015-08-17 00:42:12.295 07:42:12.290UTC [Resource] DEBUG akka.event.EventStream main EventStream(akka://resource) - logger log1-Slf4jLogger started
resource 2015-08-17 00:42:12.308 07:42:12.291UTC [Resource] DEBUG akka.event.EventStream main EventStream(akka://resource) - Default Loggers started
resource 2015-08-17 00:42:13.007 07:42:13.005UTC [Resource] INFO spray.can.server.HttpListener Resource-akka.actor.default-dispatcher-4 akka://resource/user/IO-HTTP/listener-0 - Bound to /0.0.0.0:8100
resource 2015-08-17 00:43:47.615 07:43:47.529UTC [Resource] DEBUG c.l.resource.actor.FileUploadActor Resource-akka.actor.default-dispatcher-7 akka://resource/user/$b/$b - Got 131072 bytes of chunked request PUT http://localhost:8100/resourcesvc/0.2/api/upload/resource/02521081-20e5-483a-929f-712a9e11d117/content/5adfb5-561d-4577-b6ad-c6f42eef98
resource 2015-08-17 00:43:49.220 07:43:49.204UTC [Resource] DEBUG c.l.resource.actor.FileUploadActor Resource-akka.actor.default-dispatcher-7 akka://resource/user/$b/$b - Got 131072 bytes of chunked request PUT http://localhost:8100/resourcesvc/0.2/api/upload/resource/02521081-20e5-483a-929f-712a9e11d117/content/5adfb5-561d-4577-b6ad-c6f42eef98
.
.
.
resource 2015-08-17 00:44:05.605 07:44:05.605UTC [Resource] DEBUG c.l.resource.actor.FileUploadActor Resource-akka.actor.default-dispatcher-7 akka://resource/user/$b/$b - Got 45263 bytes of chunked request PUT http://localhost:8100/resourcesvc/0.2/api/upload/resource/02521081-20e5-483a-929f-712a9e11d117/content/5adfb5-561d-4577-b6ad-c6f42eef98
resource 2015-08-17 00:44:05.633 07:44:05.633UTC [Resource] INFO c.l.resource.actor.FileUploadActor Resource-akka.actor.default-dispatcher-7 akka://resource/user/$b/$b - Got end of chunked request PUT http://localhost:8100/resourcesvc/0.2/api/upload/resource/02521081-20e5-483a-929f-712a9e11d117/content/5adfb5-561d-4577-b6ad-c6f42eef98. Writing 82129103 bytes for upload: RequestMetadata(...,multipart/form-data)
resource 2015-08-17 00:44:05.634 07:44:05.633UTC [Resource] DEBUG c.l.resource.actor.FileUploadActor Resource-akka.actor.default-dispatcher-7 akka://resource/user/$b/$b - actor is akka://resource/user/$b/$b, sender is Actor[akka://resource/temp/$a], client is Actor[akka://resource/temp/$a]
resource 2015-08-17 00:45:58.445 [Resource] DEBUG com.abc.resource.io.FileClient$ - UploadResult#109a69fb
resource 2015-08-17 00:45:58.445 [Resource] DEBUG com.abc.resource.io.FileClient$ - upload is done: true
Please let me know if you see anything weird. What is usually the reason an actor would vanish like this? Thanks in advance for your help!
UPDATE:
I added further logging and see that the from and to actors are apparently both turning into deadLetters even though that is clearly not the case in the log line above that. This is when message 'RegisterChunkHandler' is sent to sender in FileUploadService.scala.
sender ! RegisterChunkHandler(handler)
Related log:
resource 2015 - 0 8 - 17 21: 14: 32.173 20: 14: 32.173 UTC[Resource] DEBUG c.l.a.io.HttpRequestCustomHandler Resource - akka.actor.default - dispatcher - 3 akka :// Resource / user / httpcustomactor - sender is Actor[akka :// Resource / temp / $a]
resource 2015 - 0 8 - 17 21: 14: 32.175 20: 14: 32.175 UTC[Resource] INFO akka.actor.DeadLetterActorRef A4Resource-akka.actor.default-dispatcher-6 akka://A4Resource/deadLetters - Message [spray.can.Http$RegisterChunkHandler] from Actor[akka://A4Resource/user/httpcustomactor#-1286373908] to Actor[akka://A4Resource/deadLetters] was not delivered. [2] dead letters encountered. This logging can be turned off or adjusted with configuration settings 'akka.log-dead-letters' and 'akka.log-dead-letters-during-shutdown'.
resource 2015 - 0 8 - 17 21: 14: 32.176 20: 14: 32.176 UTC[Resource] DEBUG c.l.resource.actor.FileUploadActor Resource - akka.actor.default - dispatcher - 7 akka :// Resource / user / httpcustomactor / $a - pre - start
Any idea how this can be avoided?