From 563aa56517af12b4a3e412de4d5763990a7fad97 Mon Sep 17 00:00:00 2001 From: adamw Date: Tue, 14 Jan 2025 11:16:25 +0100 Subject: [PATCH] Release 4.0.0-M23 --- README.md | 14 +- generated-docs/out/backends/akka.md | 6 +- generated-docs/out/backends/catseffect.md | 17 +- generated-docs/out/backends/finagle.md | 4 +- generated-docs/out/backends/fs2.md | 19 +- generated-docs/out/backends/future.md | 12 +- generated-docs/out/backends/http4s.md | 9 +- .../out/backends/javascript/fetch.md | 20 +- generated-docs/out/backends/monix.md | 12 +- generated-docs/out/backends/native/curl.md | 27 ++- generated-docs/out/backends/pekko.md | 8 +- generated-docs/out/backends/scalaz.md | 8 +- generated-docs/out/backends/start_stop.md | 2 +- generated-docs/out/backends/summary.md | 9 +- generated-docs/out/backends/synchronous.md | 24 +- generated-docs/out/backends/wrappers/cache.md | 38 ++++ .../out/backends/wrappers/custom.md | 208 +----------------- .../out/backends/wrappers/logging.md | 4 +- .../out/backends/wrappers/opentelemetry.md | 4 +- .../out/backends/wrappers/prometheus.md | 2 +- generated-docs/out/backends/zio.md | 22 +- generated-docs/out/conf.py | 2 +- generated-docs/out/examples.md | 2 +- generated-docs/out/goals.md | 3 +- generated-docs/out/how.md | 14 +- generated-docs/out/includes/examples_list.md | 38 +++- generated-docs/out/index.md | 68 +++--- generated-docs/out/migrate_v3_v4.md | 46 ++++ generated-docs/out/model/model.md | 8 +- generated-docs/out/model/uri.md | 4 +- generated-docs/out/{ => other}/json.md | 51 ++--- generated-docs/out/{ => other}/openapi.md | 10 +- generated-docs/out/{ => other}/resilience.md | 22 +- generated-docs/out/other/sse.md | 5 + generated-docs/out/{ => other}/websockets.md | 50 ++--- generated-docs/out/{ => other}/xml.md | 33 ++- generated-docs/out/quickstart.md | 59 ++--- generated-docs/out/requests/authentication.md | 2 + generated-docs/out/requests/basics.md | 37 +--- generated-docs/out/requests/body.md | 6 +- generated-docs/out/requests/cookies.md | 2 + generated-docs/out/requests/headers.md | 15 ++ generated-docs/out/requests/multipart.md | 2 +- generated-docs/out/requests/streaming.md | 16 +- generated-docs/out/requests/type.md | 17 +- generated-docs/out/responses/basics.md | 7 +- generated-docs/out/responses/body.md | 47 ++-- generated-docs/out/responses/exceptions.md | 21 +- generated-docs/out/support.md | 11 + generated-docs/out/testing/curl.md | 19 ++ .../out/{testing.md => testing/stub.md} | 39 ++-- 51 files changed, 564 insertions(+), 561 deletions(-) create mode 100644 generated-docs/out/backends/wrappers/cache.md create mode 100644 generated-docs/out/migrate_v3_v4.md rename generated-docs/out/{ => other}/json.md (83%) rename generated-docs/out/{ => other}/openapi.md (90%) rename generated-docs/out/{ => other}/resilience.md (62%) create mode 100644 generated-docs/out/other/sse.md rename generated-docs/out/{ => other}/websockets.md (72%) rename generated-docs/out/{ => other}/xml.md (72%) create mode 100644 generated-docs/out/support.md create mode 100644 generated-docs/out/testing/curl.md rename generated-docs/out/{testing.md => testing/stub.md} (81%) diff --git a/README.md b/README.md index d6ed9ca32e..e1169633cd 100755 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ The library is available for Scala 2.12, 2.13 and 3. Supported platforms are the Here's a quick example of sttp client in action, runnable using [scala-cli](https://scala-cli.virtuslab.org): ```scala -//> using dep com.softwaremill.sttp.client4::core:4.0.0-M22 +//> using dep com.softwaremill.sttp.client4::core:4.0.0-M23 import sttp.client4.quick.* @@ -37,7 +37,7 @@ Additionally, sttp client seamlessly integrates with popular libraries for JSON Some more features: URI interpolation, a self-managed backend, and type-safe HTTP error/success representation, are demonstrated by the below example: ```scala -//> using dep com.softwaremill.sttp.client4::core:4.0.0-M22 +//> using dep com.softwaremill.sttp.client4::core:4.0.0-M23 import sttp.client4.* @@ -72,7 +72,7 @@ sttp (v2) documentation is available at [sttp.softwaremill.com/en/v2](https://st sttp (v1) documentation is available at [sttp.softwaremill.com/en/v1](https://sttp.softwaremill.com/en/v1). -scaladoc is available at [https://www.javadoc.io](https://www.javadoc.io/doc/com.softwaremill.sttp.client4/core_2.12/4.0.0-M22) +scaladoc is available at [https://www.javadoc.io](https://www.javadoc.io/doc/com.softwaremill.sttp.client4/core_2.12/4.0.0-M23) ## Quickstart with scala-cli @@ -80,7 +80,7 @@ Add the following directive to the top of your scala file to add the core sttp d If you are using [scala-cli](https://scala-cli.virtuslab.org), you can quickly start experimenting with sttp by copy-pasting the following: ``` -//> using dep "com.softwaremill.sttp.client4::core:4.0.0-M22" +//> using dep "com.softwaremill.sttp.client4::core:4.0.0-M23" import sttp.client4.quick.* quickRequest.get(uri"http://httpbin.org/ip").send() ``` @@ -92,7 +92,7 @@ The `quick` package import brings in the sttp API and a pre-configured, global s Similarly, using [Ammonite](http://ammonite.io): ```scala -import $ivy.`com.softwaremill.sttp.client4::core:4.0.0-M22` +import $ivy.`com.softwaremill.sttp.client4::core:4.0.0-M23` import sttp.client4.quick.* quickRequest.get(uri"http://httpbin.org/ip").send() ``` @@ -102,7 +102,7 @@ quickRequest.get(uri"http://httpbin.org/ip").send() Add the following dependency: ```scala -"com.softwaremill.sttp.client4" %% "core" % "4.0.0-M22" +"com.softwaremill.sttp.client4" %% "core" % "4.0.0-M23" ``` Then, import: @@ -160,7 +160,7 @@ The documentation is typechecked using [mdoc](https://scalameta.org/mdoc/). The When generating documentation, it's best to set the version to the current one, so that the generated doc files don't include modifications with the current snapshot version. -That is, in sbt run: `set ThisBuild/version := "4.0.0-M22"`, before running `mdoc` in `docs`. +That is, in sbt run: `set ThisBuild/version := "4.0.0-M23"`, before running `mdoc` in `docs`. ### Testing the Scala.JS backend diff --git a/generated-docs/out/backends/akka.md b/generated-docs/out/backends/akka.md index 8e926a2544..0d01d7d4e4 100644 --- a/generated-docs/out/backends/akka.md +++ b/generated-docs/out/backends/akka.md @@ -3,7 +3,7 @@ This backend is based on [akka-http](http://doc.akka.io/docs/akka-http/current/scala/http/). To use, add the following dependency to your project: ``` -"com.softwaremill.sttp.client4" %% "akka-http-backend" % "4.0.0-M22" +"com.softwaremill.sttp.client4" %% "akka-http-backend" % "4.0.0-M23" ``` A fully **asynchronous** backend. Uses the `Future` effect to return responses. There are also [other `Future`-based backends](future.md), which don't depend on Akka. @@ -69,11 +69,11 @@ val response: Future[Response[Either[String, Source[ByteString, Any]]]] = .send(backend) ``` -The akka-http backend support both regular and streaming [websockets](../websockets.md). +The akka-http backend support both regular and streaming [websockets](../other/websockets.md). ## Testing -Apart from testing using [the stub](../testing.md), you can create a backend using any `HttpRequest => Future[HttpResponse]` function, or an akka-http `Route`. +Apart from testing using [the stub](../testing/stub.md), you can create a backend using any `HttpRequest => Future[HttpResponse]` function, or an akka-http `Route`. That way, you can "mock" a server that the backend will talk to, without starting any actual server or making any HTTP calls. diff --git a/generated-docs/out/backends/catseffect.md b/generated-docs/out/backends/catseffect.md index 5937d2ca17..94f64566cb 100644 --- a/generated-docs/out/backends/catseffect.md +++ b/generated-docs/out/backends/catseffect.md @@ -1,7 +1,7 @@ # cats-effect backend The [Cats Effect](https://github.com/typelevel/cats-effect) backend is **asynchronous**. -It can be created for any type implementing the `cats.effect.Concurrent` typeclass, such as `cats.effect.IO`. +It can be created for any type implementing the `cats.effect.kernel.Async` typeclass, such as `cats.effect.IO`. Sending a request is a non-blocking, lazily-evaluated operation and results in a wrapped response. There's a transitive dependency on `cats-effect`. @@ -14,7 +14,7 @@ Also note that the [http4s](http4s.md) backend can also be created for a type im Firstly, add the following dependency to your project: ```scala -"com.softwaremill.sttp.client4" %% "cats" % "4.0.0-M22" +"com.softwaremill.sttp.client4" %% "cats" % "4.0.0-M23" ``` Obtain a cats-effect `Resource` which creates the backend, and closes the thread pool after the resource is no longer used: @@ -82,9 +82,10 @@ Creation of the backend can be done in two basic ways: Firstly, add the following dependency to your project: ```scala -"com.softwaremill.sttp.client4" %% "armeria-backend-cats" % "4.0.0-M22" // for cats-effect 3.x -// or -"com.softwaremill.sttp.client4" %% "armeria-backend-cats-ce2" % "4.0.0-M22" // for cats-effect 2.x +// for cats-effect 3.x +"com.softwaremill.sttp.client4" %% "armeria-backend-cats" % "4.0.0-M23" +// or for cats-effect 2.x +"com.softwaremill.sttp.client4" %% "armeria-backend-cats-ce2" % "4.0.0-M23" ``` create client: @@ -126,8 +127,8 @@ val client = WebClient.builder("https://my-service.com") val backend = ArmeriaCatsBackend.usingClient[IO](client) ``` -```{eval-rst} -.. note:: A WebClient could fail to follow redirects if the WebClient is created with a base URI and a redirect location is a different URI. +```{note} +A WebClient could fail to follow redirects if the WebClient is created with a base URI and a redirect location is a different URI. ``` This backend is build on top of [Armeria](https://armeria.dev/docs/client-http). @@ -140,4 +141,4 @@ This backend doesn't support non-blocking [streaming](../requests/streaming.md). ## Websockets -The backend doesn't support [websockets](../websockets.md). +The backend doesn't support [websockets](../other/websockets.md). diff --git a/generated-docs/out/backends/finagle.md b/generated-docs/out/backends/finagle.md index b00826a66c..65fc128385 100644 --- a/generated-docs/out/backends/finagle.md +++ b/generated-docs/out/backends/finagle.md @@ -3,7 +3,7 @@ To use, add the following dependency to your project: ``` -"com.softwaremill.sttp.client4" %% "finagle-backend" % "4.0.0-M22" +"com.softwaremill.sttp.client4" %% "finagle-backend" % "4.0.0-M23" ``` Next you'll need to add an implicit value: @@ -17,4 +17,4 @@ This backend depends on [finagle](https://twitter.github.io/finagle/), and offer Please note that: -* the backend does not support non-blocking [streaming](../requests/streaming.md) or [websockets](../websockets.md). +* the backend does not support non-blocking [streaming](../requests/streaming.md) or [websockets](../other/websockets.md). diff --git a/generated-docs/out/backends/fs2.md b/generated-docs/out/backends/fs2.md index f96c6ab611..e0b220a9f8 100644 --- a/generated-docs/out/backends/fs2.md +++ b/generated-docs/out/backends/fs2.md @@ -1,6 +1,6 @@ # fs2 backend -The [fs2](https://github.com/functional-streams-for-scala/fs2) backends are **asynchronous**. They can be created for any type implementing the `cats.effect.Async` typeclass, such as `cats.effect.IO`. Sending a request is a non-blocking, lazily-evaluated operation and results in a wrapped response. There's a transitive dependency on `cats-effect`. +The [fs2](https://github.com/functional-streams-for-scala/fs2) backends are **asynchronous**. They can be created for any type implementing the `cats.effect.kernel.Async` typeclass, such as `cats.effect.IO`. Sending a request is a non-blocking, lazily-evaluated operation and results in a wrapped response. There's a transitive dependency on `cats-effect`. ## Using HttpClient @@ -12,9 +12,9 @@ Creation of the backend can be done in two basic ways: Firstly, add the following dependency to your project: ```scala -"com.softwaremill.sttp.client4" %% "fs2" % "4.0.0-M22" // for cats-effect 3.x & fs2 3.x +"com.softwaremill.sttp.client4" %% "fs2" % "4.0.0-M23" // for cats-effect 3.x & fs2 3.x // or -"com.softwaremill.sttp.client4" %% "fs2ce2" % "4.0.0-M22" // for cats-effect 2.x & fs2 2.x +"com.softwaremill.sttp.client4" %% "fs2ce2" % "4.0.0-M23" // for cats-effect 2.x & fs2 2.x ``` Obtain a cats-effect `Resource` which creates the backend, and closes the thread pool after the resource is no longer used: @@ -77,9 +77,10 @@ Host header override is supported in environments running Java 12 onwards, but i To use, add the following dependency to your project: ```scala -"com.softwaremill.sttp.client4" %% "armeria-backend-fs2" % "4.0.0-M22" // for cats-effect 3.x & fs2 3.x -// or -"com.softwaremill.sttp.client4" %% "armeria-backend-fs2" % "4.0.0-M22" // for cats-effect 2.x & fs2 2.x +// for cats-effect 3.x & fs2 3.x +"com.softwaremill.sttp.client4" %% "armeria-backend-fs2" % "4.0.0-M23" +// or for cats-effect 2.x & fs2 2.x +"com.softwaremill.sttp.client4" %% "armeria-backend-fs2" % "4.0.0-M23" ``` create client: @@ -117,8 +118,8 @@ val client = WebClient.builder("https://my-service.com") val backend = ArmeriaFs2Backend.usingClient[IO](client, dispatcher) ``` -```{eval-rst} -.. note:: A WebClient could fail to follow redirects if the WebClient is created with a base URI and a redirect location is a different URI. +```{note} +A WebClient could fail to follow redirects if the WebClient is created with a base URI and a redirect location is a different URI. ``` This backend is built on top of [Armeria](https://armeria.dev/docs/client-http). @@ -174,7 +175,7 @@ val effect = HttpClientFs2Backend.resource[IO]().use { backend => ## Websockets -The fs2 backends support both regular and streaming [websockets](../websockets.md). +The fs2 backends support both regular and streaming [websockets](../other/websockets.md). ## Server-sent events diff --git a/generated-docs/out/backends/future.md b/generated-docs/out/backends/future.md index 792b7f5c5d..326068d05c 100644 --- a/generated-docs/out/backends/future.md +++ b/generated-docs/out/backends/future.md @@ -21,7 +21,7 @@ Class Supported stream type To use, you don't need any extra dependencies, `core` is enough: ``` -"com.softwaremill.sttp.client4" %% "core" % "4.0.0-M22" +"com.softwaremill.sttp.client4" %% "core" % "4.0.0-M23" ``` You'll need the following imports: @@ -59,7 +59,7 @@ Host header override is supported in environments running Java 12 onwards, but i To use, add the following dependency to your project: ```scala -"com.softwaremill.sttp.client4" %% "okhttp-backend" % "4.0.0-M22" +"com.softwaremill.sttp.client4" %% "okhttp-backend" % "4.0.0-M23" ``` and some imports: @@ -91,7 +91,7 @@ This backend depends on [OkHttp](http://square.github.io/okhttp/) and fully supp To use, add the following dependency to your project: ``` -"com.softwaremill.sttp.client4" %% "armeria-backend" % "4.0.0-M22" +"com.softwaremill.sttp.client4" %% "armeria-backend" % "4.0.0-M23" ``` add imports: @@ -125,8 +125,8 @@ val client = WebClient.builder("https://my-service.com") val backend = ArmeriaFutureBackend.usingClient(client) ``` -```{eval-rst} -.. note:: A WebClient could fail to follow redirects if the WebClient is created with a base URI and a redirect location is a different URI. +```{note} +A WebClient could fail to follow redirects if the WebClient is created with a base URI and a redirect location is a different URI. ``` This backend is build on top of [Armeria](https://armeria.dev/docs/client-http) and doesn't support host header override. @@ -141,4 +141,4 @@ Other backends don't support non-blocking [streaming](../requests/streaming.md). ## Websockets -Some of the backends (see above) support regular and streaming [websockets](../websockets.md). +Some of the backends (see above) support regular and streaming [websockets](../other/websockets.md). diff --git a/generated-docs/out/backends/http4s.md b/generated-docs/out/backends/http4s.md index cd5570c8fb..15747074db 100644 --- a/generated-docs/out/backends/http4s.md +++ b/generated-docs/out/backends/http4s.md @@ -3,9 +3,10 @@ This backend is based on [http4s](https://http4s.org) (client) and is **asynchronous**. To use, add the following dependency to your project: ```scala -"com.softwaremill.sttp.client4" %% "http4s-backend" % "4.0.0-M22" // for cats-effect 3.x & http4s 1.0.0-Mx -// or -"com.softwaremill.sttp.client4" %% "http4s-ce2-backend" % "4.0.0-M22" // for cats-effect 2.x & http4s 0.21.x +// for cats-effect 3.x & http4s 1.0.0-Mx +"com.softwaremill.sttp.client4" %% "http4s-backend" % "4.0.0-M23" +// or for cats-effect 2.x & http4s 0.21.x +"com.softwaremill.sttp.client4" %% "http4s-ce2-backend" % "4.0.0-M23" ``` The backend can be created in a couple of ways, e.g.: @@ -37,4 +38,4 @@ Instead, all custom timeout configuration should be done by creating a `org.http The backend supports streaming using fs2. For usage details, see the documentation on [streaming using fs2](fs2.md). -The backend doesn't support [websockets](../websockets.md). +The backend doesn't support [websockets](../other/websockets.md). diff --git a/generated-docs/out/backends/javascript/fetch.md b/generated-docs/out/backends/javascript/fetch.md index 7e072771c1..d7e90ccde8 100644 --- a/generated-docs/out/backends/javascript/fetch.md +++ b/generated-docs/out/backends/javascript/fetch.md @@ -1,4 +1,4 @@ -# JavaScript (Fetch) backend +# Scala.js (Fetch) backend A JavaScript backend with web socket support. Implemented using the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). @@ -7,7 +7,7 @@ A JavaScript backend with web socket support. Implemented using the [Fetch API]( This is the default backend, available in the main jar for JS. To use, add the following dependency to your project: ``` -"com.softwaremill.sttp.client4" %%% "core" % "4.0.0-M22" +"com.softwaremill.sttp.client4" %%% "core" % "4.0.0-M23" ``` And create the backend instance: @@ -26,7 +26,7 @@ Note that `Fetch` does not pass cookies by default. If your request needs cookie To use, add the following dependency to your project: ``` -"com.softwaremill.sttp.client4" %%% "monix" % "4.0.0-M22" +"com.softwaremill.sttp.client4" %%% "monix" % "4.0.0-M23" ``` And create the backend instance: @@ -40,7 +40,7 @@ val backend = FetchMonixBackend() To use, add the following dependency to your project: ``` -"com.softwaremill.sttp.client4" %%% "zio" % "4.0.0-M22" +"com.softwaremill.sttp.client4" %%% "zio" % "4.0.0-M23" ``` And create the backend instance: @@ -55,13 +55,13 @@ Any effect implementing the cats-effect `Concurrent` typeclass can be used. To u your project: ``` -"com.softwaremill.sttp.client4" %%% "cats" % "4.0.0-M22" +"com.softwaremill.sttp.client4" %%% "cats" % "4.0.0-M23" ``` If you are on Cats Effect 2 (CE2) you will need to add the CE2 specific dependency instead: ``` -"com.softwaremill.sttp.client4" %%% "catsce2 % "4.0.0-M22" +"com.softwaremill.sttp.client4" %%% "catsce2" % "4.0.0-M23" ``` And create the backend instance: @@ -129,7 +129,7 @@ Streaming support is provided via `FetchMonixBackend`. Note that streaming suppo To use, add the following dependency to your project: ``` -"com.softwaremill.sttp.client4" %%% "monix" % "4.0.0-M22" +"com.softwaremill.sttp.client4" %%% "monix" % "4.0.0-M23" ``` An example of streaming a response: @@ -151,13 +151,13 @@ val response: Task[Response[Observable[ByteBuffer]]] = .send(backend) ``` -```{eval-rst} -.. note:: Currently no browsers support passing a stream as the request body. As such, using the ``Fetch`` backend with a streaming request will result in it being converted into an in-memory array before being sent. Response bodies are returned as a "proper" stream. +```{note} +Currently no browsers support passing a stream as the request body. As such, using the `Fetch` backend with a streaming request will result in it being converted into an in-memory array before being sent. Response bodies are returned as a "proper" stream. ``` ## Websockets -The backend supports both regular and streaming [websockets](../../websockets.md). +The backend supports both regular and streaming [websockets](../../other/websockets.md). ## Server-sent events diff --git a/generated-docs/out/backends/monix.md b/generated-docs/out/backends/monix.md index 66cd8d445c..7eb6489b7c 100644 --- a/generated-docs/out/backends/monix.md +++ b/generated-docs/out/backends/monix.md @@ -12,7 +12,7 @@ Creation of the backend can be done in two basic ways: Firstly, add the following dependency to your project: ``` -"com.softwaremill.sttp.client4" %% "monix" % "4.0.0-M22" +"com.softwaremill.sttp.client4" %% "monix" % "4.0.0-M23" ``` and create the backend using: @@ -49,7 +49,7 @@ Host header override is supported in environments running Java 12 onwards, but i To use, add the following dependency to your project: ```scala -"com.softwaremill.sttp.client4" %% "okhttp-backend-monix" % "4.0.0-M22" +"com.softwaremill.sttp.client4" %% "okhttp-backend-monix" % "4.0.0-M23" ``` Create the backend using: @@ -75,7 +75,7 @@ This backend depends on [OkHttp](http://square.github.io/okhttp/) and fully supp To use, add the following dependency to your project: ``` -"com.softwaremill.sttp.client4" %% "armeria-backend-monix" % "4.0.0-M22" +"com.softwaremill.sttp.client4" %% "armeria-backend-monix" % "4.0.0-M23" ``` add imports: @@ -110,8 +110,8 @@ val client = WebClient.builder("https://my-service.com") val backend = ArmeriaMonixBackend.usingClient(client) ``` -```{eval-rst} -.. note:: A WebClient could fail to follow redirects if the WebClient is created with a base URI and a redirect location is a different URI. +```{note} +A WebClient could fail to follow redirects if the WebClient is created with a base URI and a redirect location is a different URI. ``` This backend is build on top of [Armeria](https://armeria.dev/docs/client-http). @@ -163,7 +163,7 @@ HttpClientMonixBackend().flatMap { backend => ## Websockets -The Monix backend supports both regular and streaming [websockets](../websockets.md). +The Monix backend supports both regular and streaming [websockets](../other/websockets.md). ## Server-sent events diff --git a/generated-docs/out/backends/native/curl.md b/generated-docs/out/backends/native/curl.md index c9eedf6355..cd53598abd 100644 --- a/generated-docs/out/backends/native/curl.md +++ b/generated-docs/out/backends/native/curl.md @@ -1,19 +1,40 @@ -# Curl backend +# Scala Native (curl) backend -A Scala Native backend implemented using [Curl](https://github.com/curl/curl/blob/master/include/curl/curl.h). +A Scala Native (0.5.x) backend implemented using [Curl](https://github.com/curl/curl/blob/master/include/curl/curl.h). To use, add the following dependency to your project: ``` -"com.softwaremill.sttp.client4" %%% "core" % "4.0.0-M22" +"com.softwaremill.sttp.client4" %%% "core" % "4.0.0-M23" ``` and initialize one of the backends: ```scala +import sttp.client4.curl.* + val backend = CurlBackend() val tryBackend = CurlTryBackend() ``` You need to have an environment with Scala Native [setup](https://scala-native.readthedocs.io/en/latest/user/setup.html) with additionally installed `libcrypto` (included in OpenSSL) and `curl` in version `7.56.0` or newer. + +## scala-cli example + +Try the following example: + +```scala +// hello.scala + +//> using platform native +//> using dep com.softwaremill.sttp.client4::core_native0.5:4.0.0-M23 + +import sttp.client4.* +import sttp.client4.curl.CurlBackend + +@main def run(): Unit = + val backend = CurlBackend() + println(basicRequest.get(uri"http://httpbin.org/ip").send(backend)) +``` + diff --git a/generated-docs/out/backends/pekko.md b/generated-docs/out/backends/pekko.md index 95e981fca1..6d649f357b 100644 --- a/generated-docs/out/backends/pekko.md +++ b/generated-docs/out/backends/pekko.md @@ -3,7 +3,7 @@ This backend is based on [pekko-http](https://pekko.apache.org/docs/pekko-http/current/). To use, add the following dependency to your project: ``` -"com.softwaremill.sttp.client4" %% "pekko-http-backend" % "4.0.0-M22" +"com.softwaremill.sttp.client4" %% "pekko-http-backend" % "4.0.0-M23" ``` A fully **asynchronous** backend. Uses the `Future` effect to return responses. There are also [other `Future`-based backends](future.md), which don't depend on Pekko. @@ -11,7 +11,7 @@ A fully **asynchronous** backend. Uses the `Future` effect to return responses. Note that you'll also need an explicit dependency on pekko-streams, as pekko-http doesn't depend on any specific pekko-streams version. So you'll also need to add, for example: ``` -"org.apache.pekko" %% "pekko-stream" % "1.1.2" +"org.apache.pekko" %% "pekko-stream" % "1.1.3" ``` Next you'll need to add create the backend instance: @@ -71,11 +71,11 @@ val response: Future[Response[Either[String, Source[ByteString, Any]]]] = .send(backend) ``` -The pekko-http backend support both regular and streaming [websockets](../websockets.md). +The pekko-http backend support both regular and streaming [websockets](../other/websockets.md). ## Testing -Apart from testing using [the stub](../testing.md), you can create a backend using any `HttpRequest => Future[HttpResponse]` function, or an pekko-http `Route`. +Apart from testing using [the stub](../testing/stub.md), you can create a backend using any `HttpRequest => Future[HttpResponse]` function, or an pekko-http `Route`. That way, you can "mock" a server that the backend will talk to, without starting any actual server or making any HTTP calls. diff --git a/generated-docs/out/backends/scalaz.md b/generated-docs/out/backends/scalaz.md index 5e18c0e370..d253f0a3b3 100644 --- a/generated-docs/out/backends/scalaz.md +++ b/generated-docs/out/backends/scalaz.md @@ -8,7 +8,7 @@ The [Scalaz](https://github.com/scalaz/scalaz) backend is **asynchronous**. Send To use, add the following dependency to your project: ``` -"com.softwaremill.sttp.client4" %% "armeria-backend-scalaz" % "4.0.0-M22" +"com.softwaremill.sttp.client4" %% "armeria-backend-scalaz" % "4.0.0-M23" ``` add imports: @@ -42,8 +42,8 @@ val client = WebClient.builder("https://my-service.com") val backend = ArmeriaScalazBackend.usingClient(client) ``` -```{eval-rst} -.. note:: A WebClient could fail to follow redirects if the WebClient is created with a base URI and a redirect location is a different URI. +```{note} +A WebClient could fail to follow redirects if the WebClient is created with a base URI and a redirect location is a different URI. ``` This backend is build on top of [Armeria](https://armeria.dev/docs/client-http). @@ -56,4 +56,4 @@ This backend doesn't support non-blocking [streaming](../requests/streaming.md). ## Websockets -The backend doesn't support [websockets](../websockets.md). +The backend doesn't support [websockets](../other/websockets.md). diff --git a/generated-docs/out/backends/start_stop.md b/generated-docs/out/backends/start_stop.md index cf604343f0..e8de0c7faa 100644 --- a/generated-docs/out/backends/start_stop.md +++ b/generated-docs/out/backends/start_stop.md @@ -4,4 +4,4 @@ In case of most backends, you should only instantiate a backend once per applica When ending the application, make sure to call `backend.close()`, which results in an effect which frees up resources used by the backend (if any). If the effect wrapper for the backend is lazily evaluated, make sure to include it when composing effects! -Note that only resources allocated by the backends are freed. For example, if you use the `AkkaHttpBackend()` the `close()` method will terminate the underlying actor system. However, if you have provided an existing actor system upon backend creation (`AkkaHttpBackend.usingActorSystem`), the `close()` method will be a no-op. +Note that only resources allocated by the backends are freed. For example, if you use the `PekkoHttpBackend()` the `close()` method will terminate the underlying actor system. However, if you have provided an existing actor system upon backend creation (`PekkoHttpBackend.usingActorSystem`), the `close()` method will be a no-op. diff --git a/generated-docs/out/backends/summary.md b/generated-docs/out/backends/summary.md index 93d1fcdba5..4c5b1fc22f 100644 --- a/generated-docs/out/backends/summary.md +++ b/generated-docs/out/backends/summary.md @@ -14,7 +14,7 @@ As a starting point, the default backends are good choice. Depending on the plat These default backends provide limited customisation options, hence for any more advanced use-cases, simply substitute them with a specific implementation. E.g. the `HttpClientSyncBackend` backend, which is the underlying implementation of `DefaultSyncBackend`, offers customisation options not available in the default one. -## Backends overview +## JVM backends Which one to choose? @@ -61,6 +61,8 @@ Backends supporting cats-effect are available in versions for cats-effect 2.x (d All backends that support asynchronous/non-blocking streams, also support server-sent events. +## Backend wrappers + There are also backends which wrap other backends to provide additional functionality. These include: * `TryBackend`, which safely wraps any exceptions thrown by a synchronous backend in `scala.util.Try` @@ -72,6 +74,9 @@ There are also backends which wrap other backends to provide additional function * `ResolveRelativeUrisBackend` to resolve relative URIs given a base URI, or an arbitrary effectful function * `ListenerBackend` to listen for backend lifecycle events. See the [dedicated section](wrappers/custom.md). * `FollowRedirectsBackend`, which handles redirects. All implementation backends are created wrapped with this one. +* `CachingBackend`, which caches responses. See the [dedicated section](wrappers/caching.md). + +## Scala.JS backends In addition, there are also backends for Scala.JS: @@ -87,6 +92,8 @@ Class Effect type Supported stre ================================ ================================ ========================================= =================== ``` +## Scala Native backends + And a backend for scala-native: ```{eval-rst} diff --git a/generated-docs/out/backends/synchronous.md b/generated-docs/out/backends/synchronous.md index e388982f26..e3d594a7da 100644 --- a/generated-docs/out/backends/synchronous.md +++ b/generated-docs/out/backends/synchronous.md @@ -7,7 +7,7 @@ There are several synchronous backend implementations. Sending a request using t The default **synchronous** backend. To use, you don't need any extra dependencies, `core` is enough: ``` -"com.softwaremill.sttp.client4" %% "core" % "4.0.0-M22" +"com.softwaremill.sttp.client4" %% "core" % "4.0.0-M23" ``` Create the backend using: @@ -40,7 +40,7 @@ Host header override is supported in environments running Java 12 onwards, but i To use, you don't need any extra dependencies, `core` is enough: ``` -"com.softwaremill.sttp.client4" %% "core" % "4.0.0-M22" +"com.softwaremill.sttp.client4" %% "core" % "4.0.0-M23" ``` Create the backend using: @@ -62,7 +62,7 @@ This backend supports host header override, but it has to be enabled by system p To use, add the following dependency to your project: ``` -"com.softwaremill.sttp.client4" %% "okhttp-backend" % "4.0.0-M22" +"com.softwaremill.sttp.client4" %% "okhttp-backend" % "4.0.0-M23" ``` Create the backend using: @@ -91,7 +91,7 @@ Synchronous backends don't support non-blocking [streaming](../requests/streamin ## Websockets -Both HttpClient and OkHttp backends support regular [websockets](../websockets.md). +Both HttpClient and OkHttp backends support regular [websockets](../other/websockets.md). ## Server-sent events @@ -99,26 +99,20 @@ Both HttpClient and OkHttp backends support regular [websockets](../websockets.m ``` // sbt dependency -"com.softwaremill.sttp.client4" %% "ox" % "4.0.0-M22", +"com.softwaremill.sttp.client4" %% "ox" % "4.0.0-M23", ``` ```scala -import ox.* -import ox.channels.Source import sttp.client4.* import sttp.client4.impl.ox.sse.OxServerSentEvents -import sttp.model.sse.ServerSentEvent import java.io.InputStream -def handleSse(is: InputStream)(using IO): Unit = - supervised { - OxServerSentEvents.parse(is).foreach(event => println(s"Received event: $event")) - } +def handleSse(is: InputStream): Unit = + OxServerSentEvents.parse(is).foreach(event => println(s"Received event: $event")) val backend = DefaultSyncBackend() -IO.unsafe: basicRequest .get(uri"https://postman-echo.com/server-events/3") - .response(asInputStreamAlways(handleSse)) - .send(backend) + .response(asInputStreamAlways(handleSse)) + .send(backend) ``` diff --git a/generated-docs/out/backends/wrappers/cache.md b/generated-docs/out/backends/wrappers/cache.md new file mode 100644 index 0000000000..348e87ed91 --- /dev/null +++ b/generated-docs/out/backends/wrappers/cache.md @@ -0,0 +1,38 @@ +# Caching backend + +To use the caching backend, add the following dependency: + +``` +"com.softwaremill.sttp.client4" %% "caching-backend" % "4.0.0-M23" +``` + +The backend caches responses to eligible requests, and returns them from the cache if a repeated request is made. A prerequisite for a request to be considered for caching is that its response-as description is "cache-friendly"; this excludes non-blocking streaming responses, file-based responses and WebSockets. + +An implementation of a `Cache` trait is required when creating the backend. The `Cache` allows storing cached values (with a TTL), and retrieving them. + +The cache is highly configurable, including: +* determining if a request is eligible for caching (before it is sent) +* computing the cache key +* computing the caching duration (basing on the response) +* serialization and deserialization of the response + +To use, wrap your backend (the below uses default configuration): + +```scala +import sttp.client4.caching.CachingBackend + +CachingBackend(delegateBackend, myCacheImplementation) +``` + +## Default configuration + +Using `CachingConfig.Default`, caching happens if: + +* the request is a `GET` or `HEAD` request +* the response contains a `Cache-Control` header with a `max-age` directive (standard HTTP semantics); the response is cached for the duration specified in this directive + +The cache key is created using the request method, URI, and the values of headers specified in the `Vary` header. + +For requests which might be cached, the response's body is read into a byte array. If the response is determined to be cached, it is serialized to JSON (using jsoniter-scala) and stored in the cache. + +See [examples](../../examples.md) for an example usage of the caching backend, using Redis. diff --git a/generated-docs/out/backends/wrappers/custom.md b/generated-docs/out/backends/wrappers/custom.md index 633aa63f84..b79d015054 100644 --- a/generated-docs/out/backends/wrappers/custom.md +++ b/generated-docs/out/backends/wrappers/custom.md @@ -8,7 +8,7 @@ Possible use-cases for wrapper-backend include: * capturing metrics * request signing (transforming the request before sending it to the delegate) -See also the section on [resilience](../../resilience.md) which covers topics such as retries, circuit breaking and rate limiting. +See also the section on [resilience](../../other/resilience.md) which covers topics such as retries, circuit breaking and rate limiting. ## Request attributes @@ -35,209 +35,17 @@ See the appropriate section in docs on [redirects](../../conf/redirects.md). A good example on how to implement a logging backend wrapper is the [logging](logging.md) backend wrapper implementation. It uses the `ListenerBackend` to get notified about request lifecycle events. To adjust the logs to your needs, or to integrate with your logging framework, simply copy the code and modify as needed. + +## Examples backend wrappers -## Example metrics backend wrapper - -Below is an example on how to implement a backend wrapper, which sends -metrics for completed requests and wraps any `Future`-based backend: - -```scala -import sttp.attributes.AttributeKey -import sttp.capabilities.Effect -import sttp.client4.* -import sttp.client4.pekkohttp.* -import sttp.client4.wrappers.DelegateBackend -import scala.concurrent.Future -import scala.concurrent.ExecutionContext.Implicits.global -import scala.util.* - -// the metrics infrastructure -trait MetricsServer: - def reportDuration(name: String, duration: Long): Unit - -class CloudMetricsServer extends MetricsServer: - override def reportDuration(name: String, duration: Long): Unit = ??? - -case class MetricPrefix(prefix: String) -val MetricPrefixAttributeKey = AttributeKey[MetricPrefix] - -// the backend wrapper -abstract class MetricWrapper[P](delegate: GenericBackend[Future, P], - metrics: MetricsServer) - extends DelegateBackend(delegate): - - override def send[T](request: GenericRequest[T, P with Effect[Future]]): Future[Response[T]] = - val start = System.currentTimeMillis() - - def report(metricSuffix: String): Unit = - val metricPrefix = request.attribute(MetricPrefixAttributeKey).getOrElse(MetricPrefix("?")) - val end = System.currentTimeMillis() - metrics.reportDuration(metricPrefix.prefix + "-" + metricSuffix, end - start) - - delegate.send(request).andThen: - case Success(response) if response.is200 => report("ok") - case Success(response) => report("notok") - case Failure(t) => report("exception") - -object MetricWrapper: - def apply[S]( - backend: WebSocketStreamBackend[Future, S], - metrics: MetricsServer - ): WebSocketStreamBackend[Future, S] = - new MetricWrapper(backend, metrics) with WebSocketStreamBackend[Future, S] {} - -// example usage -val backend = MetricWrapper(PekkoHttpBackend(), new CloudMetricsServer()) - -basicRequest - .get(uri"http://company.com/api/service1") - .attribute(MetricPrefixAttributeKey, MetricPrefix("service1")) - .send(backend) -``` - -See also the [Prometheus](prometheus.md) backend for an example implementation. - -## Example retrying backend wrapper - -Handling retries is a complex problem when it comes to HTTP requests. When is a request retryable? There are a couple of things to take into account: - -* connection exceptions are generally good candidates for retries -* only idempotent HTTP methods (such as `GET`) could potentially be retried -* some HTTP status codes might also be retryable (e.g. `500 Internal Server Error` or `503 Service Unavailable`) - -In some cases it's possible to implement a generic retry mechanism; such a mechanism should take into account logging, metrics, limiting the number of retries and a backoff mechanism. These mechanisms could be quite simple, or involve e.g. retry budgets (see [Finagle's](https://twitter.github.io/finagle/guide/Clients.html#retries) documentation on retries). In sttp, it's possible to recover from errors using the `monad`. A starting point for a retrying backend could be: - -```scala -import sttp.capabilities.Effect -import sttp.client4.* -import sttp.client4.wrappers.DelegateBackend - -class RetryingBackend[F[_], P]( - delegate: GenericBackend[F, P], - shouldRetry: RetryWhen, - maxRetries: Int) - extends DelegateBackend(delegate): - override def send[T](request: GenericRequest[T, P with Effect[F]]): F[Response[T]] = - sendWithRetryCounter(request, 0) - - private def sendWithRetryCounter[T]( - request: GenericRequest[T, P with Effect[F]], retries: Int): F[Response[T]] = - - val r = monad.handleError(delegate.send(request)): - case t if shouldRetry(request, Left(t)) && retries < maxRetries => - sendWithRetryCounter(request, retries + 1) - - monad.flatMap(r): resp => - if shouldRetry(request, Right(resp)) && retries < maxRetries then - sendWithRetryCounter(request, retries + 1) - else - monad.unit(resp) - -object RetryingBackend: - def apply[F[_]](backend: WebSocketBackend[F], shouldRetry: RetryWhen, maxRetries: Int): WebSocketBackend[F] = - new RetryingBackend(backend, shouldRetry, maxRetries) with WebSocketBackend[F] {} -``` - -## Example backend with circuit breaker - -> "When a system is seriously struggling, failing fast is better than making clients wait." - -There are many libraries that can help you achieve such a behavior: [hystrix](https://github.com/Netflix/Hystrix), [resilience4j](https://github.com/resilience4j/resilience4j), [akka's circuit breaker](https://doc.akka.io/docs/akka/current/common/circuitbreaker.html) or [monix catnap](https://monix.io/docs/3x/catnap/circuit-breaker.html) to name a few. Despite some small differences, both their apis and functionality are very similar, that's why we didn't want to support each of them explicitly. - -Below is an example on how to implement a backend wrapper, which integrates with circuit-breaker module from resilience4j library and wraps any backend: - -```scala -import io.github.resilience4j.circuitbreaker.{CallNotPermittedException, CircuitBreaker} -import sttp.capabilities.Effect -import sttp.client4.{GenericBackend, GenericRequest, Backend, Response} -import sttp.client4.wrappers.DelegateBackend -import sttp.monad.MonadError -import java.util.concurrent.TimeUnit - -class CircuitSttpBackend[F[_], P]( - circuitBreaker: CircuitBreaker, - delegate: GenericBackend[F, P]) extends DelegateBackend(delegate): - - override def send[T](request: GenericRequest[T, P with Effect[F]]): F[Response[T]] = - CircuitSttpBackend.decorateF(circuitBreaker, delegate.send(request)) - -object CircuitSttpBackend: - - def apply[F[_]](circuitBreaker: CircuitBreaker, backend: Backend[F]): Backend[F] = - new CircuitSttpBackend(circuitBreaker, backend) with Backend[F] {} - - def decorateF[F[_], T]( - circuitBreaker: CircuitBreaker, - service: => F[T] - )(implicit monadError: MonadError[F]): F[T] = - monadError.suspend: - if !circuitBreaker.tryAcquirePermission() then - monadError.error(CallNotPermittedException - .createCallNotPermittedException(circuitBreaker)) - else - val start = System.nanoTime() - try - monadError.handleError(monadError.map(service): r => - circuitBreaker.onSuccess(System.nanoTime() - start, TimeUnit.NANOSECONDS) - r - ) { - case t => - circuitBreaker.onError(System.nanoTime() - start, TimeUnit.NANOSECONDS, t) - monadError.error(t) - } - catch - case t: Throwable => - circuitBreaker.onError(System.nanoTime() - start, TimeUnit.NANOSECONDS, t) - monadError.error(t) -``` - -## Example backend with rate limiter - -> "Prepare for a scale and establish reliability and HA of your service." - -Below is an example on how to implement a backend wrapper, which integrates with rate-limiter module from resilience4j library and wraps any backend: - -```scala -import io.github.resilience4j.ratelimiter.RateLimiter -import sttp.capabilities.Effect -import sttp.monad.MonadError -import sttp.client4.{GenericBackend, GenericRequest, Response, StreamBackend} -import sttp.client4.wrappers.DelegateBackend - -class RateLimitingSttpBackend[F[_], P]( - rateLimiter: RateLimiter, - delegate: GenericBackend[F, P] - )(implicit monadError: MonadError[F]) extends DelegateBackend(delegate): - - override def send[T](request: GenericRequest[T, P with Effect[F]]): F[Response[T]] = - RateLimitingSttpBackend.decorateF(rateLimiter, delegate.send(request)) - -object RateLimitingSttpBackend: - def apply[F[_], S]( - rateLimiter: RateLimiter, - backend: StreamBackend[F, S] - )(implicit monadError: MonadError[F]): StreamBackend[F, S] = - new RateLimitingSttpBackend(rateLimiter, backend) with StreamBackend[F, S] {} - - def decorateF[F[_], T]( - rateLimiter: RateLimiter, - service: => F[T] - )(implicit monadError: MonadError[F]): F[T] = - monadError.suspend: - try - RateLimiter.waitForPermission(rateLimiter) - service - catch - case t: Throwable => - monadError.error(t) -``` +A number of example backend wrappers can be found in [examples](../../examples.md). ## Example new backend Implementing a new backend is made easy as the tests are published in the `core` jar file under the `tests` classifier. Simply add the follow dependencies to your `build.sbt`: ``` -"com.softwaremill.sttp.client4" %% "core" % "4.0.0-M22" % Test classifier "tests" +"com.softwaremill.sttp.client4" %% "core" % "4.0.0-M23" % Test classifier "tests" ``` Implement your backend and extend the `HttpTest` class: @@ -258,15 +66,15 @@ class MyCustomBackendHttpTest extends HttpTest[Future]: When implementing a backend wrapper using cats, it might be useful to import: ```scala -import sttp.client4.impl.cats.implicits._ +import sttp.client4.impl.cats.implicits.* ``` from the cats integration module. The module should be available on the classpath after adding following dependency: ```scala -"com.softwaremill.sttp.client4" %% "cats" % "4.0.0-M22" // for cats-effect 3.x +"com.softwaremill.sttp.client4" %% "cats" % "4.0.0-M23" // for cats-effect 3.x // or -"com.softwaremill.sttp.client4" %% "catsce2" % "4.0.0-M22" // for cats-effect 2.x +"com.softwaremill.sttp.client4" %% "catsce2" % "4.0.0-M23" // for cats-effect 2.x ``` The object contains implicits to convert a cats `MonadError` into the sttp `MonadError`, diff --git a/generated-docs/out/backends/wrappers/logging.md b/generated-docs/out/backends/wrappers/logging.md index 25d79c4cdd..6e84ebba1d 100644 --- a/generated-docs/out/backends/wrappers/logging.md +++ b/generated-docs/out/backends/wrappers/logging.md @@ -28,7 +28,7 @@ Log levels can be configured when creating the `LoggingBackend`, or specified in To use the [slf4j](http://www.slf4j.org) logging backend wrapper, add the following dependency to your project: ``` -"com.softwaremill.sttp.client4" %% "slf4j-backend" % "4.0.0-M22" +"com.softwaremill.sttp.client4" %% "slf4j-backend" % "4.0.0-M23" ``` There are three backend wrappers available, which log request & response information using a slf4j `Logger`. To see the logs, you'll need to use an slf4j-compatible logger implementation, e.g. [logback](http://logback.qos.ch), or use a binding, e.g. [log4j-slf4j](https://logging.apache.org/log4j/2.x/log4j-slf4j-impl.html). @@ -53,5 +53,5 @@ To create a customised logging backend, see the section on [custom backends](cus To use the [scribe](https://github.com/outr/scribe) logging backend wrapper, add the following dependency to your project: ``` -"com.softwaremill.sttp.client4" %% "scribe-backend" % "4.0.0-M22" +"com.softwaremill.sttp.client4" %% "scribe-backend" % "4.0.0-M23" ``` \ No newline at end of file diff --git a/generated-docs/out/backends/wrappers/opentelemetry.md b/generated-docs/out/backends/wrappers/opentelemetry.md index f5e1aab66d..ad8a5aae36 100644 --- a/generated-docs/out/backends/wrappers/opentelemetry.md +++ b/generated-docs/out/backends/wrappers/opentelemetry.md @@ -12,7 +12,7 @@ The backend depends only on [opentelemetry-api](https://github.com/open-telemetr following dependency to your project: ``` -"com.softwaremill.sttp.client4" %% "opentelemetry-metrics-backend" % "4.0.0-M22" +"com.softwaremill.sttp.client4" %% "opentelemetry-metrics-backend" % "4.0.0-M23" ``` Then an instance can be obtained as follows: @@ -55,7 +55,7 @@ OpenTelemetryMetricsBackend( To use, add the following dependency to your project: ``` -"com.softwaremill.sttp.client4" %% "opentelemetry-tracing-zio-backend" % "4.0.0-M22" // for ZIO 2.x +"com.softwaremill.sttp.client4" %% "opentelemetry-tracing-zio-backend" % "4.0.0-M23" // for ZIO 2.x ``` This backend depends on [zio-opentelemetry](https://github.com/zio/zio-telemetry). diff --git a/generated-docs/out/backends/wrappers/prometheus.md b/generated-docs/out/backends/wrappers/prometheus.md index 7c67fc1c86..1a95cb5875 100644 --- a/generated-docs/out/backends/wrappers/prometheus.md +++ b/generated-docs/out/backends/wrappers/prometheus.md @@ -3,7 +3,7 @@ To use, add the following dependency to your project: ``` -"com.softwaremill.sttp.client4" %% "prometheus-backend" % "4.0.0-M22" +"com.softwaremill.sttp.client4" %% "prometheus-backend" % "4.0.0-M23" ``` and some imports: diff --git a/generated-docs/out/backends/zio.md b/generated-docs/out/backends/zio.md index d7aa3197d0..63469b06b2 100644 --- a/generated-docs/out/backends/zio.md +++ b/generated-docs/out/backends/zio.md @@ -9,8 +9,8 @@ The `*-zio` modules depend on ZIO 2.x. For ZIO 1.x support, use modules with the To use, add the following dependency to your project: ``` -"com.softwaremill.sttp.client4" %% "zio" % "4.0.0-M22" // for ZIO 2.x -"com.softwaremill.sttp.client4" %% "zio1" % "4.0.0-M22" // for ZIO 1.x +"com.softwaremill.sttp.client4" %% "zio" % "4.0.0-M23" // for ZIO 2.x +"com.softwaremill.sttp.client4" %% "zio1" % "4.0.0-M23" // for ZIO 1.x ``` Create the backend using: @@ -45,8 +45,8 @@ Host header override is supported in environments running Java 12 onwards, but i To use, add the following dependency to your project: ``` -"com.softwaremill.sttp.client4" %% "armeria-backend-zio" % "4.0.0-M22" // for ZIO 2.x -"com.softwaremill.sttp.client4" %% "armeria-backend-zio1" % "4.0.0-M22" // for ZIO 1.x +"com.softwaremill.sttp.client4" %% "armeria-backend-zio" % "4.0.0-M23" // for ZIO 2.x +"com.softwaremill.sttp.client4" %% "armeria-backend-zio1" % "4.0.0-M23" // for ZIO 1.x ``` add imports: @@ -67,8 +67,8 @@ ArmeriaZioBackend.scoped().flatMap { backend => ??? } ArmeriaZioBackend.usingDefaultClient().flatMap { backend => ??? } ``` -```{eval-rst} -.. note:: The default client factory is reused to create `ArmeriaZioBackend` if a `SttpBackendOptions` is unspecified. So you only need to manage a resource when `SttpBackendOptions` is used. +```{note} +The default client factory is reused to create `ArmeriaZioBackend` if a `SttpBackendOptions` is unspecified. So you only need to manage a resource when `SttpBackendOptions` is used. ``` or, if you'd like to instantiate the [WebClient](https://armeria.dev/docs/client-http) yourself: @@ -87,8 +87,8 @@ val client = WebClient.builder("https://my-service.com") ArmeriaZioBackend.usingClient(client).flatMap { backend => ??? } ``` -```{eval-rst} -.. note:: A WebClient could fail to follow redirects if the WebClient is created with a base URI and a redirect location is a different URI. +```{note} +A WebClient could fail to follow redirects if the WebClient is created with a base URI and a redirect location is a different URI. ``` This backend is build on top of [Armeria](https://armeria.dev/docs/client-http). @@ -200,14 +200,14 @@ val response: ZIO[Any, Throwable, Response[Either[String, Stream[Throwable, Byte ## Websockets -The `HttpClient` ZIO backend supports both regular and streaming [websockets](../websockets.md). +The `HttpClient` ZIO backend supports both regular and streaming [websockets](../other/websockets.md). ## Testing A stub backend can be created through the `.stub` method on the companion object, and configured as described in the -[testing](../testing.md) section. +[testing](../testing/stub.md) section. -A layer with the stub `SttpBackend` can be then created by simply calling `ZLayer.succeed(sttpBackendStub)`. +A layer with the stub `Backend` can be then created by simply calling `ZLayer.succeed(backendStub)`. ## Server-sent events diff --git a/generated-docs/out/conf.py b/generated-docs/out/conf.py index d59d989412..4b0be4c934 100644 --- a/generated-docs/out/conf.py +++ b/generated-docs/out/conf.py @@ -63,7 +63,7 @@ # General information about the project. project = u'sttp' -copyright = u'2024, SoftwareMill' +copyright = u'2025, SoftwareMill' author = u'SoftwareMill' # The version info for the project you're documenting, acts as replacement for diff --git a/generated-docs/out/examples.md b/generated-docs/out/examples.md index bede5439bc..84dda775f8 100644 --- a/generated-docs/out/examples.md +++ b/generated-docs/out/examples.md @@ -7,7 +7,7 @@ Each example is fully self-contained and can be run using [scala-cli](https://sc to copy the content of the file, apart from scala-cli, no additional setup is required!). Hopefully this will make experimenting with sttp client as frictionless as possible! -Examples are tagged with the stack being used (Direct-style, cats-effect, ZIO, Future) and backend implementation +Examples are tagged with the stack being used (direct-style, cats-effect, ZIO, Future) and backend implementation ```{eval-rst} .. include:: includes/examples_list.md diff --git a/generated-docs/out/goals.md b/generated-docs/out/goals.md index 2c056838dd..0d0fc8d394 100644 --- a/generated-docs/out/goals.md +++ b/generated-docs/out/goals.md @@ -24,6 +24,7 @@ See also the blog posts: ## How is sttp different from other libraries? * immutable request builder which doesn't impose any order in which request parameters need to be specified. Such an approach allows defining partial requests with common cookies/headers/options, which can later be specialized using a specific URI and HTTP method. +* integration with various Scala programming stacks and libraries * support for multiple backends, both synchronous and asynchronous, with backend-specific streaming support * URI interpolator with context-aware escaping, optional parameters support and parameter collections -* description of how to handle the response is combined with the description of the request to send +* description of how to handle the response is combined with the description of the request to send \ No newline at end of file diff --git a/generated-docs/out/how.md b/generated-docs/out/how.md index 47c33af485..02dfa8364c 100644 --- a/generated-docs/out/how.md +++ b/generated-docs/out/how.md @@ -4,9 +4,9 @@ This first step when using sttp client is describing the request that you'd like to send. -A request is represented as an immutable data structure of type `RequestT` (as in Request Template). The basic request is provided as the `basicRequest` value, in the `sttp.client4` package. It can be refined using one of the available methods, such as `.header`, `.body`, `.get(Uri)`, `.responseAs`, etc. +A request is represented as an immutable data structure of type `Request`. The basic request is provided as the `basicRequest` value, in the `sttp.client4` package. It can be refined using one of the available methods, such as `.header`, `.body`, `.get(Uri)`, `.responseAs`, etc. -A `RequestT` value contains both information on what to include in the request, but also how to handle the response body. +A `Request` value contains both information on what to include in the request, but also how to handle the response body. To start describing a request, import the sttp client package and customise `basicRequest`: @@ -15,17 +15,17 @@ import sttp.client4.* val myRequest: Request[_] = ??? // basicRequest.(...) ``` -An alternative to importing the `sttp.client4._` package, is to extend the `sttp.client4.SttpApi` trait. That way, multiple integrations can be grouped in one object, thus reducing the number of necessary imports. +An alternative to importing the `sttp.client4.*` package, is to extend the `sttp.client4.SttpApi` trait. That way, multiple integrations can be grouped in one object, thus reducing the number of necessary imports. ## Send the request -Once the request is described as a value, it can be sent. To send a request, you'll need an `SttpBackend`. +Once the request is described as a value, it can be sent. To send a request, you'll need a `Backend`. The backend is where most of the work happens: the request is translated to a backend-specific form; a connection is opened, data sent and received; finally, the backend-specific response is translated to sttp's `Response`, as described in the request. -A backend can be synchronous, that is, sending a request can be a blocking operation. When invoking `myRequest.send(backend)`, you'll get a value of type `Response[T]`. Backends can also be asynchronous, and evaluate the send operation eagerly or lazily. For example, when using the [Akka backend](backends/akka.md), `myRequest.send(backend)` will return a `Future[Response[T]]`: an eagerly-evaluated, asynchronous result. When using a [Monix backend](backends/monix.md), you'll get back a `Task[Response[T]]`: a lazily-evaluated, but also non-blocking and asynchronous result. +A backend can be synchronous, that is, sending a request can be a blocking operation. When invoking `myRequest.send(backend)`, you'll get a value of type `Response[T]`. Backends can also be asynchronous, and evaluate the send operation eagerly or lazily. For example, when using the [Akka backend](backends/akka.md), `myRequest.send(backend)` will return a `Future[Response[T]]`: an eagerly-evaluated, asynchronous result. When using a [cats-effeect backend](backends/catseffect.md), you'll get back a `F[Response[T]]`: a lazily-evaluated, but also non-blocking and asynchronous result. -Backends manage the connection pool, thread pools for handling responses, depending on the implementation provide various configuration options, and optionally support [streaming](requests/streaming.md) and [websockets](websockets.md). They typically need to be created upon application startup, and closed when the application terminates. +Backends manage the connection pool, thread pools for handling responses, depending on the implementation provide various configuration options, and optionally support [streaming](requests/streaming.md) and [websockets](other/websockets.md). They typically need to be created upon application startup, and closed when the application terminates. For example, the following sends a synchronous request, using the default JVM backend: @@ -41,6 +41,6 @@ val response = myRequest.send(backend) Read more about: * [describing the request](requests/basics.md) -* the [`RequestT` type](requests/type.md) +* the [`Request` type](requests/type.md) * specifying how to handle the [response body](responses/body.md) * [available backends](backends/summary.md) diff --git a/generated-docs/out/includes/examples_list.md b/generated-docs/out/includes/examples_list.md index 2d40537dd1..a8a8862395 100644 --- a/generated-docs/out/includes/examples_list.md +++ b/generated-docs/out/includes/examples_list.md @@ -1,13 +1,27 @@ ## Hello, World! +* [Dynamic URI components](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/dynamicUriSynchronous.scala) HttpClient Direct +* [POST form data](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/PostFormSynchronous.scala) HttpClient Direct +* [POST multipart form](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/postMultipartFormSynchronous.scala) HttpClient Direct * [Post JSON data](https://github.com/softwaremill/sttp/tree/master/examples-ce2/src/main/scala/sttp/client4/examples/PostSerializeJsonMonixHttpClientCirce.scala) HttpClient Monix -* [Post form data](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/PostFormSynchronous.scala) HttpClient Direct +* [Upload file](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/fileUploadSynchronous.scala) HttpClient Direct + +## Backend wrapper + +* [A backend which adds a header to all outgoing requests](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/wrapper/addHeaderBackend.scala) HttpClient Synchronous +* [Integrate with resilience4j to implement circuit-breaking](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/wrapper/CircuitBreakerCatsEffect.scala) HttpClient cats-effect +* [Integrate with resilience4j to implement rate-limiting](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/wrapper/rateLimiterFuture.scala) HttpClient Future +* [Report metrics to a cloud service](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/wrapper/metricsWrapperPekkoHttp.scala) Pekko Future +* [Simple retrying backend wrapper](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/wrapper/retryingBackend.scala) HttpClient Synchronous +* [Use the caching backend wrapper with Redis](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/wrapper/redisCachingBackend.scala) HttpClient Synchronous ## JSON -* [Receive & parse JSON using circe](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/GetAndParseJsonZioCirce.scala) HttpClient ZIO +* [Receive & parse JSON using ZIO Json](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/json/GetAndParseJsonZioJson.scala) HttpClient ZIO +* [Receive & parse JSON using circe](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/json/GetAndParseJsonCatsEffectCirce.scala) HttpClient cats-effect * [Receive & parse JSON using circe](https://github.com/softwaremill/sttp/tree/master/examples-ce2/src/main/scala/sttp/client4/examples/GetAndParseJsonOrFailMonixCirce.scala) HttpClient Monix -* [Receive & parse JSON using json4s](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/getAndParseJsonPekkoHttpJson4s.scala) Pekko Future +* [Receive & parse JSON using json4s](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/json/getAndParseJsonPekkoHttpJson4s.scala) Pekko Future +* [Receive & parse JSON using jsoniter](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/json/getAndParseJsonSynchronousJsoniter.scala) HttpClient Direct ## Logging @@ -19,7 +33,9 @@ ## Resilience -* [Retry sending a request](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/RetryZio.scala) HttpClient ZIO +* [Rate limit sending requests using Ox](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/resilience/RateLimitOx.scala) HttpClient Direct +* [Retry sending a request using Ox](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/resilience/RetryOx.scala) HttpClient Direct +* [Retry sending a request using ZIO's retries](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/resilience/RetryZio.scala) HttpClient ZIO ## Streaming @@ -28,14 +44,14 @@ ## Testing -* [Create a backend stub which simulates interactions using multiple query parameters](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/TestEndpointMultipleQueryParameters.scala) -* [Create a backend stub which simulates interactions with a WebSocket](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/WebSocketTesting.scala) HttpClient cats-effect +* [Create a backend stub which simulates interactions using multiple query parameters](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/testing/TestEndpointMultipleQueryParameters.scala) +* [Create a backend stub which simulates interactions with a WebSocket](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/testing/WebSocketTesting.scala) HttpClient cats-effect ## WebSocket -* [Connect to & interact with a WebSocket](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/WebSocketPekko.scala) Pekko Future -* [Connect to & interact with a WebSocket](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/WebSocketZio.scala) HttpClient ZIO -* [Connect to & interact with a WebSocket](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/WebSocketSynchronous.scala) HttpClient Direct +* [Connect to & interact with a WebSocket](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/ws/WebSocketPekko.scala) Pekko Future +* [Connect to & interact with a WebSocket](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/ws/WebSocketZio.scala) HttpClient ZIO +* [Connect to & interact with a WebSocket](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/ws/WebSocketSynchronous.scala) HttpClient Direct * [Connect to & interact with a WebSocket](https://github.com/softwaremill/sttp/tree/master/examples-ce2/src/main/scala/sttp/client4/examples/WebSocketMonix.scala) HttpClient Monix -* [Connect to & interact with a WebSocket, using Ox channels for streaming](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/wsOxExample.scala) HttpClient Direct -* [Connect to & interact with a WebSocket, using fs2 streaming](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/WebSocketStreamFs2.scala) HttpClient cats-effect \ No newline at end of file +* [Connect to & interact with a WebSocket, using Ox channels for streaming](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/ws/wsOxExample.scala) HttpClient Direct +* [Connect to & interact with a WebSocket, using fs2 streaming](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/ws/WebSocketStreamFs2.scala) HttpClient cats-effect \ No newline at end of file diff --git a/generated-docs/out/index.md b/generated-docs/out/index.md index 7668f79735..8bb75eb59a 100644 --- a/generated-docs/out/index.md +++ b/generated-docs/out/index.md @@ -4,15 +4,33 @@ Welcome! -[sttp client](https://github.com/softwaremill/sttp) is an open-source library which provides a clean, programmer-friendly API to describe HTTP -requests and how to handle responses. Requests are sent using one of the backends, which wrap lower-level Scala or Java HTTP client implementations. The backends can integrate with a variety of Scala stacks, providing both synchronous and asynchronous, procedural and functional interfaces. +sttp client is an open-source HTTP client for Scala, supporting various approaches to writing Scala code: synchronous (direct-style), `Future`-based, and using functional effect systems (cats-effect, ZIO, Monix, Kyo, scalaz). -Backend implementations include the HTTP client that is shipped with Java, as well as ones based on [akka-http](https://doc.akka.io/docs/akka-http/current/scala/http/), [pekko-http](https://pekko.apache.org/docs/pekko-http/current/), [http4s](https://http4s.org), [OkHttp](http://square.github.io/okhttp/). They integrate with [Akka](https://akka.io), [Monix](https://monix.io), [fs2](https://github.com/functional-streams-for-scala/fs2), [cats-effect](https://github.com/typelevel/cats-effect), [scalaz](https://github.com/scalaz/scalaz) and [ZIO](https://github.com/zio/zio). Supported Scala versions include 2.12, 2.13 and 3, Scala.JS and Scala Native; supported Java versions include 11+. +The library is available for Scala 2.12, 2.13 and 3. Supported platforms are the JVM (Java 11+), Scala.JS and Scala Native. Here's a quick example of sttp client in action, runnable using [scala-cli](https://scala-cli.virtuslab.org): ```scala -//> using dep com.softwaremill.sttp.client4::core:4.0.0-M20 +//> using dep com.softwaremill.sttp.client4::core:4.0.0-M23 + +import sttp.client4.quick.* + +@main def run(): Unit = + println(quickRequest.get(uri"http://httpbin.org/ip").send()) +``` + +sttp client addresses common HTTP client use cases, such as interacting with JSON APIs (with automatic serialization of request bodies and deserialization of response bodies), uploading and downloading files, submitting form data, handling multipart requests, and working with WebSockets. + +The driving principle of sttp client's design is to provide a clean, programmer-friendly API to describe HTTP requests, along with response handling. This ensures that resources, such as HTTP connections, are used safely, also in the presence of errors. + +sttp client integrates with a number of lower-level Scala and Java HTTP client implementations through backends (using Java's `HttpClient`, Akka HTTP, Pekko HTTP, http4s, OkHttp, Armeria), offering a wide range of choices when it comes to protocol support, connectivity settings and programming stack compatibility. + +Additionally, sttp client seamlessly integrates with popular libraries for JSON handling (e.g., circe, uPickle, jsoniter, json4s, play-json, ZIO Json), logging, metrics, and tracing (e.g., slf4j, scribe, OpenTelemetry, Prometheus). It also supports streaming libraries (e.g., fs2, ZIO Streams, Akka Streams, Pekko Streams) and provides tools for testing HTTP interactions. + +Some more features: URI interpolation, a self-managed backend, and type-safe HTTP error/success representation, are demonstrated by the below example: + +```scala +//> using dep com.softwaremill.sttp.client4::core:4.0.0-M23 import sttp.client4.* @@ -22,7 +40,8 @@ import sttp.client4.* // the `query` parameter is automatically url-encoded // `sort` is removed, as the value is not defined - val request = basicRequest.get(uri"https://api.github.com/search/repositories?q=$query&sort=$sort") + val request = basicRequest.get( + uri"https://api.github.com/search/repositories?q=$query&sort=$sort") val backend = DefaultSyncBackend() val response = request.send(backend) @@ -30,41 +49,35 @@ import sttp.client4.* // response.header(...): Option[String] println(response.header("Content-Length")) - // response.body: by default read into an Either[String, String] to indicate failure or success + // response.body: read into an Either[String, String] to indicate failure or success println(response.body) ``` -For more examples, see the [usage examples](examples.md) section. To start using sttp client in your project, see the [quickstart](quickstart.md). Or, browse the documentation to find the topics that interest you the most! ScalaDoc is available at [https://www.javadoc.io](https://www.javadoc.io/doc/com.softwaremill.sttp.client4/core_2.12/4.0.0-M9). +But that's just a small glimpse of sttp client's features! For more examples, see the [usage examples](examples.md) section. + +To start using sttp client in your project, see the [quickstart](quickstart.md). Or, browse the documentation to find the topics that interest you the most! ScalaDoc is available at [https://www.javadoc.io](https://www.javadoc.io/doc/com.softwaremill.sttp.client4/core_2.12/4.0.0-M23). + +sttp client is licensed under Apache2, the source code is [available on GitHub](https://github.com/softwaremill/sttp). ## Other sttp projects sttp is a family of Scala HTTP-related projects, and currently includes: * sttp client: this project -* [sttp tapir](https://github.com/softwaremill/tapir): Typed API descRiptions +* [sttp tapir](https://github.com/softwaremill/tapir): rapid development of self-documenting APIs * [sttp model](https://github.com/softwaremill/sttp-model): simple HTTP model classes (used by client & tapir) * [sttp shared](https://github.com/softwaremill/sttp-shared): shared web socket, FP abstractions, capabilities and streaming code. * [sttp apispec](https://github.com/softwaremill/sttp-apispec): OpenAPI, AsyncAPI and JSON Schema models. +* [sttp openai](https://github.com/softwaremill/sttp-openai): Scala client wrapper for OpenAI and OpenAI-compatible APIs. Use the power of ChatGPT inside your code! Third party projects: * [sttp-oauth2](https://github.com/ocadotechnology/sttp-oauth2): OAuth2 client library for Scala -* [sttp openai](https://github.com/softwaremill/sttp-openai): Scala client wrapper for OpenAI (and OpenAI-compatible) API. Use the power of ChatGPT inside your code! ## Try sttp client in your browser! [Check out & play with a simple example on Scastie!](https://scastie.scala-lang.org/adamw/aOf32MZsTPesobwfWG5nDQ) -## Sponsors - -Development and maintenance of sttp client is sponsored by [SoftwareMill](https://softwaremill.com), a software development and consulting company. We help clients scale their business through software. Our areas of expertise include backends, distributed systems, machine learning, platform engineering and data analytics. - -[![](https://files.softwaremill.com/logo/logo.png "SoftwareMill")](https://softwaremill.com) - -## Commercial Support - -We offer commercial support for sttp and related technologies, as well as development services. [Contact us](https://softwaremill.com/contact/) to learn more about our offer! - # Table of contents ```{eval-rst} @@ -74,6 +87,7 @@ We offer commercial support for sttp and related technologies, as well as develo quickstart how + support goals community @@ -82,6 +96,7 @@ We offer commercial support for sttp and related technologies, as well as develo :caption: How-to's examples + migrate_v3_v4 .. toctree:: :maxdepth: 2 @@ -115,11 +130,12 @@ We offer commercial support for sttp and related technologies, as well as develo :maxdepth: 2 :caption: Other topics - websockets - json - xml - resilience - openapi + other/websockets + other/json + other/xml + other/resilience + other/openapi + other/sse .. toctree:: :maxdepth: 2 @@ -148,13 +164,15 @@ We offer commercial support for sttp and related technologies, as well as develo backends/wrappers/opentelemetry backends/wrappers/prometheus backends/wrappers/logging + backends/wrappers/cache backends/wrappers/custom .. toctree:: :maxdepth: 2 :caption: Testing - testing + testing/stub + testing/curl .. toctree:: :maxdepth: 2 diff --git a/generated-docs/out/migrate_v3_v4.md b/generated-docs/out/migrate_v3_v4.md new file mode 100644 index 0000000000..b594b42989 --- /dev/null +++ b/generated-docs/out/migrate_v3_v4.md @@ -0,0 +1,46 @@ +# Migrating to sttp-client4 + +## Top-level package + +The top-level package for sttp-client4 is `sttp.client4`. This means that sttp-client3 and sttp-client4 can be used side-by-side in the same project. They do share sttp-model and sttp-shared libraries, but these did not see a major version change, and are designed to be binary-compatible. + +## Request & backend type changes + +The `RequestT` type is retired, being replaced by `PartialRequest` and `GenericRequest`. + +`PartialRequest` is the type of requests before the method & uri are set. As before, it allows setting the headers and body, creating a reusable base for defining full requests. After the uri & method are set, the request description becomes a `Request`, which can be sent. Additionally, methods to set the body or handle the response as a non-blocking, asynchronous stream become available, as well as converting the request to a web socket one. This yields requests of type `StreamRequests`, `WebSocketRequest` and `WebSocketStreamRequest`. Hence, `GenericRequest` is never used directly in user code. + +A parallel change is from a fully-parametrized `SttpBackend` to a family of traits: `SyncBackend`, `Backend`, `StreamBackend`, `WebSocketBackend`, `WebSocketSyncBackend`, `WebSocketStreamBackend`. These specialize the backend type to the capabilities they support, and are used to send requests of the corresponding type. + +These changes are introduced to simplify the types, improve error reporting and enhance IDE completions. As a tradeoff, defining generic backend wrappers requires defining constructors for each of the backend subtype. + +Moreover, only `request.send(backend)` should be used, instead of `backend.send(request)`; both work, but the first variant is more IDE-friendly, and for the sake of consistency, it's the encouraged one, and the only one used in the documentation. + +## Removed implicit `BodySerializer` + +All request bodies have now to be explicitly converted to a `BasicBody` or one of the basic types (`String`, `InputStream`, `File`, `Array[Byte]`, `ByteBuffer`, `Map[String, String]`). This change has the highest impact when it comes to JSON bodies, which now have to be set using the `asJson` method. + +That is, importing JSON integration (e.g. through `import sttp.client4.jsoniter.*`), brings into scope both `asJson` response handling descriptions, as well as a `asJson(T): BasicBody` methods. A body can be set on a request using `request.body(asJson(...))`. + +Previously, the high level type -> `BasicBody` conversion was implicit, and the JSON integrations contained implicit conversions from high-level types to the JSON string (provided the library-specific encoders were in scope). While this design did save some keystrokes, it also provided poor error reporting, and the format to which the body was converted was not always clear when reading the code. This motivated the change to explicitly calling body conversions. + +## `...OrFailed` response handling + +New response handling descriptions are added, which fail (throw an exception / return a failed effect) if the response is not successful (2xx status code). These include `stringOrFail`, `asByteArrayOrFail`, `asJsonOrFail` etc. Note that this is different from e.g. `asStringAlways`, which always handles the response as a string, regardless of the status code. + +Any `Either`-based response description can be converted to a failing one using `.orFail` and `.orFailDeserialization`. This replaces the `.getRight` / `.getEither` extension methods. + +## Other changes + +* `BackendOptions` replaces `SttpBackendOptions` +* `Backend.monad` replaces `SttpBackend.responseMonad` +* `DefaultSyncBackend` and `DefaultHttpBackend` are provided. They allow limited customization, but are a good entry-level backend for many use-cases. +* `HttpClientBackend` is move to a dedicated package +* `SimpleHttpClient` is removed, `import sttp.client4.quick.*` provides a default backend instance with a no-arg `request.send()` extension method +* documentation & examples use Scala 3 +* the `autoDecompressionDisabled` option is superseded by `autoDecompressionEnabled` +* `async-http-client` backends are removed (as there's no reactive streams support in v3, making integration difficult) +* request attributes replace request tags (same mechanism as in Tapir) +* the parametrization of `ResponseException` is simplified, `DeserializationException` does not have a type parameter, always requiring an `Exception` as the cause instead +* when a `ResponseException` is thrown in the response handling specification, this will be logged as a successful response (as the response was received correctly), and counted as a success in the metrics as well +* `HttpError` is renamed to `UnexpectedStatusCode`, and along with `DeserializationException`, both types are nested within `ResponseException` \ No newline at end of file diff --git a/generated-docs/out/model/model.md b/generated-docs/out/model/model.md index a7505addb5..421e1cf81d 100644 --- a/generated-docs/out/model/model.md +++ b/generated-docs/out/model/model.md @@ -21,8 +21,8 @@ The model also contains aggregate/helper classes such as `Headers` and `QueryPar Example with objects: ```scala -import sttp.client4._ -import sttp.model._ +import sttp.client4.* +import sttp.model.* object Example: val request = basicRequest.header(Header.contentType(MediaType.ApplicationJson)) @@ -36,8 +36,8 @@ object Example: Example with traits: ```scala -import sttp.client4._ -import sttp.model._ +import sttp.client4.* +import sttp.model.* object Example extends HeaderNames with MediaTypes with StatusCodes: val request = basicRequest.header(ContentType, ApplicationJson.toString) diff --git a/generated-docs/out/model/uri.md b/generated-docs/out/model/uri.md index 739c1e2ce1..7d0cbb3468 100644 --- a/generated-docs/out/model/uri.md +++ b/generated-docs/out/model/uri.md @@ -11,8 +11,8 @@ The `Uri` class is immutable, and can be constructed by hand, but in many cases Using the URI interpolator it's possible to conveniently create `Uri` instances, for example: ```scala -import sttp.client4._ -import sttp.model._ +import sttp.client4.* +import sttp.model.* val user = "Mary Smith" val filter = "programming languages" diff --git a/generated-docs/out/json.md b/generated-docs/out/other/json.md similarity index 83% rename from generated-docs/out/json.md rename to generated-docs/out/other/json.md index ed7cd72757..e1697fee09 100644 --- a/generated-docs/out/json.md +++ b/generated-docs/out/other/json.md @@ -1,6 +1,6 @@ # JSON -Adding support for JSON (or other format) bodies in requests/responses is a matter of providing a [body serializer](requests/body.md) and/or a [response body specification](responses/body.md). Both are quite straightforward to implement, so integrating with your favorite JSON library shouldn't be a problem. However, there are some integrations available out-of-the-box. +Adding support for JSON (or other format) bodies in requests/responses is a matter of providing a [body serializer](../requests/body.md) and/or a [response body specification](../responses/body.md). Both are quite straightforward to implement, so integrating with your favorite JSON library shouldn't be a problem. However, there are some integrations available out-of-the-box. Each integration is available as an import, which brings `asJson` methods into scope. Alternatively, these values are grouped intro traits (e.g. `sttp.client4.circe.SttpCirceApi`), which can be extended to group multiple integrations in one object, and thus reduce the number of necessary imports. @@ -17,19 +17,20 @@ The type signatures vary depending on the underlying library (required implicits ```scala import sttp.client4.* +import sttp.client4.ResponseException.DeserializationException // request bodies def asJson[B](b: B): StringBody = ??? // response handling description -def asJson[B]: ResponseAs[Either[ResponseException[String, Exception], B]] = ??? +def asJson[B]: ResponseAs[Either[ResponseException[String], B]] = ??? def asJsonOrFail[B]: ResponseAs[B] = ??? -def asJsonAlways[B]: ResponseAs[Either[DeserializationException[Exception], B]] = ??? -def asJsonEither[E, B]: ResponseAs[Either[ResponseException[E, Exception], B]] = ??? +def asJsonAlways[B]: ResponseAs[Either[DeserializationException, B]] = ??? +def asJsonEither[E, B]: ResponseAs[Either[ResponseException[E], B]] = ??? def asJsonEitherOrFail[E, B]: ResponseAs[Either[E, B]] = ??? ``` -The response specifications can be further refined using `.orFail` and `.orFailDeserialization`, see [response body specifications](responses/body.md). +The response specifications can be further refined using `.orFail` and `.orFailDeserialization`, see [response body specifications](../responses/body.md). Following data class will be used through the next few examples: @@ -43,7 +44,7 @@ case class ResponsePayload(data: String) JSON encoding of bodies and decoding of responses can be handled using [Circe](https://circe.github.io/circe/) by the `circe` module. To use add the following dependency to your project: ```scala -"com.softwaremill.sttp.client4" %% "circe" % "4.0.0-M22" +"com.softwaremill.sttp.client4" %% "circe" % "4.0.0-M23" ``` This module adds a body serialized, so that json payloads can be sent as request bodies. To send a payload of type `T` as json, a `io.circe.Encoder[T]` implicit value must be available in scope. @@ -60,7 +61,7 @@ val backend: SyncBackend = DefaultSyncBackend() import io.circe.generic.auto._ val requestPayload = RequestPayload("some data") -val response: Response[Either[ResponseException[String, io.circe.Error], ResponsePayload]] = +val response: Response[Either[ResponseException[String], ResponsePayload]] = basicRequest .post(uri"...") .body(asJson(requestPayload)) @@ -72,10 +73,10 @@ Arbitrary JSON structures can be traversed by parsing the result as `io.circe.Js ## Json4s -To encode and decode json using json4s, add the following dependency to your project: +To encode and decode json using json4s, add the following dependencies to your project: ``` -"com.softwaremill.sttp.client4" %% "json4s" % "4.0.0-M22" +"com.softwaremill.sttp.client4" %% "json4s" % "4.0.0-M23" "org.json4s" %% "json4s-native" % "3.6.0" ``` @@ -98,7 +99,7 @@ val requestPayload = RequestPayload("some data") given Serialization = org.json4s.native.Serialization given Formats = org.json4s.DefaultFormats -val response: Response[Either[ResponseException[String, Exception], ResponsePayload]] = +val response: Response[Either[ResponseException[String], ResponsePayload]] = basicRequest .post(uri"...") .body(asJson(requestPayload)) @@ -111,7 +112,7 @@ val response: Response[Either[ResponseException[String, Exception], ResponsePayl To encode and decode JSON using [spray-json](https://github.com/spray/spray-json), add the following dependency to your project: ``` -"com.softwaremill.sttp.client4" %% "spray-json" % "4.0.0-M22" +"com.softwaremill.sttp.client4" %% "spray-json" % "4.0.0-M23" ``` Using this module it is possible to set request bodies and read response bodies as your custom types, using the implicitly available instances of `spray.json.JsonWriter` / `spray.json.JsonReader` or `spray.json.JsonFormat`. @@ -130,7 +131,7 @@ implicit val myResponseJsonFormat: RootJsonFormat[ResponsePayload] = ??? val requestPayload = RequestPayload("some data") -val response: Response[Either[ResponseException[String, Exception], ResponsePayload]] = +val response: Response[Either[ResponseException[String], ResponsePayload]] = basicRequest .post(uri"...") .body(asJson(requestPayload)) @@ -143,13 +144,13 @@ val response: Response[Either[ResponseException[String, Exception], ResponsePayl To encode and decode JSON using [play-json](https://www.playframework.com), add the following dependency to your project: ```scala -"com.softwaremill.sttp.client4" %% "play-json" % "4.0.0-M22" +"com.softwaremill.sttp.client4" %% "play-json" % "4.0.0-M23" ``` If you use older version of play (2.9.x), add the following dependency to your project: ```scala -"com.softwaremill.sttp.client4" %% "play29-json" % "4.0.0-M22" +"com.softwaremill.sttp.client4" %% "play29-json" % "4.0.0-M23" ``` To use, add an import: `import sttp.client4.playJson._`. @@ -161,13 +162,13 @@ To encode and decode JSON using the high-performance [zio-json](https://zio.gith The `zio-json` module depends on ZIO 2.x. For ZIO 1.x support, use `zio1-json`. ```scala -"com.softwaremill.sttp.client4" %% "zio-json" % "4.0.0-M22" // for ZIO 2.x -"com.softwaremill.sttp.client4" %% "zio1-json" % "4.0.0-M22" // for ZIO 1.x +"com.softwaremill.sttp.client4" %% "zio-json" % "4.0.0-M23" // for ZIO 2.x +"com.softwaremill.sttp.client4" %% "zio1-json" % "4.0.0-M23" // for ZIO 1.x ``` or for ScalaJS (cross build) projects: ```scala -"com.softwaremill.sttp.client4" %%% "zio-json" % "4.0.0-M22" // for ZIO 2.x -"com.softwaremill.sttp.client4" %%% "zio1-json" % "4.0.0-M22" // for ZIO 1.x +"com.softwaremill.sttp.client4" %%% "zio-json" % "4.0.0-M23" // for ZIO 2.x +"com.softwaremill.sttp.client4" %%% "zio1-json" % "4.0.0-M23" // for ZIO 1.x ``` To use, add an import: `import sttp.client4.ziojson._` (or extend `SttpZioJsonApi`), define an implicit `JsonCodec`, or `JsonDecoder`/`JsonEncoder` for your datatype. @@ -186,7 +187,7 @@ implicit val myResponseJsonDecoder: JsonDecoder[ResponsePayload] = DeriveJsonDec val requestPayload = RequestPayload("some data") -val response: Response[Either[ResponseException[String, String], ResponsePayload]] = +val response: Response[Either[ResponseException[String], ResponsePayload]] = basicRequest .post(uri"...") .body(asJson(requestPayload)) @@ -199,13 +200,13 @@ basicRequest To encode and decode JSON using the [high(est)-performant](https://plokhotnyuk.github.io/jsoniter-scala/) [jsoniter-scala](https://github.com/plokhotnyuk/jsoniter-scala) library, one add the following dependency to your project. ```scala -"com.softwaremill.sttp.client4" %% "jsoniter" % "4.0.0-M22" +"com.softwaremill.sttp.client4" %% "jsoniter" % "4.0.0-M23" ``` or for ScalaJS (cross build) projects: ```scala -"com.softwaremill.sttp.client4" %%% "jsoniter" % "4.0.0-M22" +"com.softwaremill.sttp.client4" %%% "jsoniter" % "4.0.0-M23" ``` To use, add an import: `import sttp.client4.jsoniter._` (or extend `SttpJsonIterJsonApi`), define an implicit `JsonCodec`, or `JsonDecoder`/`JsonEncoder` for your datatype. @@ -226,7 +227,7 @@ implicit val payloadJsonCodec: JsonValueCodec[RequestPayload] = JsonCodecMaker.m implicit val jsonEitherDecoder: JsonValueCodec[ResponsePayload] = JsonCodecMaker.make val requestPayload = RequestPayload("some data") -val response: Response[Either[ResponseException[String, Exception], ResponsePayload]] = +val response: Response[Either[ResponseException[String], ResponsePayload]] = basicRequest .post(uri"...") .body(asJson(requestPayload)) @@ -239,13 +240,13 @@ basicRequest To encode and decode JSON using the [uPickle](https://github.com/com-lihaoyi/upickle) library, add the following dependency to your project: ```scala -"com.softwaremill.sttp.client4" %% "upickle" % "4.0.0-M22" +"com.softwaremill.sttp.client4" %% "upickle" % "4.0.0-M23" ``` or for ScalaJS (cross build) projects: ```scala -"com.softwaremill.sttp.client4" %%% "upickle" % "4.0.0-M22" +"com.softwaremill.sttp.client4" %%% "upickle" % "4.0.0-M23" ``` To use, add an import: `import sttp.client4.upicklejson.default._` and define an implicit `ReadWriter` (or separately `Reader` and `Writer`) for your datatype. @@ -263,7 +264,7 @@ implicit val responsePayloadRW: ReadWriter[ResponsePayload] = macroRW[ResponsePa val requestPayload = RequestPayload("some data") -val response: Response[Either[ResponseException[String, Exception], ResponsePayload]] = +val response: Response[Either[ResponseException[String], ResponsePayload]] = basicRequest .post(uri"...") .body(asJson(requestPayload)) diff --git a/generated-docs/out/openapi.md b/generated-docs/out/other/openapi.md similarity index 90% rename from generated-docs/out/openapi.md rename to generated-docs/out/other/openapi.md index dc72d8e7c0..6502032d97 100644 --- a/generated-docs/out/openapi.md +++ b/generated-docs/out/other/openapi.md @@ -1,6 +1,6 @@ # OpenAPI -sttp-client [request definitions](requests/basics.md) can be automatically generated from [openapi](https://swagger.io/specification/) `.yaml` specifications using +sttp-client [request definitions](../requests/basics.md) can be automatically generated from [openapi](https://swagger.io/specification/) `.yaml` specifications using the `scala-sttp` code generator, included in the [openapi-generator](https://github.com/OpenAPITools/openapi-generator) project. ## Using the openapi-generator @@ -43,8 +43,8 @@ lazy val petstoreApi: Project = project openApiGeneratorName := "scala-sttp", openApiOutputDir := baseDirectory.value.name, libraryDependencies ++= Seq( - "com.softwaremill.sttp.client4" %% "core" % "4.0.0-M22", - "com.softwaremill.sttp.client4" %% "json4s" % "4.0.0-M22", + "com.softwaremill.sttp.client4" %% "core" % "4.0.0-M23", + "com.softwaremill.sttp.client4" %% "json4s" % "4.0.0-M23", "org.json4s" %% "json4s-jackson" % "3.6.8" ) ) @@ -94,8 +94,8 @@ lazy val petstoreApi: Project = project openApiOutputDir := baseDirectory.value.name, openApiIgnoreFileOverride := s"${baseDirectory.in(ThisBuild).value.getPath}/openapi-ignore-file", libraryDependencies ++= Seq( - "com.softwaremill.sttp.client4" %% "core" % "4.0.0-M22", - "com.softwaremill.sttp.client4" %% "json4s" % "4.0.0-M22", + "com.softwaremill.sttp.client4" %% "core" % "4.0.0-M23", + "com.softwaremill.sttp.client4" %% "json4s" % "4.0.0-M23", "org.json4s" %% "json4s-jackson" % "3.6.8" ), (compile in Compile) := ((compile in Compile) dependsOn openApiGenerate).value, diff --git a/generated-docs/out/resilience.md b/generated-docs/out/other/resilience.md similarity index 62% rename from generated-docs/out/resilience.md rename to generated-docs/out/other/resilience.md index a8af4cba8f..551c3fceac 100644 --- a/generated-docs/out/resilience.md +++ b/generated-docs/out/other/resilience.md @@ -14,18 +14,25 @@ Still, the input for a particular resilience model might involve both the result ## Retries +Handling retries is a complex problem when it comes to HTTP requests. When is a request retryable? There are a couple of things to take into account: + +* connection exceptions are generally good candidates for retries +* only idempotent HTTP methods (such as `GET`) could potentially be retried +* some HTTP status codes might also be retryable (e.g. `500 Internal Server Error` or `503 Service Unavailable`) + +In some cases it's possible to implement a generic retry mechanism; such a mechanism should take into account logging, metrics, limiting the number of retries and a backoff mechanism. These mechanisms could be quite simple, or involve e.g. retry budgets (see [Finagle's](https://twitter.github.io/finagle/guide/Clients.html#retries) documentation on retries). In sttp, it's possible to recover from errors using the `monad`. + +sttp client contains a default implementation of a predicate, which allows deciding if a request is retriable: if the body can be sent multiple times, and if the HTTP method is idempotent. This predicate is available as `RetryWhen.Default` and has type `(GenericRequest[_, _], Either[Throwable, Response[_]]) => Boolean`. + Here's an incomplete list of libraries which can be used to manage retries in various Scala stacks: -* for synchornous/direct-style: [ox](https://github.com/softwaremill/ox) +* for synchornous/direct-style: [Ox](https://github.com/softwaremill/ox) * for `Future`: [retry](https://github.com/softwaremill/retry) * for ZIO: [schedules](https://zio.dev/reference/schedule/), [rezilience](https://github.com/svroonland/rezilience) * for Monix/cats-effect: [cats-retry](https://github.com/cb372/cats-retry) * for Monix: `.restart` methods -sttp client contains a default implementation of a predicate, which allows deciding if a request is retriable: if the body can be sent multiple times, and if the HTTP method is idempotent. -This predicate is available as `RetryWhen.Default` and has type `(Request[_, _], Either[Throwable, Response[_]]) => Boolean`. - -See also the [retrying using ZIO](examples.html#retry-a-request-using-zio) example, as well as an example of a very simple [retrying backend wrapper](backends/wrappers/custom.html#example-retrying-backend-wrapper). +See also the "resiliency" and "backend wrapper" [examples](../examples.md). ### Backend-specific retries @@ -42,9 +49,10 @@ Some backends have built-in retry mechanisms: ## Rate limiting -* for akka-streams: [throttle in akka streams](https://doc.akka.io/docs/akka/current/stream/operators/Source-or-Flow/throttle.html) +* for synchornous/direct-style: [Ox](https://github.com/softwaremill/ox) +* for Akka Streams: [throttle in akka streams](https://doc.akka.io/docs/akka/current/stream/operators/Source-or-Flow/throttle.html) * for ZIO: [rezilience](https://github.com/svroonland/rezilience) ## Java libraries -* [resilience4j](https://github.com/resilience4j/resilience4j) +* [resilience4j](https://github.com/resilience4j/resilience4j) (rate limiting, circuit breaking, retries, other resilience patterns) diff --git a/generated-docs/out/other/sse.md b/generated-docs/out/other/sse.md new file mode 100644 index 0000000000..723d2bbecb --- /dev/null +++ b/generated-docs/out/other/sse.md @@ -0,0 +1,5 @@ +# Server-sent events + +All backends that support [streaming](../requests/streaming.md) also support [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events). Moreover, synchronous backends can also support SSE, when combined with [Ox](https://ox.softwaremill.com). + +Refer to the documentation of individual backends, for more information on how to use SSE with a given backend, as well as usage examples. \ No newline at end of file diff --git a/generated-docs/out/websockets.md b/generated-docs/out/other/websockets.md similarity index 72% rename from generated-docs/out/websockets.md rename to generated-docs/out/other/websockets.md index 51bfc303ce..0ec04e10fc 100644 --- a/generated-docs/out/websockets.md +++ b/generated-docs/out/other/websockets.md @@ -1,6 +1,6 @@ # WebSockets -One of the optional capabilities (represented as `WebSockets`) that a backend can support are websockets (see [backends summary](backends/summary.md)). Websocket requests are described exactly the same as regular requests, starting with `basicRequest`, adding headers, specifying the request method and uri. +One of the optional capabilities that a backend can support are websockets (see [backends summary](../backends/summary.md)). Websocket requests are described exactly the same as regular requests, starting with `basicRequest`, adding headers, specifying the request method and uri. A websocket request will be sent instead of a regular one if the response specification includes handling the response as a websocket. Depending on the backend you are using, there are three variants of websocket response specifications: synchronous, asynchronous and streaming. To use them, add one of the following imports: @@ -8,11 +8,13 @@ A websocket request will be sent instead of a regular one if the response specif * `import sttp.client4.ws.async.*` if you are using an asynchronous backend (e.g. based on `Future`s or `IO`s) * `import sttp.client4.ws.stream.*` if you want to handle web socket messages using a non-blocking stream (e.g. `fs2.Stream` or `akka.stream.scaladsl.Source`) -The above imports will bring into scope a number of `asWebSocket(...)` methods, giving a couple of variants of working with websockets. +The above imports will bring into scope a number of `asWebSocket(...)` methods, giving a couple of variants of working with websockets. Alternatively, you can extend the `SttpWebSocketSyncApi`, `SttpWebSocketAsyncApi` or `SttpWebSocketStreamApi` traits, to group all used sttp client features within a single object. + +Refer to the documentation of individual backends for additional notes, or restrictions, when using WebSockets. ## Using `WebSocket` -The first variant of interacting with web sockets is using `sttp.client4.ws.SyncWebSocket` (sync variant), or `sttp.ws.WebSocket[F]` (async variant), where `F` is the backend-specific effects wrapper, such as `Future` or `IO`. These classes contain two basic methods: +The first possibility of interacting with web sockets is using `sttp.client4.ws.SyncWebSocket` (sync variant), or `sttp.ws.WebSocket[F]` (async variant), where `F` is the backend-specific effects wrapper, such as `Future` or `IO`. These classes contain two basic methods: * `def receive: WebSocketFrame` (optionally wrapped with `F[_]` in the async variant) which will complete once a message is available, and return the next incoming frame (which can be a data, ping, pong or close) * `def send(f: WebSocketFrame, isContinuation: Boolean = false): Unit` (again optionally wrapped with `F[_]`), which sends a message to the websocket. The `WebSocketFrame` companion object contains methods for creating binary/text messages. When using fragmentation, the first message should be sent using `finalFragment = false`, and subsequent messages using `isContinuation = true`. @@ -32,17 +34,13 @@ import sttp.shared.Identity def asWebSocket[T](f: SyncWebSocket => T): WebSocketResponseAs[Identity, Either[String, T]] = ??? +def asWebSocketOrFail[T](f: SyncWebSocket => T): + WebSocketResponseAs[Identity, T] = ??? + def asWebSocketWithMetadata[T]( f: (SyncWebSocket, ResponseMetadata) => T ): WebSocketResponseAs[Identity, Either[String, T]] = ??? -def asWebSocketAlways[T](f: SyncWebSocket => T): - WebSocketResponseAs[Identity, T] = ??? - -def asWebSocketAlwaysWithMetadata[T]( - f: (SyncWebSocket, ResponseMetadata) => T -): WebSocketResponseAs[Identity, T] = ??? - def asWebSocketUnsafe: WebSocketResponseAs[Identity, Either[String, SyncWebSocket]] = ??? @@ -52,17 +50,17 @@ def asWebSocketAlwaysUnsafe: The first variant, `asWebSocket`, passes an open `SyncWebSocket` to the user-provided function. This function should only return once interaction with the websocket is finished. The backend can then safely close the websocket. The value that's returned as the response body is either an error (represented as a `String`), in case the websocket upgrade didn't complete successfully, or the value returned by the websocket-interacting method. -The second variant (`asWebSocketAlways`) is similar, but any errors due to failed websocket protocol upgrades are represented as exceptions. +The second (`asWebSocketOrFail`) is similar, but any errors due to failed websocket protocol upgrades are represented as exceptions. The remaining two variants return the open `SyncWebSocket` directly, as the response body. It is then the responsibility of the client code to close the websocket, once it's no longer needed. Similar response specifications, but using an effect wrapper and `WebSocket[F]`, are available in the `sttp.client4.ws.async` objet. -See also the [examples](examples.md), which include examples involving websockets. +See also the [examples](../examples.md), which include examples involving websockets. -## Using streams +## Using non-blocking, asynchronous streams -Another possibility is to work with websockets by providing a streaming stage, which transforms incoming data frames into outgoing frames. This can be e.g. an [Akka](backends/akka.md) `Flow` or a [fs2](backends/fs2.md) `Pipe`. +Another possibility is to work with websockets by providing a streaming stage, which transforms incoming data frames into outgoing frames. This can be e.g. a [Pekko](../backends/pekko.md) `Flow` or a [fs2](../backends/fs2.md) `Pipe`. The following response specifications are available: @@ -76,11 +74,11 @@ import sttp.ws.WebSocketFrame def asWebSocketStream[S](s: Streams[S])(p: s.Pipe[WebSocketFrame.Data[_], WebSocketFrame]): WebSocketStreamResponseAs[Either[String, Unit], S] = ??? -def asWebSocketStreamAlways[S](s: Streams[S])(p: s.Pipe[WebSocketFrame.Data[_], WebSocketFrame]): +def asWebSocketStreamOrFail[S](s: Streams[S])(p: s.Pipe[WebSocketFrame.Data[_], WebSocketFrame]): WebSocketStreamResponseAs[Unit, S] = ??? ``` -Using streaming websockets requires the backend to support the given streaming capability (see also [streaming](requests/streaming.md)). Streaming capabilities are described as implementations of `Streams[S]`, and are provided by backend implementations, e.g. `AkkaStreams` or `Fs2Streams[F]`. +Using streaming websockets requires the backend to support the given streaming capability (see also [streaming](../requests/streaming.md)). Streaming capabilities are described as implementations of `Streams[S]`, and are provided by backend implementations, e.g. `PekkoStreams` or `Fs2Streams[F]`. When working with streams of websocket frames keep in mind that a text payload may be fragmented into multiple frames. sttp provides two useful methods (`fromTextPipe`, `fromTextPipeF`) for each backend to aggregate these fragments back into complete messages. @@ -96,7 +94,7 @@ effect type class name ================ ========================================== ``` -## WebSockets as Ox Source and Sink +## Using blocking, synchronous Ox streams [Ox](https://ox.softwaremill.com) is a Scala 3 toolkit that allows you to handle concurrency and resiliency in direct-style, leveraging Java 21 virtual threads. If you're using Ox with `sttp`, you can use the `DefaultSyncBackend` from `sttp-core` for HTTP communication. An additional `ox` module allows handling WebSockets @@ -104,7 +102,7 @@ as Ox `Source` and `Sink`: ``` // sbt dependency -"com.softwaremill.sttp.client4" %% "ox" % "4.0.0-M22", +"com.softwaremill.sttp.client4" %% "ox" % "4.0.0-M23" ``` ```scala @@ -118,7 +116,8 @@ import sttp.ws.WebSocketFrame def useWebSocket(ws: SyncWebSocket): Unit = supervised { - val (wsSource, wsSink) = asSourceAndSink(ws) // (Source[WebSocketFrame], Sink[WebSocketFrame]) + // (Source[WebSocketFrame], Sink[WebSocketFrame]) + val (wsSource, wsSink) = asSourceAndSink(ws) // ... } @@ -129,25 +128,22 @@ basicRequest .send(backend) ``` -See the [full example here](https://github.com/softwaremill/sttp/blob/master/examples3/src/main/scala/sttp/client4/examples/WebSocketOx.scala). +See the [full example here](https://github.com/softwaremill/sttp/blob/master/examples/src/main/scala/sttp/client4/examples/ws/wsOxExample.scala). -Make sure that the `Source` is contiunually read. This will guarantee that server-side Close signal is received and handled. +Make sure that the `Source` is continually read. This will guarantee that server-side `Close` signal is received and handled. If you don't want to process frames from the server, you can at least handle it with a `fork { source.drain() }`. You don't need to manually call `ws.close()` when using this approach, this will be handled automatically underneath, according to following rules: - - If the request `Sink` is closed due to an upstream error, a Close frame is sent, and the `Source` with incoming responses gets completed as `Done`. + - If the request `Sink` is closed due to an upstream error, a `Close` frame is sent, and the `Source` with incoming responses gets completed as `Done`. - If the request `Sink` completes as `Done`, a `Close` frame is sent, and the response `Sink` keeps receiving responses until the server closes communication. - - If the response `Source` is closed by a Close frome from the server or due to an error, the request Sink is closed as `Done`, which will still send all outstanding buffered frames, and then finish. + - If the response `Source` is closed by a `Close` frame from the server or due to an error, the request Sink is closed as `Done`, which will still send all outstanding buffered frames, and then finish. Read more about Ox, structured concurrency, Sources and Sinks on the [project website](https://ox.softwaremill.com). ## Compression -For those who plan to use a lot of websocket traffic, you could consider websocket compression. See the information on -configuring individual backends for more information. - -## Implementation-specific configuration +For those who plan to use a lot of websocket traffic, you could consider websocket compression, however it's often not supported: ### OkHttp based backends diff --git a/generated-docs/out/xml.md b/generated-docs/out/other/xml.md similarity index 72% rename from generated-docs/out/xml.md rename to generated-docs/out/other/xml.md index b89388c351..8d3f3bcecd 100644 --- a/generated-docs/out/xml.md +++ b/generated-docs/out/other/xml.md @@ -6,7 +6,7 @@ Adding XML encoding/decoding support is a matter of providing a [body serializer If you possess the XML Schema definition file (`.xsd` file) consider using the scalaxb tool, which would generate needed models and serialization/deserialization logic. To use the tool please follow the documentation on [setting up](https://scalaxb.org/setup) and [running](https://scalaxb.org/running-scalaxb) scalaxb. -After code generation, create the `SttpScalaxbApi` trait (or trait with another name of your choosing) and add the following code snippet: +After code generation, create an `SttpScalaxbApi` trait (or trait with another name of your choosing) and add the following code snippet: ```scala import generated.defaultScope // import may differ depending on location of generated code @@ -20,17 +20,20 @@ import scala.xml.{NodeSeq, XML} trait SttpScalaxbApi: case class XmlElementLabel(label: String) + // request body def asXml[B](b: B)(implicit format: CanWriteXML[B], label: XmlElementLabel): StringBody = val nodeSeq: NodeSeq = toXML[B](obj = b, elementLabel = label.label, scope = defaultScope) StringBody(nodeSeq.toString(), "utf-8", MediaType.ApplicationXml) - implicit def deserializeXml[B](implicit decoder: XMLFormat[B]): String => Either[Exception, B] = (s: String) => - try - Right(fromXML[B](XML.loadString(s))) - catch - case e: Exception => Left(e) + private def deserializeXml[B](implicit decoder: XMLFormat[B]): String => Either[Exception, B] = + (s: String) => + try + Right(fromXML[B](XML.loadString(s))) + catch + case e: Exception => Left(e) - def asXml[B: XMLFormat]: ResponseAs[Either[ResponseException[String, Exception], B], Any] = + // response body handling description + def asXml[B: XMLFormat]: ResponseAs[Either[ResponseException[String], B], Any] = asString.mapWithMetadata(ResponseAs.deserializeRightWithError(deserializeXml[B])) .showAs("either(as string, as xml)") ``` @@ -49,13 +52,19 @@ Usage example: ```scala val backend: SyncBackend = DefaultSyncBackend() -val requestPayload = Outer(Inner(42, b = true, "horses"), "cats") // `Outer` and `Inner` classes are generated by scalaxb from xsd file +// `Outer` and `Inner` classes are generated by scalaxb from xsd file +val requestPayload = Outer(Inner(42, b = true, "horses"), "cats") -import sttpScalaxb._ // imports sttp related serialization / deserialization logic -implicit val label = XmlElementLabel("outer") // gives needed XmlElementLabel for the top XML node -import generated.Generated_OuterFormat // imports member of code generated by scalaxb, that provides `XMLFormat` for `Outer` type; this import may differ depending on location of generated code +// imports sttp related serialization / deserialization logic +import sttpScalaxb.* -val response: Response[Either[ResponseException[String, Exception], Outer]] = +// gives needed XmlElementLabel for the top XML node +given XmlElementLabel = XmlElementLabel("outer") +// imports member of code generated by scalaxb, that provides `XMLFormat` for `Outer` type; +// this import may differ depending on location of generated code +import generated.Generated_OuterFormat + +val response: Response[Either[ResponseException[String], Outer]] = basicRequest .post(uri"...") .body(asXml(requestPayload)) diff --git a/generated-docs/out/quickstart.md b/generated-docs/out/quickstart.md index 8f738c23ef..8d2fd43e9d 100644 --- a/generated-docs/out/quickstart.md +++ b/generated-docs/out/quickstart.md @@ -1,21 +1,18 @@ # Quickstart The core sttp client API comes in a single jar, with a transitive dependency on [sttp model](https://github.com/softwaremill/sttp-model). -This also includes [synchronous](backends/synchronous.md) and [`Future`-based] backends, based on Java's `HttpClient`. +This also includes [synchronous](backends/synchronous.md) and [`Future`-based](backends/future.md) backends, based on Java's `HttpClient`. To integrate with other parts of your application and various effect systems, you'll often need to use an alternate backend, or backend wrappers (but what's important is that the API remains the same!). See the section on [backends](backends/summary.md) for a short guide on which backend to choose, and a list of all implementations. -`sttp client` is available for Scala 2.12 and 2.13, as well as for Scala 3 and requires Java 11 or higher. - -`sttp client` is also available for Scala.js 1.0 and Scala Native. Note that not all modules are compatible with these -platforms, and that each has its own dedicated set of backends. +sttp client is available for Scala 2.12, 2.13 and 3, on the JVM (Java 11+), Scala.JS and Scala Native platforms. Note that not all modules are compatible with these platforms, and that each has its own dedicated set of backends. ## Using sbt The basic dependency which provides the API, together with a synchronous and `Future`-based backends, is: ```scala -"com.softwaremill.sttp.client4" %% "core" % "4.0.0-M22" +"com.softwaremill.sttp.client4" %% "core" % "4.0.0-M23" ``` ## Using scala-cli @@ -23,7 +20,7 @@ The basic dependency which provides the API, together with a synchronous and `Fu Add the following directive to the top of your scala file to add the core sttp dependency: ``` -//> using dep "com.softwaremill.sttp.client4::core:4.0.0-M22" +//> using dep com.softwaremill.sttp.client4::core:4.0.0-M23 ``` ## Using Ammonite @@ -31,7 +28,7 @@ Add the following directive to the top of your scala file to add the core sttp d If you are an [Ammonite](https://ammonite.io) user, you can quickly start experimenting with sttp by copy-pasting the following: ```scala -import $ivy.`com.softwaremill.sttp.client4::core:4.0.0-M22` +import $ivy.`com.softwaremill.sttp.client4::core:4.0.0-M23` ``` ## Imports @@ -39,10 +36,10 @@ import $ivy.`com.softwaremill.sttp.client4::core:4.0.0-M22` Working with sttp is most convenient if you import the `sttp.client4` package entirely: ```scala -import sttp.client4._ +import sttp.client4.* ``` -This brings into scope the starting point for defining requests and some helper methods. All examples in this guide assume that this import is in place. +This brings into scope the starting point for defining requests (`basicRequest`) and some helper methods. All examples in this guide assume that this import is in place. ## Synchronous requests @@ -66,41 +63,45 @@ should be closed using `.close()`. Typically, you should have one backend instan ## Serialising and parsing JSON To serialize a custom type to a JSON body, or to deserialize the response body that is in the JSON format, you'll need -to add an integration with a JSON library. See [json](json.md) for a list of available libraries. +to add an integration with a JSON library. See [json](other/json.md) for a list of available libraries. As an example, to integrate with the [uPickle](https://github.com/com-lihaoyi/upickle) library, add the following dependency: ```scala -"com.softwaremill.sttp.client4" %% "upickle" % "4.0.0-M22" +"com.softwaremill.sttp.client4" %% "upickle" % "4.0.0-M23" ``` Your code might then look as follows: ```scala +//> using dep com.softwaremill.sttp.client4::core:4.0.0-M23 +//> using dep com.softwaremill.sttp.client4::upickle:4.0.0-M23 + import sttp.client4.* import sttp.client4.upicklejson.default.* import upickle.default.* -val backend = DefaultSyncBackend() +@main def run(): Unit = + val backend = DefaultSyncBackend() -case class MyRequest(field1: String, field2: Int) -// selected fields from the JSON that is being returned by httpbin -case class HttpBinResponse(origin: String, headers: Map[String, String]) + case class MyRequest(field1: String, field2: Int) + // selected fields from the JSON that is being returned by httpbin + case class HttpBinResponse(origin: String, headers: Map[String, String]) -implicit val myRequestRW: ReadWriter[MyRequest] = macroRW[MyRequest] -implicit val responseRW: ReadWriter[HttpBinResponse] = macroRW[HttpBinResponse] + given ReadWriter[MyRequest] = macroRW[MyRequest] + given ReadWriter[HttpBinResponse] = macroRW[HttpBinResponse] -val request = basicRequest - .post(uri"https://httpbin.org/post") - .body(asJson(MyRequest("test", 42))) - .response(asJson[HttpBinResponse]) -val response = request.send(backend) + val request = basicRequest + .post(uri"https://httpbin.org/post") + .body(asJson(MyRequest("test", 42))) + .response(asJson[HttpBinResponse]) + val response = request.send(backend) -response.body match { - case Left(e) => println(s"Got response exception:\n$e") - case Right(r) => println(s"Origin's ip: ${r.origin}, header count: ${r.headers.size}") -} + response.body match { + case Left(e) => println(s"Got response exception:\n$e") + case Right(r) => println(s"Origin's ip: ${r.origin}, header count: ${r.headers.size}") + } ``` ## Adding logging @@ -109,7 +110,7 @@ Logging can be added using the [logging backend wrapper](backends/wrappers/loggi use slf4j, you'll need the following dependency: ``` -"com.softwaremill.sttp.client4" %% "slf4j-backend" % "4.0.0-M22" +"com.softwaremill.sttp.client4" %% "slf4j-backend" % "4.0.0-M23" ``` Then, you'll need to configure your client: @@ -123,7 +124,7 @@ val backend = Slf4jLoggingBackend(DefaultSyncBackend()) ## Even quicker -You can skip the step of creating a backend instance, by using `import sttp.client4.quick._` instead of the usual `import sttp.client4._`. +You can skip the step of creating a backend instance, by using `import sttp.client4.quick.*` instead of the usual `import sttp.client4.*`. This brings into scope the same sttp API, and additionally a synchronous backend instance, which can be used to send requests. This backend instance is global (created on first access), can't be customised and shouldn't be closed. diff --git a/generated-docs/out/requests/authentication.md b/generated-docs/out/requests/authentication.md index 785e2f763b..0bf469359c 100644 --- a/generated-docs/out/requests/authentication.md +++ b/generated-docs/out/requests/authentication.md @@ -1,5 +1,7 @@ # Authentication +## Supported schemes + sttp supports basic, bearer-token based authentication and digest authentication. Two first cases are handled by adding an `Authorization` header with the appropriate credentials. Basic authentication, using which the username and password are encoded using Base64, can be added as follows: diff --git a/generated-docs/out/requests/basics.md b/generated-docs/out/requests/basics.md index 4020b8ce82..a72e7cfe4b 100644 --- a/generated-docs/out/requests/basics.md +++ b/generated-docs/out/requests/basics.md @@ -31,46 +31,27 @@ The URI can be created programmatically (by calling methods on the `Uri` class), ## Sending a request -A request definition can be created without knowing how it will be sent. But to send a request, a backend is needed. A default, synchronous backend based on Java's `HttpURLConnection` is provided in the `core` jar. +A request definition can be created without knowing how it will be sent. But to send a request, a backend is needed. A default, synchronous backend based on Java's `HttpClient` is provided in the `core` jar. -To invoke the `send(backend)` method on a request description, you'll need an instance of `SttpBackend`: +To invoke the `send(backend)` method on a request description, you'll need an instance of `Backend`: ```scala val backend = DefaultSyncBackend() val response: Response[Either[String, String]] = request.send(backend) ``` -The default backend uses the `Identity` effect to return responses, which is equivalent to a synchronous call (no effect at all). Other asynchronous backends use other effect types. See the section on [backends](../backends/summary.md) for more details. +The default backend invokes any effects synchronously. Other, asynchronous backends, use "wrapper" effect types, such as `Future` or `IO`. See the section on [backends](../backends/summary.md) for more details. -```{eval-rst} -.. note:: - - Only requests with the request method and uri can be sent. If trying to send a request without these components specified, a compile-time error will be reported. On how this is implemented, see the documentation on the :doc:`type of request definitions `. +```{note} +Only requests with the request method and uri can be sent. When trying to send a request without these components specified, a compile-time error will be reported. On how this is implemented, see the documentation on the [type of request definitions](type.md). ``` ## Initial requests -sttp provides two initial requests: +sttp provides three initial requests: -* `basicRequest`, which is an empty request with the `Accept-Encoding: gzip, deflate` header added. That's the one that is most commonly used. +* `basicRequest`, which is an empty request with the `Accept-Encoding: gzip, deflate` header added. That's the one that is most commonly used. By default reads the response as a `Either[String, String]` (indicating HTTP 4xx/5xx failure or 2xx success). * `emptyRequest`, a completely empty request, with no headers at all. +* `quickRequest`, which by default reads the response as a `String` in case of a success, and throws an exception / returns a failed effect in case of a 4xx/5xx HTTP error response. -Both of these requests will by default read the response body into a UTF-8 `String`. How the response body is handled is also part of the request definition. See the section on [response body specifications](../responses/body.md) for more details on how to customize that. - -## Debugging requests - -sttp comes with builtin request to curl converter. To convert request to curl invocation use `.toCurl` method. - -For example: - -```scala -basicRequest.get(uri"http://httpbin.org/ip").toCurl -// res1: String = """curl \ -// --request GET \ -// --url 'http://httpbin.org/ip' \ -// --header 'Accept-Encoding: gzip, deflate' \ -// --location \ -// --max-redirs 32""" -``` - -Note that the `Accept-Encoding` header, which is added by default to all requests (`Accept-Encoding: gzip, deflate`), can make curl warn that _binary output can mess up your terminal_, when running generated command from the command line. It can be omitted by setting `omitAcceptEncoding = true` when calling `.toCurl` method. +How the response body is handled can be (and very often is) customized. See the section on [response body specifications](../responses/body.md) for more details. diff --git a/generated-docs/out/requests/body.md b/generated-docs/out/requests/body.md index b170b4b2e6..0a62d4d49c 100644 --- a/generated-docs/out/requests/body.md +++ b/generated-docs/out/requests/body.md @@ -43,10 +43,8 @@ basicRequest.body(inputStream) If not specified before, these methods will set the content type to `application/octet-stream`. When using a byte array, additionally the content length will be set to the length of the array (unless specified explicitly). -```{eval-rst} -.. note:: - - While the object defining a request is immutable, setting a mutable request body will make the whole request definition mutable as well. With ``InputStream``, the request can be moreover sent only once, as input streams can be consumed once. +```{note} +While the object defining a request is immutable, setting a mutable request body will make the whole request definition mutable as well. With `InputStream`, the request can be moreover sent only once, as input streams can be consumed once. ``` ## Uploading files diff --git a/generated-docs/out/requests/cookies.md b/generated-docs/out/requests/cookies.md index 7ea14a5a0b..759e52394d 100644 --- a/generated-docs/out/requests/cookies.md +++ b/generated-docs/out/requests/cookies.md @@ -1,5 +1,7 @@ # Cookies +## Cookies on requests + Cookies sent in requests are key-value pairs contained in the `Cookie` header. They can be set on a request in a couple of ways. The first is using the `.cookie(name: String, value: String)` method. This will yield a new request definition which, when sent, will contain the given cookie. Cookies are currently only available on the JVM. diff --git a/generated-docs/out/requests/headers.md b/generated-docs/out/requests/headers.md index 446d7e5ca3..a70f132c51 100644 --- a/generated-docs/out/requests/headers.md +++ b/generated-docs/out/requests/headers.md @@ -1,5 +1,7 @@ # Headers +## Arbitrary headers + Arbitrary headers can be set on the request using the `.header` method: ```scala @@ -42,3 +44,16 @@ basicRequest.acceptEncoding("gzip, deflate") ``` See also documentation on setting [cookies](cookies.md) and [authentication](authentication.md). + +## Header names + +To avoid using string literals for header names, the `HeaderNames` object from sttp model can be used: + +```scala +import sttp.client4.* +import sttp.model.HeaderNames +import sttp.model.headers.CacheDirective +import scala.concurrent.duration.* + +basicRequest.header(HeaderNames.CacheControl, CacheDirective.MaxAge(1.day).toString) +``` \ No newline at end of file diff --git a/generated-docs/out/requests/multipart.md b/generated-docs/out/requests/multipart.md index 1952fda478..a43e9dc0f0 100644 --- a/generated-docs/out/requests/multipart.md +++ b/generated-docs/out/requests/multipart.md @@ -18,7 +18,7 @@ The content type of each part is by default the same as when setting simple bodi The parts can be specified using either a `Seq[Multipart]` or by using multiple arguments: ```scala -import sttp.client4._ +import sttp.client4.* basicRequest.multipartBody(Seq(multipart("p1", "v1"), multipart("p2", "v2"))) basicRequest.multipartBody(multipart("p1", "v1"), multipart("p2", "v2")) diff --git a/generated-docs/out/requests/streaming.md b/generated-docs/out/requests/streaming.md index f5fbb5b275..0b5c76e629 100644 --- a/generated-docs/out/requests/streaming.md +++ b/generated-docs/out/requests/streaming.md @@ -2,15 +2,13 @@ Some backends (see [backends summary](../backends/summary.md)) support streaming bodies, as described by the `Streams[S]` capability. If that's the case, you can set a stream of the supported type as a request body using the `streamBody` method, instead of the usual `body` method. -```{eval-rst} -.. note:: - - Here, streaming refers to (usually) non-blocking, asynchronous streams of data. To send data which is available as an ``InputStream``, or a file from local storage (which is available as a ``File`` or ``Path``), no special backend support is needed. See the documenttation on :doc:`setting the request body `. +```{note} +Here, streaming refers to (usually) non-blocking, asynchronous streams of data. To send data which is available as an `InputStream`, or a file from local storage (which is available as a `File` or `Path`), no special backend support is needed. See the documenttation on [setting the request body](body.md). ``` -An implementation of the `Streams[S]` capability must be passed to the `.streamBody` method, to determine the type of streams that are supported. These implementations are provided by the backend implementations, e.g. `AkkaStreams` or `Fs2Streams[F]`. +An implementation of the `Streams[S]` capability must be passed to the `.streamBody` method, to determine the type of streams that are supported. These implementations are provided by the backend implementations, e.g. `PekkoStreams` or `Fs2Streams[F]`. -For example, using the [akka-http backend](../backends/akka.md), a request with a streaming body can be defined as follows: +For example, using the [pekko-http backend](../backends/pekko.md), a request with a streaming body can be defined as follows: ```scala import sttp.client4.* @@ -27,8 +25,8 @@ basicRequest .streamBody(PekkoStreams)(source) ``` -```{eval-rst} -.. note:: A request with the body set as a stream can only be sent using a backend supporting exactly the given type of streams. +```{note} +A request with the body set as a stream can only be sent using a backend supporting exactly the given type of streams. ``` -It's also possible to specify that the [response body should be a stream](../responses/body.html#streaming). +It's also possible to specify that the [response body should be a stream](../responses/body.md). diff --git a/generated-docs/out/requests/type.md b/generated-docs/out/requests/type.md index 63583b3a31..d7a03861b1 100644 --- a/generated-docs/out/requests/type.md +++ b/generated-docs/out/requests/type.md @@ -1,18 +1,9 @@ # The type of request definitions -All request definitions have type `RequestT[U, T, R]` (RequestT as in Request Template). If this looks a bit complex, don't worry, what the three type parameters stand for is the only thing you'll hopefully have to remember when using the API! +Most request definitions have the type `Request[T]`. The `T` specifies the type to which the response will be read. By default, this is `Either[String, String]`. But it can also be e.g. `Array[Byte]` or `Unit`, if the response should be ignored. Response body handling can be changed by calling the `.response` method. With backends which support streaming, this can also be a supported stream type. See [response body specifications](../responses/body.md) for more details. -Going one-by-one: +If you're using streaming [request](streaming.md)/[response](../responses/body.md) bodies, you'll get a `StreamRequest[T, R]`. The `R` type parameter specifies the requirements of this request. In case of streaming, this will be an implementation of `Streams[S]`, such as `Fs2Streams[F]` or `PekkoStreams`. You can only send such requests using backends, which also support this type of streaming (this is verified at compile-time)! -* `U[_]` specifies if the request method and URL are specified. Using the API, this can be either `type Empty[X] = None`, meaning that the request has neither a method nor an URI. Or, it can be `type Id[X] = X` (type-level identity), meaning that the request has both a method and an URI specified. Only requests with a specified URI & method can be sent. -* `T` specifies the type to which the response will be read. By default, this is `Either[String, String]`. But it can also be e.g. `Array[Byte]` or `Unit`, if the response should be ignored. Response body handling can be changed by calling the `.response` method. With backends which support streaming, this can also be a supported stream type. See [response body specifications](../responses/body.md) for more details. -* `R` specifies the requirements of this request. Most of the time this will be `Any`, meaning that this request does not have any special requirements, and can be sent using any backend. So most of the time you can just ignore that parameter. However, if you are using streaming [request](streaming.md)/[response](../responses/body.md) bodies or [websockets](../websockets.md), the type parameter will reflect the required capabilities. They can include `Effect[F]`, `Streams[S]` and `WebSockets`. +If you're describing a web socket request, you'll get a `WebSocketRequest[F[_], T]`. The `F[_]` type parameter describes the effect, using which the web socket is processed (by default, the request then also contains the entire logic to interact with the web socket, although this is not strictly required). For synchronous web sockets, `F[_]` will be `Identity` ("no wrapper", direct-style). For asynchronous web sockets, `F[_]` might be `Future` or `IO` (or any other effect type). As with streams, such requests can only be sent using backends which support web sockets, and the effect type used to process the web socket. -There are two type aliases for the request template that are used: - -* `type Request[T, R] = RequestT[Identity, T, R]`. A sendable request. -* `type PartialRequest[T, R] = RequestT[Empty, T, R]` - -As `basicRequest`, the starting request, by default reads the body into a `Either[String, String]`, its type is: - -`basicRequest: PartialRequest[Either[String, String], Any]` +Finally, if you're processing the websocket using streaming, you'll work with a `WebSocketStreamRequest[T, S]`. As before, `S` will contain the required streaming capability. However, it will also contain the effect type, which is used to interact with the web socket. An example value for `S` might be `PekkoStreams & Effect[Future]`. diff --git a/generated-docs/out/responses/basics.md b/generated-docs/out/responses/basics.md index 8357ec3015..f8957e1df8 100644 --- a/generated-docs/out/responses/basics.md +++ b/generated-docs/out/responses/basics.md @@ -4,8 +4,8 @@ Responses are represented as instances of the case class `Response[T]`, where `T If sending the request fails, either due to client or connection errors, an exception will be thrown (synchronous backends), or a failed effect will be returned (e.g. a failed future). -```{eval-rst} -.. note:: If the request completes, but results in a non-2xx return code, the request is still considered successful, that is, a ``Response[T]`` will be returned. See :doc:`response body specifications ` for details on how such cases are handled. +```{note} +If the request completes, but results in a non-2xx return code, the request is still considered successful, that is, a `Response[T]` will be returned. See [response body specifications](body.md) for details on how such cases are handled. ``` ## Response code @@ -23,8 +23,7 @@ import sttp.model.* import sttp.client4.* val backend = DefaultSyncBackend() -val request = basicRequest - .get(uri"https://httpbin.org/get") +val request = basicRequest.get(uri"https://httpbin.org/get") val response = request.send(backend) val singleHeader: Option[String] = response.header(HeaderNames.Server) diff --git a/generated-docs/out/responses/body.md b/generated-docs/out/responses/body.md index b0a68bb09a..2d446d9d5f 100644 --- a/generated-docs/out/responses/body.md +++ b/generated-docs/out/responses/body.md @@ -2,7 +2,7 @@ By default, the received response body will be read as a `Either[String, String]`, using the encoding specified in the `Content-Type` response header (and if none is specified, using `UTF-8`). This is of course configurable: response bodies can be ignored, deserialized into custom types, received as a stream or saved to a file. -The default `response.body` will be a: +When using `basicRequest`, the default `response.body` will be a: * `Left(errorMessage)` if the request is successful, but response code is not 2xx. * `Right(body)` if the request is successful, and the response code is 2xx. @@ -14,12 +14,12 @@ How the response body will be read is part of the request description, as alread To conveniently specify how to deserialize the response body, a number of `as(...Type...)` methods are available. They can be used to provide a value for the request description's `response` property: ```scala -import sttp.client4._ +import sttp.client4.* basicRequest.response(asByteArray) ``` -When the above request is completely described and sent, it will result in a `Response[Either[String, Array[Byte]]]` (where the left and right correspond to non-2xx and 2xx status codes, as above). +When the above request is completely described and sent, it will result in a `Response[Either[String, Array[Byte]]]` (where the left and right correspond to non-2xx and 2xx status codes, as above). Other possible response descriptions include: @@ -31,12 +31,16 @@ import java.nio.file.Path def ignore: ResponseAs[Unit] = ??? def asString: ResponseAs[Either[String, String]] = ??? def asStringAlways: ResponseAs[String] = ??? +def asStringOrFail: ResponseAs[String] = ??? def asString(encoding: String): ResponseAs[Either[String, String]] = ??? def asStringAlways(encoding: String): ResponseAs[String] = ??? +def asStringOrFail(encoding: String): ResponseAs[String] = ??? def asByteArray: ResponseAs[Either[String, Array[Byte]]] = ??? def asByteArrayAlways: ResponseAs[Array[Byte]] = ??? +def asByteArrayOrFail: ResponseAs[Array[Byte]] = ??? def asParams: ResponseAs[Either[String, Seq[(String, String)]]] = ??? def asParamsAlways: ResponseAs[Seq[(String, String)]] = ??? +def asParamsOrFail: ResponseAs[Seq[(String, String)]] = ??? def asParams(encoding: String): ResponseAs[Either[String, Seq[(String, String)]]] = ??? def asParamsAlways(encoding: String): ResponseAs[Seq[(String, String)]] = ??? def asFile(file: File): ResponseAs[Either[String, File]] = ??? @@ -71,41 +75,39 @@ val someFile = new File("some/path") basicRequest.response(asFile(someFile)) ``` -```{eval-rst} -.. note:: - - As the handling of response is specified upfront, there's no need to "consume" the response body. It can be safely discarded if not needed. +```{note} +As the handling of response is specified upfront, there's no need to "consume" the response body. It can be safely discarded if not needed. ``` +The `as...Always` response descriptions will read the response body as the target type always, regardless of the status code. + ## Failing when the response code is not 2xx -Sometimes it's convenient to get a failed effect (or an exception thrown) when the response status code is not successful. In such cases, the response description can be modified using the `.orFail` combinator: +Sometimes it's convenient to get a failed effect (or an exception thrown) when the response status code is not successful. In such cases, you should use the `...OrFail` response description, or modify an existing response description using the `.orFail` combinator: ```scala import sttp.client4.* -basicRequest.response(asString.orFail): PartialRequest[String] +basicRequest.response(asStringOrFail): PartialRequest[String] ``` -The combinator works in all cases where the response body is specified to be deserialized as an `Either`. If the left is already an exception, it will be thrown unchanged. Otherwise, the left-value will be wrapped in an `HttpError`. +The `.orFail` combinator works in all cases where the response body is specified to be deserialized as an `Either`. If the left is already an exception, it will be thrown unchanged. Otherwise, the left-value will be wrapped in an `UnexpectedStatusCode`. -```{eval-rst} -.. note:: - - While both ``asStringAlways`` and ``asString.orFail`` have the type ``ResponseAs[String, Any]``, they are different. The first will return the response body as a string always, regardless of the responses' status code. The second will return a failed effect / throw a ``HttpError`` exception for non-2xx status codes, and the string as body only for 2xx status codes. +```{note} +While both `asStringAlways` and `asStringOrFail` have the type `ResponseAs[String]`, they are different. The first will return the response body as a string always, regardless of the responses' status code. The second will return a failed effect / throw a `UnexpectedStatusCode` exception for non-2xx status codes, and the string as body only for 2xx status codes. ``` -There's also a variant of the combinator, `.getEither`, which can be used to extract typed errors and fail the effect if there's a deserialization error. +There's also a variant of the combinator, `.orFailDeserialization`, which can be used to extract typed errors and fail the effect if there's a deserialization error. ## Custom body deserializers It's possible to define custom body deserializers by taking any of the built-in response descriptions and mapping over them. Each `ResponseAs` instance has `map` and `mapWithMetadata` methods, which can be used to transform it to a description for another type (optionally using response metadata, such as headers or the status code). Each such value is immutable and can be used multiple times. -```{eval-rst} -.. note:: Alternatively, response descriptions can be modified directly from the request description, by using the ``request.mapResponse(...)`` and ``request.mapResponseRight(...)`` methods (which is available, if the response body is deserialized to an either). That's equivalent to calling ``request.response(request.response.map(...))``, that is setting a new response description, to a modified old response description; but with shorter syntax. +```{note} +Alternatively, response descriptions can be modified directly from the request description, by using the ``request.mapResponse(...)`` method. That's equivalent to calling ``request.response(request.response.map(...))``, that is setting a new response description, to a modified old response description; but with shorter syntax. ``` -As an example, to read the response body as an int, the following response description can be defined (warning: this ignores the possibility of exceptions!): +As an example, to read the response body as an `Int`, the following response description can be defined (warning: this ignores the possibility of exceptions!): ```scala import sttp.client4.* @@ -132,7 +134,7 @@ basicRequest .response(asJson) ``` -A number of JSON libraries are supported out-of-the-box, see [json support](../json.md). +A number of JSON libraries are supported out-of-the-box, see [json support](../other/json.md). ## Response-metadata dependent deserializers @@ -151,7 +153,7 @@ sealed trait MyModel case class SuccessModel(name: String, age: Int) extends MyModel case class ErrorModel(message: String) extends MyModel -val myRequest: Request[Either[ResponseException[String, io.circe.Error], MyModel]] = +val myRequest: Request[Either[ResponseException[String], MyModel]] = basicRequest .get(uri"https://example.com") .response(fromMetadata( @@ -201,6 +203,9 @@ import sttp.model.ResponseMetadata def asStream[F[_], T, S](s: Streams[S])(f: s.BinaryStream => F[T]): StreamResponseAs[Either[String, T], Effect[F] with S] = ??? +def asStreamOrFail[F[_], T, S](s: Streams[S])(f: s.BinaryStream => F[T]): + StreamResponseAs[T, S with Effect[F]] = ??? + def asStreamWithMetadata[F[_], T, S](s: Streams[S])( f: (s.BinaryStream, ResponseMetadata) => F[T] ): StreamResponseAs[Either[String, T], Effect[F] with S] = ??? @@ -219,7 +224,7 @@ def asStreamUnsafeAlways[S](s: Streams[S]): StreamResponseAs[s.BinaryStream, S] = ??? ``` -All of these descriptions require the streaming capability to be passed as a parameter, an implementation of `Streams[S]`. This is used to determine the type of binary streams that are supported, and to require that the backend used to send the request supports the given type of streams. These implementations are provided by the backend implementations, e.g. `AkkaStreams` or `Fs2Streams[F]`. +All of these descriptions require the streaming capability to be passed as a parameter, an implementation of `Streams[S]`. This is used to determine the type of binary streams that are supported, and to require that the backend used to send the request supports the given type of streams. These implementations are provided by the backend implementations, e.g. `PekkoStreams` or `Fs2Streams[F]`. The first two "safe" variants pass the response stream to the user-provided function, which should consume the stream entirely. Once the effect returned by the function is complete, the backend will try to close the stream (if the streaming implementation allows it). diff --git a/generated-docs/out/responses/exceptions.md b/generated-docs/out/responses/exceptions.md index ec206538b2..d605931125 100644 --- a/generated-docs/out/responses/exceptions.md +++ b/generated-docs/out/responses/exceptions.md @@ -7,25 +7,30 @@ HTTP requests might fail in a variety of ways! There are two basic types of fail The first type of failures is represented by exceptions, which are thrown when sending the request (using `request.send(backend)`). The second type of failure is represented as a `Response[T]`, with the appropriate response code. The response body might depend on the status code; by default the response is read as a `Either[String, String]`, where the left side represents protocol-level failure, and the right side: success. -Exceptions might be thrown directly (`Identity` synchronous backends), or returned as failed effects (other backends, e.g. failed `scala.concurrent.Future`). Backends will try to categorise these exceptions into a `SttpClientException`, which has three subclasses: +Exceptions might be thrown directly (synchronous, direct-style backends), or returned as failed effects (other backends, e.g. failed `scala.concurrent.Future`). Backends will try to categorize these exceptions into a `SttpClientException`, which has two main subclass branches: * `ConnectException`: when a connection (tcp socket) can't be established to the target host * `ReadException`: when a connection has been established, but there's any kind of problem receiving the response (e.g. a broken socket) -* `TimeoutException`: a discriminated subtype of `ReadException` for timeout fails + +Read exceptions might be further categorized into: + +* `TimeoutException`: any kind of timeout when reading +* `TooManyRedirectsException`: throw by the [follow-redirects](../conf/redirects.md) backend +* `ResponseHandlingException`: wrapping a [[ResponseException]] (exception that occurs when handling the response body, using e.g. `asJson`) In general, it's safe to assume that the request hasn't been sent in case of connect exceptions. With read exceptions, the target host might or might not have received and processed the request. -Unknown exceptions aren't categorised and are re-thrown unchanged. +Unknown exceptions aren't categorized and are re-thrown unchanged. -## Deserialization errors +## Response-handling, deserialization errors -Exceptions might also be thrown when deserializing the response body - depending on the specification of how to handle response bodies. The built-in deserializers (see e.g. [json](../json.md)) return errors represented as `ResponseException[HE, DE]`, which can either be a `HttpError` (protocol-level failures, containing a potentially deserialized body value) or a `DeserializationException` (containing a deserialization-library-specific exception). +Exceptions might be thrown when deserializing the response body - depending on the specification of how to handle response bodies. The built-in deserializers (see e.g. [json](../other/json.md)) return errors represented as `ResponseException[HE]`, which can either be a `UnexpectedStatusCode` (protocol-level failures, containing a potentially deserialized body value) or a `DeserializationException` (containing a deserialization-library-specific exception). This means that a typical `asJson` response specification will result in the body being read as: ```scala -import sttp.client4._ -def asJson[T]: ResponseAs[Either[ResponseException[String, Exception], T]] = ??? +import sttp.client4.* +def asJson[T]: ResponseAs[Either[ResponseException[String], T]] = ??? ``` There are also the `.orFail` and `.orFailDeserialization` methods on eligible response specifications, which convert http errors or deserialization exceptions as failed effects. @@ -37,5 +42,5 @@ Summing up, when the response is deserialized (e.g. to json), sending a request * network-level success (HTTP request sent and response received) * http error (4xx, 5xx), successfully parsed (**a value wrapped in `Left`, or a failed effect**) * http success (2xx), successfully parsed (**a value possibly wrapped in `Right`**) - * deseralization error (**a value wrapped in `Left`, or a failed effect**) + * deserialization error (**a value wrapped in `Left`, or a failed effect**) * network-level failure (invalid host, broken socket): failed effect diff --git a/generated-docs/out/support.md b/generated-docs/out/support.md new file mode 100644 index 0000000000..199555add7 --- /dev/null +++ b/generated-docs/out/support.md @@ -0,0 +1,11 @@ +# Support & sponsorship + +## Sponsors + +Development and maintenance of sttp client is sponsored by [SoftwareMill](https://softwaremill.com), a software development and consulting company. We help clients scale their business through software. We offer services around migrating and maintaining Java and Scala projects (e.g. to Java 21, or across Scala versions), ML/AI discovery workshops, introducing developer platforms (based on Kubernetes and observability technologies), and others. Our areas of expertise include performant backends, distributed systems, machine learning and data analytics, with a focus on Java, Scala, Kafka, TypeScript and Rust. + +[![](https://files.softwaremill.com/logo/logo.png "SoftwareMill")](https://softwaremill.com) + +## Commercial Support + +We offer commercial support for sttp and related technologies, as well as development services. [Contact us](https://softwaremill.com/contact/) to learn more about our offer! diff --git a/generated-docs/out/testing/curl.md b/generated-docs/out/testing/curl.md new file mode 100644 index 0000000000..fe7bc43845 --- /dev/null +++ b/generated-docs/out/testing/curl.md @@ -0,0 +1,19 @@ +# Converting requests to CURL commands + +sttp comes with builtin request to curl converter. To convert request to curl invocation use `.toCurl` method. + +For example: + +```scala +import sttp.client4.* + +basicRequest.get(uri"http://httpbin.org/ip").toCurl +// res0: String = """curl \ +// --request GET \ +// --url 'http://httpbin.org/ip' \ +// --header 'Accept-Encoding: gzip, deflate' \ +// --location \ +// --max-redirs 32""" +``` + +Note that the `Accept-Encoding` header, which is added by default to all requests (`Accept-Encoding: gzip, deflate`), can make curl warn that _binary output can mess up your terminal_, when running generated command from the command line. It can be omitted by setting `omitAcceptEncoding = true` when calling `.toCurl` method. \ No newline at end of file diff --git a/generated-docs/out/testing.md b/generated-docs/out/testing/stub.md similarity index 81% rename from generated-docs/out/testing.md rename to generated-docs/out/testing/stub.md index ac9f6266a7..0d733998a6 100644 --- a/generated-docs/out/testing.md +++ b/generated-docs/out/testing/stub.md @@ -1,21 +1,28 @@ -# Testing +# The stub backend -If you need a stub backend for use in tests instead of a "real" backend (you probably don't want to make HTTP calls during unit tests), you can use the `SttpBackendStub` class. It allows specifying how the backend should respond to requests matching given predicates. +If you need a stub backend for use in tests instead of a "real" backend (you probably don't want to make HTTP calls during unit tests), you can use the `BackendStub` class. It allows specifying how the backend should respond to requests matching given predicates. -You can also create a stub backend using [akka-http routes](backends/akka.md). +The [pekko-http](../backends/pekko.md) or [akka-http](../backends/akka.md) backends also provide an alternative way to create a stub, from a request-transforming function. ## Creating a stub backend An empty backend stub can be created using the following ways: * by calling `.stub` on the "real" base backend's companion object, e.g. `HttpClientZioBackend.stub` or `HttpClientMonixBackend.stub` -* by using one of the factory methods `SttpBackendStub.synchronous` or `SttpBackendStub.asynchronousFuture`, which return stubs which use the `Identity` or standard Scala's `Future` effects without streaming support +* by using one of the factory methods `BackendStub.synchronous` or `BackendStub.asynchronousFuture`, which return stubs which use the `Identity` or standard Scala's `Future` effects without streaming support * by explicitly specifying the effect and supported capabilities: - * for Monix `SttpBackendStub[Task, MonixStreams with WebSockets](TaskMonad)` - * for cats `SttpBackendStub[IO, WebSockets](implicitly[MonadAsyncError[IO]])` - * for zio `SttpBackendStub[Task, WebSockets](new RIOMonadAsyncError[Any])` + * for cats-effect `BackendStub[IO](implicitly[MonadAsyncError[IO]])` + * for ZIO `BackendStub[Task](new RIOMonadAsyncError[Any])` + * for Monix `BackendStub[Task](TaskMonad)` +* by instantiating backend stubs which support streaming or WebSockets, mirroring the hierarchy of the base backends: + * `StreamBackendStub`, e.g. `StreamBackendStub[IO, Fs2Streams[IO]](implicitly[MonadAsyncError[IO]])` (for cats-effect with fs2) + * `WebSocketBackendStub`, e.g. `WebSocketBackendStub[Task](new RIOMonadAsyncError[Any])` (for ZIO) + * `WebSocketStreamBackendStub` + * `WebSocketSyncBackendStub` * by specifying a fallback/delegate backend, see below +Responses used in the stubbing can be created either by directly instantiating the `Response` class, or by using factory methods from `ResponseStub`. + Some code which will be reused among following examples: ```scala @@ -68,13 +75,11 @@ val response2 = basicRequest.post(uri"http://example.org/partialAda").send(testi // response2.body will be Right("Ada") ``` -```{eval-rst} -.. note:: - - This approach to testing has one caveat: the responses are not type-safe. That is, the stub backend cannot match on or verify that the type of the response body matches the response body type, as it was requested. However, when a "raw" response is provided (a ``String``, ``Array[Byte]``, ``InputStream``, or a non-blocking stream wrapped in ``RawStream``), it will be handled as specified by the response specification - see below for details. +```{note} +This approach to testing has one caveat: the responses are not type-safe. That is, the stub backend cannot match on or verify that the type of the response body matches the response body type, as it was requested. However, when a "raw" response is provided (a `String`, `Array[Byte]`, `InputStream`, or a non-blocking stream wrapped in `RawStream`), it will be handled as specified by the response specification - see below for details. ``` -Another way to specify the behaviour is passing response wrapped in the effect to the stub. It is useful if you need to test a scenario with a slow server, when the response should be not returned immediately, but after some time. Example with Futures: +Another way to specify the behavior is passing response wrapped in the effect to the stub. It is useful if you need to test a scenario with a slow server, when the response should be not returned immediately, but after some time. Example with Futures: ```scala val testingBackend = BackendStub.asynchronousFuture @@ -114,7 +119,7 @@ basicRequest.get(uri"http://example.org").send(testingBackend) // Right("O basicRequest.get(uri"http://example.org").send(testingBackend) // Right("OK, first") ``` -Or multiple `Response` instances: +Or multiple `Response` instances, created using `ResponseStub`: ```scala val testingBackend: SyncBackendStub = SyncBackendStub @@ -165,7 +170,7 @@ The following conversions are supported: * `InputStream` and `String` to `Array[Byte]` * `WebSocketStub` to `WebSocket` * `WebSocketStub` and `WebSocket` are supplied to the websocket-consuming functions, if the response specification describes such interactions -* `SttpBackendStub.RawStream` is always treated as a raw stream value, and returned when the response should be returned as a stream or consumed using the provided function +* `BackendStub.RawStream` is always treated as a raw stream value, and returned when the response should be returned as a stream or consumed using the provided function * any of the above to custom types through mapped response specifications ## Example: returning JSON @@ -184,7 +189,7 @@ val response = basicRequest.get(uri"http://example.com") .send(testingBackend) ``` -In the example above, the stub's rules specify that a response with a `String`-body should be returned for any request; the request, on the other hand, specifies that response body should be parsed from a byte array to a custom `User` type. These type don't match, so the `SttpBackendStub` will in this case convert the body to the desired type. +In the example above, the stub's rules specify that a response with a `String`-body should be returned for any request; the request, on the other hand, specifies that response body should be parsed from a byte array to a custom `User` type. These type don't match, so the `BackendStub` will in this case convert the body to the desired type. ## Example: returning a file @@ -242,7 +247,7 @@ val response2 = basicRequest.post(uri"http://api.internal/b").send(testingBacken ## Testing streams -Streaming responses can be stubbed the same as ordinary values, with one difference. If the stubbed response contains the raw stream, which should be then transformed as described by the response specification, the stub must know that it handles a raw stream. This can be achieved by wrapping the stubbed stream using `SttpBackendStub.RawStream`. +Streaming responses can be stubbed the same as ordinary values, with one difference. If the stubbed response contains the raw stream, which should be then transformed as described by the response specification, the stub must know that it handles a raw stream. This can be achieved by wrapping the stubbed stream using `BackendStub.RawStream`. If the response specification is a resource-safe consumer of the stream, the function will only be invoked if the body is a `RawStream` (with the contained value). @@ -287,7 +292,7 @@ the `WebSocket` trait is recommended. Using `RecordingSttpBackend` it's possible to capture all interactions in which a backend has been involved. -The recording backend is a [backend wrapper](backends/wrappers/custom.md), and it can wrap any backend, but it's most +The recording backend is a [backend wrapper](../backends/wrappers/custom.md), and it can wrap any backend, but it's most useful when combined with the backend stub. Example usage: