├── .github ├── mergify.yml ├── workflows │ ├── publish.yml │ ├── release-drafter.yml │ ├── dependency-graph.yml │ └── build-test.yml ├── scala-steward.conf ├── dependabot.yml ├── PULL_REQUEST_TEMPLATE.md └── ISSUE_TEMPLATE.md ├── integration-tests └── src │ └── test │ ├── resources │ ├── test.json │ └── logback-test.xml │ ├── java │ └── play │ │ └── libs │ │ └── ws │ │ └── ahc │ │ ├── HeaderAppendingFilter.java │ │ ├── CallbackRequestFilter.java │ │ ├── StandaloneWSClientSupport.scala │ │ ├── XMLRequestTest.java │ │ ├── JsonRequestTest.java │ │ └── ByteStringRequestTest.java │ └── scala │ └── play │ ├── api │ └── libs │ │ └── ws │ │ └── ahc │ │ ├── StandaloneWSClientSupport.scala │ │ ├── cache │ │ └── CachingSpec.scala │ │ ├── XMLRequestSpec.scala │ │ └── JsonRequestSpec.scala │ ├── NettyServerProvider.scala │ └── libs │ └── ws │ └── ahc │ ├── AhcWSClientSpec.scala │ └── AhcWSRequestFilterSpec.scala ├── project ├── build.properties ├── plugins.sbt ├── AutomaticModuleName.scala ├── CleanShadedPlugin.scala └── Dependencies.scala ├── .git-blame-ignore-revs ├── .gitignore ├── play-ws-standalone └── src │ ├── main │ ├── java │ │ └── play │ │ │ └── libs │ │ │ └── ws │ │ │ ├── WSSignatureCalculator.java │ │ │ ├── WSAuthScheme.java │ │ │ ├── BodyReadable.java │ │ │ ├── BodyWritable.java │ │ │ ├── WSRequestExecutor.java │ │ │ ├── WSBody.java │ │ │ ├── WSRequestFilter.java │ │ │ ├── InMemoryBodyWritable.java │ │ │ ├── WSAuthInfo.java │ │ │ ├── WSCookie.java │ │ │ ├── StandaloneWSClient.java │ │ │ ├── WSCookieBuilder.java │ │ │ ├── SourceBodyWritable.java │ │ │ ├── DefaultWSCookie.java │ │ │ ├── DefaultBodyReadables.java │ │ │ └── StandaloneWSResponse.java │ ├── scala │ │ └── play │ │ │ └── api │ │ │ └── libs │ │ │ └── ws │ │ │ ├── StandaloneWSClient.scala │ │ │ ├── WSClientConfig.scala │ │ │ ├── Body.scala │ │ │ ├── WSRequestFilter.scala │ │ │ ├── WSConfigParser.scala │ │ │ ├── DefaultBodyReadables.scala │ │ │ ├── WS.scala │ │ │ ├── DefaultBodyWritables.scala │ │ │ └── StandaloneWSResponse.scala │ └── resources │ │ └── reference.conf │ └── test │ └── scala │ └── play │ └── api │ └── libs │ └── ws │ └── WSConfigParserSpec.scala ├── play-ws-standalone-json └── src │ ├── main │ ├── scala │ │ └── play │ │ │ ├── libs │ │ │ └── ws │ │ │ │ └── DefaultObjectMapper.scala │ │ │ └── api │ │ │ └── libs │ │ │ └── ws │ │ │ ├── JsonBodyReadables.scala │ │ │ └── JsonBodyWritables.scala │ └── java │ │ └── play │ │ └── libs │ │ └── ws │ │ ├── JsonBodyReadables.java │ │ └── JsonBodyWritables.java │ └── test │ └── scala │ └── play │ └── api │ └── libs │ └── ws │ └── JsonBodyReadablesSpec.scala ├── play-ahc-ws-standalone └── src │ ├── test │ └── scala │ │ └── play │ │ ├── api │ │ └── libs │ │ │ ├── ws │ │ │ └── ahc │ │ │ │ ├── cache │ │ │ │ ├── BackgroundAsyncHandlerSpec.scala │ │ │ │ ├── CacheAsyncHandlerSpec.scala │ │ │ │ ├── StubHttpCache.scala │ │ │ │ ├── CacheableResponseSpec.scala │ │ │ │ └── AhcWSCacheSpec.scala │ │ │ │ └── AhcWSClientConfigParserSpec.scala │ │ │ └── oauth │ │ │ └── OAuthSpec.scala │ │ └── libs │ │ └── ws │ │ └── ahc │ │ └── AhcWSResponseSpec.scala │ └── main │ ├── scala │ └── play │ │ └── api │ │ └── libs │ │ ├── ws │ │ └── ahc │ │ │ ├── CaseInsensitiveOrdered.scala │ │ │ ├── AhcUtilities.scala │ │ │ ├── AhcWSUtils.scala │ │ │ ├── cache │ │ │ ├── EffectiveURIKey.scala │ │ │ ├── Cache.scala │ │ │ ├── Debug.scala │ │ │ ├── BackgroundAsyncHandler.scala │ │ │ └── CacheAsyncConnection.scala │ │ │ ├── AhcLoggerFactory.scala │ │ │ ├── OrderPreserving.scala │ │ │ ├── StandaloneAhcWSResponse.scala │ │ │ ├── CookieBuilder.scala │ │ │ ├── Streamed.scala │ │ │ ├── FormUrlEncodedParser.scala │ │ │ ├── AhcCurlRequestLogger.scala │ │ │ └── StreamedResponse.scala │ │ └── oauth │ │ └── OAuth.scala │ ├── resources │ └── reference.conf │ └── java │ └── play │ └── libs │ ├── ws │ └── ahc │ │ ├── AhcWSClientConfigFactory.java │ │ ├── CookieBuilder.java │ │ ├── StandaloneAhcWSResponse.java │ │ ├── StreamedResponse.java │ │ └── AhcCurlRequestLogger.java │ └── oauth │ └── OAuth.java ├── play-ws-standalone-xml └── src │ └── main │ ├── java │ └── play │ │ └── libs │ │ └── ws │ │ ├── XMLBodyReadables.java │ │ ├── XMLBodyWritables.java │ │ └── XML.java │ └── scala │ └── play │ └── api │ └── libs │ └── ws │ ├── XMLBodyReadables.scala │ ├── XMLBodyWritables.scala │ └── XML.scala ├── .scalafmt.conf └── bench └── src └── main └── scala └── play └── api └── libs └── ws └── ahc └── StandaloneAhcWSRequestBenchMapsBench.scala /.github/mergify.yml: -------------------------------------------------------------------------------- 1 | extends: .github 2 | -------------------------------------------------------------------------------- /integration-tests/src/test/resources/test.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playframework/play-ws/main/integration-tests/src/test/resources/test.json -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | # 4 | sbt.version=1.11.7 5 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Scala Steward: Reformat with scalafmt 3.5.8 2 | 115fb7c2782ba8ca7d31b43673832ee5a6851de0 3 | 4 | # Scala Steward: Reformat with scalafmt 3.8.3 5 | b890253f092631d1c460353190098a0768b59176 6 | 7 | # Scala Steward: Reformat with scalafmt 3.9.7 8 | 9a1d18ffe89f79afb720fa31e58b15e123a0bd61 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | target 3 | .idea 4 | .idea_modules 5 | .classpath 6 | .project 7 | .settings 8 | RUNNING_PID 9 | generated.keystore 10 | generated.truststore 11 | *.log 12 | .bsp/ 13 | 14 | # Scala-IDE specific 15 | .scala_dependencies 16 | .project 17 | .settings 18 | .cache-main 19 | .cache-tests 20 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | branches: # Snapshots 6 | - main 7 | tags: ["**"] # Releases 8 | 9 | jobs: 10 | publish-artifacts: 11 | name: Publish / Artifacts 12 | uses: playframework/.github/.github/workflows/publish.yml@v4 13 | secrets: inherit 14 | -------------------------------------------------------------------------------- /.github/scala-steward.conf: -------------------------------------------------------------------------------- 1 | commits.message = "${artifactName} ${nextVersion} (was ${currentVersion})" 2 | 3 | pullRequests.grouping = [ 4 | { name = "patches", "title" = "Patch updates", "filter" = [{"version" = "patch"}] } 5 | ] 6 | 7 | updates.pin = [ 8 | { groupId = "com.typesafe.netty", artifactId = "netty-reactive-streams", version = "2." } 9 | ] 10 | -------------------------------------------------------------------------------- /play-ws-standalone/src/main/java/play/libs/ws/WSSignatureCalculator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.libs.ws; 6 | 7 | /** 8 | * Sign a WS call. 9 | */ 10 | public interface WSSignatureCalculator { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /play-ws-standalone/src/main/java/play/libs/ws/WSAuthScheme.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.libs.ws; 6 | 7 | public enum WSAuthScheme { 8 | DIGEST, 9 | BASIC, 10 | NTLM, 11 | SPNEGO, 12 | KERBEROS, 13 | NONE 14 | } 15 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.3.1") 2 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.6") 3 | addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.4") 4 | addSbtPlugin("com.github.sbt" % "sbt-header" % "5.11.0") 5 | addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.8") 6 | addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.11.2") 7 | -------------------------------------------------------------------------------- /play-ws-standalone/src/main/java/play/libs/ws/BodyReadable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.libs.ws; 6 | 7 | import java.util.function.Function; 8 | 9 | /** 10 | * Converts a response body into type R 11 | */ 12 | public interface BodyReadable extends Function { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /play-ws-standalone-json/src/main/scala/play/libs/ws/DefaultObjectMapper.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.libs.ws 6 | 7 | import com.fasterxml.jackson.databind.ObjectMapper 8 | 9 | object DefaultObjectMapper { 10 | def instance(): ObjectMapper = play.api.libs.json.jackson.JacksonJson.get.mapper() 11 | } 12 | -------------------------------------------------------------------------------- /play-ws-standalone/src/main/java/play/libs/ws/BodyWritable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.libs.ws; 6 | 7 | /** 8 | * Writes out a {@code WSBody} 9 | * 10 | * @param the type of body. 11 | */ 12 | public interface BodyWritable { 13 | WSBody body(); 14 | 15 | String contentType(); 16 | } 17 | -------------------------------------------------------------------------------- /play-ws-standalone/src/main/java/play/libs/ws/WSRequestExecutor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.libs.ws; 6 | 7 | import java.util.concurrent.CompletionStage; 8 | import java.util.function.Function; 9 | 10 | 11 | public interface WSRequestExecutor extends Function> { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | target-branch: "3.0.x" 12 | commit-message: 13 | prefix: "[3.0.x] " 14 | - package-ecosystem: "github-actions" 15 | directory: "/" 16 | schedule: 17 | interval: "weekly" 18 | target-branch: "2.2.x" 19 | commit-message: 20 | prefix: "[2.2.x] " 21 | -------------------------------------------------------------------------------- /play-ahc-ws-standalone/src/test/scala/play/api/libs/ws/ahc/cache/BackgroundAsyncHandlerSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws.ahc.cache 6 | 7 | import org.specs2.mutable.Specification 8 | 9 | class BackgroundAsyncHandlerSpec extends Specification { 10 | 11 | "BackgroundAsyncHandler" should { 12 | 13 | "assemble a response" in { 14 | pending 15 | } 16 | 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /play-ahc-ws-standalone/src/test/scala/play/api/libs/oauth/OAuthSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.oauth 6 | 7 | import org.specs2.mutable.Specification 8 | 9 | class OAuthSpec extends Specification { 10 | "OAuth" should { 11 | "be able to use signpost OAuth" in { 12 | Class.forName("play.shaded.oauth.oauth.signpost.OAuth") must not(throwA[ClassNotFoundException]) 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | update_release_draft: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: release-drafter/release-drafter@v6 13 | with: 14 | name: "Play WS, async HTTP client $RESOLVED_VERSION" 15 | config-name: release-drafts/increasing-minor-version.yml # located in .github/ in the default branch within this or the .github repo 16 | commitish: ${{ github.ref_name }} 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | -------------------------------------------------------------------------------- /play-ahc-ws-standalone/src/test/scala/play/api/libs/ws/ahc/cache/CacheAsyncHandlerSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws.ahc.cache 6 | 7 | import org.specs2.mutable.Specification 8 | 9 | class CacheAsyncHandlerSpec extends Specification { 10 | 11 | "CacheAsyncHandlerSpec" should { 12 | 13 | "validate" in { 14 | pending 15 | // val cache = generateCache 16 | // val handler = new CacheAsyncHandler(cache) 17 | } 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /play-ws-standalone-xml/src/main/java/play/libs/ws/XMLBodyReadables.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.libs.ws; 6 | 7 | import org.w3c.dom.Document; 8 | import org.xml.sax.InputSource; 9 | 10 | import java.io.ByteArrayInputStream; 11 | 12 | /** 13 | * 14 | */ 15 | public interface XMLBodyReadables { 16 | 17 | default BodyReadable xml() { 18 | return response -> XML.fromInputSource(new InputSource(new ByteArrayInputStream(response.getBodyAsBytes().toArray()))); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /play-ws-standalone/src/main/java/play/libs/ws/WSBody.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.libs.ws; 6 | 7 | import java.util.function.Supplier; 8 | 9 | /** 10 | * A WS body marker interface. The client is responsible for creating these instances: 11 | */ 12 | public interface WSBody extends Supplier { 13 | 14 | } 15 | 16 | abstract class AbstractWSBody implements WSBody { 17 | private final A body; 18 | 19 | AbstractWSBody(A body) { 20 | this.body = body; 21 | } 22 | 23 | public A get() { 24 | return body; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/dependency-graph.yml: -------------------------------------------------------------------------------- 1 | name: Dependency Graph 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | concurrency: 8 | # Only run once for latest commit per ref and cancel other (previous) runs. 9 | group: dependency-graph-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | permissions: 13 | contents: write # this permission is needed to submit the dependency graph 14 | 15 | jobs: 16 | dependency-graph: 17 | name: Submit dependencies to GitHub 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v6 21 | with: 22 | fetch-depth: 0 23 | ref: ${{ inputs.ref }} 24 | - uses: sbt/setup-sbt@v1 25 | - uses: scalacenter/sbt-dependency-submission@v3 26 | -------------------------------------------------------------------------------- /integration-tests/src/test/java/play/libs/ws/ahc/HeaderAppendingFilter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.libs.ws.ahc; 6 | 7 | import play.libs.ws.*; 8 | 9 | public class HeaderAppendingFilter implements WSRequestFilter { 10 | 11 | private final String key; 12 | private final String value; 13 | 14 | public HeaderAppendingFilter(String key, String value) { 15 | this.key = key; 16 | this.value = value; 17 | } 18 | 19 | @Override 20 | public WSRequestExecutor apply(WSRequestExecutor executor) { 21 | return request -> executor.apply(request.addHeader(key, value)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /integration-tests/src/test/scala/play/api/libs/ws/ahc/StandaloneWSClientSupport.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws.ahc 6 | 7 | import org.apache.pekko.stream.Materializer 8 | import org.specs2.execute.Result 9 | 10 | trait StandaloneWSClientSupport { 11 | 12 | def materializer: Materializer 13 | 14 | def withClient( 15 | config: AhcWSClientConfig = AhcWSClientConfigFactory.forConfig() 16 | )(block: StandaloneAhcWSClient => Result): Result = { 17 | val client = StandaloneAhcWSClient(config)(materializer) 18 | try { 19 | block(client) 20 | } finally { 21 | client.close() 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /play-ahc-ws-standalone/src/main/scala/play/api/libs/ws/ahc/CaseInsensitiveOrdered.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws.ahc 6 | 7 | /** 8 | * Case Insensitive Ordering. We first compare by length, then 9 | * use a case insensitive lexicographic order. This allows us to 10 | * use a much faster length comparison before we even start looking 11 | * at the content of the strings. 12 | */ 13 | private[ahc] object CaseInsensitiveOrdered extends Ordering[String] { 14 | def compare(x: String, y: String): Int = { 15 | val xl = x.length 16 | val yl = y.length 17 | if (xl < yl) -1 else if (xl > yl) 1 else x.compareToIgnoreCase(y) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Pull Request Checklist 2 | 3 | * [ ] Have you read through the [contributor guidelines](https://www.playframework.com/contributing)? 4 | * [ ] Have you [squashed your commits](https://www.playframework.com/documentation/latest/WorkingWithGit#Squashing-commits)? 5 | * [ ] Have you added copyright headers to new files? 6 | * [ ] Have you checked that both Scala and Java APIs are updated? 7 | * [ ] Have you updated the documentation for both Scala and Java sections? 8 | * [ ] Have you added tests for any changed functionality? 9 | 10 | ## Fixes 11 | 12 | Fixes #xxxx 13 | 14 | ## Purpose 15 | 16 | What does this PR do? 17 | 18 | ## Background Context 19 | 20 | Why did you take this approach? 21 | 22 | ## References 23 | 24 | Are there any relevant issues / PRs / mailing lists discussions? 25 | -------------------------------------------------------------------------------- /integration-tests/src/test/java/play/libs/ws/ahc/CallbackRequestFilter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.libs.ws.ahc; 6 | 7 | import play.libs.ws.*; 8 | 9 | import java.util.List; 10 | 11 | public class CallbackRequestFilter implements WSRequestFilter { 12 | 13 | private final List callList; 14 | private final Integer value; 15 | 16 | public CallbackRequestFilter(List callList, Integer value) { 17 | this.callList = callList; 18 | this.value = value; 19 | } 20 | 21 | @Override 22 | public WSRequestExecutor apply(WSRequestExecutor executor) { 23 | callList.add(value); 24 | return executor; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /play-ws-standalone-xml/src/main/java/play/libs/ws/XMLBodyWritables.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.libs.ws; 6 | 7 | import org.apache.pekko.util.ByteString; 8 | import org.w3c.dom.Document; 9 | 10 | /** 11 | * 12 | */ 13 | public interface XMLBodyWritables { 14 | 15 | /** 16 | * Creates a {@link InMemoryBodyWritable} for JSON, setting the content-type to "application/json". 17 | * 18 | * @param document the node to pass in. 19 | * @return a {@link InMemoryBodyWritable} instance. 20 | */ 21 | default BodyWritable body(Document document) { 22 | return new InMemoryBodyWritable(XML.toBytes(document), "application/xml"); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | runner.dialect = scala213 2 | align.preset = true 3 | assumeStandardLibraryStripMargin = true 4 | danglingParentheses.preset = true 5 | docstrings.style = Asterisk 6 | docstrings.wrap = false 7 | maxColumn = 120 8 | project.git = true 9 | rewrite.rules = [ AvoidInfix, ExpandImportSelectors, RedundantParens, SortModifiers, PreferCurlyFors ] 10 | rewrite.sortModifiers.order = [ "private", "protected", "final", "sealed", "abstract", "implicit", "override", "lazy" ] 11 | spaces.inImportCurlyBraces = true # more idiomatic to include whitepsace in import x.{ yyy } 12 | trailingCommas = preserve 13 | newlines.afterCurlyLambda = preserve 14 | version = 3.10.2 15 | rewrite.scala3.convertToNewSyntax = true 16 | rewrite.scala3.newSyntax.control = false 17 | runner.dialectOverride { 18 | allowSignificantIndentation = false 19 | allowAsForImportRename = false 20 | allowStarWildcardImport = false 21 | } 22 | -------------------------------------------------------------------------------- /play-ahc-ws-standalone/src/main/scala/play/api/libs/ws/ahc/AhcUtilities.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws.ahc 6 | 7 | import play.shaded.ahc.io.netty.handler.codec.http.HttpHeaders 8 | 9 | import scala.collection.immutable.TreeMap 10 | 11 | /** 12 | * Useful mapping code. 13 | */ 14 | trait AhcUtilities { 15 | 16 | def headersToMap(headers: HttpHeaders): TreeMap[String, Seq[String]] = { 17 | import scala.jdk.CollectionConverters._ 18 | val mutableMap = scala.collection.mutable.HashMap[String, Seq[String]]() 19 | headers.names().asScala.foreach { name => 20 | mutableMap.put(name, headers.getAll(name).asScala.toSeq) 21 | } 22 | TreeMap[String, Seq[String]]()(CaseInsensitiveOrdered) ++ mutableMap 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /play-ahc-ws-standalone/src/test/scala/play/api/libs/ws/ahc/cache/StubHttpCache.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws.ahc.cache 6 | 7 | import scala.collection.mutable 8 | import scala.concurrent.Future 9 | 10 | class StubHttpCache extends Cache { 11 | 12 | private val underlying = new mutable.HashMap[EffectiveURIKey, ResponseEntry]() 13 | 14 | override def remove(key: EffectiveURIKey): Future[Unit] = Future.successful(underlying.remove(key)) 15 | 16 | override def put(key: EffectiveURIKey, entry: ResponseEntry): Future[Unit] = 17 | Future.successful(underlying.put(key, entry)) 18 | 19 | override def get(key: EffectiveURIKey): Future[Option[ResponseEntry]] = Future.successful(underlying.get(key)) 20 | 21 | override def close(): Unit = {} 22 | 23 | } 24 | -------------------------------------------------------------------------------- /project/AutomaticModuleName.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | package play.ws; 5 | 6 | import sbt.Def 7 | import sbt._ 8 | import sbt.Keys._ 9 | 10 | /** 11 | * Helper to set Automatic-Module-Name in projects. 12 | * 13 | * !! DO NOT BE TEMPTED INTO AUTOMATICALLY DERIVING THE NAMES FROM PROJECT NAMES !! 14 | * 15 | * The names carry a lot of implications and DO NOT have to always align 1:1 with the group ids or package names, 16 | * though there should be of course a strong relationship between them. 17 | */ 18 | object AutomaticModuleName { 19 | private val AutomaticModuleName = "Automatic-Module-Name" 20 | 21 | def settings(name: String): Seq[Def.Setting[Task[Seq[PackageOption]]]] = 22 | Seq( 23 | Compile / packageBin / packageOptions += Package.ManifestAttributes(AutomaticModuleName -> name) 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /play-ws-standalone-xml/src/main/scala/play/api/libs/ws/XMLBodyReadables.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws 6 | 7 | import java.io.ByteArrayInputStream 8 | 9 | import scala.xml.InputSource 10 | 11 | trait XMLBodyReadables { 12 | 13 | import scala.xml.Elem 14 | 15 | /** 16 | * Converts a response body into XML document: 17 | * 18 | * {{{ 19 | * import scala.xml.Elem 20 | * 21 | * import play.api.libs.ws.StandaloneWSResponse 22 | * import play.api.libs.ws.XMLBodyReadables._ 23 | * 24 | * def foo(resp: StandaloneWSResponse): Elem = resp.body[Elem] 25 | * }}} 26 | */ 27 | implicit val readableAsXml: BodyReadable[Elem] = BodyReadable { response => 28 | XML.parser.load(new InputSource(new ByteArrayInputStream(response.bodyAsBytes.toArray))) 29 | } 30 | 31 | } 32 | 33 | object XMLBodyReadables extends XMLBodyReadables 34 | -------------------------------------------------------------------------------- /play-ws-standalone-json/src/main/scala/play/api/libs/ws/JsonBodyReadables.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws 6 | 7 | /** 8 | * Provides implicit for converting a response to JsValue. 9 | * 10 | * See https://github.com/playframework/play-json for details of Play-JSON. 11 | */ 12 | trait JsonBodyReadables { 13 | 14 | import play.api.libs.json._ 15 | 16 | /** 17 | * Converts a response body into Play JSON format: 18 | * 19 | * {{{ 20 | * import play.api.libs.ws.StandaloneWSResponse 21 | * import play.api.libs.ws.JsonBodyReadables._ 22 | * 23 | * def json(r: StandaloneWSResponse) = r.body[play.api.libs.json.JsValue] 24 | * }}} 25 | */ 26 | implicit val readableAsJson: BodyReadable[JsValue] = BodyReadable { response => 27 | Json.parse(response.body) 28 | } 29 | } 30 | 31 | object JsonBodyReadables extends JsonBodyReadables 32 | -------------------------------------------------------------------------------- /play-ws-standalone/src/main/java/play/libs/ws/WSRequestFilter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.libs.ws; 6 | 7 | import java.util.function.Function; 8 | 9 | /** 10 | * Request Filter. Use this to add your own filters onto a request at execution time. 11 | * 12 | *
13 |  * {@code
14 |  * public class HeaderAppendingFilter implements WSRequestFilter {
15 |  *     private final String key;
16 |  *     private final String value;
17 |  *
18 |  *     public HeaderAppendingFilter(String key, String value) {
19 |  *         this.key = key;
20 |  *         this.value = value;
21 |  *     }
22 |  *
23 |  *     @Override
24 |  *     public WSRequestExecutor apply(WSRequestExecutor executor) {
25 |  *         return request -> executor.apply(request.setHeader(key, value));
26 |  *     }
27 |  * }
28 |  * }
29 | */ 30 | public interface WSRequestFilter extends Function { 31 | 32 | } 33 | -------------------------------------------------------------------------------- /play-ws-standalone-json/src/main/scala/play/api/libs/ws/JsonBodyWritables.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws 6 | 7 | import org.apache.pekko.util.ByteString 8 | import com.fasterxml.jackson.databind.JsonNode 9 | import com.fasterxml.jackson.databind.ObjectMapper 10 | import play.api.libs.json.JsValue 11 | import play.api.libs.json.Json 12 | 13 | trait JsonBodyWritables { 14 | 15 | /** 16 | * Creates an InMemoryBody with "application/json" content type, using the static ObjectMapper. 17 | */ 18 | implicit val writeableOf_JsValue: BodyWritable[JsValue] = { 19 | BodyWritable(a => InMemoryBody(ByteString.fromArrayUnsafe(Json.toBytes(a))), "application/json") 20 | } 21 | 22 | def body(objectMapper: ObjectMapper): BodyWritable[JsonNode] = 23 | BodyWritable( 24 | json => InMemoryBody(ByteString.fromArrayUnsafe(objectMapper.writer.writeValueAsBytes(json))), 25 | "application/json" 26 | ) 27 | } 28 | 29 | object JsonBodyWritables extends JsonBodyWritables 30 | -------------------------------------------------------------------------------- /play-ws-standalone/src/main/java/play/libs/ws/InMemoryBodyWritable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.libs.ws; 6 | 7 | import org.apache.pekko.util.ByteString; 8 | 9 | /** 10 | * A body writable that takes a ByteString with InMemoryBody. 11 | * 12 | * @see ByteString 13 | */ 14 | public class InMemoryBodyWritable implements BodyWritable { 15 | private final InMemoryBody body; 16 | private final String contentType; 17 | 18 | public InMemoryBodyWritable(ByteString byteString, String contentType) { 19 | this.body = new InMemoryBody(byteString); 20 | this.contentType = contentType; 21 | } 22 | 23 | @Override 24 | public WSBody body() { 25 | return body; 26 | } 27 | 28 | @Override 29 | public String contentType() { 30 | return contentType; 31 | } 32 | } 33 | 34 | class InMemoryBody extends AbstractWSBody { 35 | InMemoryBody(ByteString body) { 36 | super(body); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /play-ws-standalone/src/main/java/play/libs/ws/WSAuthInfo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.libs.ws; 6 | 7 | /** 8 | * Holds information for request authentication. 9 | * 10 | * @see WSAuthScheme 11 | * @see StandaloneWSRequest#setAuth(String) 12 | * @see
RFC 7235 - Hypertext Transfer Protocol (HTTP/1.1): Authentication 13 | */ 14 | public class WSAuthInfo { 15 | 16 | private final String username; 17 | private final String password; 18 | private final WSAuthScheme scheme; 19 | 20 | public WSAuthInfo(String username, String password, WSAuthScheme scheme) { 21 | this.username = username; 22 | this.password = password; 23 | this.scheme = scheme; 24 | } 25 | 26 | public String getUsername() { 27 | return username; 28 | } 29 | 30 | public String getPassword() { 31 | return password; 32 | } 33 | 34 | public WSAuthScheme getScheme() { 35 | return scheme; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /integration-tests/src/test/java/play/libs/ws/ahc/StandaloneWSClientSupport.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.libs.ws.ahc 6 | 7 | import org.apache.pekko.stream.Materializer 8 | import org.specs2.execute.Result 9 | import play.api.libs.ws.ahc.AhcConfigBuilder 10 | import play.api.libs.ws.ahc.AhcWSClientConfig 11 | import play.api.libs.ws.ahc.{ AhcWSClientConfigFactory => ScalaAhcWSClientConfigFactory } 12 | import play.shaded.ahc.org.asynchttpclient.DefaultAsyncHttpClient 13 | 14 | trait StandaloneWSClientSupport { 15 | 16 | def materializer: Materializer 17 | 18 | def withClient( 19 | config: AhcWSClientConfig = ScalaAhcWSClientConfigFactory.forConfig() 20 | )(block: StandaloneAhcWSClient => Result): Result = { 21 | val asyncHttpClient = new DefaultAsyncHttpClient(new AhcConfigBuilder(config).build()) 22 | val client = new StandaloneAhcWSClient(asyncHttpClient, materializer) 23 | try { 24 | block(client) 25 | } finally { 26 | client.close() 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /integration-tests/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | %level %logger{15} - %message%n%ex{short} 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /play-ahc-ws-standalone/src/main/scala/play/api/libs/ws/ahc/AhcWSUtils.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws.ahc 6 | 7 | import play.shaded.ahc.org.asynchttpclient.util.HttpUtils 8 | import java.nio.charset.Charset 9 | import java.nio.charset.StandardCharsets 10 | 11 | /** 12 | * INTERNAL API: Utilities for handling the response for both Java and Scala APIs 13 | */ 14 | private[ws] object AhcWSUtils { 15 | def getResponseBody(ahcResponse: play.shaded.ahc.org.asynchttpclient.Response): String = { 16 | val contentType = Option(ahcResponse.getContentType).getOrElse("application/octet-stream") 17 | val charset = getCharset(contentType) 18 | ahcResponse.getResponseBody(charset) 19 | } 20 | 21 | def getCharset(contentType: String): Charset = { 22 | Option(HttpUtils.extractContentTypeCharsetAttribute(contentType)).getOrElse { 23 | if (contentType.startsWith("text/")) 24 | StandardCharsets.ISO_8859_1 25 | else 26 | StandardCharsets.UTF_8 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /play-ws-standalone-json/src/main/java/play/libs/ws/JsonBodyReadables.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.libs.ws; 6 | 7 | import com.fasterxml.jackson.databind.JsonNode; 8 | import com.fasterxml.jackson.databind.ObjectMapper; 9 | 10 | import java.io.IOException; 11 | 12 | /** 13 | * 14 | */ 15 | public interface JsonBodyReadables { 16 | JsonBodyReadables instance = new JsonBodyReadables() {}; 17 | 18 | default BodyReadable json() { 19 | ObjectMapper defaultObjectMapper = DefaultObjectMapper.instance(); 20 | return json(defaultObjectMapper); 21 | } 22 | 23 | default BodyReadable json(ObjectMapper objectMapper) { 24 | return (response -> { 25 | // Jackson will automatically detect the correct encoding according to the rules in RFC-4627 26 | try { 27 | return objectMapper.readTree(response.getBody()); 28 | } catch(IOException e) { 29 | throw new RuntimeException("Error parsing JSON from WS response wsBody", e); 30 | } 31 | }); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /play-ws-standalone/src/main/java/play/libs/ws/WSCookie.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.libs.ws; 6 | 7 | import java.util.Optional; 8 | 9 | /** 10 | * A WS Cookie. 11 | */ 12 | public interface WSCookie { 13 | 14 | /** 15 | * @return the cookie name. 16 | */ 17 | String getName(); 18 | 19 | /** 20 | * @return the cookie value. 21 | */ 22 | String getValue(); 23 | 24 | /** 25 | * @return the cookie path. 26 | */ 27 | Optional getPath(); 28 | 29 | /** 30 | * @return the cookie domain. 31 | */ 32 | Optional getDomain(); 33 | 34 | /** 35 | * @return the cookie max age, in seconds. 36 | */ 37 | Optional getMaxAge(); 38 | 39 | /** 40 | * @return if the cookie is secure or not. 41 | */ 42 | boolean isSecure(); 43 | 44 | /** 45 | * @return if the cookie is accessed only server side. 46 | */ 47 | boolean isHttpOnly(); 48 | 49 | // Cookie ports should not be used; cookies for a given host are shared across 50 | // all the ports on that host. 51 | } 52 | -------------------------------------------------------------------------------- /play-ws-standalone/src/main/scala/play/api/libs/ws/StandaloneWSClient.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws 6 | 7 | import java.io.Closeable 8 | import java.io.IOException 9 | 10 | /** 11 | * The WSClient holds the configuration information needed to build a request, and provides a way to get a request holder. 12 | */ 13 | trait StandaloneWSClient extends Closeable { 14 | 15 | /** 16 | * The underlying implementation of the client, if any. You must cast explicitly to the type you want. 17 | * 18 | * @tparam T the type you are expecting (i.e. isInstanceOf) 19 | * @return the backing class. 20 | */ 21 | def underlying[T]: T 22 | 23 | /** 24 | * Generates a request. Throws IllegalArgumentException if the URL is invalid. 25 | * 26 | * @param url The base URL to make HTTP requests to. 27 | * @return a request 28 | */ 29 | @throws[IllegalArgumentException] 30 | def url(url: String): StandaloneWSRequest 31 | 32 | /** 33 | * Closes this client, and releases underlying resources. 34 | */ 35 | @throws[IOException] 36 | def close(): Unit 37 | } 38 | -------------------------------------------------------------------------------- /play-ahc-ws-standalone/src/main/scala/play/api/libs/ws/ahc/cache/EffectiveURIKey.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws.ahc.cache 6 | 7 | import java.net.URI 8 | import java.time.Instant 9 | import java.time.ZonedDateTime 10 | 11 | import org.playframework.cachecontrol.HeaderName 12 | import play.shaded.ahc.org.asynchttpclient._ 13 | 14 | case class EffectiveURIKey(method: String, uri: URI) { 15 | override def toString: String = method + " " + uri.toString 16 | } 17 | 18 | object EffectiveURIKey { 19 | def apply(request: Request): EffectiveURIKey = { 20 | require(request != null) 21 | EffectiveURIKey(request.getMethod, request.getUri.toJavaNetURI) 22 | } 23 | } 24 | 25 | /** 26 | * A cache entry with an optional expiry time 27 | */ 28 | case class ResponseEntry( 29 | response: CacheableResponse, 30 | requestMethod: String, 31 | nominatedHeaders: Map[HeaderName, Seq[String]], 32 | expiresAt: Option[ZonedDateTime] 33 | ) { 34 | 35 | /** 36 | * Has the entry expired yet? 37 | */ 38 | def isExpired: Boolean = expiresAt.exists(_.toInstant.isBefore(Instant.now())) 39 | } 40 | -------------------------------------------------------------------------------- /play-ws-standalone-xml/src/main/scala/play/api/libs/ws/XMLBodyWritables.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws 6 | 7 | import org.apache.pekko.util.ByteString 8 | import org.w3c.dom.Document 9 | 10 | /** 11 | */ 12 | trait XMLBodyWritables { 13 | 14 | /** 15 | * Creates an InMemoryBody with "text/xml" content type. 16 | */ 17 | implicit def writeableOf_NodeSeq[C <: scala.xml.NodeSeq]: BodyWritable[C] = { 18 | BodyWritable(xml => InMemoryBody(ByteString.fromString(xml.toString())), "text/xml") 19 | } 20 | 21 | /** 22 | * Creates an InMemoryBody with "text/xml" content type. 23 | */ 24 | implicit val writeableOf_NodeBuffer: BodyWritable[scala.xml.NodeBuffer] = { 25 | BodyWritable(xml => InMemoryBody(ByteString.fromString(xml.toString())), "text/xml") 26 | } 27 | 28 | /** 29 | * Creates an InMemoryBody with "text/xml" content type. 30 | */ 31 | implicit val writeableOf_Document: BodyWritable[Document] = { 32 | BodyWritable(xml => InMemoryBody(play.libs.ws.XML.toBytes(xml)), "text/xml") 33 | } 34 | 35 | } 36 | 37 | object XMLBodyWritables extends XMLBodyWritables 38 | -------------------------------------------------------------------------------- /play-ws-standalone/src/main/java/play/libs/ws/StandaloneWSClient.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.libs.ws; 6 | 7 | import java.io.IOException; 8 | 9 | /** 10 | * This is the WS Client interface. 11 | */ 12 | public interface StandaloneWSClient extends java.io.Closeable { 13 | 14 | /** 15 | * The underlying implementation of the client, if any. You must cast the returned value to the type you want. 16 | * 17 | * @return the backing class. 18 | */ 19 | Object getUnderlying(); 20 | 21 | /** 22 | * Returns a StandaloneWSRequest object representing the URL. You can append additional 23 | * properties on the StandaloneWSRequest by chaining calls, and execute the request to 24 | * return an asynchronous {@code CompletionStage}. 25 | * 26 | * @param url the URL to request 27 | * @return the request 28 | */ 29 | StandaloneWSRequest url(String url); 30 | 31 | /** 32 | * Closes this client, and releases underlying resources. 33 | *

34 | * Use this for manually instantiated clients. 35 | */ 36 | void close() throws IOException; 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/build-test.yml: -------------------------------------------------------------------------------- 1 | name: Check 2 | 3 | on: 4 | pull_request: 5 | 6 | push: 7 | branches: 8 | - main # Check branch after merge 9 | 10 | concurrency: 11 | # Only run once for latest commit per ref and cancel other (previous) runs. 12 | group: ci-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | check-code-style: 17 | name: Code Style 18 | uses: playframework/.github/.github/workflows/cmd.yml@v4 19 | with: 20 | cmd: sbt validateCode 21 | 22 | check-binary-compatibility: 23 | name: Binary Compatibility 24 | uses: playframework/.github/.github/workflows/binary-check.yml@v4 25 | 26 | check-docs: 27 | name: Docs 28 | uses: playframework/.github/.github/workflows/cmd.yml@v4 29 | with: 30 | cmd: sbt doc 31 | 32 | tests: 33 | name: Tests 34 | needs: 35 | - "check-code-style" 36 | - "check-binary-compatibility" 37 | - "check-docs" 38 | uses: playframework/.github/.github/workflows/cmd.yml@v4 39 | with: 40 | java: 21, 17 41 | scala: 2.13.x, 3.x 42 | cmd: sbt ++$MATRIX_SCALA 'testOnly -- xonly timefactor 5' 43 | 44 | finish: 45 | name: Finish 46 | if: github.event_name == 'pull_request' 47 | needs: # Should be last 48 | - "tests" 49 | uses: playframework/.github/.github/workflows/rtm.yml@v4 50 | -------------------------------------------------------------------------------- /play-ahc-ws-standalone/src/main/scala/play/api/libs/ws/ahc/AhcLoggerFactory.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws.ahc 6 | 7 | import com.typesafe.sslconfig.util.LoggerFactory 8 | import com.typesafe.sslconfig.util.NoDepsLogger 9 | import org.slf4j.ILoggerFactory 10 | import org.slf4j.{ LoggerFactory => SLF4JLoggerFactory } 11 | 12 | class AhcLoggerFactory(lf: ILoggerFactory = SLF4JLoggerFactory.getILoggerFactory) extends LoggerFactory { 13 | 14 | private[ahc] def createLogger(name: String) = { 15 | new NoDepsLogger { 16 | private[ahc] val logger = lf.getLogger(name) 17 | 18 | def warn(msg: String): Unit = logger.warn(msg) 19 | def isDebugEnabled: Boolean = logger.isDebugEnabled 20 | def error(msg: String): Unit = logger.error(msg) 21 | def error(msg: String, throwable: Throwable): Unit = logger.error(msg, throwable) 22 | def debug(msg: String): Unit = logger.debug(msg) 23 | def info(msg: String): Unit = logger.info(msg) 24 | } 25 | } 26 | 27 | def apply(clazz: Class[?]): NoDepsLogger = createLogger(clazz.getName) 28 | def apply(name: String): NoDepsLogger = createLogger(name) 29 | 30 | } 31 | -------------------------------------------------------------------------------- /play-ahc-ws-standalone/src/main/resources/reference.conf: -------------------------------------------------------------------------------- 1 | play { 2 | ws { 3 | # Configuration specific to the Ahc implementation of the WS client 4 | ahc { 5 | # Pools connections. Replaces setAllowPoolingConnections and setAllowPoolingSslConnections. 6 | keepAlive = true 7 | 8 | # The maximum number of connections to make per host. -1 means no maximum. 9 | maxConnectionsPerHost = -1 10 | 11 | # The maximum total number of connections. -1 means no maximum. 12 | maxConnectionsTotal = -1 13 | 14 | # The maximum number of redirects. 15 | maxNumberOfRedirects = 5 16 | 17 | # The maximum number of times to retry a request if it fails. 18 | maxRequestRetry = 5 19 | 20 | # If non null, the maximum time that a connection should live for in the pool. 21 | maxConnectionLifetime = null 22 | 23 | # If non null, the time after which a connection that has been idle in the pool should be closed. 24 | idleConnectionInPoolTimeout = 1 minute 25 | 26 | # If non null, the frequency to cleanup timeout idle connections 27 | connectionPoolCleanerPeriod = 1 second 28 | 29 | # Whether the raw URL should be used. 30 | disableUrlEncoding = false 31 | 32 | # Whether to use LAX(no cookie name/value verification) or STRICT (verifies cookie name/value) cookie decoder 33 | useLaxCookieEncoder = false 34 | 35 | # Whether to use a cookie store 36 | useCookieStore = false 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /play-ahc-ws-standalone/src/main/scala/play/api/libs/ws/ahc/OrderPreserving.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws.ahc 6 | 7 | import scala.collection.immutable.ListMap 8 | import scala.collection.mutable 9 | 10 | private[ahc] object OrderPreserving { 11 | 12 | def groupBy[K, V](seq: Seq[(K, V)])(f: ((K, V)) => K): Map[K, Seq[V]] = { 13 | // This mutable map will not retain insertion order for the seq, but it is fast for retrieval. The value is 14 | // a builder for the desired Seq[String] in the final result. 15 | val m = mutable.Map.empty[K, mutable.Builder[V, Seq[V]]] 16 | 17 | // Run through the seq and create builders for each unique key, effectively doing the grouping 18 | for ((key, value) <- seq) m.getOrElseUpdate(key, Seq.newBuilder[V]) += value 19 | 20 | // Create a builder for the resulting ListMap. Note that this one is immutable and will retain insertion order 21 | val b = ListMap.newBuilder[K, Seq[V]] 22 | 23 | // Note that we are NOT going through m (didn't retain order) but we are iterating over the original seq 24 | // just to get the keys so we can look up the values in m with them. This is how order is maintained. 25 | for ((k, v) <- seq.iterator) b += k -> m.getOrElse(k, Seq.newBuilder[V]).result() 26 | 27 | // Get the builder to produce the final result 28 | b.result() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /play-ws-standalone/src/main/java/play/libs/ws/WSCookieBuilder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.libs.ws; 6 | 7 | public class WSCookieBuilder { 8 | private String name; 9 | private String value; 10 | private String domain; 11 | private String path; 12 | private Long maxAge; 13 | private boolean secure; 14 | private boolean httpOnly; 15 | 16 | public WSCookieBuilder setName(String name) { 17 | this.name = name; 18 | return this; 19 | } 20 | 21 | public WSCookieBuilder setValue(String value) { 22 | this.value = value; 23 | return this; 24 | } 25 | 26 | public WSCookieBuilder setDomain(String domain) { 27 | this.domain = domain; 28 | return this; 29 | } 30 | 31 | public WSCookieBuilder setPath(String path) { 32 | this.path = path; 33 | return this; 34 | } 35 | 36 | public WSCookieBuilder setMaxAge(Long maxAge) { 37 | this.maxAge = maxAge; 38 | return this; 39 | } 40 | 41 | public WSCookieBuilder setSecure(boolean secure) { 42 | this.secure = secure; 43 | return this; 44 | } 45 | 46 | public WSCookieBuilder setHttpOnly(boolean httpOnly) { 47 | this.httpOnly = httpOnly; 48 | return this; 49 | } 50 | 51 | public WSCookie build() { 52 | return new DefaultWSCookie(name, value, domain, path, maxAge, secure, httpOnly); 53 | } 54 | } -------------------------------------------------------------------------------- /play-ws-standalone/src/main/java/play/libs/ws/SourceBodyWritable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.libs.ws; 6 | 7 | import org.apache.pekko.stream.javadsl.Source; 8 | import org.apache.pekko.util.ByteString; 9 | 10 | /** 11 | * 12 | */ 13 | public class SourceBodyWritable implements BodyWritable> { 14 | private final SourceBody body; 15 | private final String contentType; 16 | 17 | /** 18 | * A SourceBody with a content type 19 | * @param body a source of bytestring 20 | * @param contentType the content type 21 | */ 22 | public SourceBodyWritable(Source body, String contentType) { 23 | this.body = new SourceBody(body); 24 | this.contentType = contentType; 25 | } 26 | 27 | /** 28 | * A SourceBody with a content type of "application/octet-stream" 29 | * @param body a source of bytestring. 30 | */ 31 | public SourceBodyWritable(Source body) { 32 | this.body = new SourceBody(body); 33 | this.contentType = "application/octet-stream"; 34 | } 35 | 36 | @Override 37 | public WSBody> body() { 38 | return body; 39 | } 40 | 41 | @Override 42 | public String contentType() { 43 | return contentType; 44 | } 45 | } 46 | 47 | class SourceBody extends AbstractWSBody> { 48 | SourceBody(Source body) { 49 | super(body); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /play-ahc-ws-standalone/src/main/java/play/libs/ws/ahc/AhcWSClientConfigFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.libs.ws.ahc; 6 | 7 | import com.typesafe.config.Config; 8 | import play.api.libs.ws.WSClientConfig; 9 | import play.api.libs.ws.ahc.AhcWSClientConfig; 10 | 11 | /** 12 | * This is a factory that provides AhcWSClientConfig 13 | * configuration objects without having to go through individual parsers 14 | * and so forth individually. 15 | */ 16 | public final class AhcWSClientConfigFactory { 17 | 18 | /** 19 | * Creates a AhcWSClientConfig from a Typesafe Config object. 20 | * 21 | * This is intended to be called from Java API. 22 | * 23 | * @param config the config file containing settings for WSConfigParser 24 | * @param classLoader the classloader 25 | * @return a AhcWSClientConfig configuration object. 26 | */ 27 | public static AhcWSClientConfig forConfig(Config config, ClassLoader classLoader) { 28 | return play.api.libs.ws.ahc.AhcWSClientConfigFactory$.MODULE$.forConfig(config, classLoader); 29 | } 30 | 31 | /** 32 | * Creates a AhcWSClientConfig with defaults from a WSClientConfig configuration object. 33 | * 34 | * @param config the basic WSClientConfig configuration object. 35 | * @return 36 | */ 37 | public static AhcWSClientConfig forClientConfig(WSClientConfig config) { 38 | return play.api.libs.ws.ahc.AhcWSClientConfigFactory$.MODULE$.forClientConfig(config); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /play-ws-standalone-xml/src/main/scala/play/api/libs/ws/XML.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws 6 | 7 | import javax.xml.XMLConstants 8 | import javax.xml.parsers.SAXParserFactory 9 | 10 | import scala.xml.Elem 11 | import scala.xml.factory.XMLLoader 12 | 13 | /** 14 | */ 15 | object XML { 16 | import play.libs.ws.XML.Constants 17 | 18 | /* 19 | * We want control over the sax parser used so we specify the factory required explicitly. We know that 20 | * SAXParserFactoryImpl will yield a SAXParser having looked at its source code, despite there being 21 | * no explicit doco stating this is the case. That said, there does not appear to be any other way than 22 | * declaring a factory in order to yield a parser of a specific type. 23 | */ 24 | private[play] val xercesSaxParserFactory = SAXParserFactory.newInstance() 25 | xercesSaxParserFactory.setFeature(Constants.SAX_FEATURE_PREFIX + Constants.EXTERNAL_GENERAL_ENTITIES_FEATURE, false) 26 | xercesSaxParserFactory.setFeature(Constants.SAX_FEATURE_PREFIX + Constants.EXTERNAL_PARAMETER_ENTITIES_FEATURE, false) 27 | xercesSaxParserFactory.setFeature(Constants.XERCES_FEATURE_PREFIX + Constants.DISALLOW_DOCTYPE_DECL_FEATURE, true) 28 | xercesSaxParserFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true) 29 | 30 | /* 31 | * A parser to be used that is configured to ensure that no schemas are loaded. 32 | */ 33 | def parser: XMLLoader[Elem] = scala.xml.XML.withSAXParser(xercesSaxParserFactory.newSAXParser()) 34 | 35 | } 36 | -------------------------------------------------------------------------------- /play-ahc-ws-standalone/src/main/scala/play/api/libs/ws/ahc/cache/Cache.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws.ahc.cache 6 | 7 | import scala.concurrent.Future 8 | 9 | /** 10 | * A very simple cache trait. 11 | * 12 | * Implementations can write adapters that map through to this trait, i.e. 13 | * 14 | * {{{ 15 | * import java.util.concurrent.TimeUnit 16 | * import scala.concurrent.Future 17 | * 18 | * import com.github.benmanes.caffeine.cache.{ Caffeine, Ticker } 19 | * 20 | * import play.api.libs.ws.ahc.cache.{ 21 | * Cache, EffectiveURIKey, ResponseEntry 22 | * } 23 | * 24 | * class CaffeineHttpCache extends Cache { 25 | * val underlying = Caffeine.newBuilder() 26 | * .ticker(Ticker.systemTicker()) 27 | * .expireAfterWrite(365, TimeUnit.DAYS) 28 | * .build[EffectiveURIKey, ResponseEntry]() 29 | * 30 | * def remove(key: EffectiveURIKey) = 31 | * Future.successful(Option(underlying.invalidate(key))) 32 | * 33 | * def put(key: EffectiveURIKey, entry: ResponseEntry) = 34 | * Future.successful(underlying.put(key, entry)) 35 | * 36 | * def get(key: EffectiveURIKey) = 37 | * Future.successful(Option(underlying getIfPresent key )) 38 | * 39 | * def close(): Unit = underlying.cleanUp() 40 | * } 41 | * }}} 42 | */ 43 | trait Cache { 44 | 45 | def get(key: EffectiveURIKey): Future[Option[ResponseEntry]] 46 | 47 | def put(key: EffectiveURIKey, entry: ResponseEntry): Future[Unit] 48 | 49 | def remove(key: EffectiveURIKey): Future[Unit] 50 | 51 | def close(): Unit 52 | 53 | } 54 | -------------------------------------------------------------------------------- /play-ws-standalone/src/test/scala/play/api/libs/ws/WSConfigParserSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws 6 | 7 | import org.specs2.mutable._ 8 | import com.typesafe.config.ConfigFactory 9 | 10 | import scala.concurrent.duration._ 11 | 12 | class WSConfigParserSpec extends Specification { 13 | 14 | "WSConfigParser" should { 15 | 16 | def parseThis(input: String) = { 17 | val config = ConfigFactory.parseString(input).withFallback(ConfigFactory.defaultReference()) 18 | val parser = new WSConfigParser(config, this.getClass.getClassLoader) 19 | parser.parse() 20 | } 21 | 22 | "parse ws base section" in { 23 | val actual = parseThis(""" 24 | |play.ws.timeout.connection = 9999 ms 25 | |play.ws.timeout.idle = 666 ms 26 | |play.ws.timeout.request = 1234 ms 27 | |play.ws.followRedirects = false 28 | |play.ws.useProxyProperties = false 29 | |play.ws.useragent = "FakeUserAgent" 30 | """.stripMargin) 31 | 32 | actual.connectionTimeout must_== 9999.millis 33 | actual.idleTimeout must_== 666.millis 34 | actual.requestTimeout must_== 1234.millis 35 | 36 | // default: true 37 | actual.followRedirects must beFalse 38 | 39 | // default: true 40 | actual.useProxyProperties must beFalse 41 | 42 | actual.userAgent must beSome[String].which(_ must_== "FakeUserAgent") 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /integration-tests/src/test/java/play/libs/ws/ahc/XMLRequestTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.libs.ws.ahc; 6 | 7 | import static org.assertj.core.api.Assertions.*; 8 | 9 | import com.typesafe.config.ConfigFactory; 10 | import org.junit.Test; 11 | import org.w3c.dom.Document; 12 | import play.libs.ws.XML; 13 | import play.libs.ws.XMLBodyWritables; 14 | import play.shaded.ahc.io.netty.handler.codec.http.HttpHeaderNames; 15 | import play.shaded.ahc.org.asynchttpclient.Request; 16 | 17 | public class XMLRequestTest implements XMLBodyWritables { 18 | 19 | @Test 20 | public void setXML() { 21 | Document document = XML.fromString("" + 22 | "" + 23 | "hello" + 24 | "world" + 25 | ""); 26 | 27 | StandaloneAhcWSClient client = StandaloneAhcWSClient.create( 28 | AhcWSClientConfigFactory.forConfig(ConfigFactory.load(), this.getClass().getClassLoader()), /*materializer*/ null); 29 | StandaloneAhcWSRequest ahcWSRequest = new StandaloneAhcWSRequest(client, "http://playframework.com/", null) 30 | .setBody(body(document)); 31 | 32 | Request req = ahcWSRequest.buildRequest(); 33 | 34 | assertThat(req.getHeaders().get(HttpHeaderNames.CONTENT_TYPE)).isEqualTo("application/xml"); 35 | 36 | Document responseXml = XML.fromString(req.getStringData()); 37 | responseXml.normalizeDocument(); 38 | assertThat(responseXml.isEqualNode(document)).isTrue(); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /play-ws-standalone/src/main/scala/play/api/libs/ws/WSClientConfig.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws 6 | 7 | import com.typesafe.sslconfig.ssl.SSLConfigSettings 8 | 9 | import scala.concurrent.duration._ 10 | 11 | /** 12 | * WS client config 13 | * 14 | * @param connectionTimeout The maximum time to wait when connecting to the remote host (default is 120 seconds). 15 | * @param idleTimeout The maximum time the request can stay idle (connection is established but waiting for more data) (default is 120 seconds). 16 | * @param requestTimeout The total time you accept a request to take (it will be interrupted even if the remote host is still sending data) (default is 120 seconds). 17 | * @param followRedirects Configures the client to follow 301 and 302 redirects (default is true). 18 | * @param useProxyProperties To use the JVM system’s HTTP proxy settings (http.proxyHost, http.proxyPort) (default is true). 19 | * @param userAgent To configure the User-Agent header field (default is None). 20 | * @param compressionEnabled Set it to true to use gzip/deflater encoding (default is false). 21 | * @param ssl use custom SSL / TLS configuration, see https://lightbend.github.io/ssl-config/ for documentation. 22 | */ 23 | case class WSClientConfig( 24 | connectionTimeout: Duration = 2.minutes, 25 | idleTimeout: Duration = 2.minutes, 26 | requestTimeout: Duration = 2.minutes, 27 | followRedirects: Boolean = true, 28 | useProxyProperties: Boolean = true, 29 | userAgent: Option[String] = None, 30 | compressionEnabled: Boolean = false, 31 | ssl: SSLConfigSettings = SSLConfigSettings() 32 | ) 33 | -------------------------------------------------------------------------------- /play-ws-standalone-json/src/main/java/play/libs/ws/JsonBodyWritables.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.libs.ws; 6 | 7 | import org.apache.pekko.util.ByteString; 8 | import com.fasterxml.jackson.databind.JsonNode; 9 | import com.fasterxml.jackson.databind.ObjectMapper; 10 | 11 | public interface JsonBodyWritables { 12 | 13 | JsonBodyWritables instance = new JsonBodyWritables() {}; 14 | 15 | /** 16 | * Creates a {@link InMemoryBodyWritable} for JSON, setting the content-type to "application/json", using the 17 | * default object mapper. 18 | * 19 | * @param node the node to pass in. 20 | * @return a {@link InMemoryBodyWritable} instance. 21 | */ 22 | default BodyWritable body(JsonNode node) { 23 | return body(node, DefaultObjectMapper.instance()); 24 | } 25 | 26 | /** 27 | * Creates a {@link InMemoryBodyWritable} for JSON, setting the content-type to "application/json". 28 | * 29 | * @param node the node to pass in. 30 | * @param objectMapper the object mapper to create a JSON document. 31 | * @return a {@link InMemoryBodyWritable} instance. 32 | */ 33 | default BodyWritable body(JsonNode node, ObjectMapper objectMapper) { 34 | try { 35 | Object json = objectMapper.readValue(node.toString(), Object.class); 36 | byte[] bytes = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsBytes(json); 37 | return new InMemoryBodyWritable(ByteString.fromArrayUnsafe(bytes), "application/json"); 38 | } catch (Exception e) { 39 | throw new IllegalStateException(e); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /play-ws-standalone/src/main/scala/play/api/libs/ws/Body.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws 6 | 7 | import org.apache.pekko.stream.scaladsl.Source 8 | import org.apache.pekko.util.ByteString 9 | 10 | import scala.annotation.implicitNotFound 11 | 12 | /** 13 | * A body for the request. 14 | */ 15 | sealed trait WSBody 16 | 17 | case object EmptyBody extends WSBody 18 | 19 | /** 20 | * An in memory body 21 | * 22 | * @param bytes The bytes of the body 23 | */ 24 | case class InMemoryBody(bytes: ByteString) extends WSBody 25 | 26 | /** 27 | * A body containing a source of bytes 28 | * 29 | * @param source A flow of the bytes of the body 30 | */ 31 | case class SourceBody(source: Source[ByteString, ?]) extends WSBody 32 | 33 | @implicitNotFound( 34 | "Cannot find an instance of StandaloneWSResponse to ${R}. Define a BodyReadable[${R}] or extend play.api.libs.ws.ahc.DefaultBodyReadables" 35 | ) 36 | class BodyReadable[+R](val transform: StandaloneWSResponse => R) 37 | 38 | object BodyReadable { 39 | def apply[R](transform: StandaloneWSResponse => R): BodyReadable[R] = new BodyReadable[R](transform) 40 | } 41 | 42 | /** 43 | * This is a type class pattern for writing different types of bodies to a WS request. 44 | */ 45 | @implicitNotFound( 46 | "Cannot find an instance of ${A} to WSBody. Define a BodyWritable[${A}] or extend play.api.libs.ws.ahc.DefaultBodyWritables" 47 | ) 48 | class BodyWritable[-A](val transform: A => WSBody, val contentType: String) { 49 | def map[B](f: B => A): BodyWritable[B] = new BodyWritable(b => transform(f(b)), contentType) 50 | } 51 | 52 | object BodyWritable { 53 | def apply[A](transform: (A => WSBody), contentType: String): BodyWritable[A] = 54 | new BodyWritable(transform, contentType) 55 | } 56 | -------------------------------------------------------------------------------- /play-ahc-ws-standalone/src/test/scala/play/api/libs/ws/ahc/cache/CacheableResponseSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws.ahc.cache 6 | 7 | import org.specs2.mutable.Specification 8 | import play.shaded.ahc.io.netty.handler.codec.http.HttpHeaders.Names._ 9 | import play.shaded.ahc.org.asynchttpclient.DefaultAsyncHttpClientConfig 10 | 11 | class CacheableResponseSpec extends Specification { 12 | val achConfig = new DefaultAsyncHttpClientConfig.Builder().build() 13 | 14 | "CacheableResponse" should { 15 | 16 | "get body" in { 17 | 18 | "when it is text/plain" in { 19 | val response = CacheableResponse(200, "https://playframework.com/", "PlayFramework Homepage", achConfig) 20 | .withHeaders(CONTENT_TYPE -> "text/plain") 21 | response.getResponseBody must beEqualTo("PlayFramework Homepage") 22 | response.getContentType must beEqualTo("text/plain") 23 | } 24 | 25 | "when it is application/json" in { 26 | val response = CacheableResponse(200, "https://playframework.com/", """{ "a": "b" }""", achConfig).withHeaders( 27 | "Content-Type" -> "application/json" 28 | ) 29 | response.getResponseBody must beEqualTo("""{ "a": "b" }""") 30 | response.getContentType must beEqualTo("application/json") 31 | } 32 | 33 | "when it is application/json; charset=utf-8" in { 34 | val response = CacheableResponse(200, "https://playframework.com/", """{ "a": "b" }""", achConfig).withHeaders( 35 | "Content-Type" -> "application/json; charset=utf-8" 36 | ) 37 | response.getResponseBody must beEqualTo("""{ "a": "b" }""") 38 | response.getContentType must beEqualTo("application/json; charset=utf-8") 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /play-ws-standalone/src/main/java/play/libs/ws/DefaultWSCookie.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.libs.ws; 6 | 7 | import java.util.Optional; 8 | 9 | /** 10 | * The implementation of a WS cookie. 11 | */ 12 | public class DefaultWSCookie implements WSCookie { 13 | private String name; 14 | private String value; 15 | private String domain; 16 | private String path; 17 | private Long maxAge; 18 | private boolean secure = false; 19 | private boolean httpOnly = false; 20 | 21 | public DefaultWSCookie(String name, String value, String domain, String path, Long maxAge, boolean secure, boolean httpOnly) { 22 | this.name = name; 23 | this.value = value; 24 | this.domain = domain; 25 | this.path = path; 26 | this.maxAge = maxAge; 27 | this.secure = secure; 28 | this.httpOnly = httpOnly; 29 | } 30 | 31 | @Override 32 | public String getName() { 33 | return name; 34 | } 35 | 36 | @Override 37 | public String getValue() { 38 | return value; 39 | } 40 | 41 | @Override 42 | public Optional getDomain() { 43 | return Optional.ofNullable(domain); 44 | } 45 | 46 | @Override 47 | public Optional getPath() { 48 | return Optional.ofNullable(path); 49 | } 50 | 51 | @Override 52 | public Optional getMaxAge() { 53 | if (maxAge != null && maxAge.longValue() > -1L) { 54 | return Optional.of(maxAge); 55 | } else { 56 | return Optional.ofNullable(maxAge); 57 | } 58 | } 59 | 60 | @Override 61 | public boolean isSecure() { 62 | return secure; 63 | } 64 | 65 | @Override 66 | public boolean isHttpOnly() { 67 | return httpOnly; 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /play-ws-standalone/src/main/scala/play/api/libs/ws/WSRequestFilter.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws 6 | 7 | import scala.concurrent.Future 8 | 9 | /** 10 | * A request filter. Override this trait to implement your own filters: 11 | * 12 | * {{{ 13 | * import play.api.libs.ws.{ WSRequestFilter, WSRequestExecutor } 14 | * 15 | * class HeaderAppendingFilter(key: String, value: String) extends WSRequestFilter { 16 | * override def apply(executor: WSRequestExecutor): WSRequestExecutor = { 17 | * WSRequestExecutor(r => executor(r.withHttpHeaders((key, value)))) 18 | * } 19 | * } 20 | * }}} 21 | */ 22 | trait WSRequestFilter extends (WSRequestExecutor => WSRequestExecutor) 23 | 24 | object WSRequestFilter { 25 | 26 | /** 27 | * Creates an adhoc filter from a function: 28 | * 29 | * {{{ 30 | * import play.api.libs.ws.{ WSRequestFilter, WSRequestExecutor } 31 | * 32 | * val filter: WSRequestFilter = WSRequestFilter { e => 33 | * WSRequestExecutor(r => e.apply(r.withQueryStringParameters("bed" -> "1"))) 34 | * } 35 | * }}} 36 | * 37 | * @param f a function that returns executors 38 | * @return a filter that calls the passed in function. 39 | */ 40 | def apply(f: WSRequestExecutor => WSRequestExecutor): WSRequestFilter = { 41 | new WSRequestFilter() { 42 | override def apply(v1: WSRequestExecutor): WSRequestExecutor = f(v1) 43 | } 44 | } 45 | } 46 | 47 | trait WSRequestExecutor extends (StandaloneWSRequest => Future[StandaloneWSResponse]) 48 | 49 | object WSRequestExecutor { 50 | def apply(f: StandaloneWSRequest => Future[StandaloneWSResponse]): WSRequestExecutor = { 51 | new WSRequestExecutor { 52 | override def apply(v1: StandaloneWSRequest): Future[StandaloneWSResponse] = f.apply(v1) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /integration-tests/src/test/scala/play/NettyServerProvider.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play 6 | 7 | import org.apache.pekko.actor.ActorSystem 8 | import org.specs2.concurrent.ExecutionEnv 9 | import org.specs2.specification.BeforeAfterAll 10 | 11 | import scala.concurrent.duration._ 12 | import scala.concurrent.Await 13 | import org.apache.pekko.stream.Materializer 14 | 15 | import play.api.mvc.Handler 16 | import play.api.mvc.RequestHeader 17 | import play.core.server.NettyServer 18 | import play.core.server.ServerConfig 19 | import play.api.BuiltInComponents 20 | import play.api.Mode 21 | 22 | trait NettyServerProvider extends BeforeAfterAll { 23 | 24 | /** 25 | * @return Routes to be used by the test. 26 | */ 27 | def routes(components: BuiltInComponents): PartialFunction[RequestHeader, Handler] 28 | 29 | /** 30 | * The execution context environment. 31 | */ 32 | def executionEnv: ExecutionEnv 33 | 34 | lazy val testServerPort: Int = server.httpPort.getOrElse(sys.error("undefined port number")) 35 | val defaultTimeout: FiniteDuration = 5.seconds 36 | 37 | // Create Pekko system for thread and streaming management 38 | implicit val system: ActorSystem = ActorSystem() 39 | implicit val materializer: Materializer = Materializer.matFromSystem 40 | 41 | // Using 0 (zero) means that a random free port will be used. 42 | // So our tests can run in parallel and won't mess with each other. 43 | val server = NettyServer.fromRouterWithComponents( 44 | ServerConfig( 45 | port = Option(0), 46 | mode = Mode.Test 47 | ) 48 | )(components => routes(components)) 49 | 50 | override def beforeAll(): Unit = {} 51 | 52 | override def afterAll(): Unit = { 53 | server.stop() 54 | val terminate = system.terminate() 55 | Await.ready(terminate, defaultTimeout) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Are you looking for help? 2 | 3 | This is an issue tracker, used to manage and track the development of Play WS. It is not a support system and so it is not a place to ask questions or get help. If you're not sure if you have found a bug, the best place to start is with either the [Play Discussion Forum](https://github.com/playframework/playframework/discussions) or [Stack Overflow](http://stackoverflow.com/questions/ask?tags=playframework). 4 | 5 | ### Play WS Version (2.5.x / etc) 6 | 7 | 8 | 9 | ### API (Scala / Java / Neither / Both) 10 | 11 | 12 | 13 | ### Operating System (Ubuntu 15.10 / MacOS 10.10 / Windows 10) 14 | 15 | Use `uname -a` if on Linux. 16 | 17 | ### JDK (Oracle 1.8.0_72, OpenJDK 1.8.x, Azul Zing) 18 | 19 | Paste the output from `java -version` at the command line. 20 | 21 | ### Library Dependencies 22 | 23 | If this is an issue that involves integration with another system, include the exact version and OS of the other system, including any intermediate drivers or APIs i.e. if you connect to a PostgreSQL database, include both the version / OS of PostgreSQL and the JDBC driver version used to connect to the database. 24 | 25 | ### Expected Behavior 26 | 27 | Please describe the expected behavior of the issue, starting from the first action. 28 | 29 | 1. 30 | 2. 31 | 3. 32 | 33 | ### Actual Behavior 34 | 35 | Please provide a description of what actually happens, working from the same starting point. 36 | 37 | Be descriptive: "it doesn't work" does not describe what the behavior actually is -- instead, say "the page renders a 500 error code with no body content." Copy and paste logs, and include any URLs. Turn on internal Play WS logging with `` if there is no log output. 38 | 39 | 1. 40 | 2. 41 | 3. 42 | 43 | ### Reproducible Test Case 44 | 45 | Please provide a PR with a failing test. 46 | 47 | If the issue is more complex or requires configuration, please provide a link to a project on Github that reproduces the issue. 48 | -------------------------------------------------------------------------------- /play-ahc-ws-standalone/src/main/scala/play/api/libs/ws/ahc/StandaloneAhcWSResponse.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws.ahc 6 | 7 | import org.apache.pekko.stream.scaladsl.Source 8 | import org.apache.pekko.util.ByteString 9 | import play.api.libs.ws.DefaultBodyReadables 10 | import play.api.libs.ws.StandaloneWSResponse 11 | import play.api.libs.ws.WSCookie 12 | import play.shaded.ahc.org.asynchttpclient.{ Response => AHCResponse } 13 | 14 | import scala.jdk.CollectionConverters._ 15 | 16 | /** 17 | * A WS HTTP response backed by org.asynchttpclient.Response. 18 | */ 19 | class StandaloneAhcWSResponse(ahcResponse: AHCResponse) 20 | extends StandaloneWSResponse 21 | with DefaultBodyReadables 22 | with WSCookieConverter 23 | with AhcUtilities { 24 | 25 | override lazy val headers: Map[String, Seq[String]] = headersToMap(ahcResponse.getHeaders) 26 | 27 | override def underlying[T]: T = ahcResponse.asInstanceOf[T] 28 | 29 | override def uri = ahcResponse.getUri.toJavaNetURI 30 | 31 | override def status: Int = ahcResponse.getStatusCode 32 | 33 | override def statusText: String = ahcResponse.getStatusText 34 | 35 | override lazy val cookies: Seq[WSCookie] = ahcResponse.getCookies.asScala.map(asCookie).toSeq 36 | 37 | override def cookie(name: String): Option[WSCookie] = cookies.find(_.name == name) 38 | 39 | override def toString: String = s"StandaloneAhcWSResponse($status, $statusText)" 40 | 41 | override lazy val body: String = { 42 | AhcWSUtils.getResponseBody(ahcResponse) 43 | } 44 | 45 | /** 46 | * The response body as a byte string. 47 | */ 48 | override lazy val bodyAsBytes: ByteString = ByteString.fromArray(underlying[AHCResponse].getResponseBodyAsBytes) 49 | 50 | override lazy val bodyAsSource: Source[ByteString, ?] = Source.single(bodyAsBytes) 51 | } 52 | 53 | object StandaloneAhcWSResponse { 54 | def apply(ahcResponse: AHCResponse): StandaloneAhcWSResponse = { 55 | new StandaloneAhcWSResponse(ahcResponse) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /play-ahc-ws-standalone/src/main/scala/play/api/libs/ws/ahc/CookieBuilder.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws.ahc 6 | 7 | import play.api.libs.ws.DefaultWSCookie 8 | import play.api.libs.ws.WSCookie 9 | import play.shaded.ahc.io.netty.handler.codec.http.HttpHeaderNames._ 10 | import play.shaded.ahc.io.netty.handler.codec.http.cookie.ClientCookieDecoder 11 | import play.shaded.ahc.io.netty.handler.codec.http.cookie.Cookie 12 | import play.shaded.ahc.io.netty.handler.codec.http.cookie.DefaultCookie 13 | 14 | trait CookieBuilder extends WSCookieConverter { 15 | def buildCookies(headers: Map[String, scala.collection.Seq[String]]): scala.collection.Seq[WSCookie] = { 16 | val option = headers.get(SET_COOKIE2.toString).orElse(headers.get(SET_COOKIE.toString)) 17 | option 18 | .map { cookiesHeaders => 19 | for { 20 | value <- cookiesHeaders 21 | Some(c) = Some( 22 | if (useLaxCookieEncoder) ClientCookieDecoder.LAX.decode(value) 23 | else ClientCookieDecoder.STRICT.decode(value) 24 | ) 25 | } yield asCookie(c) 26 | } 27 | .getOrElse(Seq.empty) 28 | } 29 | 30 | def useLaxCookieEncoder: Boolean 31 | } 32 | 33 | /** 34 | * Converts between AHC cookie and the WS cookie. 35 | */ 36 | trait WSCookieConverter { 37 | 38 | def asCookie(cookie: WSCookie): Cookie = { 39 | val c = new DefaultCookie(cookie.name, cookie.value) 40 | c.setWrap(false) 41 | c.setDomain(cookie.domain.orNull) 42 | c.setPath(cookie.path.orNull) 43 | c.setMaxAge(cookie.maxAge.getOrElse(-1L)) 44 | c.setSecure(cookie.secure) 45 | c.setHttpOnly(cookie.httpOnly) 46 | c 47 | } 48 | 49 | def asCookie(c: Cookie): WSCookie = { 50 | DefaultWSCookie( 51 | name = c.name, 52 | value = c.value, 53 | domain = Option(c.domain), 54 | path = Option(c.path), 55 | maxAge = Option(c.maxAge).filterNot(_ < 0), 56 | secure = c.isSecure, 57 | httpOnly = c.isHttpOnly 58 | ) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /play-ahc-ws-standalone/src/main/java/play/libs/ws/ahc/CookieBuilder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.libs.ws.ahc; 6 | 7 | import play.libs.ws.WSCookie; 8 | import play.libs.ws.WSCookieBuilder; 9 | import play.shaded.ahc.io.netty.handler.codec.http.cookie.ClientCookieDecoder; 10 | import play.shaded.ahc.io.netty.handler.codec.http.cookie.Cookie; 11 | 12 | import java.util.ArrayList; 13 | import java.util.Collections; 14 | import java.util.List; 15 | import java.util.Map; 16 | 17 | import static play.shaded.ahc.io.netty.handler.codec.http.HttpHeaderNames.SET_COOKIE; 18 | import static play.shaded.ahc.io.netty.handler.codec.http.HttpHeaderNames.SET_COOKIE2; 19 | import static play.shaded.ahc.org.asynchttpclient.util.MiscUtils.isNonEmpty; 20 | 21 | interface CookieBuilder { 22 | 23 | default List buildCookies(Map> headers) { 24 | List setCookieHeaders = headers.get(SET_COOKIE2); 25 | 26 | if (!isNonEmpty(setCookieHeaders)) { 27 | setCookieHeaders = headers.get(SET_COOKIE); 28 | } 29 | 30 | if (isNonEmpty(setCookieHeaders)) { 31 | List cookies = new ArrayList<>(setCookieHeaders.size()); 32 | for (String value : setCookieHeaders) { 33 | Cookie c = isUseLaxCookieEncoder() ? ClientCookieDecoder.LAX.decode(value) : ClientCookieDecoder.STRICT.decode(value); 34 | if (c != null) { 35 | WSCookie wsCookie = new WSCookieBuilder() 36 | .setName(c.name()) 37 | .setValue(c.value()) 38 | .setDomain(c.domain()) 39 | .setPath(c.path()) 40 | .setMaxAge(c.maxAge()) 41 | .setSecure(c.isSecure()) 42 | .setHttpOnly(c.isHttpOnly()) 43 | .build(); 44 | cookies.add(wsCookie); 45 | } 46 | } 47 | return Collections.unmodifiableList(cookies); 48 | } 49 | 50 | return Collections.emptyList(); 51 | } 52 | 53 | boolean isUseLaxCookieEncoder(); 54 | } 55 | -------------------------------------------------------------------------------- /play-ws-standalone/src/main/java/play/libs/ws/DefaultBodyReadables.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.libs.ws; 6 | 7 | import org.apache.pekko.stream.javadsl.Source; 8 | import org.apache.pekko.util.ByteString; 9 | 10 | import java.io.ByteArrayInputStream; 11 | import java.io.InputStream; 12 | import java.nio.ByteBuffer; 13 | 14 | /** 15 | * This interface defines a set of reads for converting a response 16 | * into a readable format. 17 | */ 18 | public interface DefaultBodyReadables { 19 | 20 | /** 21 | * Converts a response body into an org.apache.pekko.util.ByteString: 22 | * 23 | * {{{ 24 | * ByteString byteString = response.body(byteString()) 25 | * }}} 26 | */ 27 | default BodyReadable byteString() { 28 | return StandaloneWSResponse::getBodyAsBytes; 29 | } 30 | 31 | /** 32 | * Converts a response body into a String. 33 | * 34 | * Note: this is only a best-guess effort and does not handle all content types. See 35 | * {@link StandaloneWSResponse#getBody()} for more information. 36 | * 37 | * {{{ 38 | * String string = response.body(string()) 39 | * }}} 40 | */ 41 | default BodyReadable string() { 42 | return StandaloneWSResponse::getBody; 43 | } 44 | 45 | /** 46 | * Converts a response body into ByteBuffer. 47 | * 48 | * {{{ 49 | * ByteBuffer buffer = response.body(byteBuffer()) 50 | * }}} 51 | */ 52 | default BodyReadable byteBuffer() { 53 | // toByteBuffer returns a copy of the bytes 54 | return response -> response.getBodyAsBytes().toByteBuffer(); 55 | } 56 | 57 | /** 58 | * Converts a response body into an array of bytes. 59 | * 60 | * {{{ 61 | * byte[] byteArray = response.body(bytes()) 62 | * }}} 63 | */ 64 | default BodyReadable bytes() { 65 | return response -> response.getBodyAsBytes().toArray(); 66 | } 67 | 68 | default BodyReadable inputStream() { 69 | return (response -> new ByteArrayInputStream(response.getBodyAsBytes().toArray())); 70 | } 71 | 72 | default BodyReadable> source() { 73 | return StandaloneWSResponse::getBodyAsSource; 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /play-ws-standalone/src/main/scala/play/api/libs/ws/WSConfigParser.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws 6 | 7 | import jakarta.inject.Inject 8 | import jakarta.inject.Provider 9 | import jakarta.inject.Singleton 10 | 11 | import com.typesafe.config.Config 12 | import com.typesafe.config.ConfigException 13 | import com.typesafe.sslconfig.ssl.SSLConfigParser 14 | import com.typesafe.sslconfig.util.EnrichedConfig 15 | 16 | import scala.concurrent.duration.Duration 17 | 18 | /** 19 | * This class creates a WSClientConfig object from a Typesafe Config object. 20 | * 21 | * You can create a client config from an application.conf file by running 22 | * 23 | * {{{ 24 | * import play.api.libs.ws.WSConfigParser 25 | * import com.typesafe.config.ConfigFactory 26 | * 27 | * val wsClientConfig = new WSConfigParser( 28 | * ConfigFactory.load(), this.getClass.getClassLoader).parse() 29 | * }}} 30 | */ 31 | @Singleton 32 | class WSConfigParser @Inject() (config: Config, classLoader: ClassLoader) extends Provider[WSClientConfig] { 33 | 34 | def parse(): WSClientConfig = { 35 | val wsConfig = config.getConfig("play.ws") 36 | 37 | val connectionTimeout = Duration(wsConfig.getString("timeout.connection")) 38 | val idleTimeout = Duration(wsConfig.getString("timeout.idle")) 39 | val requestTimeout = Duration(wsConfig.getString("timeout.request")) 40 | 41 | val followRedirects = wsConfig.getBoolean("followRedirects") 42 | val useProxyProperties = wsConfig.getBoolean("useProxyProperties") 43 | 44 | val userAgent = { 45 | try { 46 | Some(wsConfig.getString("useragent")) 47 | } catch { 48 | case e: ConfigException.Null => 49 | None 50 | } 51 | } 52 | 53 | val compressionEnabled = wsConfig.getBoolean("compressionEnabled") 54 | 55 | val sslConfig = new SSLConfigParser(EnrichedConfig(wsConfig.getConfig("ssl")), classLoader).parse() 56 | 57 | WSClientConfig( 58 | connectionTimeout = connectionTimeout, 59 | idleTimeout = idleTimeout, 60 | requestTimeout = requestTimeout, 61 | followRedirects = followRedirects, 62 | useProxyProperties = useProxyProperties, 63 | userAgent = userAgent, 64 | compressionEnabled = compressionEnabled, 65 | ssl = sslConfig 66 | ) 67 | } 68 | 69 | override lazy val get: WSClientConfig = parse() 70 | } 71 | -------------------------------------------------------------------------------- /play-ahc-ws-standalone/src/main/scala/play/api/libs/ws/ahc/cache/Debug.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws.ahc.cache 6 | 7 | import play.api.libs.ws.ahc.AhcUtilities 8 | import play.shaded.ahc.io.netty.handler.codec.http.HttpHeaders 9 | import play.shaded.ahc.org.asynchttpclient._ 10 | 11 | /** 12 | * Debugging trait. 13 | */ 14 | private[ahc] trait Debug extends AhcUtilities { 15 | 16 | def debug(cfg: AsyncHttpClientConfig): String = { 17 | s"AsyncHttpClientConfig(requestFilters = ${cfg.getRequestFilters})" 18 | } 19 | 20 | def debug(request: Request): String = { 21 | Option(request) 22 | .map { r => 23 | s"Request(${r.getMethod} ${r.getUrl})" 24 | } 25 | .getOrElse("null") 26 | } 27 | 28 | def debug(response: Response): String = { 29 | Option(response) 30 | .map { 31 | case cr: CacheableResponse => 32 | cr.toString 33 | case r => 34 | s"Response(${r.getStatusCode} ${r.getStatusText})" 35 | } 36 | .getOrElse("null") 37 | } 38 | 39 | def debug(responseStatus: HttpResponseStatus): String = { 40 | Option(responseStatus) 41 | .map { 42 | case cs: CacheableHttpResponseStatus => 43 | cs.toString 44 | case s => 45 | s"HttpResponseStatus(${s.getProtocolName} ${s.getStatusCode} ${s.getStatusText})" 46 | } 47 | .getOrElse("null") 48 | } 49 | 50 | def debug(responseHeaders: HttpHeaders): String = { 51 | Option(responseHeaders) 52 | .map { rh => 53 | s"HttpResponseHeaders(${headersToMap(rh)})" 54 | } 55 | .getOrElse("null") 56 | } 57 | 58 | def debug(bodyParts: java.util.List[HttpResponseBodyPart]): String = { 59 | import scala.jdk.CollectionConverters._ 60 | bodyParts.asScala.map(debug).toString() 61 | } 62 | 63 | def debug[T](handler: AsyncHandler[T]): String = { 64 | s"AsyncHandler($handler)" 65 | } 66 | 67 | def debug[T](ctx: play.shaded.ahc.org.asynchttpclient.filter.FilterContext[T]): String = { 68 | s"FilterContext(request = ${debug(ctx.getRequest)}}, responseStatus = ${debug(ctx.getResponseStatus)}}, responseHeaders = ${debug(ctx.getResponseHeaders)}})" 69 | } 70 | 71 | def debug(bodyPart: HttpResponseBodyPart): String = { 72 | bodyPart match { 73 | case cbp: CacheableHttpResponseBodyPart => 74 | cbp.toString 75 | case otherBodyPart => 76 | s"HttpResponseBodyPart(length = ${otherBodyPart.length()}})" 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /play-ws-standalone/src/main/scala/play/api/libs/ws/DefaultBodyReadables.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws 6 | 7 | import java.nio.ByteBuffer 8 | 9 | import org.apache.pekko.stream.scaladsl.Source 10 | import org.apache.pekko.util.ByteString 11 | 12 | /** 13 | * Defines common BodyReadable for a response backed by `org.asynchttpclient.Response`. 14 | */ 15 | trait DefaultBodyReadables { 16 | 17 | /** 18 | * Converts a response body into an `org.apache.pekko.util.ByteString`: 19 | * 20 | * {{{ 21 | * import org.apache.pekko.util.ByteString 22 | * import play.api.libs.ws.DefaultBodyReadables._ 23 | * 24 | * def example(response: play.api.libs.ws.StandaloneWSResponse): ByteString = 25 | * response.body[ByteString] 26 | * }}} 27 | */ 28 | implicit val readableAsByteString: BodyReadable[ByteString] = BodyReadable(_.bodyAsBytes) 29 | 30 | /** 31 | * Converts a response body into a `String`. 32 | * 33 | * Note: this is only a best-guess effort and does not handle all content types. See 34 | * [[StandaloneWSResponse.body:String*]] for more information. 35 | * 36 | * {{{ 37 | * import play.api.libs.ws.DefaultBodyReadables._ 38 | * 39 | * def example(response: play.api.libs.ws.StandaloneWSResponse): String = 40 | * response.body[String] 41 | * }}} 42 | */ 43 | implicit val readableAsString: BodyReadable[String] = BodyReadable(_.body) 44 | 45 | /** 46 | * Converts a response body into a read only `ByteBuffer`. 47 | * 48 | * {{{ 49 | * import java.nio.ByteBuffer 50 | * import play.api.libs.ws.DefaultBodyReadables._ 51 | * 52 | * def example(response: play.api.libs.ws.StandaloneWSResponse): ByteBuffer = 53 | * response.body[ByteBuffer] 54 | * }}} 55 | */ 56 | implicit val readableAsByteBuffer: BodyReadable[ByteBuffer] = BodyReadable(_.bodyAsBytes.asByteBuffer) 57 | 58 | /** 59 | * Converts a response body into `Array[Byte]`. 60 | * 61 | * {{{ 62 | * import play.api.libs.ws.DefaultBodyReadables._ 63 | * 64 | * def example(response: play.api.libs.ws.StandaloneWSResponse): Array[Byte] = 65 | * response.body[Array[Byte]] 66 | * }}} 67 | */ 68 | implicit val readableAsByteArray: BodyReadable[Array[Byte]] = BodyReadable(_.bodyAsBytes.toArray) 69 | 70 | /** 71 | * Converts a response body into `Source[ByteString, _]`. 72 | */ 73 | implicit val readableAsSource: BodyReadable[Source[ByteString, ?]] = BodyReadable(_.bodyAsSource) 74 | } 75 | 76 | object DefaultBodyReadables extends DefaultBodyReadables 77 | -------------------------------------------------------------------------------- /integration-tests/src/test/scala/play/api/libs/ws/ahc/cache/CachingSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws.ahc.cache 6 | 7 | import org.mockito.ArgumentMatchers.any 8 | import org.mockito.Mockito 9 | import org.mockito.Mockito.when 10 | import org.specs2.concurrent.ExecutionEnv 11 | import org.specs2.matcher.FutureMatchers 12 | import org.specs2.mutable.Specification 13 | import org.specs2.specification.AfterAll 14 | import play.NettyServerProvider 15 | import play.api.BuiltInComponents 16 | import play.api.libs.ws.ahc._ 17 | import play.api.libs.ws.DefaultBodyReadables._ 18 | import play.api.mvc.Handler 19 | import play.api.mvc.RequestHeader 20 | import play.api.mvc.Results 21 | import play.api.routing.sird._ 22 | import play.shaded.ahc.org.asynchttpclient._ 23 | 24 | import scala.concurrent.Future 25 | import scala.reflect.ClassTag 26 | 27 | class CachingSpec(implicit val executionEnv: ExecutionEnv) 28 | extends Specification 29 | with NettyServerProvider 30 | with AfterAll 31 | with FutureMatchers { 32 | 33 | private def mock[A](implicit a: ClassTag[A]): A = 34 | Mockito.mock(a.runtimeClass).asInstanceOf[A] 35 | 36 | val asyncHttpClient: AsyncHttpClient = { 37 | val config = AhcWSClientConfigFactory.forClientConfig() 38 | val ahcConfig: AsyncHttpClientConfig = new AhcConfigBuilder(config).build() 39 | new DefaultAsyncHttpClient(ahcConfig) 40 | } 41 | 42 | def routes(components: BuiltInComponents): PartialFunction[RequestHeader, Handler] = { case GET(p"/hello") => 43 | components.defaultActionBuilder( 44 | Results 45 | .Ok(

Say hello to play

) 46 | .withHeaders(("Cache-Control", "public")) 47 | ) 48 | } 49 | 50 | override def afterAll(): Unit = { 51 | super.afterAll() 52 | asyncHttpClient.close() 53 | } 54 | 55 | "GET" should { 56 | 57 | "work once" in { 58 | val cache = mock[Cache] 59 | when(cache.get(any[EffectiveURIKey]())).thenReturn(Future.successful(None)) 60 | 61 | val cachingAsyncHttpClient = new CachingAsyncHttpClient(asyncHttpClient, new AhcHttpCache(cache)) 62 | val ws = new StandaloneAhcWSClient(cachingAsyncHttpClient) 63 | 64 | ws.url(s"http://localhost:$testServerPort/hello") 65 | .get() 66 | .map { response => 67 | response.body[String] must be_==("

Say hello to play

") 68 | } 69 | .await 70 | 71 | Mockito.verify(cache).get(EffectiveURIKey("GET", new java.net.URI(s"http://localhost:$testServerPort/hello"))) 72 | success 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /project/CleanShadedPlugin.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | * 4 | */ 5 | 6 | // https://github.com/sbt/sbt-dirty-money/blob/master/src/main/scala/sbtdirtymoney/DirtyMoneyPlugin.scala 7 | 8 | import sbt._ 9 | import Keys._ 10 | 11 | object CleanShadedPlugin extends AutoPlugin { 12 | override def requires = plugins.IvyPlugin 13 | override def trigger = allRequirements 14 | 15 | object autoImport { 16 | val cleanCacheIvyDirectory: SettingKey[File] = settingKey[File]("") 17 | val cleanCache: InputKey[Unit] = inputKey[Unit]("") 18 | val cleanCacheFiles: InputKey[Seq[File]] = inputKey[Seq[File]]("") 19 | val cleanLocal: InputKey[Unit] = inputKey[Unit]("") 20 | val cleanLocalFiles: InputKey[Seq[File]] = inputKey[Seq[File]]("") 21 | } 22 | import autoImport._ 23 | 24 | object CleanShaded { 25 | import sbt.complete.Parser 26 | import sbt.complete.DefaultParsers._ 27 | 28 | final case class ModuleParam(organization: String, name: Option[String]) 29 | 30 | def parseParam: Parser[Option[ModuleParam]] = 31 | (parseOrg ~ parseName.?).map { case o ~ n => ModuleParam(o, n) }.? 32 | 33 | private def parseOrg: Parser[String] = Space ~> token(StringBasic.examples("\"organization\"")) 34 | 35 | private def parseName: Parser[String] = 36 | Space ~> token(token("%") ~> Space ~> StringBasic.examples("\"name\"")) 37 | 38 | def query(base: File, param: Option[ModuleParam], org: String, name: String): Seq[File] = { 39 | val base1 = PathFinder(base) 40 | val pathFinder = param match { 41 | case None => base1 ** stringToGlob(org) ** stringToGlob(name) 42 | case Some(ModuleParam(org, None)) => base1 ** stringToGlob(org) 43 | case Some(ModuleParam(org, Some(name))) => base1 ** stringToGlob(org) ** stringToGlob(name) 44 | } 45 | pathFinder.get() 46 | } 47 | 48 | private def stringToGlob(s: String) = if (s == "*") "*" else s"*$s*" 49 | } 50 | 51 | override def projectSettings = 52 | Seq( 53 | cleanCacheIvyDirectory := ivyPaths.value.ivyHome.getOrElse(Path.userHome / ".ivy2"), 54 | cleanCache := IO.delete(cleanCacheFiles.evaluated), 55 | cleanLocal := IO.delete(cleanLocalFiles.evaluated), 56 | cleanCacheFiles := { 57 | val base = cleanCacheIvyDirectory.value / "cache" 58 | val param = CleanShaded.parseParam.parsed 59 | CleanShaded.query(base, param, organization.value, moduleName.value) 60 | }, 61 | cleanLocalFiles := { 62 | val base = cleanCacheIvyDirectory.value / "local" 63 | val param = CleanShaded.parseParam.parsed 64 | CleanShaded.query(base, param, organization.value, moduleName.value) 65 | } 66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /play-ahc-ws-standalone/src/main/scala/play/api/libs/ws/ahc/Streamed.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws.ahc 6 | 7 | import java.net.URI 8 | 9 | import org.reactivestreams.Subscriber 10 | import org.reactivestreams.Subscription 11 | import org.reactivestreams.Publisher 12 | import play.shaded.ahc.io.netty.handler.codec.http.HttpHeaders 13 | import org.apache.pekko.Done 14 | import play.shaded.ahc.org.asynchttpclient.AsyncHandler.State 15 | import play.shaded.ahc.org.asynchttpclient._ 16 | import play.shaded.ahc.org.asynchttpclient.handler.StreamedAsyncHandler 17 | 18 | import scala.concurrent.Promise 19 | 20 | case class StreamedState( 21 | statusCode: Int = -1, 22 | statusText: String = "", 23 | uriOption: Option[URI] = None, 24 | responseHeaders: Map[String, scala.collection.Seq[String]] = Map.empty, 25 | publisher: Publisher[HttpResponseBodyPart] = EmptyPublisher 26 | ) 27 | 28 | class DefaultStreamedAsyncHandler[T]( 29 | f: java.util.function.Function[StreamedState, T], 30 | streamStarted: Promise[T], 31 | streamDone: Promise[Done] 32 | ) extends StreamedAsyncHandler[Unit] 33 | with AhcUtilities { 34 | private var state = StreamedState() 35 | 36 | def onStream(publisher: Publisher[HttpResponseBodyPart]): State = { 37 | if (this.state.publisher != EmptyPublisher) State.ABORT 38 | else { 39 | this.state = state.copy(publisher = publisher) 40 | streamStarted.success(f(state)) 41 | State.CONTINUE 42 | } 43 | } 44 | 45 | override def onStatusReceived(status: HttpResponseStatus): State = { 46 | if (this.state.publisher != EmptyPublisher) State.ABORT 47 | else { 48 | state = state.copy( 49 | statusCode = status.getStatusCode, 50 | statusText = status.getStatusText, 51 | uriOption = Option(status.getUri.toJavaNetURI) 52 | ) 53 | State.CONTINUE 54 | } 55 | } 56 | 57 | override def onHeadersReceived(h: HttpHeaders): State = { 58 | if (this.state.publisher != EmptyPublisher) State.ABORT 59 | else { 60 | state = state.copy(responseHeaders = headersToMap(h)) 61 | State.CONTINUE 62 | } 63 | } 64 | 65 | override def onBodyPartReceived(bodyPart: HttpResponseBodyPart): State = 66 | throw new IllegalStateException("Should not have received bodypart") 67 | 68 | override def onCompleted(): Unit = { 69 | // EmptyPublisher can be replaces with `Source.empty` when we carry out the refactoring 70 | // mentioned in the `execute2` method. 71 | streamStarted.trySuccess(f(state.copy(publisher = EmptyPublisher))) 72 | streamDone.trySuccess(Done) 73 | } 74 | 75 | override def onThrowable(t: Throwable): Unit = { 76 | streamStarted.tryFailure(t) 77 | streamDone.tryFailure(t) 78 | } 79 | } 80 | 81 | private case object EmptyPublisher extends Publisher[HttpResponseBodyPart] { 82 | def subscribe(s: Subscriber[? >: HttpResponseBodyPart]): Unit = { 83 | if (s eq null) 84 | throw new NullPointerException("Subscriber must not be null, rule 1.9") 85 | s.onSubscribe(CancelledSubscription) 86 | s.onComplete() 87 | } 88 | private case object CancelledSubscription extends Subscription { 89 | override def request(elements: Long): Unit = () 90 | override def cancel(): Unit = () 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /play-ws-standalone/src/main/scala/play/api/libs/ws/WS.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws 6 | 7 | /** 8 | */ 9 | trait WSAuthScheme { 10 | // Purposely not sealed in case clients want to add their own auth schemes. 11 | } 12 | 13 | object WSAuthScheme { 14 | 15 | case object DIGEST extends WSAuthScheme 16 | 17 | case object BASIC extends WSAuthScheme 18 | 19 | case object NTLM extends WSAuthScheme 20 | 21 | case object SPNEGO extends WSAuthScheme 22 | 23 | case object KERBEROS extends WSAuthScheme 24 | 25 | } 26 | 27 | /** 28 | * A WS Cookie. 29 | */ 30 | trait WSCookie { 31 | 32 | /** 33 | * The cookie name. 34 | */ 35 | def name: String 36 | 37 | /** 38 | * The cookie value. 39 | */ 40 | def value: String 41 | 42 | /** 43 | * The domain. 44 | */ 45 | def domain: Option[String] 46 | 47 | /** 48 | * The path. 49 | */ 50 | def path: Option[String] 51 | 52 | /** 53 | * The maximum age. If negative, then returns None. 54 | */ 55 | def maxAge: Option[Long] 56 | 57 | /** 58 | * If the cookie is secure. 59 | */ 60 | def secure: Boolean 61 | 62 | /** 63 | * If the cookie is HTTPOnly. 64 | */ 65 | def httpOnly: Boolean 66 | } 67 | 68 | case class DefaultWSCookie( 69 | name: String, 70 | value: String, 71 | domain: Option[String] = None, 72 | path: Option[String] = None, 73 | maxAge: Option[Long] = None, 74 | secure: Boolean = false, 75 | httpOnly: Boolean = false 76 | ) extends WSCookie 77 | 78 | /** 79 | * A WS proxy. 80 | */ 81 | trait WSProxyServer { 82 | 83 | /** The hostname of the proxy server. */ 84 | def host: String 85 | 86 | /** The port of the proxy server. */ 87 | def port: Int 88 | 89 | /** The protocol of the proxy server. Use "http" or "https". Defaults to "http" if not specified. */ 90 | def protocol: Option[String] 91 | 92 | /** The principal (aka username) of the credentials for the proxy server. */ 93 | def principal: Option[String] 94 | 95 | /** The password for the credentials for the proxy server. */ 96 | def password: Option[String] 97 | 98 | def ntlmDomain: Option[String] 99 | 100 | /** The realm's charset. */ 101 | def encoding: Option[String] 102 | 103 | def nonProxyHosts: Option[Seq[String]] 104 | } 105 | 106 | /** 107 | * A WS proxy. 108 | */ 109 | case class DefaultWSProxyServer( 110 | /* The hostname of the proxy server. */ 111 | host: String, 112 | /* The port of the proxy server. */ 113 | port: Int, 114 | /* The protocol of the proxy server. Use "http" or "https". Defaults to "http" if not specified. */ 115 | protocol: Option[String] = None, 116 | /* The principal (aka username) of the credentials for the proxy server. */ 117 | principal: Option[String] = None, 118 | /* The password for the credentials for the proxy server. */ 119 | password: Option[String] = None, 120 | ntlmDomain: Option[String] = None, 121 | /* The realm's charset. */ 122 | encoding: Option[String] = None, 123 | nonProxyHosts: Option[Seq[String]] = None 124 | ) extends WSProxyServer 125 | 126 | /** 127 | * Sign a WS call with OAuth. 128 | */ 129 | trait WSSignatureCalculator 130 | -------------------------------------------------------------------------------- /play-ahc-ws-standalone/src/test/scala/play/api/libs/ws/ahc/AhcWSClientConfigParserSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws.ahc 6 | 7 | import com.typesafe.config.ConfigFactory 8 | import org.specs2.mutable._ 9 | import play.api.libs.ws.WSClientConfig 10 | 11 | import scala.concurrent.duration._ 12 | 13 | class AhcWSClientConfigParserSpec extends Specification { 14 | 15 | val defaultWsConfig = WSClientConfig() 16 | val defaultConfig = AhcWSClientConfig(defaultWsConfig) 17 | 18 | "AhcWSClientConfigParser" should { 19 | 20 | def parseThis(input: String) = { 21 | val classLoader = this.getClass.getClassLoader 22 | val config = ConfigFactory.parseString(input).withFallback(ConfigFactory.defaultReference()) 23 | val parser = new AhcWSClientConfigParser(defaultWsConfig, config, classLoader) 24 | parser.parse() 25 | } 26 | 27 | "case class defaults must match reference.conf defaults" in { 28 | val s1 = parseThis("") 29 | val s2 = AhcWSClientConfig() 30 | 31 | // since we use typesafe ssl-config we can't match the objects directly since they aren't case classes, 32 | // and also AhcWSClientConfig has a duration which will be parsed into nanocseconds while the case class uses minutes 33 | s1.wsClientConfig.toString must_== s2.wsClientConfig.toString 34 | s1.maxConnectionsPerHost must_== s2.maxConnectionsPerHost 35 | s1.maxConnectionsTotal must_== s2.maxConnectionsTotal 36 | s1.maxConnectionLifetime must_== s2.maxConnectionLifetime 37 | s1.idleConnectionInPoolTimeout must_== s2.idleConnectionInPoolTimeout 38 | s1.maxNumberOfRedirects must_== s2.maxNumberOfRedirects 39 | s1.maxRequestRetry must_== s2.maxRequestRetry 40 | s1.disableUrlEncoding must_== s2.disableUrlEncoding 41 | s1.keepAlive must_== s2.keepAlive 42 | } 43 | 44 | "parse ws ahc section" in { 45 | val actual = parseThis(""" 46 | |play.ws.ahc.maxConnectionsPerHost = 3 47 | |play.ws.ahc.maxConnectionsTotal = 6 48 | |play.ws.ahc.maxConnectionLifetime = 1 minute 49 | |play.ws.ahc.idleConnectionInPoolTimeout = 30 seconds 50 | |play.ws.ahc.connectionPoolCleanerPeriod = 10 seconds 51 | |play.ws.ahc.maxNumberOfRedirects = 0 52 | |play.ws.ahc.maxRequestRetry = 99 53 | |play.ws.ahc.disableUrlEncoding = true 54 | |play.ws.ahc.keepAlive = false 55 | """.stripMargin) 56 | 57 | actual.maxConnectionsPerHost must_== 3 58 | actual.maxConnectionsTotal must_== 6 59 | actual.maxConnectionLifetime must_== 1.minute 60 | actual.idleConnectionInPoolTimeout must_== 30.seconds 61 | actual.connectionPoolCleanerPeriod must_== 10.seconds 62 | actual.maxNumberOfRedirects must_== 0 63 | actual.maxRequestRetry must_== 99 64 | actual.disableUrlEncoding must beTrue 65 | actual.keepAlive must beFalse 66 | } 67 | 68 | "with keepAlive" should { 69 | "parse keepAlive default as true" in { 70 | val actual = parseThis("""""".stripMargin) 71 | 72 | actual.keepAlive must beTrue 73 | } 74 | } 75 | 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /bench/src/main/scala/play/api/libs/ws/ahc/StandaloneAhcWSRequestBenchMapsBench.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws.ahc 6 | 7 | import java.util.concurrent.TimeUnit 8 | 9 | import org.apache.pekko.stream.Materializer 10 | import org.openjdk.jmh.annotations._ 11 | import org.openjdk.jmh.infra.Blackhole 12 | import play.api.libs.ws.StandaloneWSRequest 13 | 14 | /** 15 | * Tests Map-backed features of [[StandaloneAhcWSRequest]]. 16 | * 17 | * ==Quick Run from sbt== 18 | * 19 | * > bench/jmh:run .*StandaloneAhcWSRequestBenchMapsBench 20 | * 21 | * ==Using Oracle Flight Recorder== 22 | * 23 | * To record a Flight Recorder file from a JMH run, run it using the jmh.extras.JFR profiler: 24 | * > bench/jmh:run -prof jmh.extras.JFR .*StandaloneAhcWSRequestBenchMapsBench 25 | * 26 | * Compare your results before/after on your machine. Don't trust the ones in scaladoc. 27 | * 28 | * Sample benchmark results: 29 | * 30 | * {{{ 31 | * // not compilable 32 | * > bench/jmh:run .*StandaloneAhcWSRequestBenchMapsBench 33 | * [info] Benchmark (size) Mode Cnt Score Error Units 34 | * [info] StandaloneAhcWSRequestBenchMapsBench.addHeaders 1 avgt 162.673 ns/op 35 | * [info] StandaloneAhcWSRequestBenchMapsBench.addHeaders 10 avgt 195.672 ns/op 36 | * [info] StandaloneAhcWSRequestBenchMapsBench.addHeaders 100 avgt 278.829 ns/op 37 | * [info] StandaloneAhcWSRequestBenchMapsBench.addHeaders 1000 avgt 356.446 ns/op 38 | * [info] StandaloneAhcWSRequestBenchMapsBench.addHeaders 10000 avgt 308.384 ns/op 39 | * [info] StandaloneAhcWSRequestBenchMapsBench.addQueryParams 1 avgt 42.123 ns/op 40 | * [info] StandaloneAhcWSRequestBenchMapsBench.addQueryParams 10 avgt 82.650 ns/op 41 | * [info] StandaloneAhcWSRequestBenchMapsBench.addQueryParams 100 avgt 90.095 ns/op 42 | * [info] StandaloneAhcWSRequestBenchMapsBench.addQueryParams 1000 avgt 123.221 ns/op 43 | * [info] StandaloneAhcWSRequestBenchMapsBench.addQueryParams 10000 avgt 141.556 ns/op 44 | * }}} 45 | * 46 | * @see https://github.com/ktoso/sbt-jmh 47 | */ 48 | @OutputTimeUnit(TimeUnit.NANOSECONDS) 49 | @BenchmarkMode(Array(Mode.AverageTime)) 50 | @Fork(jvmArgsAppend = Array("-Xmx350m", "-XX:+HeapDumpOnOutOfMemoryError"), value = 1) 51 | @State(Scope.Benchmark) 52 | class StandaloneAhcWSRequestBenchMapsBench { 53 | 54 | private implicit val materializer: Materializer = null // we're not actually going to execute anything. 55 | private var exampleRequest: StandaloneWSRequest = _ 56 | 57 | @Param(Array("1", "10", "100", "1000", "10000")) 58 | private var size: Int = _ 59 | 60 | @Setup def setup(): Unit = { 61 | val params = (1 to size) 62 | .map(_.toString) 63 | .map(s => s -> s) 64 | 65 | exampleRequest = StandaloneAhcWSRequest(new StandaloneAhcWSClient(null), "https://www.example.com") 66 | .addQueryStringParameters(params: _*) 67 | .addHttpHeaders(params: _*) 68 | } 69 | 70 | @Benchmark 71 | def addQueryParams(bh: Blackhole): Unit = { 72 | bh.consume(exampleRequest.addQueryStringParameters("nthParam" -> "nthParam")) 73 | } 74 | 75 | @Benchmark 76 | def addHeaders(bh: Blackhole): Unit = { 77 | bh.consume(exampleRequest.addHttpHeaders("nthHeader" -> "nthHeader")) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /integration-tests/src/test/scala/play/api/libs/ws/ahc/XMLRequestSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws.ahc 6 | 7 | import java.nio.charset.StandardCharsets 8 | 9 | import org.apache.pekko.actor.ActorSystem 10 | import org.apache.pekko.stream.Materializer 11 | import org.apache.pekko.util.ByteString 12 | import org.specs2.matcher.MustMatchers 13 | import org.specs2.mutable.Specification 14 | import org.specs2.specification.AfterAll 15 | import play.api.libs.ws._ 16 | 17 | import scala.xml.Elem 18 | 19 | /** 20 | */ 21 | class XMLRequestSpec extends Specification with AfterAll with MustMatchers { 22 | sequential 23 | 24 | implicit val system: ActorSystem = ActorSystem() 25 | implicit val materializer: Materializer = Materializer.matFromSystem 26 | 27 | override def afterAll(): Unit = { 28 | system.terminate() 29 | } 30 | class StubResponse(byteArray: Array[Byte]) extends StandaloneWSResponse { 31 | override def uri: java.net.URI = ??? 32 | 33 | override def headers: Map[String, Seq[String]] = ??? 34 | 35 | override def underlying[T]: T = ??? 36 | 37 | override def status: Int = ??? 38 | 39 | override def statusText: String = ??? 40 | 41 | override def cookies: Seq[WSCookie] = ??? 42 | 43 | override def cookie(name: String): Option[WSCookie] = ??? 44 | 45 | override def body: String = ??? 46 | 47 | override def bodyAsBytes: ByteString = ByteString.fromArray(byteArray) 48 | 49 | override def bodyAsSource: org.apache.pekko.stream.scaladsl.Source[ByteString, ?] = ??? 50 | } 51 | 52 | "write an XML node" in { 53 | import XMLBodyWritables._ 54 | 55 | val xml = XML.parser.loadString("") 56 | val client = StandaloneAhcWSClient() 57 | val req = new StandaloneAhcWSRequest(client, "http://playframework.com/", null) 58 | .withBody(xml) 59 | .asInstanceOf[StandaloneAhcWSRequest] 60 | .buildRequest() 61 | 62 | req.getHeaders.get("Content-Type") must be_==("text/xml; charset=UTF-8") 63 | ByteString.fromArray(req.getByteData).utf8String must be_==("") 64 | } 65 | 66 | "read an XML node in Utf-8" in { 67 | val test = 68 | """ 69 | | 70 | |Tove 71 | |Jani 72 | |Reminder 73 | |Don't forget me this weekend! 74 | | 75 | """.stripMargin 76 | val readables = new XMLBodyReadables() {} 77 | /* UTF-8 */ 78 | val value: Elem = readables.readableAsXml.transform(new StubResponse(test.getBytes(StandardCharsets.UTF_8))) 79 | (value \\ "note" \ "to").text must be_==("Tove") 80 | (value \\ "note" \ "from").text must be_==("Jani") 81 | (value \\ "note" \ "heading").text must be_==("Reminder") 82 | } 83 | 84 | "read an XML node in Utf-16" in { 85 | val test = 86 | """ 87 | | 88 | |Tove 89 | |Jani 90 | |Reminder 91 | |Don't forget me this weekend! 92 | | 93 | """.stripMargin 94 | val readables = new XMLBodyReadables() {} 95 | /* UTF-16 */ 96 | val value: Elem = readables.readableAsXml.transform(new StubResponse(test.getBytes(StandardCharsets.UTF_16))) 97 | (value \\ "note" \ "to").text must be_==("Tove") 98 | (value \\ "note" \ "from").text must be_==("Jani") 99 | (value \\ "note" \ "heading").text must be_==("Reminder") 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /integration-tests/src/test/java/play/libs/ws/ahc/JsonRequestTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.libs.ws.ahc; 6 | 7 | import static org.assertj.core.api.Assertions.*; 8 | import static org.mockito.Mockito.*; 9 | 10 | import com.fasterxml.jackson.databind.JsonNode; 11 | import com.typesafe.config.ConfigFactory; 12 | import org.junit.Test; 13 | import play.libs.ws.*; 14 | import play.shaded.ahc.io.netty.handler.codec.http.HttpHeaderNames; 15 | import play.shaded.ahc.org.asynchttpclient.Request; 16 | import play.shaded.ahc.org.asynchttpclient.Response; 17 | 18 | import java.io.*; 19 | import java.nio.charset.StandardCharsets; 20 | 21 | public class JsonRequestTest implements JsonBodyWritables { 22 | 23 | @Test 24 | public void setJson() throws IOException { 25 | JsonNode node = DefaultObjectMapper.instance().readTree("{\"k1\":\"v2\"}"); 26 | 27 | StandaloneAhcWSClient client = StandaloneAhcWSClient.create( 28 | AhcWSClientConfigFactory.forConfig(ConfigFactory.load(), this.getClass().getClassLoader()), /*materializer*/ null); 29 | StandaloneAhcWSRequest ahcWSRequest = new StandaloneAhcWSRequest(client, "http://playframework.com/", null) 30 | .setBody(body(node)); 31 | 32 | Request req = ahcWSRequest.buildRequest(); 33 | 34 | assertThat(req.getHeaders().get(HttpHeaderNames.CONTENT_TYPE)).isEqualTo("application/json"); 35 | assertThat(node).isEqualTo(DefaultObjectMapper.instance().readTree("{\"k1\":\"v2\"}")); 36 | } 37 | 38 | @Test 39 | public void test_getBodyAsJsonWithoutCharset() throws IOException { 40 | InputStream resourceAsStream = getClass().getClassLoader().getResourceAsStream("test.json"); 41 | InputStreamReader isr = new InputStreamReader(resourceAsStream, StandardCharsets.ISO_8859_1); 42 | String bodyString = new BufferedReader(isr).readLine(); 43 | 44 | final Response ahcResponse = mock(Response.class); 45 | final StandaloneAhcWSResponse response = new StandaloneAhcWSResponse(ahcResponse); 46 | 47 | when(ahcResponse.getContentType()).thenReturn("application/json"); 48 | when(ahcResponse.getResponseBody(StandardCharsets.UTF_8)).thenReturn(bodyString); 49 | 50 | JsonNode responseBody = response.getBody(JsonBodyReadables.instance.json()); 51 | 52 | verify(ahcResponse, times(1)).getContentType(); 53 | verify(ahcResponse, times(1)).getResponseBody(StandardCharsets.UTF_8); 54 | assertThat(responseBody.toString()).isEqualTo(bodyString); 55 | } 56 | 57 | @Test 58 | public void test_getBodyAsJsonWithCharset() throws IOException { 59 | InputStream resourceAsStream = getClass().getClassLoader().getResourceAsStream("test.json"); 60 | InputStreamReader isr = new InputStreamReader(resourceAsStream, StandardCharsets.ISO_8859_1); 61 | String bodyString = new BufferedReader(isr).readLine(); 62 | 63 | final Response ahcResponse = mock(Response.class); 64 | final StandaloneAhcWSResponse response = new StandaloneAhcWSResponse(ahcResponse); 65 | 66 | when(ahcResponse.getContentType()).thenReturn("application/json;charset=iso-8859-1"); 67 | when(ahcResponse.getResponseBody(StandardCharsets.ISO_8859_1)).thenReturn(bodyString); 68 | 69 | JsonNode responseBody = response.getBody(JsonBodyReadables.instance.json()); 70 | 71 | verify(ahcResponse, times(1)).getContentType(); 72 | verify(ahcResponse, times(1)).getResponseBody(StandardCharsets.ISO_8859_1); 73 | assertThat(responseBody.toString()).isEqualTo(bodyString); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /play-ahc-ws-standalone/src/test/scala/play/api/libs/ws/ahc/cache/AhcWSCacheSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws.ahc.cache 6 | 7 | import java.net.URI 8 | 9 | import org.playframework.cachecontrol.HttpDate._ 10 | import org.playframework.cachecontrol._ 11 | import org.specs2.mutable.Specification 12 | import play.shaded.ahc.io.netty.handler.codec.http.DefaultHttpHeaders 13 | import play.shaded.ahc.io.netty.handler.codec.http.HttpHeaders 14 | import play.shaded.ahc.org.asynchttpclient.DefaultAsyncHttpClientConfig 15 | import play.shaded.ahc.org.asynchttpclient.Request 16 | import play.shaded.ahc.org.asynchttpclient.RequestBuilder 17 | 18 | class AhcWSCacheSpec extends Specification { 19 | 20 | "freshness heuristics flag" should { 21 | 22 | "calculate LM freshness" in { 23 | import scala.concurrent.ExecutionContext.Implicits.global 24 | 25 | implicit val cache: AhcHttpCache = new AhcHttpCache(new StubHttpCache(), true) 26 | val url = "http://localhost:9000" 27 | 28 | val uri = new URI(url) 29 | val lastModifiedDate: String = format(now.minusHours(1)) 30 | val request: CacheRequest = CacheRequest(uri, "GET", Map()) 31 | val response: CacheResponse = 32 | StoredResponse(uri, 200, Map(HeaderName("Last-Modified") -> Seq(lastModifiedDate)), "GET", Map()) 33 | 34 | val actual = cache.calculateFreshnessFromHeuristic(request, response) 35 | 36 | actual must beSome[Seconds].which { case value => 37 | value must be_==(Seconds.seconds(360)) // 0.1 hours 38 | } 39 | } 40 | 41 | "be disabled when set to false" in { 42 | import scala.concurrent.ExecutionContext.Implicits.global 43 | 44 | implicit val cache: AhcHttpCache = new AhcHttpCache(new StubHttpCache(), false) 45 | val url = "http://localhost:9000" 46 | 47 | val uri = new URI(url) 48 | val lastModifiedDate: String = "Wed, 09 Apr 2008 23:55:38 GMT" 49 | val request: CacheRequest = CacheRequest(uri, "GET", Map()) 50 | val response: CacheResponse = 51 | StoredResponse(uri, 200, Map(HeaderName("Last-Modified") -> Seq(lastModifiedDate)), "GET", Map()) 52 | 53 | val actual = cache.calculateFreshnessFromHeuristic(request, response) 54 | 55 | actual must beNone 56 | } 57 | 58 | } 59 | 60 | "calculateSecondaryKeys" should { 61 | 62 | "calculate keys correctly" in { 63 | import scala.concurrent.ExecutionContext.Implicits.global 64 | 65 | implicit val cache: AhcHttpCache = new AhcHttpCache(new StubHttpCache(), false) 66 | val achConfig = new DefaultAsyncHttpClientConfig.Builder().build() 67 | 68 | val url = "http://localhost:9000" 69 | 70 | val request = generateRequest(url)(headers => headers.add("Accept-Encoding", "gzip")) 71 | val response = CacheableResponse(200, url, achConfig).withHeaders("Vary" -> "Accept-Encoding") 72 | 73 | val actual = cache.calculateSecondaryKeys(request, response) 74 | 75 | actual must beSome[Map[HeaderName, Seq[String]]].which { d => 76 | d must haveKey(HeaderName("Accept-Encoding")) 77 | d(HeaderName("Accept-Encoding")) must be_==(Seq("gzip")) 78 | } 79 | 80 | } 81 | 82 | } 83 | 84 | def generateRequest(url: String)(block: HttpHeaders => HttpHeaders): Request = { 85 | val requestBuilder = new RequestBuilder() 86 | val requestHeaders = block(new DefaultHttpHeaders()) 87 | 88 | requestBuilder 89 | .setUrl(url) 90 | .setHeaders(requestHeaders) 91 | .build 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /play-ahc-ws-standalone/src/main/scala/play/api/libs/ws/ahc/cache/BackgroundAsyncHandler.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws.ahc.cache 6 | 7 | import play.shaded.ahc.org.asynchttpclient._ 8 | import org.playframework.cachecontrol.ResponseCachingActions.DoCacheResponse 9 | import org.playframework.cachecontrol.ResponseCachingActions.DoNotCacheResponse 10 | import org.slf4j.Logger 11 | import org.slf4j.LoggerFactory 12 | import play.shaded.ahc.io.netty.handler.codec.http.HttpHeaders 13 | 14 | import scala.concurrent.Await 15 | 16 | /** 17 | * An async handler that accumulates a response and stores it to cache in the background. 18 | */ 19 | class BackgroundAsyncHandler[T](request: Request, cache: AhcHttpCache, ahcConfig: AsyncHttpClientConfig) 20 | extends AsyncHandler[T] 21 | with Debug { 22 | 23 | import BackgroundAsyncHandler.logger 24 | 25 | private val timeout = scala.concurrent.duration.Duration(1, "second") 26 | 27 | private val builder = new CacheableResponseBuilder(ahcConfig) 28 | 29 | private val key = EffectiveURIKey(request) 30 | 31 | @throws(classOf[Exception]) 32 | def onBodyPartReceived(content: HttpResponseBodyPart): AsyncHandler.State = { 33 | builder.accumulate(content) 34 | AsyncHandler.State.CONTINUE 35 | } 36 | 37 | @throws(classOf[Exception]) 38 | def onStatusReceived(status: HttpResponseStatus): AsyncHandler.State = { 39 | builder.reset() 40 | builder.accumulate(status) 41 | AsyncHandler.State.CONTINUE 42 | } 43 | 44 | @throws(classOf[Exception]) 45 | def onHeadersReceived(headers: HttpHeaders): AsyncHandler.State = { 46 | builder.accumulate(headers) 47 | AsyncHandler.State.CONTINUE 48 | } 49 | 50 | def onThrowable(t: Throwable): Unit = { 51 | logger.error(s"onThrowable: received on request $request", t) 52 | } 53 | 54 | override def onCompleted(): T = { 55 | val response: CacheableResponse = builder.build 56 | 57 | if (cache.isNotModified(response)) { 58 | processNotModifiedResponse(response) 59 | } else { 60 | processFullResponse(response) 61 | } 62 | 63 | response.asInstanceOf[T] 64 | } 65 | 66 | protected def processFullResponse(fullResponse: CacheableResponse): Unit = { 67 | logger.debug(s"processFullResponse: fullResponse = ${debug(fullResponse)}") 68 | 69 | cache.cachingAction(request, fullResponse) match { 70 | case DoNotCacheResponse(reason) => 71 | logger.debug(s"onCompleted: DO NOT CACHE, because $reason") 72 | case DoCacheResponse(reason) => 73 | logger.debug(s"isCacheable: DO CACHE, because $reason") 74 | cache.cacheResponse(request, fullResponse) 75 | } 76 | } 77 | 78 | protected def processNotModifiedResponse(notModifiedResponse: CacheableResponse): Unit = { 79 | logger.trace(s"processNotModifiedResponse: notModifiedResponse = $notModifiedResponse") 80 | 81 | val result = Await.result(cache.get(key), timeout) 82 | logger.debug(s"processNotModifiedResponse: result = $result") 83 | 84 | // FIXME XXX Find the response which matches the secondary keys... 85 | result match { 86 | case Some(entry) => 87 | val newHeaders = notModifiedResponse.getHeaders 88 | val freshResponse = cache.freshenResponse(newHeaders, entry.response) 89 | cache.cacheResponse(request, freshResponse) 90 | case None => 91 | // XXX FIXME what do we do if we have a 304 and there's nothing in the cache for it? 92 | // If we make another call and it sends us another 304 back, we can get stuck in an 93 | // endless loop? 94 | 95 | } 96 | 97 | } 98 | 99 | } 100 | 101 | object BackgroundAsyncHandler { 102 | private val logger: Logger = LoggerFactory.getLogger("play.api.libs.ws.ahc.cache.BackgroundAsyncHandler") 103 | } 104 | -------------------------------------------------------------------------------- /play-ahc-ws-standalone/src/main/scala/play/api/libs/ws/ahc/FormUrlEncodedParser.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws.ahc 6 | 7 | /** An object for parsing application/x-www-form-urlencoded data */ 8 | private[ahc] object FormUrlEncodedParser { 9 | 10 | /** 11 | * Parse the content type "application/x-www-form-urlencoded" which consists of a bunch of & separated key=value 12 | * pairs, both of which are URL encoded. 13 | * @param data The body content of the request, or whatever needs to be so parsed 14 | * @param encoding The character encoding of data 15 | * @return A ListMap of keys to the sequence of values for that key 16 | */ 17 | def parseNotPreservingOrder(data: String, encoding: String = "utf-8"): Map[String, Seq[String]] = { 18 | // Generate the pairs of values from the string. 19 | parseToPairs(data, encoding) 20 | .groupBy(_._1) 21 | .map(param => param._1 -> param._2.map(_._2)) 22 | } 23 | 24 | /** 25 | * Parse the content type "application/x-www-form-urlencoded" which consists of a bunch of & separated key=value 26 | * pairs, both of which are URL encoded. We are careful in this parser to maintain the original order of the 27 | * keys by using OrderPreserving.groupBy as some applications depend on the original browser ordering. 28 | * @param data The body content of the request, or whatever needs to be so parsed 29 | * @param encoding The character encoding of data 30 | * @return A ListMap of keys to the sequence of values for that key 31 | */ 32 | def parse(data: String, encoding: String = "utf-8"): Map[String, Seq[String]] = { 33 | 34 | // Generate the pairs of values from the string. 35 | val pairs: Seq[(String, String)] = parseToPairs(data, encoding) 36 | 37 | // Group the pairs by the key (first item of the pair) being sure to preserve insertion order 38 | OrderPreserving.groupBy(pairs)(_._1) 39 | } 40 | 41 | /** 42 | * Parse the content type "application/x-www-form-urlencoded", mapping to a Java compatible format. 43 | * @param data The body content of the request, or whatever needs to be so parsed 44 | * @param encoding The character encoding of data 45 | * @return A Map of keys to the sequence of values for that key 46 | */ 47 | def parseAsJava(data: String, encoding: String): java.util.Map[String, java.util.List[String]] = { 48 | import scala.jdk.CollectionConverters._ 49 | parse(data, encoding).map { case (key, values) => 50 | key -> values.asJava 51 | }.asJava 52 | } 53 | 54 | /** 55 | * Parse the content type "application/x-www-form-urlencoded", mapping to a Java compatible format. 56 | * @param data The body content of the request, or whatever needs to be so parsed 57 | * @param encoding The character encoding of data 58 | * @return A Map of keys to the sequence of array values for that key 59 | */ 60 | def parseAsJavaArrayValues(data: String, encoding: String): java.util.Map[String, Array[String]] = { 61 | import scala.jdk.CollectionConverters._ 62 | parse(data, encoding).map { case (key, values) => 63 | key -> values.toArray 64 | }.asJava 65 | } 66 | 67 | private[this] val parameterDelimiter = "[&;]".r 68 | 69 | /** 70 | * Do the basic parsing into a sequence of key/value pairs 71 | * @param data The data to parse 72 | * @param encoding The encoding to use for interpreting the data 73 | * @return The sequence of key/value pairs 74 | */ 75 | private def parseToPairs(data: String, encoding: String): Seq[(String, String)] = { 76 | if (data.isEmpty) { 77 | Seq.empty 78 | } else { 79 | parameterDelimiter.split(data).toIndexedSeq.map { param => 80 | val parts = param.split("=", -1) 81 | val key = java.net.URLDecoder.decode(parts(0), encoding) 82 | val value = java.net.URLDecoder.decode(parts.lift(1).getOrElse(""), encoding) 83 | key -> value 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /play-ahc-ws-standalone/src/test/scala/play/libs/ws/ahc/AhcWSResponseSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.libs.ws.ahc 6 | 7 | import org.mockito.Mockito.when 8 | import org.mockito.Mockito 9 | import org.specs2.mutable._ 10 | import play.libs.ws._ 11 | import play.shaded.ahc.io.netty.handler.codec.http.DefaultHttpHeaders 12 | import play.shaded.ahc.org.asynchttpclient.Response 13 | 14 | import scala.jdk.CollectionConverters._ 15 | import scala.jdk.OptionConverters._ 16 | import scala.reflect.ClassTag 17 | 18 | class AhcWSResponseSpec extends Specification with DefaultBodyReadables with DefaultBodyWritables { 19 | 20 | private def mock[A](implicit a: ClassTag[A]): A = 21 | Mockito.mock(a.runtimeClass).asInstanceOf[A] 22 | 23 | "getUnderlying" should { 24 | 25 | "return the underlying response" in { 26 | val srcResponse = mock[Response] 27 | val response = new StandaloneAhcWSResponse(srcResponse) 28 | response.getUnderlying must_== srcResponse 29 | } 30 | 31 | } 32 | 33 | "get headers" should { 34 | 35 | "get headers map which retrieves headers case insensitively" in { 36 | val srcResponse = mock[Response] 37 | val srcHeaders = new DefaultHttpHeaders() 38 | .add("Foo", "a") 39 | .add("foo", "b") 40 | .add("FOO", "b") 41 | .add("Bar", "baz") 42 | when(srcResponse.getHeaders).thenReturn(srcHeaders) 43 | val response = new StandaloneAhcWSResponse(srcResponse) 44 | val headers = response.getHeaders 45 | headers.get("foo").asScala must_== Seq("a", "b", "b") 46 | headers.get("BAR").asScala must_== Seq("baz") 47 | } 48 | 49 | "get headers map which retrieves headers case insensitively (for streamed responses)" in { 50 | val srcHeaders = Map("Foo" -> Seq("a"), "foo" -> Seq("b"), "FOO" -> Seq("b"), "Bar" -> Seq("baz")) 51 | val response = new StreamedResponse(null, 200, "", null, srcHeaders, null, true) 52 | val headers = response.getHeaders 53 | headers.get("foo").asScala must_== Seq("a", "b", "b") 54 | headers.get("BAR").asScala must_== Seq("baz") 55 | } 56 | 57 | "get a single header" in { 58 | val srcResponse = mock[Response] 59 | val srcHeaders = new DefaultHttpHeaders() 60 | .add("Foo", "a") 61 | .add("foo", "b") 62 | .add("FOO", "b") 63 | .add("Bar", "baz") 64 | when(srcResponse.getHeaders).thenReturn(srcHeaders) 65 | val response = new StandaloneAhcWSResponse(srcResponse) 66 | 67 | response.getSingleHeader("Foo").toScala must beSome("a") 68 | response.getSingleHeader("Bar").toScala must beSome("baz") 69 | } 70 | 71 | "get an empty optional when header is not present" in { 72 | val srcResponse = mock[Response] 73 | val srcHeaders = new DefaultHttpHeaders() 74 | .add("Foo", "a") 75 | .add("foo", "b") 76 | .add("FOO", "b") 77 | .add("Bar", "baz") 78 | when(srcResponse.getHeaders).thenReturn(srcHeaders) 79 | val response = new StandaloneAhcWSResponse(srcResponse) 80 | 81 | response.getSingleHeader("Non").toScala must beNone 82 | } 83 | 84 | "get all values for a header" in { 85 | val srcResponse = mock[Response] 86 | val srcHeaders = new DefaultHttpHeaders() 87 | .add("Foo", "a") 88 | .add("foo", "b") 89 | .add("FOO", "b") 90 | .add("Bar", "baz") 91 | when(srcResponse.getHeaders).thenReturn(srcHeaders) 92 | val response = new StandaloneAhcWSResponse(srcResponse) 93 | 94 | response.getHeaderValues("Foo").asScala must containTheSameElementsAs(Seq("a", "b", "b")) 95 | } 96 | } 97 | 98 | /* 99 | getStatus 100 | getStatusText 101 | getHeader 102 | getCookies 103 | getCookie 104 | getBody 105 | asXml 106 | asJson 107 | getBodyAsStream 108 | asByteArray 109 | getUriOption 110 | */ 111 | 112 | } 113 | -------------------------------------------------------------------------------- /integration-tests/src/test/scala/play/api/libs/ws/ahc/JsonRequestSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws.ahc 6 | 7 | import java.nio.charset.StandardCharsets 8 | 9 | import org.apache.pekko.actor.ActorSystem 10 | import org.apache.pekko.stream.Materializer 11 | import org.apache.pekko.util.ByteString 12 | import org.mockito.Mockito.times 13 | import org.mockito.Mockito.verify 14 | import org.mockito.Mockito.when 15 | import org.mockito.Mockito 16 | 17 | import org.specs2.mutable.Specification 18 | import org.specs2.specification.AfterAll 19 | import play.api.libs.json.JsString 20 | import play.api.libs.json.JsValue 21 | import play.api.libs.json.Json 22 | import play.api.libs.ws.JsonBodyReadables 23 | import play.api.libs.ws.JsonBodyWritables 24 | import play.libs.ws.DefaultObjectMapper 25 | import play.shaded.ahc.org.asynchttpclient.Response 26 | 27 | import scala.io.Codec 28 | import scala.reflect.ClassTag 29 | 30 | /** 31 | */ 32 | class JsonRequestSpec extends Specification with AfterAll with JsonBodyWritables { 33 | sequential 34 | 35 | private def mock[A](implicit a: ClassTag[A]): A = 36 | Mockito.mock(a.runtimeClass).asInstanceOf[A] 37 | 38 | implicit val system: ActorSystem = ActorSystem() 39 | implicit val materializer: Materializer = Materializer.matFromSystem 40 | 41 | override def afterAll(): Unit = { 42 | system.terminate() 43 | } 44 | 45 | "set a json node" in { 46 | val jsValue = Json.obj("k1" -> JsString("v1")) 47 | val client = StandaloneAhcWSClient() 48 | val req = new StandaloneAhcWSRequest(client, "http://playframework.com/", null) 49 | .withBody(jsValue) 50 | .asInstanceOf[StandaloneAhcWSRequest] 51 | .buildRequest() 52 | 53 | req.getHeaders.get("Content-Type") must be_==("application/json") 54 | ByteString.fromArray(req.getByteData).utf8String must be_==("""{"k1":"v1"}""") 55 | } 56 | 57 | "set a json node using the default object mapper" in { 58 | val objectMapper = DefaultObjectMapper.instance() 59 | 60 | implicit val jsonReadable = body(objectMapper) 61 | val jsonNode = objectMapper.readTree("""{"k1":"v1"}""") 62 | val client = StandaloneAhcWSClient() 63 | val req = new StandaloneAhcWSRequest(client, "http://playframework.com/", null) 64 | .withBody(jsonNode) 65 | .asInstanceOf[StandaloneAhcWSRequest] 66 | .buildRequest() 67 | 68 | req.getHeaders.get("Content-Type") must be_==("application/json") 69 | ByteString.fromArray(req.getByteData).utf8String must be_==("""{"k1":"v1"}""") 70 | } 71 | 72 | "read an encoding of UTF-8" in { 73 | val json = scala.io.Source.fromResource("test.json")(Codec.ISO8859).getLines().mkString 74 | 75 | val ahcResponse = mock[Response] 76 | val response = new StandaloneAhcWSResponse(ahcResponse) 77 | 78 | when(ahcResponse.getResponseBody(StandardCharsets.UTF_8)).thenReturn(json) 79 | when(ahcResponse.getContentType).thenReturn("application/json") 80 | 81 | val value: JsValue = JsonBodyReadables.readableAsJson.transform(response) 82 | verify(ahcResponse, times(1)).getResponseBody(StandardCharsets.UTF_8) 83 | verify(ahcResponse, times(1)).getContentType 84 | value.toString must beEqualTo(json) 85 | } 86 | 87 | "read an encoding of ISO-8859-1" in { 88 | val json = scala.io.Source.fromResource("test.json")(Codec.ISO8859).getLines().mkString 89 | 90 | val ahcResponse = mock[Response] 91 | val response = new StandaloneAhcWSResponse(ahcResponse) 92 | 93 | when(ahcResponse.getResponseBody(StandardCharsets.ISO_8859_1)).thenReturn(json) 94 | when(ahcResponse.getContentType).thenReturn("application/json;charset=iso-8859-1") 95 | 96 | val value: JsValue = JsonBodyReadables.readableAsJson.transform(response) 97 | verify(ahcResponse, times(1)).getResponseBody(StandardCharsets.ISO_8859_1) 98 | verify(ahcResponse, times(1)).getContentType 99 | value.toString must beEqualTo(json) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /integration-tests/src/test/scala/play/libs/ws/ahc/AhcWSClientSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.libs.ws.ahc 6 | 7 | import org.apache.pekko.stream.javadsl.Sink 8 | import org.apache.pekko.util.ByteString 9 | import org.specs2.concurrent.ExecutionEnv 10 | import org.specs2.matcher.FutureMatchers 11 | import org.specs2.mutable.Specification 12 | import play.NettyServerProvider 13 | import play.api.BuiltInComponents 14 | import play.api.mvc.AnyContentAsText 15 | import play.api.mvc.AnyContentAsXml 16 | import play.api.mvc.Results 17 | import play.api.routing.sird._ 18 | import play.libs.ws._ 19 | 20 | import scala.jdk.FutureConverters._ 21 | import scala.concurrent.Future 22 | import scala.concurrent.duration._ 23 | 24 | class AhcWSClientSpec(implicit val executionEnv: ExecutionEnv) 25 | extends Specification 26 | with NettyServerProvider 27 | with StandaloneWSClientSupport 28 | with FutureMatchers 29 | with XMLBodyWritables 30 | with XMLBodyReadables { 31 | 32 | override def routes(components: BuiltInComponents) = { 33 | case GET(_) => 34 | components.defaultActionBuilder { 35 | Results.Ok( 36 |

Say hello to play

37 | ) 38 | } 39 | case POST(_) => 40 | components.defaultActionBuilder { req => 41 | req.body match { 42 | case AnyContentAsText(txt) => 43 | Results.Ok(txt) 44 | case AnyContentAsXml(xml) => 45 | Results.Ok(xml) 46 | case _ => 47 | Results.NotFound 48 | } 49 | } 50 | } 51 | 52 | "play.libs.ws.ahc.StandaloneAhcWSClient" should { 53 | 54 | "get successfully" in withClient() { client => 55 | def someOtherMethod(string: String) = { 56 | new InMemoryBodyWritable(org.apache.pekko.util.ByteString.fromString(string), "text/plain") 57 | } 58 | client 59 | .url(s"http://localhost:$testServerPort") 60 | .post(someOtherMethod("hello world")) 61 | .asScala 62 | .map(response => response.getBody() must be_==("hello world")) 63 | .await(retries = 0, timeout = 5.seconds) 64 | } 65 | 66 | "source successfully" in withClient() { client => 67 | val future = client.url(s"http://localhost:$testServerPort").stream().asScala 68 | val result: Future[ByteString] = future.flatMap { (response: StandaloneWSResponse) => 69 | response.getBodyAsSource.runWith(Sink.head[ByteString](), materializer).asScala 70 | } 71 | val expected: ByteString = ByteString.fromString("

Say hello to play

") 72 | result must be_==(expected).await(retries = 0, timeout = 5.seconds) 73 | } 74 | 75 | "round trip XML successfully" in withClient() { client => 76 | val document = XML.fromString(""" 77 | | 78 | | hello 79 | | world 80 | |""".stripMargin) 81 | document.normalizeDocument() 82 | 83 | client 84 | .url(s"http://localhost:$testServerPort") 85 | .post(body(document)) 86 | .asScala 87 | .map { response => 88 | import javax.xml.parsers.DocumentBuilderFactory 89 | val dbf = DocumentBuilderFactory.newInstance 90 | dbf.setNamespaceAware(true) 91 | dbf.setCoalescing(true) 92 | dbf.setIgnoringElementContentWhitespace(true) 93 | dbf.setIgnoringComments(true) 94 | dbf.newDocumentBuilder 95 | 96 | val responseXml = response.getBody(xml()) 97 | responseXml.normalizeDocument() 98 | 99 | (responseXml.isEqualNode(document) must beTrue).and { 100 | response.getUri must beEqualTo(new java.net.URI(s"http://localhost:$testServerPort")) 101 | } 102 | } 103 | .await(retries = 0, timeout = 5.seconds) 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /play-ahc-ws-standalone/src/main/scala/play/api/libs/ws/ahc/AhcCurlRequestLogger.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws.ahc 6 | 7 | import java.nio.charset.StandardCharsets 8 | import java.util.Base64 9 | 10 | import org.slf4j.LoggerFactory 11 | import play.api.libs.ws._ 12 | import play.shaded.ahc.org.asynchttpclient.util.HttpUtils 13 | import play.api.libs.ws.EmptyBody 14 | 15 | /** 16 | * Logs StandaloneWSRequest and pulls information into Curl format to an SLF4J logger. 17 | * 18 | * @param logger an SLF4J logger 19 | * 20 | * @see https://curl.haxx.se/ 21 | */ 22 | class AhcCurlRequestLogger(logger: org.slf4j.Logger) extends WSRequestFilter with CurlFormat { 23 | def apply(executor: WSRequestExecutor): WSRequestExecutor = { 24 | WSRequestExecutor { request => 25 | logger.info(toCurl(request.asInstanceOf[StandaloneAhcWSRequest])) 26 | executor(request) 27 | } 28 | } 29 | } 30 | 31 | object AhcCurlRequestLogger { 32 | 33 | private val logger = LoggerFactory.getLogger("play.api.libs.ws.ahc.AhcCurlRequestLogger") 34 | 35 | private val instance = new AhcCurlRequestLogger(logger) 36 | 37 | def apply() = instance 38 | 39 | def apply(logger: org.slf4j.Logger): AhcCurlRequestLogger = { 40 | new AhcCurlRequestLogger(logger) 41 | } 42 | } 43 | 44 | trait CurlFormat { 45 | def toCurl(request: StandaloneAhcWSRequest): String = { 46 | val b = new StringBuilder("curl \\\n") 47 | 48 | // verbose, since it's a fair bet this is for debugging 49 | b.append(" --verbose") 50 | b.append(" \\\n") 51 | 52 | // method 53 | b.append(s" --request ${request.method}") 54 | b.append(" \\\n") 55 | 56 | // authentication 57 | request.auth match { 58 | case Some((userName, password, WSAuthScheme.BASIC)) => 59 | val encodedPassword = 60 | Base64.getUrlEncoder.encodeToString(s"$userName:$password".getBytes(StandardCharsets.US_ASCII)) 61 | b.append(s""" --header 'Authorization: Basic ${quote(encodedPassword)}'""") 62 | b.append(" \\\n") 63 | case _ => () 64 | } 65 | 66 | // headers 67 | request.headers.foreach { case (k, values) => 68 | values.foreach { v => 69 | b.append(s" --header '${quote(k)}: ${quote(v)}'") 70 | b.append(" \\\n") 71 | } 72 | } 73 | 74 | // cookies 75 | request.cookies.foreach { cookie => 76 | b.append(s""" --cookie '${cookie.name}=${cookie.value}'""") 77 | b.append(" \\\n") 78 | } 79 | 80 | // body (note that this has only been checked for text, not binary) 81 | request.body match { 82 | case (InMemoryBody(byteString)) => 83 | val charset = findCharset(request) 84 | val bodyString = byteString.decodeString(charset) 85 | // XXX Need to escape any quotes within the body of the string. 86 | b.append(s" --data '${quote(bodyString)}'") 87 | b.append(" \\\n") 88 | case EmptyBody => // Do nothing. 89 | case other => 90 | throw new UnsupportedOperationException("Unsupported body type " + other.getClass) 91 | } 92 | 93 | // pull out some underlying values from the request. This creates a new Request 94 | // but should be harmless. 95 | val asyncHttpRequest = request.buildRequest() 96 | val proxyServer = asyncHttpRequest.getProxyServer 97 | if (proxyServer != null) { 98 | b.append(s" --proxy ${proxyServer.getHost}:${proxyServer.getPort}") 99 | b.append(" \\\n") 100 | } 101 | 102 | // url 103 | b.append(s" '${quote(asyncHttpRequest.getUrl)}'") 104 | 105 | val curlOptions = b.toString() 106 | curlOptions 107 | } 108 | 109 | protected def findCharset(request: StandaloneAhcWSRequest): String = { 110 | request.contentType 111 | .map { ct => 112 | Option(HttpUtils.extractContentTypeCharsetAttribute(ct)) 113 | .getOrElse { 114 | StandardCharsets.UTF_8 115 | } 116 | .name() 117 | } 118 | .getOrElse(HttpUtils.extractContentTypeCharsetAttribute("UTF-8").name()) 119 | } 120 | 121 | def quote(unsafe: String): String = unsafe.replace("'", "'\\''") 122 | } 123 | -------------------------------------------------------------------------------- /play-ws-standalone-json/src/test/scala/play/api/libs/ws/JsonBodyReadablesSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws 6 | 7 | import java.nio.charset.Charset 8 | import java.nio.charset.StandardCharsets._ 9 | 10 | import org.apache.pekko.stream.scaladsl.Source 11 | import org.apache.pekko.util.ByteString 12 | import org.specs2.matcher.MustMatchers 13 | import org.specs2.mutable.Specification 14 | import play.api.libs.json.JsSuccess 15 | import play.api.libs.json.JsValue 16 | 17 | class JsonBodyReadablesSpec extends Specification with MustMatchers { 18 | 19 | class StubResponse(byteArray: Array[Byte], charset: Charset = UTF_8) extends StandaloneWSResponse { 20 | override def uri: java.net.URI = ??? 21 | 22 | override def headers: Map[String, Seq[String]] = ??? 23 | 24 | override def underlying[T]: T = ??? 25 | 26 | override def status: Int = ??? 27 | 28 | override def statusText: String = ??? 29 | 30 | override def cookies: Seq[WSCookie] = ??? 31 | 32 | override def cookie(name: String): Option[WSCookie] = ??? 33 | 34 | override def body: String = new String(byteArray, charset) 35 | 36 | override def bodyAsBytes: ByteString = ByteString.fromArray(byteArray) 37 | 38 | override def bodyAsSource: Source[ByteString, ?] = ??? 39 | } 40 | 41 | "decode encodings correctly" should { 42 | 43 | "read an encoding of UTF-32BE" in { 44 | val readables = new JsonBodyReadables() {} 45 | val json = """{"menu": {"id": "file", "value": "File"} }""" 46 | val charsetName = "UTF-32BE" 47 | val value: JsValue = 48 | readables.readableAsJson.transform(new StubResponse(json.getBytes(charsetName), Charset.forName(charsetName))) 49 | (value \ "menu" \ "id").validate[String] must beEqualTo(JsSuccess("file")) 50 | } 51 | 52 | "read an encoding of UTF-32LE" in { 53 | val readables = new JsonBodyReadables() {} 54 | val json = """{"menu": {"id": "file", "value": "File"} }""" 55 | val charsetName = "UTF-32LE" 56 | val value: JsValue = 57 | readables.readableAsJson.transform(new StubResponse(json.getBytes(charsetName), Charset.forName(charsetName))) 58 | (value \ "menu" \ "id").validate[String] must beEqualTo(JsSuccess("file")) 59 | } 60 | 61 | "read an encoding of UTF-16BE" in { 62 | val readables = new JsonBodyReadables() {} 63 | val json = """{"menu": {"id": "file", "value": "File"} }""" 64 | val charset = UTF_16BE 65 | val value: JsValue = readables.readableAsJson.transform(new StubResponse(json.getBytes(charset), charset)) 66 | (value \ "menu" \ "id").validate[String] must beEqualTo(JsSuccess("file")) 67 | } 68 | 69 | "read an encoding of UTF-16LE" in { 70 | val readables = new JsonBodyReadables() {} 71 | val json = """{"menu": {"id": "file", "value": "File"} }""" 72 | val charset = UTF_16LE 73 | val value: JsValue = readables.readableAsJson.transform(new StubResponse(json.getBytes(charset), charset)) 74 | (value \ "menu" \ "id").validate[String] must beEqualTo(JsSuccess("file")) 75 | } 76 | 77 | "read an encoding of UTF-8" in { 78 | val readables = new JsonBodyReadables() {} 79 | val json = """{"menu": {"id": "file", "value": "File"} }""" 80 | val value: JsValue = readables.readableAsJson.transform(new StubResponse(json.getBytes(UTF_8))) 81 | (value \ "menu" \ "id").validate[String] must beEqualTo(JsSuccess("file")) 82 | } 83 | 84 | "read an encoding of UTF-8 with empty object" in { 85 | val readables = new JsonBodyReadables() {} 86 | val json = "{}" 87 | val value: JsValue = readables.readableAsJson.transform(new StubResponse(json.getBytes(UTF_8))) 88 | value.toString() must beEqualTo("{}") 89 | } 90 | 91 | "read an encoding of UTF-8 with empty array" in { 92 | val readables = new JsonBodyReadables() {} 93 | val json = "[]" 94 | val value: JsValue = readables.readableAsJson.transform(new StubResponse(json.getBytes(UTF_8))) 95 | value.toString() must beEqualTo("[]") 96 | } 97 | 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /project/Dependencies.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | import sbt._ 5 | 6 | object Dependencies { 7 | 8 | // Should be sync with GA (.github/workflows/build-test.yml) 9 | val scala213 = "2.13.18" 10 | val scala3 = "3.3.7" 11 | 12 | val logback = Seq("ch.qos.logback" % "logback-core" % "1.5.22") 13 | 14 | val assertj = Seq("org.assertj" % "assertj-core" % "3.27.6") 15 | 16 | val awaitility = Seq("org.awaitility" % "awaitility" % "4.3.0") 17 | 18 | val specsVersion = "4.23.0" 19 | val specsBuild = Seq( 20 | "specs2-core", 21 | ).map("org.specs2" %% _ % specsVersion) 22 | 23 | val mockito = Seq("org.mockito" % "mockito-core" % "5.21.0") 24 | 25 | val slf4jtest = Seq("uk.org.lidalia" % "slf4j-test" % "1.2.0") 26 | 27 | val junitInterface = Seq("com.github.sbt" % "junit-interface" % "0.13.3") 28 | 29 | val playJson = Seq("org.playframework" %% "play-json" % "3.1.0-M9") 30 | 31 | val slf4jApi = Seq("org.slf4j" % "slf4j-api" % "2.0.17") 32 | 33 | val jakartaInject = Seq("jakarta.inject" % "jakarta.inject-api" % "2.0.1") 34 | 35 | val sslConfigCore = Seq("com.typesafe" %% "ssl-config-core" % "0.7.1") 36 | 37 | val scalaXml = Seq("org.scala-lang.modules" %% "scala-xml" % "2.4.0") 38 | 39 | val oauth = Seq("oauth.signpost" % "signpost-core" % "2.1.1") 40 | 41 | val cachecontrol = Seq("org.playframework" %% "cachecontrol" % "3.1.0-M2") 42 | 43 | val nettyVersion = "4.1.130.Final" // Keep in sync with the netty version netty-reactive-streams uses (see below) 44 | val asyncHttpClient = Seq( 45 | ("org.asynchttpclient" % "async-http-client" % "2.12.4") // 2.12.x comes with outdated netty-reactive-streams and netty, so we ... 46 | .exclude("com.typesafe.netty", "netty-reactive-streams") // ... exclude netty-reactive-streams and ... 47 | .excludeAll(ExclusionRule("io.netty")), // ... also exclude all netty dependencies and pull in ... 48 | "com.typesafe.netty" % "netty-reactive-streams" % "2.0.17", // ... a new netty-reactive-streams (ahc v3 will drop it btw) ... 49 | "io.netty" % "netty-codec-http" % nettyVersion, // ... and the (up-to-date) netty artifacts async-http-client needs. 50 | "io.netty" % "netty-codec-socks" % nettyVersion, // Same. 51 | "io.netty" % "netty-handler-proxy" % nettyVersion, // Same. 52 | "io.netty" % "netty-handler" % nettyVersion, // Same. 53 | "io.netty" % "netty-buffer" % nettyVersion, // Almost same - needed by async-http-client-netty-utils. 54 | 55 | ) 56 | 57 | val pekkoVersion = "1.4.0" 58 | 59 | val pekkoStreams = Seq("org.apache.pekko" %% "pekko-stream" % pekkoVersion) 60 | 61 | val backendServerTestDependencies = Seq( 62 | "org.playframework" %% "play-netty-server" % "3.0.9", 63 | // Following dependencies are pulled in by play-netty-server, we just make sure 64 | // now that we use the same pekko version here like pekko-stream above. 65 | // This is because when upgrading the pekko version in Play and play-ws here we usually release 66 | // a new Play version before we can bump it here, so the versions will always differ for a short time. 67 | // Since these deps are only used in tests it does not matter anyway. 68 | "org.apache.pekko" %% "pekko-actor-typed" % pekkoVersion, 69 | "org.apache.pekko" %% "pekko-serialization-jackson" % pekkoVersion, 70 | // play-json pulls in newer jackson version than pekkoVersion ships, need to override to avoid exceptions: 71 | // https://github.com/apache/pekko/blob/v1.2.1/project/Dependencies.scala#L110-L111 72 | ("com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.20.1") 73 | .excludeAll(ExclusionRule(organization = "org.scala-lang")), 74 | "org.apache.pekko" %% "pekko-slf4j" % pekkoVersion 75 | ).map(_ % Test) 76 | 77 | val reactiveStreams = Seq("org.reactivestreams" % "reactive-streams" % "1.0.4") 78 | 79 | val testDependencies = 80 | (mockito ++ specsBuild ++ junitInterface ++ assertj ++ awaitility ++ slf4jtest ++ logback).map(_ % Test) 81 | 82 | val standaloneApiWSDependencies = jakartaInject ++ sslConfigCore ++ pekkoStreams ++ testDependencies 83 | 84 | val standaloneAhcWSDependencies = cachecontrol ++ slf4jApi ++ reactiveStreams ++ testDependencies 85 | 86 | val standaloneAhcWSJsonDependencies = playJson ++ testDependencies 87 | 88 | val standaloneAhcWSXMLDependencies = scalaXml ++ testDependencies 89 | 90 | } 91 | -------------------------------------------------------------------------------- /play-ahc-ws-standalone/src/main/scala/play/api/libs/ws/ahc/StreamedResponse.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws.ahc 6 | 7 | import org.apache.pekko.stream.scaladsl.Source 8 | import org.apache.pekko.util.ByteString 9 | import org.reactivestreams.Publisher 10 | import play.api.libs.ws.StandaloneWSResponse 11 | import play.api.libs.ws.WSCookie 12 | import play.shaded.ahc.org.asynchttpclient.HttpResponseBodyPart 13 | 14 | import scala.collection.immutable.TreeMap 15 | import scala.collection.mutable 16 | 17 | /** 18 | * A streamed response containing a response header and a streamable body. 19 | * 20 | * Note that this is only usable with a stream call, i.e. 21 | * 22 | * {{{ 23 | * import scala.concurrent.{ ExecutionContext, Future } 24 | * 25 | * import org.apache.pekko.util.ByteString 26 | * import org.apache.pekko.stream.scaladsl.Source 27 | * 28 | * import play.api.libs.ws.DefaultBodyReadables._ 29 | * import play.api.libs.ws.ahc.StandaloneAhcWSClient 30 | * 31 | * class MyClass(ws: StandaloneAhcWSClient) { 32 | * def doIt(implicit ec: ExecutionContext): Future[String] = 33 | * ws.url("http://example.com").stream().map { response => 34 | * val _ = response.body[Source[ByteString, _]] 35 | * ??? // process source to String 36 | * } 37 | * } 38 | * }}} 39 | */ 40 | class StreamedResponse( 41 | client: StandaloneAhcWSClient, 42 | val status: Int, 43 | val statusText: String, 44 | val uri: java.net.URI, 45 | publisher: Publisher[HttpResponseBodyPart], 46 | val useLaxCookieEncoder: Boolean 47 | ) extends StandaloneWSResponse 48 | with CookieBuilder { 49 | 50 | def this( 51 | client: StandaloneAhcWSClient, 52 | status: Int, 53 | statusText: String, 54 | uri: java.net.URI, 55 | headers: Map[String, scala.collection.Seq[String]], 56 | publisher: Publisher[HttpResponseBodyPart], 57 | useLaxCookieEncoder: Boolean 58 | ) = { 59 | this( 60 | client, 61 | status, 62 | statusText, 63 | uri, 64 | publisher, 65 | useLaxCookieEncoder 66 | ) 67 | origHeaders = headers 68 | } 69 | 70 | private var origHeaders: Map[String, scala.collection.Seq[String]] = Map.empty 71 | 72 | /** 73 | * Get the underlying response object. 74 | */ 75 | override def underlying[T]: T = publisher.asInstanceOf[T] 76 | 77 | override lazy val headers: Map[String, scala.collection.Seq[String]] = { 78 | val mutableMap = mutable.TreeMap[String, scala.collection.Seq[String]]()(CaseInsensitiveOrdered) 79 | origHeaders.keys.foreach { name => 80 | mutableMap.updateWith(name) { 81 | case Some(value) => Some(value ++ origHeaders.getOrElse(name, Seq.empty)) 82 | case None => Some(origHeaders.getOrElse(name, Seq.empty)) 83 | } 84 | } 85 | TreeMap[String, scala.collection.Seq[String]]()(CaseInsensitiveOrdered) ++ mutableMap 86 | } 87 | 88 | /** 89 | * Get all the cookies. 90 | */ 91 | override lazy val cookies: scala.collection.Seq[WSCookie] = buildCookies(headers) 92 | 93 | /** 94 | * Get only one cookie, using the cookie name. 95 | */ 96 | override def cookie(name: String): Option[WSCookie] = cookies.find(_.name == name) 97 | 98 | /** 99 | * THIS IS A BLOCKING OPERATION. It should not be used in production. 100 | * 101 | * Note that this is not a charset aware operation, as the stream does not have access to the underlying machinery 102 | * that disambiguates responses. 103 | * 104 | * @return the body as a String 105 | */ 106 | override lazy val body: String = bodyAsBytes.decodeString(AhcWSUtils.getCharset(contentType)) 107 | 108 | /** 109 | * THIS IS A BLOCKING OPERATION. It should not be used in production. 110 | * 111 | * Note that this is not a charset aware operation, as the stream does not have access to the underlying machinery 112 | * that disambiguates responses. 113 | * 114 | * @return the body as a ByteString 115 | */ 116 | override lazy val bodyAsBytes: ByteString = client.blockingToByteString(bodyAsSource) 117 | 118 | override lazy val bodyAsSource: Source[ByteString, ?] = { 119 | Source 120 | .fromPublisher(publisher) 121 | .map((bodyPart: HttpResponseBodyPart) => ByteString.fromArray(bodyPart.getBodyPartBytes)) 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /play-ahc-ws-standalone/src/main/scala/play/api/libs/ws/ahc/cache/CacheAsyncConnection.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws.ahc.cache 6 | 7 | import java.util.concurrent.Callable 8 | import java.util.concurrent.CompletableFuture 9 | import java.util.concurrent.Executor 10 | import java.util.concurrent.TimeUnit 11 | import java.util.function.BiConsumer 12 | 13 | import play.shaded.ahc.org.asynchttpclient.AsyncHandler 14 | import play.shaded.ahc.org.asynchttpclient.ListenableFuture 15 | import play.shaded.ahc.org.asynchttpclient.Request 16 | import org.slf4j.LoggerFactory 17 | import play.shaded.ahc.org.asynchttpclient.handler.ProgressAsyncHandler 18 | 19 | /** 20 | * Calls the relevant methods on the async handler, providing it with the cached response. 21 | */ 22 | class AsyncCacheableConnection[T]( 23 | asyncHandler: AsyncHandler[T], 24 | request: Request, 25 | response: CacheableResponse, 26 | future: ListenableFuture[T] 27 | ) extends Callable[T] 28 | with Debug { 29 | 30 | import AsyncCacheableConnection._ 31 | 32 | override def call(): T = { 33 | // Because this is running directly against an executor service, 34 | // the usual uncaught exception handler will not apply, and so 35 | // any kind of logging must wrap EVERYTHING in an explicit try / catch 36 | // block. 37 | try { 38 | if (logger.isTraceEnabled) { 39 | logger.trace(s"call: request = ${debug(request)}, response = ${debug(response)}") 40 | } 41 | var state = asyncHandler.onStatusReceived(response.status) 42 | 43 | if (state eq AsyncHandler.State.CONTINUE) { 44 | state = asyncHandler.onHeadersReceived(response.headers) 45 | } 46 | 47 | if (state eq AsyncHandler.State.CONTINUE) { 48 | import scala.jdk.CollectionConverters._ 49 | response.bodyParts.asScala.foreach { bodyPart => 50 | asyncHandler.onBodyPartReceived(bodyPart) 51 | } 52 | } 53 | 54 | asyncHandler match { 55 | case progressAsyncHandler: ProgressAsyncHandler[?] => 56 | progressAsyncHandler.onHeadersWritten() 57 | progressAsyncHandler.onContentWritten() 58 | case _ => 59 | } 60 | 61 | val t: T = asyncHandler.onCompleted 62 | future.done() 63 | t 64 | } catch { 65 | case t: Throwable => 66 | logger.error("call: ", t) 67 | val ex: RuntimeException = new RuntimeException 68 | ex.initCause(t) 69 | throw ex 70 | } 71 | } 72 | 73 | override def toString: String = { 74 | s"AsyncCacheableConnection(request = ${debug(request)}})" 75 | } 76 | } 77 | 78 | object AsyncCacheableConnection { 79 | private val logger = LoggerFactory.getLogger("play.api.libs.ws.ahc.cache.AsyncCacheableConnection") 80 | } 81 | 82 | /** 83 | * A wrapper to return a ListenableFuture. 84 | */ 85 | class CacheFuture[T](handler: AsyncHandler[T]) extends ListenableFuture[T] { 86 | 87 | private var innerFuture: java.util.concurrent.CompletableFuture[T] = _ 88 | 89 | def setInnerFuture(future: java.util.concurrent.CompletableFuture[T]) = { 90 | innerFuture = future 91 | } 92 | 93 | override def isDone: Boolean = innerFuture.isDone 94 | 95 | override def done(): Unit = {} 96 | 97 | override def touch(): Unit = {} 98 | 99 | override def abort(t: Throwable): Unit = { 100 | innerFuture.completeExceptionally(t) 101 | } 102 | 103 | override def isCancelled: Boolean = { 104 | innerFuture.isCancelled 105 | } 106 | 107 | override def get(): T = { 108 | get(1000L, java.util.concurrent.TimeUnit.MILLISECONDS) 109 | } 110 | 111 | override def get(timeout: Long, unit: TimeUnit): T = { 112 | innerFuture.get(timeout, unit) 113 | } 114 | 115 | override def cancel(mayInterruptIfRunning: Boolean): Boolean = { 116 | innerFuture.cancel(mayInterruptIfRunning) 117 | } 118 | 119 | override def toString: String = { 120 | s"CacheFuture" 121 | } 122 | 123 | override def toCompletableFuture: CompletableFuture[T] = innerFuture 124 | 125 | override def addListener(listener: Runnable, executor: Executor): ListenableFuture[T] = { 126 | innerFuture.whenCompleteAsync( 127 | new BiConsumer[T, Throwable]() { 128 | override def accept(t: T, u: Throwable): Unit = listener.run() 129 | }, 130 | executor 131 | ) 132 | this 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /integration-tests/src/test/java/play/libs/ws/ahc/ByteStringRequestTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.libs.ws.ahc; 6 | 7 | import org.apache.pekko.Done; 8 | import org.apache.pekko.actor.ActorSystem; 9 | import org.apache.pekko.stream.Materializer; 10 | import org.apache.pekko.stream.javadsl.Sink; 11 | import org.apache.pekko.stream.javadsl.Source; 12 | import org.apache.pekko.util.ByteString; 13 | 14 | import org.junit.Test; 15 | 16 | import play.libs.ws.DefaultBodyReadables; 17 | import play.shaded.ahc.org.asynchttpclient.Response; 18 | 19 | import java.nio.charset.StandardCharsets; 20 | import java.util.concurrent.CompletionStage; 21 | import java.util.concurrent.ExecutionException; 22 | 23 | import static org.assertj.core.api.Assertions.assertThat; 24 | import static org.mockito.Mockito.*; 25 | 26 | public class ByteStringRequestTest implements DefaultBodyReadables { 27 | 28 | @Test 29 | public void testGetBodyAsString() { 30 | final Response ahcResponse = mock(Response.class); 31 | final StandaloneAhcWSResponse response = new StandaloneAhcWSResponse(ahcResponse); 32 | when(ahcResponse.getContentType()).thenReturn(null); 33 | when(ahcResponse.getResponseBody(StandardCharsets.UTF_8)).thenReturn("wsBody"); 34 | 35 | final String body = response.getBody(); 36 | verify(ahcResponse, times(1)).getResponseBody(any()); 37 | assertThat(body).isEqualTo("wsBody"); 38 | } 39 | 40 | @Test 41 | public void testGetBodyAsString_applicationJson() { 42 | final Response ahcResponse = mock(Response.class); 43 | final String bodyString = "{\"foo\": \"☺\"}"; 44 | final StandaloneAhcWSResponse response = new StandaloneAhcWSResponse(ahcResponse); 45 | when(ahcResponse.getContentType()).thenReturn("application/json"); 46 | when(ahcResponse.getResponseBody(StandardCharsets.UTF_8)).thenReturn(bodyString); 47 | 48 | final String body = response.getBody(); 49 | verify(ahcResponse, times(1)).getResponseBody(any()); 50 | assertThat(body).isEqualTo("{\"foo\": \"☺\"}"); 51 | } 52 | 53 | @Test 54 | public void testGetBodyAsString_textHtml() { 55 | final Response ahcResponse = mock(Response.class); 56 | final StandaloneAhcWSResponse response = new StandaloneAhcWSResponse(ahcResponse); 57 | final String bodyString = "☺"; 58 | when(ahcResponse.getContentType()).thenReturn("text/html"); 59 | when(ahcResponse.getResponseBody(StandardCharsets.ISO_8859_1)).thenReturn(bodyString); 60 | 61 | final String body = response.getBody(); 62 | verify(ahcResponse, times(1)).getResponseBody(any()); 63 | assertThat(body).isEqualTo(bodyString); 64 | } 65 | 66 | @Test 67 | public void testGetBodyAsBytes() { 68 | final Response ahcResponse = mock(Response.class); 69 | final StandaloneAhcWSResponse response = new StandaloneAhcWSResponse(ahcResponse); 70 | when(ahcResponse.getContentType()).thenReturn(null); 71 | when(ahcResponse.getResponseBodyAsBytes()).thenReturn("wsBody".getBytes()); 72 | 73 | final String body = response.getBodyAsBytes().utf8String(); 74 | verify(ahcResponse, times(1)).getResponseBodyAsBytes(); 75 | assertThat(body).isEqualTo("wsBody"); 76 | } 77 | 78 | @Test 79 | public void testGetBodyAsSource() throws ExecutionException, InterruptedException { 80 | final Response ahcResponse = mock(Response.class); 81 | final StandaloneAhcWSResponse response = new StandaloneAhcWSResponse(ahcResponse); 82 | when(ahcResponse.getContentType()).thenReturn(null); 83 | when(ahcResponse.getResponseBodyAsBytes()).thenReturn("wsBody".getBytes()); 84 | 85 | final StringBuilder result = new StringBuilder(); 86 | 87 | final ActorSystem system = ActorSystem.create("test-body-as-bytes"); 88 | final Materializer materializer = Materializer.matFromSystem(system); 89 | 90 | Source bodyAsSource = response.getBodyAsSource().map(ByteString::utf8String); 91 | Sink> appender = Sink.foreach(result::append); 92 | 93 | // run and wait for completion 94 | bodyAsSource.runWith(appender, materializer).toCompletableFuture().get(); 95 | 96 | verify(ahcResponse, times(1)).getResponseBodyAsBytes(); 97 | assertThat(result.toString()).isEqualTo("wsBody"); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /play-ws-standalone/src/main/scala/play/api/libs/ws/DefaultBodyWritables.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws 6 | 7 | import java.io.File 8 | import java.nio.ByteBuffer 9 | import java.util.function.Supplier 10 | 11 | import org.apache.pekko.stream.scaladsl.StreamConverters.fromInputStream 12 | import org.apache.pekko.stream.scaladsl.FileIO 13 | import org.apache.pekko.stream.scaladsl.Source 14 | import org.apache.pekko.util.ByteString 15 | 16 | import scala.jdk.FunctionConverters._ 17 | 18 | /** 19 | * Default BodyWritable for a request body, for use with 20 | * requests that take a body such as PUT, POST and PATCH. 21 | * 22 | * {{{ 23 | * import scala.concurrent.ExecutionContext 24 | * 25 | * import play.api.libs.ws.StandaloneWSClient 26 | * import play.api.libs.ws.DefaultBodyWritables._ 27 | * 28 | * class MyClass(ws: StandaloneWSClient) { 29 | * def postBody()(implicit ec: ExecutionContext) = { 30 | * val getBody: String = "..." 31 | * ws.url("...").post(getBody).map { response => ??? } 32 | * } 33 | * } 34 | * }}} 35 | */ 36 | trait DefaultBodyWritables { 37 | 38 | /** 39 | * Creates an SourceBody with "application/octet-stream" content type from a file. 40 | */ 41 | implicit val writableOf_File: BodyWritable[File] = { 42 | BodyWritable(file => SourceBody(FileIO.fromPath(file.toPath)), "application/octet-stream") 43 | } 44 | 45 | /** 46 | * Creates an SourceBody with "application/octet-stream" content type from an inputstream. 47 | */ 48 | implicit val writableOf_InputStream: BodyWritable[Supplier[java.io.InputStream]] = { 49 | BodyWritable(supplier => SourceBody(fromInputStream(supplier.asScala)), "application/octet-stream") 50 | } 51 | 52 | /** 53 | * Creates an SourceBody with "application/octet-stream" content type from a file. 54 | */ 55 | implicit val writableOf_Source: BodyWritable[Source[ByteString, ?]] = { 56 | BodyWritable(source => SourceBody(source), "application/octet-stream") 57 | } 58 | 59 | /** 60 | * Creates an InMemoryBody with "text/plain" content type. 61 | */ 62 | implicit val writeableOf_String: BodyWritable[String] = { 63 | BodyWritable(str => InMemoryBody(ByteString.fromString(str)), "text/plain") 64 | } 65 | 66 | /** 67 | * Creates an InMemoryBody with "text/plain" content type from a StringBuilder 68 | */ 69 | implicit val writeableOf_StringBuilder: BodyWritable[StringBuilder] = { 70 | BodyWritable(str => InMemoryBody(ByteString.fromString(str.toString())), "text/plain") 71 | } 72 | 73 | /** 74 | * Creates an InMemoryBody with "application/octet-stream" content type from an array of bytes. 75 | */ 76 | implicit val writeableOf_ByteArray: BodyWritable[Array[Byte]] = { 77 | BodyWritable(bytes => InMemoryBody(ByteString(bytes)), "application/octet-stream") 78 | } 79 | 80 | /** 81 | * Creates an InMemoryBody with "application/octet-stream" content type from a bytebuffer. 82 | */ 83 | implicit val writeableOf_ByteBuffer: BodyWritable[ByteBuffer] = { 84 | BodyWritable(buffer => InMemoryBody(ByteString.fromByteBuffer(buffer)), "application/octet-stream") 85 | } 86 | 87 | /** 88 | * Creates an InMemoryBody with "application/octet-stream" content type. 89 | */ 90 | implicit val writeableOf_Bytes: BodyWritable[ByteString] = { 91 | BodyWritable(byteString => InMemoryBody(byteString), "application/octet-stream") 92 | } 93 | 94 | /** 95 | * Creates a BodyWritable with an identity function, with "application/octet-stream" content type. 96 | */ 97 | implicit val writeableOf_WsBody: BodyWritable[WSBody] = { 98 | BodyWritable(identity, "application/octet-stream") 99 | } 100 | 101 | /** 102 | * Creates an InMemoryBody with "application/x-www-form-urlencoded" content type. 103 | */ 104 | implicit val writeableOf_urlEncodedForm: BodyWritable[Map[String, Seq[String]]] = { 105 | import java.net.URLEncoder 106 | BodyWritable( 107 | formData => 108 | InMemoryBody( 109 | ByteString.fromString( 110 | formData.flatMap(item => item._2.map(c => s"${item._1}=${URLEncoder.encode(c, "UTF-8")}")).mkString("&") 111 | ) 112 | ), 113 | "application/x-www-form-urlencoded" 114 | ) 115 | } 116 | 117 | implicit val writeableOf_urlEncodedSimpleForm: BodyWritable[Map[String, String]] = { 118 | writeableOf_urlEncodedForm.map[Map[String, String]](_.map(kv => kv._1 -> Seq(kv._2))) 119 | } 120 | 121 | } 122 | 123 | object DefaultBodyWritables extends DefaultBodyWritables 124 | -------------------------------------------------------------------------------- /play-ws-standalone/src/main/scala/play/api/libs/ws/StandaloneWSResponse.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.ws 6 | 7 | import org.apache.pekko.stream.scaladsl.Source 8 | import org.apache.pekko.util.ByteString 9 | 10 | /** 11 | * A WS HTTP response. 12 | */ 13 | trait StandaloneWSResponse { 14 | 15 | /** 16 | * Returns the URI for this response, which can differ from the request one 17 | * in case of redirection. 18 | */ 19 | def uri: java.net.URI 20 | 21 | /** 22 | * Returns the current headers for this response. 23 | */ 24 | def headers: Map[String, scala.collection.Seq[String]] 25 | 26 | /** 27 | * Get the value of the header with the specified name. If there are more than one values 28 | * for this header, the first value is returned. If there are no values, than a None is 29 | * returned. 30 | * 31 | * @param name the header name 32 | * @return the header value 33 | */ 34 | def header(name: String): Option[String] = headerValues(name).headOption 35 | 36 | /** 37 | * Get all the values of header with the specified name. If there are no values for 38 | * the header with the specified name, than an empty sequence is returned. 39 | * 40 | * @param name the header name. 41 | * @return all the values for this header name. 42 | */ 43 | def headerValues(name: String): scala.collection.Seq[String] = headers.getOrElse(name, Seq.empty) 44 | 45 | /** 46 | * Get the underlying response object. 47 | */ 48 | def underlying[T]: T 49 | 50 | /** 51 | * The response status code. 52 | */ 53 | def status: Int 54 | 55 | /** 56 | * The response status message. 57 | */ 58 | def statusText: String 59 | 60 | /** 61 | * Get all the cookies. 62 | */ 63 | def cookies: scala.collection.Seq[WSCookie] 64 | 65 | /** 66 | * Get only one cookie, using the cookie name. 67 | */ 68 | def cookie(name: String): Option[WSCookie] 69 | 70 | /** 71 | * @return the content type. 72 | */ 73 | def contentType: String = header("Content-Type").getOrElse("application/octet-stream") 74 | 75 | /** 76 | * The response body as the given type. This renders as the given type. 77 | * You must have a BodyReadable in implicit scope. 78 | * 79 | * The simplest use case is 80 | * 81 | * {{{ 82 | * import play.api.libs.ws.StandaloneWSResponse 83 | * import play.api.libs.ws.DefaultBodyReadables._ 84 | * 85 | * def responseBodyAsString(response: StandaloneWSResponse): String = 86 | * response.body[String] 87 | * }}} 88 | * 89 | * But you can also render as JSON 90 | * 91 | * {{{ 92 | * // not compilable: requires `play-ws-standalone-json` dependency 93 | * import play.api.libs.json.JsValue 94 | * import play.api.libs.ws.StandaloneWSResponse 95 | * 96 | * def responseBodyAsJson(response: StandaloneWSResponse): JsValue = 97 | * response.body[JsValue] 98 | * }}} 99 | * 100 | * or as binary: 101 | * 102 | * {{{ 103 | * import org.apache.pekko.util.ByteString 104 | * import play.api.libs.ws.StandaloneWSResponse 105 | * import play.api.libs.ws.DefaultBodyReadables._ 106 | * 107 | * def responseBodyAsByteString(response: StandaloneWSResponse): ByteString = 108 | * response.body[ByteString] 109 | * }}} 110 | */ 111 | def body[T: BodyReadable]: T = { 112 | val readable = implicitly[BodyReadable[T]] 113 | readable.transform(this) 114 | } 115 | 116 | /** 117 | * The response body decoded as String, using a simple algorithm to guess the encoding. 118 | * 119 | * This decodes the body to a string representation based on the following algorithm: 120 | * 121 | * 1. Look for a "charset" parameter on the Content-Type. If it exists, set `charset` to its value and go to step 3. 122 | * 2. If the Content-Type is of type "text", set charset to "ISO-8859-1"; else set `charset` to "UTF-8". 123 | * 3. Decode the raw bytes of the body using `charset`. 124 | * 125 | * Note that this does not take into account any special cases for specific content types. For example, for 126 | * application/json, we do not support encoding autodetection and will trust the charset parameter if provided.. 127 | * 128 | * @return the response body parsed as a String using the above algorithm. 129 | */ 130 | def body: String 131 | 132 | /** 133 | * @return The response body as ByteString. 134 | */ 135 | def bodyAsBytes: ByteString 136 | 137 | /** 138 | * @return the response as a source of bytes 139 | */ 140 | def bodyAsSource: Source[ByteString, ?] 141 | } 142 | -------------------------------------------------------------------------------- /play-ws-standalone-xml/src/main/java/play/libs/ws/XML.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.libs.ws; 6 | 7 | import org.apache.pekko.util.ByteString; 8 | import org.apache.pekko.util.ByteString$; 9 | import org.apache.pekko.util.ByteStringBuilder; 10 | import org.w3c.dom.Document; 11 | import org.xml.sax.InputSource; 12 | import org.xml.sax.SAXException; 13 | 14 | import javax.xml.XMLConstants; 15 | import javax.xml.parsers.DocumentBuilder; 16 | import javax.xml.parsers.DocumentBuilderFactory; 17 | import javax.xml.parsers.ParserConfigurationException; 18 | import javax.xml.transform.TransformerException; 19 | import javax.xml.transform.TransformerFactory; 20 | import javax.xml.transform.dom.DOMSource; 21 | import javax.xml.transform.stream.StreamResult; 22 | import java.io.ByteArrayInputStream; 23 | import java.io.IOException; 24 | import java.io.InputStream; 25 | import java.io.UnsupportedEncodingException; 26 | 27 | /** 28 | * XML utilities. 29 | */ 30 | public class XML { 31 | 32 | /** 33 | * Parses an XML string as DOM using UTF-8 charset and encoding. 34 | * 35 | * @param xml the input XML string 36 | * @return the parsed XML DOM root. 37 | */ 38 | public static Document fromString(String xml) { 39 | try { 40 | return fromInputStream( 41 | new ByteArrayInputStream(xml.getBytes("utf-8")), 42 | "utf-8" 43 | ); 44 | } catch (UnsupportedEncodingException e) { 45 | throw new RuntimeException(e); 46 | } 47 | } 48 | 49 | /** 50 | * Parses an InputStream as DOM. 51 | * 52 | * @param in the inputstream to parse. 53 | * @param encoding the encoding of the input stream, if not null. 54 | * @return the parsed XML DOM. 55 | */ 56 | public static Document fromInputStream(InputStream in, String encoding) { 57 | InputSource is = new InputSource(in); 58 | if (encoding != null) { 59 | is.setEncoding(encoding); 60 | } 61 | 62 | return fromInputSource(is); 63 | } 64 | 65 | /** 66 | * Parses the input source as DOM. 67 | * 68 | * @param source The source to parse. 69 | * @return The Document. 70 | */ 71 | public static Document fromInputSource(InputSource source) { 72 | try { 73 | DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 74 | factory.setFeature(Constants.SAX_FEATURE_PREFIX + Constants.EXTERNAL_GENERAL_ENTITIES_FEATURE, false); 75 | factory.setFeature(Constants.SAX_FEATURE_PREFIX + Constants.EXTERNAL_PARAMETER_ENTITIES_FEATURE, false); 76 | factory.setFeature(Constants.XERCES_FEATURE_PREFIX + Constants.DISALLOW_DOCTYPE_DECL_FEATURE, true); 77 | factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); 78 | factory.setNamespaceAware(true); 79 | DocumentBuilder builder = factory.newDocumentBuilder(); 80 | 81 | return builder.parse(source); 82 | } catch (ParserConfigurationException | SAXException | IOException e) { 83 | throw new RuntimeException(e); 84 | } 85 | } 86 | 87 | /** 88 | * Converts the document to bytes. 89 | * 90 | * @param document The document to convert. 91 | * @return The ByteString representation of the document. 92 | */ 93 | public static ByteString toBytes(Document document) { 94 | ByteStringBuilder builder = ByteString$.MODULE$.newBuilder(); 95 | try { 96 | TransformerFactory.newInstance().newTransformer() 97 | .transform(new DOMSource(document), new StreamResult(builder.asOutputStream())); 98 | } catch (TransformerException e) { 99 | throw new RuntimeException(e); 100 | } 101 | return builder.result(); 102 | } 103 | 104 | /** 105 | * Includes the SAX prefixes from 'com.sun.org.apache.xerces.internal.impl.Constants' 106 | * since they will likely be internal in JDK9 107 | */ 108 | public static class Constants { 109 | public static final String SAX_FEATURE_PREFIX = "http://xml.org/sax/features/"; 110 | public static final String XERCES_FEATURE_PREFIX = "http://apache.org/xml/features/"; 111 | public static final String EXTERNAL_GENERAL_ENTITIES_FEATURE = "external-general-entities"; 112 | public static final String EXTERNAL_PARAMETER_ENTITIES_FEATURE = "external-parameter-entities"; 113 | public static final String DISALLOW_DOCTYPE_DECL_FEATURE = "disallow-doctype-decl"; 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /play-ahc-ws-standalone/src/main/java/play/libs/ws/ahc/StandaloneAhcWSResponse.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.libs.ws.ahc; 6 | 7 | import org.apache.pekko.util.ByteString; 8 | 9 | import play.api.libs.ws.ahc.AhcWSUtils; 10 | import play.libs.ws.BodyReadable; 11 | import play.libs.ws.StandaloneWSResponse; 12 | import play.libs.ws.WSCookie; 13 | import play.libs.ws.WSCookieBuilder; 14 | 15 | import play.shaded.ahc.io.netty.handler.codec.http.HttpHeaders; 16 | import play.shaded.ahc.io.netty.handler.codec.http.cookie.Cookie; 17 | 18 | import java.net.URI; 19 | import java.net.URISyntaxException; 20 | 21 | import java.util.List; 22 | import java.util.Map; 23 | import java.util.Optional; 24 | import java.util.TreeMap; 25 | 26 | import static java.util.stream.Collectors.toList; 27 | 28 | /** 29 | * A WS response. 30 | */ 31 | public class StandaloneAhcWSResponse implements StandaloneWSResponse { 32 | 33 | private final play.shaded.ahc.org.asynchttpclient.Response ahcResponse; 34 | 35 | public StandaloneAhcWSResponse(play.shaded.ahc.org.asynchttpclient.Response ahcResponse) { 36 | this.ahcResponse = ahcResponse; 37 | } 38 | 39 | @Override 40 | public Object getUnderlying() { 41 | return this.ahcResponse; 42 | } 43 | 44 | /** 45 | * Get the HTTP status code of the response 46 | */ 47 | @Override 48 | public int getStatus() { 49 | return ahcResponse.getStatusCode(); 50 | } 51 | 52 | /** 53 | * Get the HTTP status text of the response 54 | */ 55 | @Override 56 | public String getStatusText() { 57 | return ahcResponse.getStatusText(); 58 | } 59 | 60 | /** 61 | * Get all the HTTP headers of the response as a case-insensitive map 62 | */ 63 | @Override 64 | public Map> getHeaders() { 65 | final Map> headerMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); 66 | final HttpHeaders headers = ahcResponse.getHeaders(); 67 | for (String name : headers.names()) { 68 | final List values = headers.getAll(name); 69 | headerMap.put(name, values); 70 | } 71 | return headerMap; 72 | } 73 | 74 | /** 75 | * Get all the cookies. 76 | */ 77 | @Override 78 | public List getCookies() { 79 | return ahcResponse.getCookies().stream().map(this::asCookie).collect(toList()); 80 | } 81 | 82 | public WSCookie asCookie(Cookie c) { 83 | return new WSCookieBuilder() 84 | .setName(c.name()) 85 | .setValue(c.value()) 86 | .setDomain(c.domain()) 87 | .setPath(c.path()) 88 | .setMaxAge(c.maxAge()) 89 | .setSecure(c.isSecure()) 90 | .setHttpOnly(c.isHttpOnly()).build(); 91 | } 92 | 93 | /** 94 | * Get only one cookie, using the cookie name. 95 | */ 96 | @Override 97 | public Optional getCookie(String name) { 98 | for (Cookie ahcCookie : ahcResponse.getCookies()) { 99 | // safe -- cookie.getName() will never return null 100 | if (ahcCookie.name().equals(name)) { 101 | return Optional.of(asCookie(ahcCookie)); 102 | } 103 | } 104 | return Optional.empty(); 105 | } 106 | 107 | @Override 108 | public String getBody() { 109 | return AhcWSUtils.getResponseBody(ahcResponse); 110 | } 111 | 112 | @Override 113 | public ByteString getBodyAsBytes() { 114 | return ByteString.fromArray(this.ahcResponse.getResponseBodyAsBytes()); 115 | } 116 | 117 | @Override 118 | public T getBody(BodyReadable readable) { 119 | return readable.apply(this); 120 | } 121 | 122 | @Override 123 | public String getContentType() { 124 | String contentType = ahcResponse.getContentType(); 125 | if (contentType == null) { 126 | // As defined by RFC-2616#7.2.1 127 | contentType = "application/octet-stream"; 128 | } 129 | return contentType; 130 | } 131 | 132 | /** 133 | * Return the request {@link URI}. Note that if the request got redirected, the value of the 134 | * {@link URI} will be the last valid redirect url. 135 | * 136 | * @return the request {@link URI}. 137 | */ 138 | public URI getUri() { 139 | try { 140 | return ahcResponse.getUri().toJavaNetURI(); 141 | } catch (URISyntaxException e) { 142 | throw new RuntimeException(e); 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /play-ws-standalone/src/main/resources/reference.conf: -------------------------------------------------------------------------------- 1 | play { 2 | 3 | # Configuration for Play WS 4 | ws { 5 | 6 | timeout { 7 | 8 | # If non null, the connection timeout, this is how long to wait for a TCP connection to be made 9 | connection = 2 minutes 10 | 11 | # If non null, the idle timeout, this is how long to wait for any IO activity from the remote host 12 | # while making a request 13 | idle = 2 minutes 14 | 15 | # If non null, the request timeout, this is the maximum amount of time to wait for the whole request 16 | request = 2 minutes 17 | } 18 | 19 | # Whether redirects should be followed 20 | followRedirects = true 21 | 22 | # Whether the JDK proxy properties should be read 23 | useProxyProperties = true 24 | 25 | # If non null, will set the User-Agent header on requests to this 26 | useragent = null 27 | 28 | # Whether compression should be used on incoming and outgoing requests 29 | compressionEnabled = false 30 | 31 | # ssl configuration 32 | ssl { 33 | 34 | # Whether we should use the default JVM SSL configuration or not 35 | # When false additional configuration will be applied on the context (as configured below). 36 | default = false 37 | 38 | # The ssl protocol to use 39 | protocol = "TLSv1.3" 40 | 41 | # Whether revocation lists should be checked, if null, defaults to platform default setting. 42 | checkRevocation = null 43 | 44 | # A sequence of URLs for obtaining revocation lists 45 | revocationLists = [] 46 | 47 | # The enabled cipher suites. If empty, uses the platform default. 48 | enabledCipherSuites = [] 49 | 50 | # The enabled protocols. If empty, uses the platform default. 51 | enabledProtocols = ["TLSv1.3", "TLSv1.2"] 52 | 53 | # Configuration for the key manager 54 | keyManager { 55 | # The key manager algorithm. If empty, uses the platform default. 56 | algorithm = null 57 | 58 | # The key stores 59 | stores = [ 60 | ] 61 | # The key stores should look like this 62 | prototype.stores { 63 | # The store type. If null, defaults to the platform default store type, ie JKS. 64 | type = null 65 | 66 | # The path to the keystore file. Either this must be non null, or data must be non null. 67 | path = null 68 | 69 | # The data for the keystore. Either this must be non null, or path must be non null. 70 | data = null 71 | 72 | # The password for loading the keystore. If null, uses no password. 73 | # It's recommended to load password using environment variable 74 | password = null 75 | } 76 | } 77 | 78 | trustManager { 79 | # The trust manager algorithm. If empty, uses the platform default. 80 | algorithm = null 81 | 82 | # The trust stores 83 | stores = [ 84 | ] 85 | # The key stores should look like this 86 | prototype.stores { 87 | # The store type. If null, defaults to the platform default store type, ie JKS. 88 | type = null 89 | 90 | # The path to the keystore file. Either this must be non null, or data must be non null. 91 | path = null 92 | 93 | # The data for the keystore. Either this must be non null, or path must be non null. 94 | data = null 95 | 96 | # The password for loading the truststore. If null, uses no password. 97 | # It's recommended to load password using environment variable 98 | password = null 99 | } 100 | 101 | } 102 | 103 | # The loose ssl options. These allow configuring ssl to be more loose about what it accepts, 104 | # at the cost of introducing potential security issues. 105 | loose { 106 | 107 | # If non null, overrides the platform default for whether legacy hello messages should be allowed. 108 | allowLegacyHelloMessages = null 109 | 110 | # If non null, overrides the platform default for whether unsafe renegotiation should be allowed. 111 | allowUnsafeRenegotiation = null 112 | 113 | # Whether any certificate should be accepted or not 114 | acceptAnyCertificate = false 115 | } 116 | 117 | # Debug configuration 118 | debug { 119 | 120 | # Turn on all debugging 121 | all = false 122 | 123 | # Turn on ssl debugging 124 | ssl = false 125 | 126 | # Print SSLContext tracing 127 | sslctx = false 128 | 129 | # Print key manager tracing 130 | keymanager = false 131 | 132 | # Print trust manager tracing 133 | trustmanager = false 134 | } 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /play-ahc-ws-standalone/src/main/java/play/libs/ws/ahc/StreamedResponse.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.libs.ws.ahc; 6 | 7 | import org.apache.pekko.stream.javadsl.Source; 8 | import org.apache.pekko.util.ByteString; 9 | import org.reactivestreams.Publisher; 10 | import play.api.libs.ws.ahc.AhcWSUtils; 11 | import play.libs.ws.BodyReadable; 12 | import play.libs.ws.StandaloneWSResponse; 13 | import play.libs.ws.WSCookie; 14 | import play.shaded.ahc.io.netty.handler.codec.http.HttpHeaderNames; 15 | import play.shaded.ahc.org.asynchttpclient.HttpResponseBodyPart; 16 | import scala.collection.Seq; 17 | import scala.jdk.javaapi.StreamConverters; 18 | 19 | import java.net.URI; 20 | import java.util.ArrayList; 21 | import java.util.List; 22 | import java.util.Map; 23 | import java.util.Optional; 24 | import java.util.TreeMap; 25 | import java.util.function.Predicate; 26 | 27 | import static java.util.stream.Collectors.toMap; 28 | import scala.jdk.javaapi.CollectionConverters; 29 | 30 | public class StreamedResponse implements StandaloneWSResponse, CookieBuilder { 31 | 32 | private final int status; 33 | private final Map> headers; 34 | private final String statusText; 35 | private final URI uri; 36 | private final Publisher publisher; 37 | private final StandaloneAhcWSClient client; 38 | private final boolean useLaxCookieEncoder; 39 | 40 | private List cookies; 41 | 42 | public StreamedResponse(StandaloneAhcWSClient client, 43 | int status, 44 | String statusText, URI uri, 45 | scala.collection.Map> headers, 46 | Publisher publisher, 47 | boolean useLaxCookieEncoder) { 48 | this.client = client; 49 | this.status = status; 50 | this.statusText = statusText; 51 | this.uri = uri; 52 | this.headers = asJava(headers); 53 | this.publisher = publisher; 54 | this.useLaxCookieEncoder = useLaxCookieEncoder; 55 | } 56 | 57 | @Override 58 | public int getStatus() { 59 | return status; 60 | } 61 | 62 | @Override 63 | public String getStatusText() { 64 | return statusText; 65 | } 66 | 67 | @Override 68 | public Map> getHeaders() { 69 | return headers; 70 | } 71 | 72 | @Override 73 | public Object getUnderlying() { 74 | return publisher; 75 | } 76 | 77 | @Override 78 | public List getCookies() { 79 | if (cookies == null) { 80 | cookies = buildCookies(headers); 81 | } 82 | return cookies; 83 | } 84 | 85 | @Override 86 | public Optional getCookie(String name) { 87 | Predicate predicate = (WSCookie c) -> c.getName().equals(name); 88 | return getCookies().stream().filter(predicate).findFirst(); 89 | } 90 | 91 | @Override 92 | public String getContentType() { 93 | return getSingleHeader(HttpHeaderNames.CONTENT_TYPE.toString()).orElse("application/octet-stream"); 94 | } 95 | 96 | @Override 97 | public T getBody(BodyReadable readable) { 98 | return readable.apply(this); 99 | } 100 | 101 | @Override 102 | public String getBody() { 103 | return getBodyAsBytes().decodeString(AhcWSUtils.getCharset(getContentType())); 104 | } 105 | 106 | @Override 107 | public ByteString getBodyAsBytes() { 108 | return client.blockingToByteString(getBodyAsSource()); 109 | } 110 | 111 | @Override 112 | public Source getBodyAsSource() { 113 | return Source.fromPublisher(publisher).map(bodyPart -> ByteString.fromArray(bodyPart.getBodyPartBytes())); 114 | } 115 | 116 | public boolean isUseLaxCookieEncoder() { 117 | return useLaxCookieEncoder; 118 | } 119 | 120 | @Override 121 | public URI getUri() { 122 | return this.uri; 123 | } 124 | 125 | private static java.util.Map> asJava(scala.collection.Map> scalaMap) { 126 | return StreamConverters.asJavaSeqStream(scalaMap).collect(toMap(f -> f._1(), f -> CollectionConverters.asJava(f._2()), 127 | (l, r) -> { 128 | final List merged = new ArrayList<>(l.size() + r.size()); 129 | merged.addAll(l); 130 | merged.addAll(r); 131 | return merged; 132 | }, 133 | () -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER) 134 | ) 135 | ); 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /play-ahc-ws-standalone/src/main/java/play/libs/ws/ahc/AhcCurlRequestLogger.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.libs.ws.ahc; 6 | 7 | import play.libs.ws.InMemoryBodyWritable; 8 | import play.libs.ws.StandaloneWSRequest; 9 | import play.libs.ws.WSRequestExecutor; 10 | import play.libs.ws.WSRequestFilter; 11 | import play.shaded.ahc.org.asynchttpclient.Request; 12 | import play.shaded.ahc.org.asynchttpclient.proxy.ProxyServer; 13 | import play.shaded.ahc.org.asynchttpclient.util.HttpUtils; 14 | 15 | import java.nio.charset.StandardCharsets; 16 | import java.util.Base64; 17 | import java.util.Optional; 18 | import java.util.regex.Matcher; 19 | import java.util.regex.Pattern; 20 | 21 | /** 22 | * Logs {@link StandaloneWSRequest} and pulls information into Curl format to an SLF4J logger. 23 | * 24 | * @see https://curl.haxx.se/ 25 | */ 26 | public class AhcCurlRequestLogger implements WSRequestFilter { 27 | 28 | private final org.slf4j.Logger logger; 29 | 30 | public AhcCurlRequestLogger(org.slf4j.Logger logger) { 31 | this.logger = logger; 32 | } 33 | 34 | public AhcCurlRequestLogger() { 35 | this(org.slf4j.LoggerFactory.getLogger(AhcCurlRequestLogger.class)); 36 | } 37 | 38 | private static final Pattern SINGLE_QUOTE_REPLACE = Pattern.compile("'", Pattern.LITERAL); 39 | 40 | @Override 41 | public WSRequestExecutor apply(WSRequestExecutor requestExecutor) { 42 | return request -> { 43 | logger.info(toCurl((StandaloneAhcWSRequest)request)); 44 | return requestExecutor.apply(request); 45 | }; 46 | } 47 | 48 | private String toCurl(StandaloneAhcWSRequest request) { 49 | final StringBuilder b = new StringBuilder("curl \\\n"); 50 | 51 | // verbose, since it's a fair bet this is for debugging 52 | b.append(" --verbose") 53 | .append(" \\\n"); 54 | 55 | // method 56 | b.append(" --request ").append(request.getMethod()) 57 | .append(" \\\n"); 58 | 59 | //authentication 60 | request.getAuth().ifPresent(auth -> { 61 | String encodedPasswd = Base64.getUrlEncoder().encodeToString((auth.getUsername() + ':' + auth.getPassword()).getBytes(StandardCharsets.US_ASCII)); 62 | b.append(" --header 'Authorization: Basic ").append(quote(encodedPasswd)).append('\'') 63 | .append(" \\\n"); 64 | }); 65 | 66 | // headers 67 | request.getHeaders().forEach((name, values) -> 68 | values.forEach(v -> 69 | b.append(" --header '").append(quote(name)).append(": ").append(quote(v)).append('\'') 70 | .append(" \\\n") 71 | ) 72 | ); 73 | 74 | // cookies 75 | request.getCookies().forEach(cookie -> 76 | b.append(" --cookie '").append(cookie.getName()).append('=').append(cookie.getValue()).append('\'') 77 | .append(" \\\n") 78 | ); 79 | 80 | // body 81 | request.getBody().ifPresent(requestBody -> { 82 | if (requestBody instanceof InMemoryBodyWritable) { 83 | InMemoryBodyWritable inMemoryBody = (InMemoryBodyWritable) requestBody; 84 | 85 | String charset = findCharset(request); 86 | String bodyString = inMemoryBody.body().get().decodeString(charset); 87 | 88 | b.append(" --data '").append(quote(bodyString)).append('\'') 89 | .append(" \\\n"); 90 | } else { 91 | throw new UnsupportedOperationException("Unsupported body type " + requestBody.getClass()); 92 | } 93 | }); 94 | 95 | // pull out some underlying values from the request. This creates a new Request 96 | // but should be harmless. 97 | Request ahcRequest = request.buildRequest(); 98 | ProxyServer proxyServer = ahcRequest.getProxyServer(); 99 | if (proxyServer != null) { 100 | b.append(" --proxy ").append(proxyServer.getHost()).append(':').append(proxyServer.getPort()) 101 | .append(" \\\n"); 102 | } 103 | 104 | // url 105 | b.append(" '").append(quote(ahcRequest.getUrl())).append('\''); 106 | return b.toString(); 107 | } 108 | 109 | private static String findCharset(StandaloneAhcWSRequest request) { 110 | return Optional.ofNullable(request.getContentType()) 111 | .flatMap(contentType -> contentType.map(HttpUtils::extractContentTypeCharsetAttribute)) 112 | .orElse(StandardCharsets.UTF_8).name(); 113 | } 114 | 115 | private static String quote(String unsafe) { 116 | return SINGLE_QUOTE_REPLACE.matcher( 117 | unsafe 118 | ).replaceAll(Matcher.quoteReplacement("'\\''")); 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /integration-tests/src/test/scala/play/libs/ws/ahc/AhcWSRequestFilterSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.libs.ws.ahc 6 | 7 | import org.specs2.concurrent.ExecutionEnv 8 | import org.specs2.matcher.FutureMatchers 9 | import org.specs2.mutable.Specification 10 | import play.NettyServerProvider 11 | import play.api.BuiltInComponents 12 | import play.api.mvc.Results 13 | 14 | import scala.concurrent.duration._ 15 | import scala.jdk.FutureConverters._ 16 | 17 | class AhcWSRequestFilterSpec(implicit val executionEnv: ExecutionEnv) 18 | extends Specification 19 | with NettyServerProvider 20 | with StandaloneWSClientSupport 21 | with FutureMatchers { 22 | 23 | override def routes(components: BuiltInComponents) = { case _ => 24 | components.defaultActionBuilder { req => 25 | val res = Results 26 | .Ok( 27 |

Say hello to play

28 | ) 29 | req.headers.get("X-Request-Id").fold(res) { value => 30 | res.withHeaders(("X-Request-Id", value)) 31 | } 32 | } 33 | } 34 | 35 | "setRequestFilter" should { 36 | 37 | "work with one request filter" in withClient() { client => 38 | import scala.jdk.CollectionConverters._ 39 | val callList = new java.util.ArrayList[Integer]() 40 | val responseFuture = 41 | client 42 | .url(s"http://localhost:$testServerPort") 43 | .setRequestFilter(new CallbackRequestFilter(callList, 1)) 44 | .get() 45 | .asScala 46 | responseFuture 47 | .map { _ => 48 | callList.asScala must contain(1) 49 | } 50 | .await(retries = 0, timeout = 5.seconds) 51 | } 52 | 53 | "stream with one request filter" in withClient() { client => 54 | import scala.jdk.CollectionConverters._ 55 | val callList = new java.util.ArrayList[Integer]() 56 | val responseFuture = 57 | client 58 | .url(s"http://localhost:$testServerPort") 59 | .setRequestFilter(new CallbackRequestFilter(callList, 1)) 60 | .stream() 61 | .asScala 62 | responseFuture 63 | .map { _ => 64 | callList.asScala must contain(1) 65 | } 66 | .await(retries = 0, timeout = 5.seconds) 67 | } 68 | 69 | "work with three request filter" in withClient() { client => 70 | import scala.jdk.CollectionConverters._ 71 | val callList = new java.util.ArrayList[Integer]() 72 | val responseFuture = 73 | client 74 | .url(s"http://localhost:$testServerPort") 75 | .setRequestFilter(new CallbackRequestFilter(callList, 1)) 76 | .setRequestFilter(new CallbackRequestFilter(callList, 2)) 77 | .setRequestFilter(new CallbackRequestFilter(callList, 3)) 78 | .get() 79 | .asScala 80 | responseFuture 81 | .map { _ => 82 | callList.asScala must containTheSameElementsAs(Seq(1, 2, 3)) 83 | } 84 | .await(retries = 0, timeout = 5.seconds) 85 | } 86 | 87 | "stream with three request filters" in withClient() { client => 88 | import scala.jdk.CollectionConverters._ 89 | val callList = new java.util.ArrayList[Integer]() 90 | val responseFuture = 91 | client 92 | .url(s"http://localhost:$testServerPort") 93 | .setRequestFilter(new CallbackRequestFilter(callList, 1)) 94 | .setRequestFilter(new CallbackRequestFilter(callList, 2)) 95 | .setRequestFilter(new CallbackRequestFilter(callList, 3)) 96 | .stream() 97 | .asScala 98 | responseFuture 99 | .map { _ => 100 | callList.asScala must containTheSameElementsAs(Seq(1, 2, 3)) 101 | } 102 | .await(retries = 0, timeout = 5.seconds) 103 | } 104 | 105 | "should allow filters to modify the request" in withClient() { client => 106 | val appendedHeader = "X-Request-Id" 107 | val appendedHeaderValue = "someid" 108 | val responseFuture = 109 | client 110 | .url(s"http://localhost:$testServerPort") 111 | .setRequestFilter(new HeaderAppendingFilter(appendedHeader, appendedHeaderValue)) 112 | .get() 113 | .asScala 114 | 115 | responseFuture 116 | .map { response => 117 | response.getHeaders.get("X-Request-Id").get(0) must be_==("someid") 118 | } 119 | .await(retries = 0, timeout = 5.seconds) 120 | } 121 | 122 | "allow filters to modify the streaming request" in withClient() { client => 123 | val appendedHeader = "X-Request-Id" 124 | val appendedHeaderValue = "someid" 125 | val responseFuture = 126 | client 127 | .url(s"http://localhost:$testServerPort") 128 | .setRequestFilter(new HeaderAppendingFilter(appendedHeader, appendedHeaderValue)) 129 | .stream() 130 | .asScala 131 | 132 | responseFuture 133 | .map { response => 134 | response.getHeaders.get("X-Request-Id").get(0) must be_==("someid") 135 | } 136 | .await(retries = 0, timeout = 5.seconds) 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /play-ws-standalone/src/main/java/play/libs/ws/StandaloneWSResponse.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.libs.ws; 6 | 7 | import org.apache.pekko.stream.javadsl.Source; 8 | import org.apache.pekko.util.ByteString; 9 | 10 | import java.util.Collections; 11 | import java.util.List; 12 | import java.util.Map; 13 | import java.util.Optional; 14 | 15 | import java.net.URI; 16 | 17 | /** 18 | * This is the WS response from the server. 19 | */ 20 | public interface StandaloneWSResponse { 21 | /** 22 | * Gets the response URI. 23 | * 24 | * @return the response URI 25 | */ 26 | URI getUri(); 27 | 28 | /** 29 | * @return all the headers from the response. 30 | */ 31 | Map> getHeaders(); 32 | 33 | /** 34 | * Get all the values of header with the specified name. If there are no values for 35 | * the header with the specified name, than an empty List is returned. 36 | * 37 | * @param name the header name. 38 | * @return all the values for this header name. 39 | */ 40 | default List getHeaderValues(String name) { 41 | return getHeaders().getOrDefault(name, Collections.emptyList()); 42 | } 43 | 44 | /** 45 | * Get the value of the header with the specified name. If there are more than one values 46 | * for this header, the first value is returned. If there are no values, than an empty 47 | * Optional is returned. 48 | * 49 | * @param name the header name 50 | * @return the header value 51 | */ 52 | default Optional getSingleHeader(String name) { 53 | return getHeaderValues(name).stream().findFirst(); 54 | } 55 | 56 | /** 57 | * @return the underlying implementation response object, if any. 58 | */ 59 | Object getUnderlying(); 60 | 61 | /** 62 | * @return the HTTP status code from the response. 63 | */ 64 | int getStatus(); 65 | 66 | /** 67 | * @return the text associated with the status code. 68 | */ 69 | String getStatusText(); 70 | 71 | /** 72 | * @return all the cookies from the response. 73 | */ 74 | List getCookies(); 75 | 76 | /** 77 | * @param name the cookie name 78 | * @return a single cookie from the response, if any. 79 | */ 80 | Optional getCookie(String name); 81 | 82 | /** 83 | * @return the content type. 84 | */ 85 | String getContentType(); 86 | 87 | /** 88 | * Returns the response getBody as a particular type, through a 89 | * {@link BodyReadable} transformation. You can define your 90 | * own {@link BodyReadable} types: 91 | * 92 | *
 93 |      * {@code public class MyClass {
 94 |      *   private BodyReadable fooReadable = (response) -> new Foo();
 95 |      *
 96 |      *   public void readAsFoo(StandaloneWSResponse response) {
 97 |      *       Foo foo = response.getBody(fooReadable);
 98 |      *   }
 99 |      * }
100 |      * }
101 |      * 
102 | * 103 | * or use {@code play.libs.ws.ahc.DefaultResponseReadables} 104 | * for the built-ins: 105 | * 106 | *
107 |      * {@code public class MyClass implements DefaultResponseReadables {
108 |      *     public void readAsString(StandaloneWSResponse response) {
109 |      *         String getBody = response.getBody(string());
110 |      *     }
111 |      *
112 |      *     public void readAsJson(StandaloneWSResponse response) {
113 |      *         JsonNode json = response.getBody(json());
114 |      *     }
115 |      * }
116 |      * }
117 |      * 
118 | * 119 | * @param readable the readable to convert the response to a T 120 | * @param the end type to return 121 | * @return the response getBody transformed into an instance of T 122 | */ 123 | T getBody(BodyReadable readable); 124 | 125 | /** 126 | * The response body decoded as String, using a simple algorithm to guess the encoding. 127 | * 128 | * This decodes the body to a string representation based on the following algorithm: 129 | * 130 | * 1. Look for a "charset" parameter on the Content-Type. If it exists, set `charset` to its value and goto step 3. 131 | * 2. If the Content-Type is of type "text", set $charset to "ISO-8859-1"; else set `charset` to "UTF-8". 132 | * 3. Decode the raw bytes of the body using `charset`. 133 | * 134 | * Note that this does not take into account any special cases for specific content types. For example, for 135 | * application/json, we do not support encoding autodetection and will trust the charset parameter if provided. 136 | * 137 | * @return the response body parsed as a String using the above algorithm. 138 | */ 139 | String getBody(); 140 | 141 | ByteString getBodyAsBytes(); 142 | 143 | /** 144 | * Converts a response body into Source[ByteString, NotUsed]. 145 | * 146 | * Note that this is only usable with a streaming request: 147 | * 148 | *
149 |      * {@code
150 |      * wsClient.url("https://playframework.com")
151 |      *         .stream() // this returns a CompletionStage
152 |      *         .thenApply(StandaloneWSResponse::getBodyAsSource);
153 |      * }
154 |      * 
155 | */ 156 | default Source getBodyAsSource() { 157 | return Source.single(getBodyAsBytes()); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /play-ahc-ws-standalone/src/main/scala/play/api/libs/oauth/OAuth.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.oauth 6 | 7 | import play.shaded.oauth.oauth.signpost.basic.DefaultOAuthConsumer 8 | import play.shaded.oauth.oauth.signpost.basic.DefaultOAuthProvider 9 | import play.shaded.oauth.oauth.signpost.exception.OAuthException 10 | import play.shaded.ahc.org.asynchttpclient.oauth.OAuthSignatureCalculator 11 | import play.shaded.ahc.org.asynchttpclient.Request 12 | import play.shaded.ahc.org.asynchttpclient.RequestBuilderBase 13 | import play.shaded.ahc.org.asynchttpclient.SignatureCalculator 14 | import play.api.libs.ws.WSSignatureCalculator 15 | 16 | import play.shaded.ahc.org.asynchttpclient.oauth.{ ConsumerKey => AHCConsumerKey } 17 | import play.shaded.ahc.org.asynchttpclient.oauth.{ RequestToken => AHCRequestToken } 18 | 19 | /** 20 | * Library to access resources protected by OAuth 1.0a. 21 | * 22 | * @param info the service information, including the required URLs and the application id and secret 23 | * @param use10a whether the service should use the 1.0 version of the spec, or the 1.0a version fixing a security issue. 24 | * You must use the version corresponding to the 25 | */ 26 | case class OAuth(info: ServiceInfo, use10a: Boolean = true) { 27 | 28 | private val provider = { 29 | val p = new DefaultOAuthProvider(info.requestTokenURL, info.accessTokenURL, info.authorizationURL) 30 | p.setOAuth10a(use10a) 31 | p 32 | } 33 | 34 | /** 35 | * Request the request token and secret. 36 | * 37 | * @param callbackURL the URL where the provider should redirect to (usually a URL on the current app) 38 | * @return A Right(RequestToken) in case of success, Left(OAuthException) otherwise 39 | */ 40 | def retrieveRequestToken(callbackURL: String): Either[OAuthException, RequestToken] = { 41 | val consumer = new DefaultOAuthConsumer(info.key.key, info.key.secret) 42 | try { 43 | provider.retrieveRequestToken(consumer, callbackURL) 44 | Right(RequestToken(consumer.getToken(), consumer.getTokenSecret())) 45 | } catch { 46 | case e: OAuthException => Left(e) 47 | } 48 | } 49 | 50 | /** 51 | * Exchange a request token for an access token. 52 | * 53 | * @param token the token/secret pair obtained from a previous call 54 | * @param verifier a string you got through your user, with redirection 55 | * @return A Right(RequestToken) in case of success, Left(OAuthException) otherwise 56 | */ 57 | def retrieveAccessToken(token: RequestToken, verifier: String): Either[OAuthException, RequestToken] = { 58 | val consumer = new DefaultOAuthConsumer(info.key.key, info.key.secret) 59 | consumer.setTokenWithSecret(token.token, token.secret) 60 | try { 61 | provider.retrieveAccessToken(consumer, verifier) 62 | Right(RequestToken(consumer.getToken(), consumer.getTokenSecret())) 63 | } catch { 64 | case e: OAuthException => Left(e) 65 | } 66 | } 67 | 68 | /** 69 | * The URL where the user needs to be redirected to grant authorization to your application. 70 | * 71 | * @param token request token 72 | */ 73 | def redirectUrl(token: String): String = { 74 | import play.shaded.oauth.oauth.signpost.{ OAuth => O } 75 | O.addQueryParameters( 76 | provider.getAuthorizationWebsiteUrl(), 77 | O.OAUTH_TOKEN, 78 | token 79 | ) 80 | } 81 | 82 | } 83 | 84 | /** 85 | * A consumer key / consumer secret pair that the OAuth provider gave you, to identify your application. 86 | */ 87 | case class ConsumerKey(key: String, secret: String) 88 | 89 | /** 90 | * A request token / token secret pair, to be used for a specific user. 91 | */ 92 | case class RequestToken(token: String, secret: String) 93 | 94 | /** 95 | * The information identifying a oauth provider: URLs and the consumer key / consumer secret pair. 96 | */ 97 | case class ServiceInfo(requestTokenURL: String, accessTokenURL: String, authorizationURL: String, key: ConsumerKey) 98 | 99 | /** 100 | * The public AsyncHttpClient implementation of WSSignatureCalculator. 101 | */ 102 | class OAuthCalculator(consumerKey: ConsumerKey, requestToken: RequestToken) 103 | extends WSSignatureCalculator 104 | with SignatureCalculator { 105 | 106 | private val ahcConsumerKey = new AHCConsumerKey(consumerKey.key, consumerKey.secret) 107 | private val ahcRequestToken = new AHCRequestToken(requestToken.token, requestToken.secret) 108 | private val calculator = new OAuthSignatureCalculator(ahcConsumerKey, ahcRequestToken) 109 | 110 | override def calculateAndAddSignature(request: Request, requestBuilder: RequestBuilderBase[?]): Unit = { 111 | calculator.calculateAndAddSignature(request, requestBuilder) 112 | } 113 | } 114 | 115 | /** 116 | * Object for creating signature calculator for the Play WS API. 117 | * 118 | * Example: 119 | * {{{ 120 | * import play.api.libs.oauth.{ ConsumerKey, OAuthCalculator, RequestToken } 121 | * import play.api.libs.ws.ahc.StandaloneAhcWSClient 122 | * 123 | * def example( 124 | * twitterConsumerKey: String, 125 | * twitterConsumerSecret: String, 126 | * accessTokenKey: String, 127 | * accessTokenSecret: String, 128 | * ws: StandaloneAhcWSClient) = { 129 | * val consumerKey: ConsumerKey = 130 | * ConsumerKey(twitterConsumerKey, twitterConsumerSecret) 131 | * 132 | * val requestToken: RequestToken = 133 | * RequestToken(accessTokenKey, accessTokenSecret) 134 | * 135 | * ws.url("http://example.com/protected"). 136 | * sign(OAuthCalculator(consumerKey, requestToken)).get() 137 | * } 138 | * }}} 139 | */ 140 | object OAuthCalculator { 141 | def apply(consumerKey: ConsumerKey, token: RequestToken): WSSignatureCalculator = { 142 | new OAuthCalculator(consumerKey, token) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /play-ahc-ws-standalone/src/main/java/play/libs/oauth/OAuth.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 3 | */ 4 | 5 | package play.libs.oauth; 6 | 7 | import play.shaded.oauth.oauth.signpost.OAuthConsumer; 8 | import play.shaded.oauth.oauth.signpost.OAuthProvider; 9 | import play.shaded.oauth.oauth.signpost.basic.DefaultOAuthConsumer; 10 | import play.shaded.oauth.oauth.signpost.basic.DefaultOAuthProvider; 11 | import play.shaded.oauth.oauth.signpost.exception.OAuthException; 12 | import play.shaded.ahc.org.asynchttpclient.oauth.OAuthSignatureCalculator; 13 | import play.libs.ws.WSSignatureCalculator; 14 | 15 | public class OAuth { 16 | 17 | private ServiceInfo info; 18 | private OAuthProvider provider; 19 | 20 | public OAuth(ServiceInfo info) { 21 | this(info, true); 22 | } 23 | 24 | public OAuth(ServiceInfo info, boolean use10a) { 25 | this.info = info; 26 | this.provider = new DefaultOAuthProvider(info.requestTokenURL, info.accessTokenURL, info.authorizationURL); 27 | this.provider.setOAuth10a(use10a); 28 | } 29 | 30 | public ServiceInfo getInfo() { 31 | return info; 32 | } 33 | 34 | public OAuthProvider getProvider() { 35 | return provider; 36 | } 37 | 38 | /** 39 | * Request the request token and secret. 40 | * 41 | * @param callbackURL the URL where the provider should redirect to (usually a URL on the current app) 42 | * @return A Right(RequestToken) in case of success, Left(OAuthException) otherwise 43 | */ 44 | public RequestToken retrieveRequestToken(String callbackURL) { 45 | OAuthConsumer consumer = new DefaultOAuthConsumer(info.key.key, info.key.secret); 46 | try { 47 | provider.retrieveRequestToken(consumer, callbackURL); 48 | return new RequestToken(consumer.getToken(), consumer.getTokenSecret()); 49 | } catch (OAuthException ex) { 50 | throw new RuntimeException(ex); 51 | } 52 | } 53 | 54 | /** 55 | * Exchange a request token for an access token. 56 | * 57 | * @param token the token/secret pair obtained from a previous call 58 | * @param verifier a string you got through your user, with redirection 59 | * @return A Right(RequestToken) in case of success, Left(OAuthException) otherwise 60 | */ 61 | public RequestToken retrieveAccessToken(RequestToken token, String verifier) { 62 | OAuthConsumer consumer = new DefaultOAuthConsumer(info.key.key, info.key.secret); 63 | consumer.setTokenWithSecret(token.token, token.secret); 64 | try { 65 | provider.retrieveAccessToken(consumer, verifier); 66 | return new RequestToken(consumer.getToken(), consumer.getTokenSecret()); 67 | } catch (OAuthException ex) { 68 | throw new RuntimeException(ex); 69 | } 70 | } 71 | 72 | /** 73 | * The URL where the user needs to be redirected to grant authorization to your application. 74 | * 75 | * @param token request token 76 | * @return the url 77 | */ 78 | public String redirectUrl(String token) { 79 | return play.shaded.oauth.oauth.signpost.OAuth.addQueryParameters( 80 | provider.getAuthorizationWebsiteUrl(), 81 | play.shaded.oauth.oauth.signpost.OAuth.OAUTH_TOKEN, 82 | token 83 | ); 84 | } 85 | 86 | /** 87 | * A consumer key / consumer secret pair that the OAuth provider gave you, to identify your application. 88 | */ 89 | public static class ConsumerKey { 90 | public String key; 91 | public String secret; 92 | 93 | public ConsumerKey(String key, String secret) { 94 | this.key = key; 95 | this.secret = secret; 96 | } 97 | } 98 | 99 | /** 100 | * A request token / token secret pair, to be used for a specific user. 101 | */ 102 | public static class RequestToken { 103 | public String token; 104 | public String secret; 105 | 106 | public RequestToken(String token, String secret) { 107 | this.token = token; 108 | this.secret = secret; 109 | } 110 | } 111 | 112 | /** 113 | * The information identifying a oauth provider: URLs and the consumer key / consumer secret pair. 114 | */ 115 | public static class ServiceInfo { 116 | public String requestTokenURL; 117 | public String accessTokenURL; 118 | public String authorizationURL; 119 | public ConsumerKey key; 120 | 121 | public ServiceInfo(String requestTokenURL, String accessTokenURL, String authorizationURL, ConsumerKey key) { 122 | this.requestTokenURL = requestTokenURL; 123 | this.accessTokenURL = accessTokenURL; 124 | this.authorizationURL = authorizationURL; 125 | this.key = key; 126 | } 127 | } 128 | 129 | /** 130 | * A signature calculator for the Play WS API. 131 | *

132 | * Example: 133 | * {{{ 134 | * WS.url("http://example.com/protected").sign(OAuthCalculator(service, token)).get() 135 | * }}} 136 | */ 137 | public static class OAuthCalculator implements WSSignatureCalculator { 138 | 139 | private OAuthSignatureCalculator calculator; 140 | 141 | public OAuthCalculator(ConsumerKey consumerKey, RequestToken token) { 142 | play.shaded.ahc.org.asynchttpclient.oauth.ConsumerKey ahcConsumerKey = new play.shaded.ahc.org.asynchttpclient.oauth.ConsumerKey(consumerKey.key, consumerKey.secret); 143 | play.shaded.ahc.org.asynchttpclient.oauth.RequestToken ahcRequestToken = new play.shaded.ahc.org.asynchttpclient.oauth.RequestToken(token.token, token.secret); 144 | calculator = new OAuthSignatureCalculator(ahcConsumerKey, ahcRequestToken); 145 | } 146 | 147 | public OAuthSignatureCalculator getCalculator() { 148 | return calculator; 149 | } 150 | } 151 | 152 | } 153 | --------------------------------------------------------------------------------