From 70a01d232c37ed07638e45f0232163e30be8c0d9 Mon Sep 17 00:00:00 2001 From: adamw Date: Fri, 3 Jan 2025 11:41:43 +0100 Subject: [PATCH] Examples, docs --- build.sbt | 2 + docs/backends/catseffect.md | 13 ++--- docs/backends/fs2.md | 13 ++--- docs/backends/future.md | 4 +- docs/backends/http4s.md | 7 +-- docs/backends/javascript/fetch.md | 4 +- docs/backends/monix.md | 4 +- docs/backends/scalaz.md | 4 +- docs/backends/start_stop.md | 2 +- docs/backends/synchronous.md | 16 ++---- docs/backends/wrappers/custom.md | 2 +- docs/backends/zio.md | 8 +-- docs/conf.py | 2 +- docs/examples.md | 2 +- docs/other/json.md | 2 +- docs/other/resilience.md | 9 ++-- docs/other/websockets.md | 44 +++++++--------- docs/other/xml.md | 29 +++++++---- docs/requests/basics.md | 6 +-- docs/requests/body.md | 6 +-- docs/responses/basics.md | 7 ++- docs/responses/body.md | 43 ++++++++------- docs/responses/exceptions.md | 4 +- docs/testing/stub.md | 6 +-- .../examples/PostFormSynchronous.scala | 6 +-- .../examples/fileUploadSynchronous.scala | 31 +++++++++++ .../getAndParseJsonSynchronousJsoniter.scala | 52 +++++++++++++++++++ .../postMultipartFormSynchronous.scala | 41 +++++++++++++++ .../main/scala/sttp/client4/examples/x.scala | 20 ------- 29 files changed, 247 insertions(+), 142 deletions(-) create mode 100644 examples/src/main/scala/sttp/client4/examples/fileUploadSynchronous.scala create mode 100644 examples/src/main/scala/sttp/client4/examples/getAndParseJsonSynchronousJsoniter.scala create mode 100644 examples/src/main/scala/sttp/client4/examples/postMultipartFormSynchronous.scala delete mode 100644 examples/src/main/scala/sttp/client4/examples/x.scala diff --git a/build.sbt b/build.sbt index 20d08fd8cf..ce972cf999 100644 --- a/build.sbt +++ b/build.sbt @@ -972,6 +972,7 @@ lazy val examples = (projectMatrix in file("examples")) libraryDependencies ++= Seq( "io.circe" %% "circe-generic" % circeVersion, "org.json4s" %% "json4s-native" % json4sVersion, + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % jsoniterVersion, pekkoStreams, logback ), @@ -986,6 +987,7 @@ lazy val examples = (projectMatrix in file("examples")) json4s, circe, upickle, + jsoniter, scribeBackend, slf4jBackend, ox diff --git a/docs/backends/catseffect.md b/docs/backends/catseffect.md index 162cb9d29d..7150d7f507 100644 --- a/docs/backends/catseffect.md +++ b/docs/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`. @@ -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" % "@VERSION@" // for cats-effect 3.x -// or -"com.softwaremill.sttp.client4" %% "armeria-backend-cats-ce2" % "@VERSION@" // for cats-effect 2.x +// for cats-effect 3.x +"com.softwaremill.sttp.client4" %% "armeria-backend-cats" % "@VERSION@" +// or for cats-effect 2.x +"com.softwaremill.sttp.client4" %% "armeria-backend-cats-ce2" % "@VERSION@" ``` 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). diff --git a/docs/backends/fs2.md b/docs/backends/fs2.md index 66ccb33632..11163117b8 100644 --- a/docs/backends/fs2.md +++ b/docs/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 @@ -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" % "@VERSION@" // for cats-effect 3.x & fs2 3.x -// or -"com.softwaremill.sttp.client4" %% "armeria-backend-fs2" % "@VERSION@" // for cats-effect 2.x & fs2 2.x +// for cats-effect 3.x & fs2 3.x +"com.softwaremill.sttp.client4" %% "armeria-backend-fs2" % "@VERSION@" +// or for cats-effect 2.x & fs2 2.x +"com.softwaremill.sttp.client4" %% "armeria-backend-fs2" % "@VERSION@" ``` 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). diff --git a/docs/backends/future.md b/docs/backends/future.md index 8397c0426d..7d19592efa 100644 --- a/docs/backends/future.md +++ b/docs/backends/future.md @@ -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. diff --git a/docs/backends/http4s.md b/docs/backends/http4s.md index 315a63eeb9..deb554b68a 100644 --- a/docs/backends/http4s.md +++ b/docs/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" % "@VERSION@" // for cats-effect 3.x & http4s 1.0.0-Mx -// or -"com.softwaremill.sttp.client4" %% "http4s-ce2-backend" % "@VERSION@" // 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" % "@VERSION@" +// or for cats-effect 2.x & http4s 0.21.x +"com.softwaremill.sttp.client4" %% "http4s-ce2-backend" % "@VERSION@" ``` The backend can be created in a couple of ways, e.g.: diff --git a/docs/backends/javascript/fetch.md b/docs/backends/javascript/fetch.md index e4a31a10d1..6d51112022 100644 --- a/docs/backends/javascript/fetch.md +++ b/docs/backends/javascript/fetch.md @@ -151,8 +151,8 @@ 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 diff --git a/docs/backends/monix.md b/docs/backends/monix.md index 558366fe78..e5b9cd226b 100644 --- a/docs/backends/monix.md +++ b/docs/backends/monix.md @@ -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). diff --git a/docs/backends/scalaz.md b/docs/backends/scalaz.md index c161a96279..e77dd966ae 100644 --- a/docs/backends/scalaz.md +++ b/docs/backends/scalaz.md @@ -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). diff --git a/docs/backends/start_stop.md b/docs/backends/start_stop.md index cf604343f0..e8de0c7faa 100644 --- a/docs/backends/start_stop.md +++ b/docs/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/docs/backends/synchronous.md b/docs/backends/synchronous.md index 9b1d48e501..723905bcc8 100644 --- a/docs/backends/synchronous.md +++ b/docs/backends/synchronous.md @@ -102,23 +102,17 @@ Both HttpClient and OkHttp backends support regular [websockets](../other/websoc "com.softwaremill.sttp.client4" %% "ox" % "@VERSION@", ``` -```scala -import ox.* -import ox.channels.Source +```scala mdoc:compile-only 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/docs/backends/wrappers/custom.md b/docs/backends/wrappers/custom.md index 872428894b..ef62bacfb8 100644 --- a/docs/backends/wrappers/custom.md +++ b/docs/backends/wrappers/custom.md @@ -258,7 +258,7 @@ 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: diff --git a/docs/backends/zio.md b/docs/backends/zio.md index 92b79026d2..9ab044807b 100644 --- a/docs/backends/zio.md +++ b/docs/backends/zio.md @@ -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). diff --git a/docs/conf.py b/docs/conf.py index d59d989412..4b0be4c934 100644 --- a/docs/conf.py +++ b/docs/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/docs/examples.md b/docs/examples.md index bede5439bc..84dda775f8 100644 --- a/docs/examples.md +++ b/docs/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/docs/other/json.md b/docs/other/json.md index 634683956e..1b7ff0b61c 100644 --- a/docs/other/json.md +++ b/docs/other/json.md @@ -72,7 +72,7 @@ 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" % "@VERSION@" diff --git a/docs/other/resilience.md b/docs/other/resilience.md index b58e694753..f17a76ba9e 100644 --- a/docs/other/resilience.md +++ b/docs/other/resilience.md @@ -16,16 +16,16 @@ Still, the input for a particular resilience model might involve both the result 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`. +This predicate is available as `RetryWhen.Default` and has type `(GenericRequest[_, _], Either[Throwable, Response[_]]) => Boolean`. -See also the "retrying using ZIO" [example](../examples.md), as well as an example of a very simple [retrying backend wrapper](../backends/wrappers/custom.md). +See also the "resiliency" [examples](../examples.md), as well as an example of a very simple [retrying backend wrapper](../backends/wrappers/custom.md). ### Backend-specific retries @@ -42,7 +42,8 @@ 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 diff --git a/docs/other/websockets.md b/docs/other/websockets.md index 64402a5f21..adf43f5d58 100644 --- a/docs/other/websockets.md +++ b/docs/other/websockets.md @@ -8,7 +8,7 @@ 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. ## Using `WebSocket` @@ -32,17 +32,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,7 +48,7 @@ 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. @@ -60,9 +56,9 @@ Similar response specifications, but using an effect wrapper and `WebSocket[F]`, 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 +72,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]): - WebSocketStreamResponseAs[Unit, S] = ??? +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 +92,7 @@ effect type class name ================ ========================================== ``` -## WebSockets as Ox Source and Sink +## Using blocking, sycnhronous 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 +100,7 @@ as Ox `Source` and `Sink`: ``` // sbt dependency -"com.softwaremill.sttp.client4" %% "ox" % "@VERSION@", +"com.softwaremill.sttp.client4" %% "ox" % "@VERSION@" ``` ```scala @@ -118,7 +114,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 +126,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/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 contiunually 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` 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. 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/docs/other/xml.md b/docs/other/xml.md index b89388c351..d19ab57cfb 100644 --- a/docs/other/xml.md +++ b/docs/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,16 +20,19 @@ 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) + // response body handling description def asXml[B: XMLFormat]: ResponseAs[Either[ResponseException[String, Exception], B], Any] = asString.mapWithMetadata(ResponseAs.deserializeRightWithError(deserializeXml[B])) .showAs("either(as string, as xml)") @@ -49,11 +52,17 @@ 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.* + +// 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, Exception], Outer]] = basicRequest diff --git a/docs/requests/basics.md b/docs/requests/basics.md index 33de6087af..77c707ca89 100644 --- a/docs/requests/basics.md +++ b/docs/requests/basics.md @@ -42,10 +42,8 @@ val response: Response[Either[String, String]] = request.send(backend) 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. 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 :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 diff --git a/docs/requests/body.md b/docs/requests/body.md index a9242a5aeb..b6d04ead17 100644 --- a/docs/requests/body.md +++ b/docs/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/docs/responses/basics.md b/docs/responses/basics.md index 5254dab1c4..617b5bb20a 100644 --- a/docs/responses/basics.md +++ b/docs/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/docs/responses/body.md b/docs/responses/body.md index 3f63cbebe2..429b23baa0 100644 --- a/docs/responses/body.md +++ b/docs/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 mdoc:compile-only -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 mdoc:compile-only 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 `HttpError`. -```{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 ``HttpError`` 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 mdoc:compile-only import sttp.client4.* @@ -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/docs/responses/exceptions.md b/docs/responses/exceptions.md index f85f039696..fd5e824831 100644 --- a/docs/responses/exceptions.md +++ b/docs/responses/exceptions.md @@ -7,7 +7,7 @@ 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 categorise these exceptions into a `SttpClientException`, which has three subclasses: * `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) @@ -24,7 +24,7 @@ Exceptions might also be thrown when deserializing the response body - depending This means that a typical `asJson` response specification will result in the body being read as: ```scala mdoc:silent -import sttp.client4._ +import sttp.client4.* def asJson[T]: ResponseAs[Either[ResponseException[String, Exception], T]] = ??? ``` diff --git a/docs/testing/stub.md b/docs/testing/stub.md index 32144b9ca9..bb82f23078 100644 --- a/docs/testing/stub.md +++ b/docs/testing/stub.md @@ -68,10 +68,8 @@ 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: diff --git a/examples/src/main/scala/sttp/client4/examples/PostFormSynchronous.scala b/examples/src/main/scala/sttp/client4/examples/PostFormSynchronous.scala index d8eee71d5f..66e5561c27 100644 --- a/examples/src/main/scala/sttp/client4/examples/PostFormSynchronous.scala +++ b/examples/src/main/scala/sttp/client4/examples/PostFormSynchronous.scala @@ -1,6 +1,6 @@ -// {cat=Hello, World!; effects=Direct; backend=HttpClient}: Post form data +// {cat=Hello, World!; effects=Direct; backend=HttpClient}: POST form data -//> using dep com.softwaremill.sttp.client4::core:4.0.0-M20 +//> using dep com.softwaremill.sttp.client4::core:4.0.0-M22 package sttp.client4.examples @@ -18,5 +18,5 @@ import sttp.client4.* val backend = DefaultSyncBackend() val response = request.send(backend) + // the resposne body should contain a "form" field with the uploaded form data println(response.body) - println(response.headers) diff --git a/examples/src/main/scala/sttp/client4/examples/fileUploadSynchronous.scala b/examples/src/main/scala/sttp/client4/examples/fileUploadSynchronous.scala new file mode 100644 index 0000000000..ca30362eae --- /dev/null +++ b/examples/src/main/scala/sttp/client4/examples/fileUploadSynchronous.scala @@ -0,0 +1,31 @@ +// {cat=Hello, World!; effects=Direct; backend=HttpClient}: Upload file + +//> using dep com.softwaremill.sttp.client4::core:4.0.0-M22 + +package sttp.client4.examples + +import sttp.client4.* +import java.nio.file.Files +import java.nio.file.Path + +@main def fileUploadSynchronous(): Unit = + withTemporaryFile("Hello, World!".getBytes) { file => + val request = basicRequest + .body(file) + .post(uri"https://httpbin.org/post") + + val backend: SyncBackend = DefaultSyncBackend() + val response: Response[Either[String, String]] = request.send(backend) + + // the uploaded data should be echoed in the "data" field of the response body + println(response.body) + } + +private def withTemporaryFile[T](data: Array[Byte])(f: Path => T): T = { + val file = Files.createTempFile("sttp", "demo") + try + Files.write(file, data) + f(file) + finally + val _ = Files.deleteIfExists(file) +} diff --git a/examples/src/main/scala/sttp/client4/examples/getAndParseJsonSynchronousJsoniter.scala b/examples/src/main/scala/sttp/client4/examples/getAndParseJsonSynchronousJsoniter.scala new file mode 100644 index 0000000000..a867b62740 --- /dev/null +++ b/examples/src/main/scala/sttp/client4/examples/getAndParseJsonSynchronousJsoniter.scala @@ -0,0 +1,52 @@ +// {cat=JSON; effects=Direct; backend=HttpClient}: Receive & parse JSON using jsoniter + +//> using dep com.softwaremill.sttp.client4::jsoniter:4.0.0-M22 +//> using dep com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-macros:2.33.0 + +package sttp.client4.examples + +import sttp.client4.* +import sttp.client4.jsoniter.* +import com.github.plokhotnyuk.jsoniter_scala.core.JsonValueCodec +import com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker +import com.github.plokhotnyuk.jsoniter_scala.core.JsonWriter +import com.github.plokhotnyuk.jsoniter_scala.core.JsonKeyCodec +import com.github.plokhotnyuk.jsoniter_scala.core.JsonReader + +@main def getAndParseJsonSynchronousJsoniter(): Unit = + // data structures + case class Address(street: String, city: String) + enum AddressType: + case Home, Work + case class PersonalData(name: String, age: Int, addresses: Map[AddressType, Address]) + case class HttpBinResponse(origin: String, headers: Map[String, String], data: String) + + // jsoniter codecs + given JsonKeyCodec[AddressType] with + def decodeKey(in: JsonReader): AddressType = AddressType.valueOf(in.readKeyAsString()) + def encodeKey(x: AddressType, out: JsonWriter): Unit = out.writeKey(x.toString()) + given JsonValueCodec[PersonalData] = JsonCodecMaker.make + given JsonValueCodec[HttpBinResponse] = JsonCodecMaker.make + + // sending & receiving JSON + val request = basicRequest + .post(uri"https://httpbin.org/post") + .body( + asJson( + PersonalData( + "Alice", + 25, + Map( + AddressType.Home -> Address("Marszałkowska", "Warsaw"), + AddressType.Work -> Address("Długa", "Gdańsk") + ) + ) + ) + ) + .response(asJsonOrFail[HttpBinResponse]) + + val backend: SyncBackend = DefaultSyncBackend() + val response: Response[HttpBinResponse] = request.send(backend) + + println(s"Got response code: ${response.code}") + println(response.body) diff --git a/examples/src/main/scala/sttp/client4/examples/postMultipartFormSynchronous.scala b/examples/src/main/scala/sttp/client4/examples/postMultipartFormSynchronous.scala new file mode 100644 index 0000000000..6a41512d45 --- /dev/null +++ b/examples/src/main/scala/sttp/client4/examples/postMultipartFormSynchronous.scala @@ -0,0 +1,41 @@ +// {cat=Hello, World!; effects=Direct; backend=HttpClient}: POST multipart form + +//> using dep com.softwaremill.sttp.client4::core:4.0.0-M22 + +package sttp.client4.examples + +import sttp.client4.* +import java.nio.file.Files +import java.nio.file.Path +import sttp.model.MediaType + +@main def postMultipartFormSynchronous(): Unit = + withTemporaryFile("Hello, World!".getBytes) { file1 => + withTemporaryFile("".getBytes) { file2 => + val request = basicRequest + .multipartBody( + List( + multipart("name", "John"), + multipartFile("bio", file1), + multipartFile("avatar", file2).contentType(MediaType.ImagePng), + multipart("link", "http://john.doe.com") + ) + ) + .post(uri"https://httpbin.org/post") + + val backend: SyncBackend = DefaultSyncBackend() + val response: Response[Either[String, String]] = request.send(backend) + + // the resposne body should contain a "files" and "form" fields with the uploaded multipart data + println(response.body) + } + } + +private def withTemporaryFile[T](data: Array[Byte])(f: Path => T): T = { + val file = Files.createTempFile("sttp", "demo") + try + Files.write(file, data) + f(file) + finally + val _ = Files.deleteIfExists(file) +} diff --git a/examples/src/main/scala/sttp/client4/examples/x.scala b/examples/src/main/scala/sttp/client4/examples/x.scala deleted file mode 100644 index 42d8bc22db..0000000000 --- a/examples/src/main/scala/sttp/client4/examples/x.scala +++ /dev/null @@ -1,20 +0,0 @@ -//> using dep com.softwaremill.sttp.client4::core:4.0.0-M20 - -import sttp.client4.* - -@main def sttpDemo(): Unit = - val sort: Option[String] = None - val query = "http language:scala" - - // 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 backend = DefaultSyncBackend() - val response = request.send(backend) - - // response.header(...): Option[String] - println(response.header("Content-Length")) - - // response.body: by default read into an Either[String, String] to indicate failure or success - println(response.body)