-
Notifications
You must be signed in to change notification settings - Fork 86
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WORK IN PROGRESS: Akka-HTTP backend by @2m #205
Conversation
Separate projects for the implmenetation and tests that are shared between the implementations.
"throw an exception on invalid url" in { | ||
withClient() { client => | ||
{ client.url("localhost") } must throwAn[IllegalArgumentException] | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, consistent with existing WS I guess?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure play-ws has a consistent behavior between Java and Scala APIs though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, it is consistent that both versions throw an Exception:
In Scala:
java.lang.IllegalArgumentException: Invalid URL localhost
at play.api.libs.ws.ahc.StandaloneAhcWSClient.validate(StandaloneAhcWSClient.scala:84)
at play.api.libs.ws.ahc.StandaloneAhcWSClient.url(StandaloneAhcWSClient.scala:42)
... 39 elided
Caused by: java.lang.IllegalArgumentException: localhost could not be parsed into a proper Uri, missing scheme
at play.shaded.ahc.org.asynchttpclient.uri.Uri.create(Uri.java:40)
at play.shaded.ahc.org.asynchttpclient.uri.Uri.create(Uri.java:32)
at play.api.libs.ws.ahc.StandaloneAhcWSClient.validate(StandaloneAhcWSClient.scala:81)
... 40 more
And in Java:
java.lang.RuntimeException: java.net.MalformedURLException: no protocol: localhost
at play.libs.ws.ahc.StandaloneAhcWSRequest.<init>(StandaloneAhcWSRequest.java:89)
at play.libs.ws.ahc.StandaloneAhcWSClient.url(StandaloneAhcWSClient.java:60)
... 39 elided
Caused by: java.net.MalformedURLException: no protocol: localhost
at java.net.URL.<init>(URL.java:593)
at java.net.URL.<init>(URL.java:490)
at java.net.URL.<init>(URL.java:439)
at play.libs.ws.ahc.StandaloneAhcWSRequest.<init>(StandaloneAhcWSRequest.java:75)
... 40 more
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The URL parsing happens immediately, not in the Future, but the exact exception wasn't specified in code (no @throws in the scala trait, at least).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The tests are only shared between the different backends but are separate for Scala and Java APIs. So the functionality is encoded per API basis and I have modified akka-http backend implementation to follow the behavior of AHC backend.
@@ -0,0 +1,16 @@ | |||
package play.api.libs.ws.akkahttp |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
copyright
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh, right. Added manually. Would be best to use sbt-header plugin in the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
client.close() | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So you did pull off the sharing tests :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea. Tests are shared between the backends, but are different between the Scala and Java APIs.
It might be possible to share the tests between the Scala and Java APIs as well, but it would need another abstraction layer. Maybe implemented using FreeMonad. :)
withClient() { client => | ||
val callList = scala.collection.mutable.ArrayBuffer[Int]() | ||
client.url(s"http://localhost:$testServerPort") | ||
.withRequestFilter(new CallbackRequestFilter(callList, 1)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
are we sure that's safe? (concurrency wise)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh heck no, that's a implementation for the spec.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea. For the test its ok, since every tests has its own immutable request filter.
|
||
class AkkaHttpWSRequestFilterSpec(implicit val executionEnv: ExecutionEnv) extends WSRequestFilterSpec { | ||
def withClient()(block: StandaloneWSClient => Result): Result = { | ||
val client = StandaloneAkkaHttpWSClient() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, so our goal is I guess to just have people change their impl class here and it should be source compatible.
I thought we could do it as just a configuration flag which backend is to be used hm
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When used in Play framework, the client is injected using the API interface (implementation non-specific) like so:
class Application @Inject() (ws: WSClient) extends Controller {}
So to use the akka-http backend client one will have to change the dependency in the project. Something like
<<< libraryDependencies += ws
>>> libraryDependencies += wsAkkaHttp
or similar.
*/ | ||
@Override | ||
public Object getUnderlying() { | ||
return null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should be Http.get(system)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, added.
@Override | ||
public void close() throws IOException { | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could be httpshutdownAllConnectionPools()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, added.
|
||
private final ActorSystem sys; | ||
private final Materializer mat; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Store the val http = Http()
created from those and pass it into the StandaloneAkkaHttpWSRequest
.
This way if someone calls the close here the right things would be closed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As Http
is an extension, Http.get(sys).shutdownAllConnectionPools()
in close
is going to be the same as using the stored reference, right?
*/ | ||
@Override | ||
public Map<String, List<String>> getHeaders() { | ||
return null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FIXME?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea. :) I treat return null
and ???
as fixme's currently. WIll not merge until any of these are left.
import java.util.Map; | ||
import java.util.Optional; | ||
|
||
public final class StandaloneAkkaHttpWSResponse implements StandaloneWSResponse { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems a bit inversed? Thought this would wrap the Akka HTTP HttpResponse?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. Wrapping Scala API did not fly. Now it wraps akka.http.javadsl.model.HttpResponse
/** | ||
* Get the underlying response object. | ||
*/ | ||
override def underlying[T]: T = ??? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return the response
?
|
||
final class StandaloneAkkaHttpWSResponse private (val response: HttpResponse)(implicit val mat: Materializer) extends StandaloneWSResponse { | ||
|
||
final val UnmarshalTimeout = 1.second |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could it be configurable somehow?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Configuration still needs to be tackled. Noted in the mega ticket.
/** | ||
* The response status message. | ||
*/ | ||
override def statusText: String = ??? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's the reason
field for us : scala> akka.http.scaladsl.model.StatusCodes.OK.reason
*/ | ||
override def body: String = { | ||
import akka.http.scaladsl.unmarshalling.PredefinedFromEntityUnmarshallers.stringUnmarshaller | ||
Await.result(Unmarshal(response).to[String], UnmarshalTimeout) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would rather suggest that we do a toStrict
on the response, and store the strictified entity.
The current impl would blow up if we do r.body; r.body
I think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I agree, that will only work once. We first need to clarify the semantics whether these methods can be called several times and then figure out how to be able to make it work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think play people value convenience a lot.in this API... perhaps we can do some cheating with a CAS ing state machine here? like, here spin a future and await hehe other to drain? a hack, but it would get us to ship this WS impl. and once we do the smarter things in Akka http we could remove the hack here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It could probably just be a lazy val that collects all the stuff. The question is in which way, so that the below methods would also work. That's what I meant with "clarify the semantics", i.e. whether it is allowed to call one or more of those body interpreting/accessing methods and whether it is allowed to call them only once or several times.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
responded only seeing the first comment
Yea, current impl will blow up with such a testcase and needs to be fixed.
I am not sure about calling toStrict every time. If the user is using the client for streamed responses and is materializing stream from bodyAsSource
only once then we should not waste resources with by calling toStrict
internally. Take a look at 8d4a2e8 where I store strict response only when getBody
or getBodyBytes
is called.
/** | ||
* @return the response as a source of bytes | ||
*/ | ||
override def bodyAsSource: Source[ByteString, _] = response.entity.dataBytes |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
naisu :-)
/** | ||
* Execute this request | ||
*/ | ||
override def execute(): Future[Response] = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good to ask play team about semantics here.
Does execute
mean strict and stream
"streaming" as our default mode is?
If so, then we should rather implement the stream()
and the execute()
should be implemented in terms of it, by applying a toStrict()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does execute mean strict and stream "streaming" as our default mode is?
Yes. We should make this API clearer later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. That makes sense.
* @return a future with the response for the PATCH request | ||
*/ | ||
override def patch[T: BodyWritable](body: T): Future[Response] = | ||
withBody(body).execute("PATCH") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
naisu ^_^
/** | ||
* Sets the proxy server to use in this request | ||
*/ | ||
override def withProxyServer(proxyServer: WSProxyServer): Self = ??? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
missing feature in our impl right? AFAIR we still only do https proxy but not any http?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add fixme and link to ticket in akka-http
/** | ||
* Sets whether redirects (301, 302) should be followed automatically | ||
*/ | ||
override def withFollowRedirects(follow: Boolean): Self = ??? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing Akka HTTP feature, please link to the ticket from here :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And let's mark the ticket as "play integration" (we have such tag)
project/Dependencies.scala
Outdated
@@ -47,15 +47,20 @@ object Dependencies { | |||
|
|||
val akkaVersion = "2.5.3" | |||
val akkaStreams = Seq("com.typesafe.akka" %% "akka-stream" % akkaVersion) | |||
val akkaHttp = Seq("com.typesafe.akka" %% "akka-http" % "10.0.8") | |||
val akkaHttp = Seq("com.typesafe.akka" %% "akka-http" % "10.0.11") | |||
val akkaHttpCore = Seq("com.typesafe.akka" %% "akka-http" % "10.0.11") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this duplication intentional? (akkaHttp
is used, at least once, in build.sbt
but not in Dependencies
)
If the duplication is intentional, what is the difference between akkaHttp
and akkaHttpCore
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, messed this up. My idea was to make the new akka-http backend depend only on akka-http-core. But later I needed more (marshalling) from akka-http. Fixed in 108ee30
*/ | ||
@Override | ||
public CompletionStage<? extends StandaloneWSResponse> get() { | ||
return execute("GET"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we use the constant values here where we know it statically like HttpMethods.GET
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in f00d8b9
import java.util.concurrent.TimeUnit; | ||
import java.util.concurrent.TimeoutException; | ||
|
||
public final class StandaloneAkkaHttpWSRequest implements StandaloneWSRequest { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we talked about that already, but is there any good reason to implement these classes in Java? Keeping the interfaces written in Java might make sense but the implementation could still be done in Scala, right? If that's not possible I would still prefer an approach where the Java classes just delegate to the real implementation which could then be written in Scala.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not possible that easily:
This was an expirment i did in the past: https://github.com/schmitch/play-ws-akka
The biggest probem is that if something returns java.lang.Boolean
instead of boolean
or everything that returns a Future
/CompletionStage
I created a WrappedFuture
to support that: https://github.com/schmitch/play-ws-akka/blob/master/play-ws-akka-standalone/src/main/scala/play/api/libs/ws/akkaws/WrappedFuture.scala Basically all overlapping types, might be problematic.
The delegate approach might be more feasible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At least doing it this way uncovered some missing Java API methods in Akka Http.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there any good reason to implement these classes in Java
Similar to what @2m said, the main reason is idiom -- if you're changing or adding to the API in Java but writing the tests in Scala, you often don't feel the pain of refactoring because you can leverage the Scala Collections API. It also helps for discovery, because Java users don't have to decipher Scala tests to see how the code should work, and writing documentation, because the tests and examples match up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was only talking about implementation. Interfaces and tests could and probably should be done in Java, I agree.
Addressed review feedback. Will continue implementing missing features and adding tests further. |
This PR now contains everything, except the features listed in #207 I have also published a JAR from this PR for anyone that would like to try it out: resolvers += Resolver.bintrayRepo("2m", "maven")
libraryDependencies += "com.typesafe.play" %% "play-akka-http-ws-standalone" % "1.0.0+62-7728dc21" This PR does not touch the existing implementation, so after a review it can be safely merged and the work can continue in separate PRs. |
@@ -47,7 +47,7 @@ object Dependencies { | |||
|
|||
val akkaVersion = "2.5.3" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
2.5.8?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, we could update it. But should probably be done in a separate PR.
Added one more commit which adds a HTTPS test and also allows to use a custom HTTPS connection context. This is ready to be reviewed and merged so the following work for implementing #207 can be done as separate PRs. |
Closing, Martynas will submit the same branch as not-WIP PR :) |
Follow up PR: #208 |
Opening a PR of @2m's work just so it's easier to comment and review :)
THIS IS NOT YET INTENDED TO BE MERGED.