diff --git a/README.md b/README.md index 6258f0f80..d6ed9ca32 100755 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ import sttp.client4.quick.* 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 multi-part requests, and working with WebSockets. +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. diff --git a/core/src/main/scala/sttp/client4/testing/BackendStub.scala b/core/src/main/scala/sttp/client4/testing/BackendStub.scala index aa86e6814..c126bdf71 100644 --- a/core/src/main/scala/sttp/client4/testing/BackendStub.scala +++ b/core/src/main/scala/sttp/client4/testing/BackendStub.scala @@ -19,7 +19,6 @@ import scala.concurrent.ExecutionContext * Predicates can match requests basing on the URI or headers. A [[ClassCastException]] might occur if for a given * request, a response is specified with the incorrect or inconvertible body type. */ - class BackendStub[F[_]]( monad: MonadError[F], matchers: PartialFunction[GenericRequest[_, _], F[Response[_]]], diff --git a/docs/backends/zio.md b/docs/backends/zio.md index 9ab044807..5993fca6a 100644 --- a/docs/backends/zio.md +++ b/docs/backends/zio.md @@ -207,7 +207,7 @@ The `HttpClient` ZIO backend supports both regular and streaming [websockets](.. A stub backend can be created through the `.stub` method on the companion object, and configured as described in the [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/docs/index.md b/docs/index.md index b852808b7..aa0bd0e5f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -19,7 +19,7 @@ import sttp.client4.quick.* 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 multi-part requests, and working with WebSockets. +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. @@ -135,6 +135,7 @@ Third party projects: other/xml other/resilience other/openapi + other/sse .. toctree:: :maxdepth: 2 diff --git a/docs/other/sse.md b/docs/other/sse.md new file mode 100644 index 000000000..723d2bbec --- /dev/null +++ b/docs/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/docs/other/websockets.md b/docs/other/websockets.md index 9b5aa6a00..6cc95ffff 100644 --- a/docs/other/websockets.md +++ b/docs/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: @@ -10,9 +10,11 @@ A websocket request will be sent instead of a regular one if the response specif 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`. @@ -92,7 +94,7 @@ effect type class name ================ ========================================== ``` -## Using blocking, sycnhronous Ox streams +## 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 @@ -128,14 +130,14 @@ basicRequest 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` 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). diff --git a/docs/testing/stub.md b/docs/testing/stub.md index bb82f2307..7a559b20e 100644 --- a/docs/testing/stub.md +++ b/docs/testing/stub.md @@ -1,21 +1,28 @@ # 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 mdoc @@ -72,7 +79,7 @@ val response2 = basicRequest.post(uri"http://example.org/partialAda").send(testi 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 mdoc:compile-only val testingBackend = BackendStub.asynchronousFuture @@ -112,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 mdoc:compile-only val testingBackend: SyncBackendStub = SyncBackendStub @@ -163,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 @@ -182,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 @@ -240,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). diff --git a/effects/cats/src/main/scalajvm/sttp/client4/httpclient/cats/HttpClientCatsBackend.scala b/effects/cats/src/main/scalajvm/sttp/client4/httpclient/cats/HttpClientCatsBackend.scala index 25da7fb66..cca7208ae 100644 --- a/effects/cats/src/main/scalajvm/sttp/client4/httpclient/cats/HttpClientCatsBackend.scala +++ b/effects/cats/src/main/scalajvm/sttp/client4/httpclient/cats/HttpClientCatsBackend.scala @@ -139,7 +139,7 @@ object HttpClientCatsBackend { /** Create a stub backend for testing, which uses the [[F]] response wrapper. * - * See [[SttpBackendStub]] for details on how to configure stub responses. + * See [[sttp.client4.testing.BackendStub]] for details on how to configure stub responses. */ def stub[F[_]: Async]: WebSocketBackendStub[F] = WebSocketBackendStub(new CatsMonadAsyncError[F]) } diff --git a/effects/fs2-ce2/src/main/scalajvm/sttp/client4/httpclient/fs2/HttpClientFs2Backend.scala b/effects/fs2-ce2/src/main/scalajvm/sttp/client4/httpclient/fs2/HttpClientFs2Backend.scala index 73da9b9d4..1bf0d8f33 100644 --- a/effects/fs2-ce2/src/main/scalajvm/sttp/client4/httpclient/fs2/HttpClientFs2Backend.scala +++ b/effects/fs2-ce2/src/main/scalajvm/sttp/client4/httpclient/fs2/HttpClientFs2Backend.scala @@ -152,7 +152,7 @@ object HttpClientFs2Backend { /** Create a stub backend for testing, which uses the [[F]] response wrapper, and supports `Stream[F, Byte]` * streaming. * - * See [[SttpBackendStub]] for details on how to configure stub responses. + * See [[sttp.client4.testing.BackendStub]] for details on how to configure stub responses. */ def stub[F[_]: Concurrent]: WebSocketStreamBackendStub[F, Fs2Streams[F]] = WebSocketStreamBackendStub(implicitly) } diff --git a/effects/fs2/src/main/scalajvm/sttp/client4/httpclient/fs2/HttpClientFs2Backend.scala b/effects/fs2/src/main/scalajvm/sttp/client4/httpclient/fs2/HttpClientFs2Backend.scala index e943b9c4c..5539d6b6c 100644 --- a/effects/fs2/src/main/scalajvm/sttp/client4/httpclient/fs2/HttpClientFs2Backend.scala +++ b/effects/fs2/src/main/scalajvm/sttp/client4/httpclient/fs2/HttpClientFs2Backend.scala @@ -160,7 +160,7 @@ object HttpClientFs2Backend { /** Create a stub backend for testing, which uses the [[F]] response wrapper, and supports `Stream[F, Byte]` * streaming. * - * See [[SttpBackendStub]] for details on how to configure stub responses. + * See [[sttp.client4.testing.BackendStub]] for details on how to configure stub responses. */ def stub[F[_]: Async]: WebSocketStreamBackendStub[F, Fs2Streams[F]] = WebSocketStreamBackendStub(implicitly) } diff --git a/effects/monix/src/main/scalajvm/sttp/client4/httpclient/monix/HttpClientMonixBackend.scala b/effects/monix/src/main/scalajvm/sttp/client4/httpclient/monix/HttpClientMonixBackend.scala index baa2e5792..3957668dc 100644 --- a/effects/monix/src/main/scalajvm/sttp/client4/httpclient/monix/HttpClientMonixBackend.scala +++ b/effects/monix/src/main/scalajvm/sttp/client4/httpclient/monix/HttpClientMonixBackend.scala @@ -141,7 +141,7 @@ object HttpClientMonixBackend { /** Create a stub backend for testing, which uses the [[Task]] response wrapper, and supports `Observable[ByteBuffer]` * streaming. * - * See [[SttpBackendStub]] for details on how to configure stub responses. + * See [[sttp.client4.testing.BackendStub]] for details on how to configure stub responses. */ def stub: WebSocketStreamBackendStub[Task, MonixStreams] = WebSocketStreamBackendStub(TaskMonadAsyncError) } diff --git a/effects/zio1/src/main/scalajvm/sttp/client4/httpclient/zio/HttpClientZioBackend.scala b/effects/zio1/src/main/scalajvm/sttp/client4/httpclient/zio/HttpClientZioBackend.scala index 0b0b2b408..b1077994a 100644 --- a/effects/zio1/src/main/scalajvm/sttp/client4/httpclient/zio/HttpClientZioBackend.scala +++ b/effects/zio1/src/main/scalajvm/sttp/client4/httpclient/zio/HttpClientZioBackend.scala @@ -175,7 +175,7 @@ object HttpClientZioBackend { /** Create a stub backend for testing, which uses the [[Task]] response wrapper, and supports `Stream[Throwable, * ByteBuffer]` streaming. * - * See [[SttpBackendStub]] for details on how to configure stub responses. + * See [[sttp.client4.testing.BackendStub]] for details on how to configure stub responses. */ def stub: WebSocketStreamBackendStub[Task, ZioStreams] = WebSocketStreamBackendStub(new RIOMonadAsyncError[Any])