Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,19 @@ def postExampleString(ws: play.api.libs.ws.StandaloneWSClient)(
}
```

To perform a QUERY request ([RFC 10008](https://www.rfc-editor.org/rfc/rfc10008)), which is safe and idempotent like GET but carries a request body:

```scala
import scala.concurrent.ExecutionContext
import play.api.libs.ws.DefaultBodyWritables._ // required

def queryExample(ws: play.api.libs.ws.StandaloneWSClient)(
implicit ec: ExecutionContext) = {
val queryBody = """{"search": "play framework"}"""
ws.url("...").query(queryBody).map { response => /* do something */ }
}
```

You can also define your own custom BodyReadable:

```scala
Expand Down Expand Up @@ -222,6 +235,18 @@ class MyClass {
}
```

To perform a QUERY request ([RFC 10008](https://www.rfc-editor.org/rfc/rfc10008)), which is safe and idempotent like GET but carries a request body:

```java
public class MyClient implements DefaultBodyWritables, DefaultBodyReadables {
public CompletionStage<String> doQuery() {
return client.url("http://example.com").query(body("{\"search\": \"play framework\"}")).thenApply(response ->
response.body(string())
);
}
}
```

You can define a custom `BodyReadable`:

```java
Expand Down
3 changes: 3 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ lazy val mimaSettings = Seq(
ProblemFilters.exclude[FinalClassProblem]("play.libs.ws.DefaultObjectMapper"),
// instance is now a method, not "just" a field anymore, so it always returns the current mapper
ProblemFilters.exclude[MissingFieldProblem]("play.libs.ws.DefaultObjectMapper.instance"),
// Add HTTP QUERY method support (RFC 10008)
ProblemFilters.exclude[ReversedMissingMethodProblem]("play.api.libs.ws.StandaloneWSRequest.query"),
ProblemFilters.exclude[ReversedMissingMethodProblem]("play.libs.ws.StandaloneWSRequest.query"),
)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ class AhcWSClientSpec(implicit val executionEnv: ExecutionEnv)
Results.Ok("Say hello to play")
case POST(_) =>
Results.Ok(s"POST: ${request.body.asText.getOrElse("")}")
case _ if request.method == "QUERY" =>
Results.Ok(s"QUERY: ${request.body.asText.getOrElse("")}")
case _ =>
Results.NotFound
}
Expand Down Expand Up @@ -129,6 +131,16 @@ class AhcWSClientSpec(implicit val executionEnv: ExecutionEnv)
}
}

"perform a QUERY request with body" in {
withClient() { client =>
val result = Await.result(
client.url(s"http://localhost:$testServerPort/index").query("query body").map(res => res.body[String]),
defaultTimeout
)
result must_=== "QUERY: query body"
}
}

"request a url as a Foo" in {
case class Foo(body: String)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,11 @@ public CompletionStage<? extends StandaloneWSResponse> put(BodyWritable body) {
return setMethod("PUT").setBody(body).execute();
}

@Override
public CompletionStage<? extends StandaloneWSResponse> query(BodyWritable body) {
return setMethod("QUERY").setBody(body).execute();
}

@Override
public CompletionStage<? extends StandaloneWSResponse> delete() {
return setMethod("DELETE").execute();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,11 @@ case class StandaloneAhcWSRequest(
withBody(body).execute("PUT")
}

/** Performs a QUERY request. */
override def query[T: BodyWritable](body: T): Future[Response] = {
withBody(body).execute("QUERY")
}

/**
* Sets the body for this request.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -684,4 +684,42 @@ class AhcWSRequestSpec extends Specification with AfterAll with DefaultBodyReada
req.getHeaders.getAll(HttpHeaderNames.CONTENT_TYPE.toString()).asScala must_== Seq("text/plain; charset=US-ASCII")
}

"For QUERY requests" in {

"set the method to QUERY and attach the body" in withClient { client =>
val req: AHCRequest = client
.url("http://playframework.com/")
.withBody("HELLO WORLD")
.withMethod("QUERY")
.asInstanceOf[StandaloneAhcWSRequest]
.buildRequest()
req.getMethod must_=== "QUERY"
(new String(req.getByteData, "UTF-8")) must_=== "HELLO WORLD"
}

"set content-type for the body" in withClient { client =>
val req: AHCRequest = client
.url("http://playframework.com/")
.withBody("HELLO WORLD")
.withMethod("QUERY")
.asInstanceOf[StandaloneAhcWSRequest]
.buildRequest()
req.getHeaders.get(HttpHeaderNames.CONTENT_TYPE.toString()) must_=== "text/plain; charset=UTF-8"
}

"send binary data as body" in withClient { client =>
val binData = ByteString((0 to 127).map(_.toByte).toArray)
val req: AHCRequest = client
.url("http://playframework.com/")
.addHttpHeaders(HttpHeaderNames.CONTENT_TYPE.toString() -> "application/octet-stream")
.withBody(binData)
.withMethod("QUERY")
.asInstanceOf[StandaloneAhcWSRequest]
.buildRequest()
(req.getMethod must_=== "QUERY").and {
ByteString(req.getByteData) must_=== binData
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,35 @@ class AhcWSRequestSpec extends Specification with DefaultBodyReadables with Defa
req.getRealm.isUsePreemptiveAuth must beFalse
}

"For QUERY requests" in {

"set method to QUERY and attach the body" in {
val client = StandaloneAhcWSClient.create(
AhcWSClientConfigFactory.forConfig(ConfigFactory.load(), this.getClass.getClassLoader), /*materializer*/ null
)
val req = new StandaloneAhcWSRequest(client, "http://playframework.com/", null)
.setBody(body("HELLO WORLD"))
.setMethod("QUERY")
.asInstanceOf[StandaloneAhcWSRequest]
.buildRequest()
(req.getMethod must_=== "QUERY").and {
req.getStringData must_=== "HELLO WORLD"
}
}

"set content-type for the body" in {
val client = StandaloneAhcWSClient.create(
AhcWSClientConfigFactory.forConfig(ConfigFactory.load(), this.getClass.getClassLoader), /*materializer*/ null
)
val req = new StandaloneAhcWSRequest(client, "http://playframework.com/", null)
.setBody(body("HELLO WORLD"))
.setMethod("QUERY")
.asInstanceOf[StandaloneAhcWSRequest]
.buildRequest()
req.getHeaders.get(HttpHeaderNames.CONTENT_TYPE) must_=== "text/plain; charset=UTF-8"
}
}

"Set Realm.UsePreemptiveAuth to true when WSAuthScheme.DIGEST not being used" in {
val client = StandaloneAhcWSClient.create(
AhcWSClientConfigFactory.forConfig(ConfigFactory.load(), this.getClass.getClassLoader), /*materializer*/ null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,18 @@ public interface StandaloneWSRequest {
*/
CompletionStage<? extends StandaloneWSResponse> put(BodyWritable body);

//-------------------------------------------------------------------------
// "QUERY"
//-------------------------------------------------------------------------

/**
* Perform a QUERY on the request asynchronously.
*
* @param body the BodyWritable
* @return a promise to the response
*/
CompletionStage<? extends StandaloneWSResponse> query(BodyWritable body);

//-------------------------------------------------------------------------
// Miscellaneous execution methods
//-------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,14 @@ trait StandaloneWSRequest {
*/
def put[T: BodyWritable](body: T): Future[Response]

/**
* Performs a QUERY request.
*
* @param body the payload body submitted with this request
* @return a future with the response for the QUERY request
*/
def query[T: BodyWritable](body: T): Future[Response]

/**
* Perform a DELETE on the request asynchronously.
*/
Expand Down
Loading