├── .adr-dir ├── .git-blame-ignore-revs ├── .github ├── labeler.yml ├── pull_request_template.md ├── release-drafter.yml └── workflows │ ├── ci.yml │ └── scala-steward.yml ├── .gitignore ├── .readthedocs.yaml ├── .sbtopts ├── .scala-steward.conf ├── .scalafmt.conf ├── LICENSE ├── README.md ├── akka-http-backend └── src │ ├── main │ └── scala │ │ └── sttp │ │ └── client4 │ │ └── akkahttp │ │ ├── AkkaCompressor.scala │ │ ├── AkkaHttpBackend.scala │ │ ├── AkkaHttpClient.scala │ │ ├── AkkaHttpServerSentEvents.scala │ │ ├── BodyFromAkka.scala │ │ ├── BodyToAkka.scala │ │ ├── FromAkka.scala │ │ ├── ToAkka.scala │ │ ├── Util.scala │ │ └── akkaDecompressors.scala │ └── test │ └── scala │ └── sttp │ └── client4 │ └── akkahttp │ ├── AkkaHttpClientHttpTest.scala │ ├── AkkaHttpRouteBackendTest.scala │ ├── AkkaHttpStreamingTest.scala │ ├── AkkaHttpWebSocketTest.scala │ └── BackendStubAkkaTests.scala ├── armeria-backend ├── cats-ce2 │ └── src │ │ ├── main │ │ └── scala │ │ │ └── sttp │ │ │ └── client4 │ │ │ └── armeria │ │ │ └── cats │ │ │ └── ArmeriaCatsBackend.scala │ │ └── test │ │ └── scala │ │ └── sttp │ │ └── client4 │ │ └── armeria │ │ └── cats │ │ └── ArmeriaCatsHttpTest.scala ├── cats │ └── src │ │ ├── main │ │ └── scala │ │ │ └── sttp │ │ │ └── client4 │ │ │ └── armeria │ │ │ └── cats │ │ │ └── ArmeriaCatsBackend.scala │ │ └── test │ │ └── scala │ │ └── sttp │ │ └── client4 │ │ └── armeria │ │ └── cats │ │ └── ArmeriaCatsHttpTest.scala ├── fs2-ce2 │ └── src │ │ ├── main │ │ └── scala │ │ │ └── sttp │ │ │ └── client4 │ │ │ └── armeria │ │ │ └── fs2 │ │ │ └── ArmeriaFs2Backend.scala │ │ └── test │ │ └── scala │ │ └── sttp │ │ └── client4 │ │ └── armeria │ │ └── fs2 │ │ ├── ArmeriaFs2HttpTest.scala │ │ └── ArmeriaFs2StreamingTest.scala ├── fs2 │ └── src │ │ ├── main │ │ └── scala │ │ │ └── sttp │ │ │ └── client4 │ │ │ └── armeria │ │ │ └── fs2 │ │ │ └── ArmeriaFs2Backend.scala │ │ └── test │ │ └── scala │ │ └── sttp │ │ └── client4 │ │ └── armeria │ │ └── fs2 │ │ ├── ArmeriaFs2HttpTest.scala │ │ └── ArmeriaFs2StreamingTest.scala ├── monix │ └── src │ │ ├── main │ │ └── scala │ │ │ └── sttp │ │ │ └── client4 │ │ │ └── armeria │ │ │ └── monix │ │ │ └── ArmeriaMonixBackend.scala │ │ └── test │ │ └── scala │ │ └── sttp │ │ └── client4 │ │ └── armeria │ │ └── monix │ │ ├── ArmeriaMonixHttpTest.scala │ │ └── ArmeriaMonixStreamingTest.scala ├── scalaz │ └── src │ │ ├── main │ │ └── scala │ │ │ └── sttp │ │ │ └── client4 │ │ │ └── armeria │ │ │ └── scalaz │ │ │ └── ArmeriaScalazBackend.scala │ │ └── test │ │ └── scala │ │ └── sttp │ │ └── client4 │ │ └── armeria │ │ └── scalaz │ │ └── ArmeriaScalazHttpTest.scala ├── src │ ├── main │ │ └── scala │ │ │ └── sttp │ │ │ └── client4 │ │ │ └── armeria │ │ │ ├── AbstractArmeriaBackend.scala │ │ │ ├── ArmeriaWebClient.scala │ │ │ ├── AuthProxyConfigSelector.scala │ │ │ ├── BodyFromStreamMessage.scala │ │ │ ├── StreamMessageAggregator.scala │ │ │ ├── UnknownStatusException.scala │ │ │ └── future │ │ │ └── ArmeriaFutureBackend.scala │ └── test │ │ └── scala │ │ └── sttp │ │ └── client4 │ │ └── armeria │ │ └── future │ │ └── ArmeriaFutureHttpTest.scala ├── zio │ └── src │ │ ├── main │ │ └── scala │ │ │ └── sttp │ │ │ └── client4 │ │ │ └── armeria │ │ │ └── zio │ │ │ ├── ArmeriaZioBackend.scala │ │ │ └── package.scala │ │ └── test │ │ └── scala │ │ └── sttp │ │ └── client4 │ │ └── armeria │ │ └── zio │ │ ├── ArmeriaZioHttpTest.scala │ │ └── ArmeriaZioStreamingTest.scala └── zio1 │ └── src │ ├── main │ └── scala │ │ └── sttp │ │ └── client4 │ │ └── armeria │ │ └── zio │ │ ├── ArmeriaZioBackend.scala │ │ └── zio.scala │ └── test │ └── scala │ └── sttp │ └── client4 │ └── armeria │ └── zio │ ├── ArmeriaZioHttpTest.scala │ └── ArmeriaZioStreamingTest.scala ├── banner.png ├── build.sbt ├── caching └── src │ ├── main │ └── scala │ │ └── sttp │ │ └── client4 │ │ └── caching │ │ ├── Cache.scala │ │ ├── CachedResponse.scala │ │ ├── CachingBackend.scala │ │ └── CachingConfig.scala │ └── test │ └── scala │ └── sttp │ └── client4 │ └── caching │ └── CachingBackendTest.scala ├── core └── src │ ├── main │ ├── resources │ │ └── scala-native │ │ │ └── ffi.c │ ├── scala │ │ └── sttp │ │ │ └── client4 │ │ │ ├── BackendOptions.scala │ │ │ ├── RequestOptions.scala │ │ │ ├── Response.scala │ │ │ ├── ResponseAs.scala │ │ │ ├── ResponseException.scala │ │ │ ├── RetryWhen.scala │ │ │ ├── SpecifyAuthScheme.scala │ │ │ ├── SttpApi.scala │ │ │ ├── SttpClientException.scala │ │ │ ├── SttpWebSocketAsyncApi.scala │ │ │ ├── SttpWebSocketStreamApi.scala │ │ │ ├── SttpWebSocketSyncApi.scala │ │ │ ├── backend.scala │ │ │ ├── compression │ │ │ ├── CompressionHandlers.scala │ │ │ ├── Compressor.scala │ │ │ └── Decompressor.scala │ │ │ ├── internal │ │ │ ├── BodyFromResponseAs.scala │ │ │ ├── DigestAuthenticator.scala │ │ │ ├── LimitedInputStream.scala │ │ │ ├── NoStreams.scala │ │ │ ├── OnEndInputStream.scala │ │ │ ├── SttpFile.scala │ │ │ ├── ToCurlConverter.scala │ │ │ ├── ToRfc2616Converter.scala │ │ │ ├── WwwAuthHeaderParser.scala │ │ │ ├── package.scala │ │ │ └── ws │ │ │ │ ├── FutureSimpleQueue.scala │ │ │ │ ├── SimpleQueue.scala │ │ │ │ ├── SyncQueue.scala │ │ │ │ └── WebSocketEvent.scala │ │ │ ├── listener │ │ │ ├── ListenerBackend.scala │ │ │ └── RequestListener.scala │ │ │ ├── logging │ │ │ ├── Log.scala │ │ │ ├── LogConfig.scala │ │ │ ├── LogContext.scala │ │ │ ├── Logger.scala │ │ │ ├── LoggingBackend.scala │ │ │ ├── LoggingOptions.scala │ │ │ └── ResponseTimings.scala │ │ │ ├── monad │ │ │ ├── FunctionK.scala │ │ │ └── MapEffect.scala │ │ │ ├── package.scala │ │ │ ├── request.scala │ │ │ ├── requestBody.scala │ │ │ ├── requestBuilder.scala │ │ │ ├── testing │ │ │ ├── AbstractBackendStub.scala │ │ │ ├── AtomicCyclicIterator.scala │ │ │ ├── BackendStub.scala │ │ │ ├── RecordingBackend.scala │ │ │ ├── ResponseStub.scala │ │ │ ├── StreamBackendStub.scala │ │ │ ├── StubBody.scala │ │ │ ├── SyncBackendStub.scala │ │ │ ├── WebSocketBackendStub.scala │ │ │ ├── WebSocketStreamBackendStub.scala │ │ │ ├── WebSocketStreamConsumer.scala │ │ │ ├── WebSocketSyncBackendStub.scala │ │ │ └── package.scala │ │ │ ├── wrappers │ │ │ ├── DelegateBackend.scala │ │ │ ├── DigestAuthenticationBackend.scala │ │ │ ├── EitherBackend.scala │ │ │ ├── FollowRedirectsBackend.scala │ │ │ ├── MappedEffectBackend.scala │ │ │ ├── ResolveRelativeUrisBackend.scala │ │ │ └── TryBackend.scala │ │ │ └── ws │ │ │ ├── SyncWebSocket.scala │ │ │ ├── exceptions.scala │ │ │ └── package.scala │ ├── scalajs │ │ └── sttp │ │ │ └── client4 │ │ │ ├── DefaultFutureBackend.scala │ │ │ ├── PartialRequestExtensions.scala │ │ │ ├── SttpClientExceptionExtensions.scala │ │ │ ├── SttpExtensions.scala │ │ │ ├── ToCurlConverterTestExtension.scala │ │ │ ├── WebSocketImpl.scala │ │ │ ├── compression │ │ │ ├── CompressorExtensions.scala │ │ │ └── DecompressorExtensions.scala │ │ │ ├── fetch │ │ │ ├── AbstractFetchBackend.scala │ │ │ └── FetchBackend.scala │ │ │ ├── internal │ │ │ ├── ConvertFromFuture.scala │ │ │ ├── JSSimpleQueue.scala │ │ │ ├── MessageDigestCompatibility.scala │ │ │ ├── SparkMD5.scala │ │ │ └── SttpFileExtensions.scala │ │ │ └── quick.scala │ ├── scalajvm │ │ └── sttp │ │ │ └── client4 │ │ │ ├── DefaultFutureBackend.scala │ │ │ ├── DefaultSyncBackend.scala │ │ │ ├── PartialRequestExtensions.scala │ │ │ ├── SttpClientExceptionExtensions.scala │ │ │ ├── SttpExtensions.scala │ │ │ ├── compression │ │ │ ├── CompressorExtensions.scala │ │ │ ├── DecompressorExtensions.scala │ │ │ ├── GZIPCompressingInputStream.scala │ │ │ ├── defaultCompressors.scala │ │ │ └── defaultDecompressors.scala │ │ │ ├── httpclient │ │ │ ├── BodyProgressCallback.scala │ │ │ ├── HttpClientAsyncBackend.scala │ │ │ ├── HttpClientBackend.scala │ │ │ ├── HttpClientFutureBackend.scala │ │ │ └── HttpClientSyncBackend.scala │ │ │ ├── httpurlconnection │ │ │ └── HttpURLConnectionBackend.scala │ │ │ ├── internal │ │ │ ├── FileHelpers.scala │ │ │ ├── MessageDigestCompatibility.scala │ │ │ ├── SafeGZIPInputStream.scala │ │ │ ├── SttpFileExtensions.scala │ │ │ ├── SttpToJavaConverters.scala │ │ │ └── httpclient │ │ │ │ ├── BodyFromHttpClient.scala │ │ │ │ ├── BodyToHttpClient.scala │ │ │ │ ├── DelegatingWebSocketListener.scala │ │ │ │ ├── FutureSequencer.scala │ │ │ │ ├── IdSequencer.scala │ │ │ │ ├── InputStreamBodyFromHttpClient.scala │ │ │ │ ├── MultiPartBodyPublisher.java │ │ │ │ ├── MultipartBodyBuilder.scala │ │ │ │ ├── Sequencer.scala │ │ │ │ ├── WebSocketImpl.scala │ │ │ │ └── package.scala │ │ │ └── quick.scala │ └── scalanative │ │ └── sttp │ │ └── client4 │ │ ├── DefaultSyncBackend.scala │ │ ├── FileHelpers.scala │ │ ├── PartialRequestExtensions.scala │ │ ├── SttpClientExceptionExtensions.scala │ │ ├── SttpExtensions.scala │ │ ├── compression │ │ ├── CompressorExtensions.scala │ │ └── DecompressorExtensions.scala │ │ ├── curl │ │ ├── AbstractCurlBackend.scala │ │ ├── CurlBackends.scala │ │ └── internal │ │ │ ├── CCurl.scala │ │ │ ├── CurlApi.scala │ │ │ ├── CurlCode.scala │ │ │ ├── CurlInfo.scala │ │ │ ├── CurlList.scala │ │ │ ├── CurlOption.scala │ │ │ ├── CurlSpaces.scala │ │ │ └── package.scala │ │ ├── internal │ │ ├── CryptoMd5.scala │ │ ├── MessageDigestCompatibility.scala │ │ └── SttpFileExtensions.scala │ │ └── quick.scala │ └── test │ ├── scala │ └── sttp │ │ └── client4 │ │ ├── BackendOptionsProxyTest.scala │ │ ├── FollowRedirectsBackendTest.scala │ │ ├── LogContextTests.scala │ │ ├── LogTests.scala │ │ ├── RequestTests.scala │ │ ├── ResolveRelativeUrisBackendTest.scala │ │ ├── RetryWhenDefaultTest.scala │ │ ├── ToCurlConverterTest.scala │ │ ├── ToRfc2616ConverterTest.scala │ │ ├── internal │ │ ├── DigestAuthenticatorTest.scala │ │ └── WwwAuthHeaderParserTest.scala │ │ └── testing │ │ ├── BackendStubTests.scala │ │ ├── ConvertToFuture.scala │ │ ├── HttpTest.scala │ │ ├── TestStreams.scala │ │ ├── ToFutureWrapper.scala │ │ ├── streaming │ │ └── StreamingTest.scala │ │ └── websocket │ │ ├── WebSocketBufferOverflowTest.scala │ │ ├── WebSocketConcurrentTest.scala │ │ ├── WebSocketStreamingTest.scala │ │ └── WebSocketTest.scala │ ├── scalajs │ └── sttp │ │ └── client4 │ │ ├── FetchBackendHttpTest.scala │ │ ├── FetchBackendWebSocketTest.scala │ │ ├── TestPlatform.scala │ │ └── testing │ │ ├── AbstractFetchHttpTest.scala │ │ ├── AsyncExecutionContext.scala │ │ ├── AsyncRetries.scala │ │ ├── HttpTestExtensions.scala │ │ ├── Platform.scala │ │ └── streaming │ │ └── StreamingTestExtensions.scala │ ├── scalajvm │ └── sttp │ │ └── client4 │ │ ├── BackendOptionsProxyTest2.scala │ │ ├── CookieRequestTests.scala │ │ ├── HttpClientFutureHttpTest.scala │ │ ├── HttpClientFutureWebSocketTest.scala │ │ ├── HttpClientSyncHttpTest.scala │ │ ├── HttpClientSyncWebSocketTest.scala │ │ ├── HttpURLConnectionBackendHttpTest.scala │ │ ├── TestPlatform.scala │ │ ├── ToCurlConverterTestExtension.scala │ │ ├── TryBackendExceptionsTest.scala │ │ ├── TryBackendTest.scala │ │ ├── compression │ │ └── GZIPCompressingInputStreamTest.scala │ │ ├── internal │ │ └── SafeGZIPInputStreamTest.scala │ │ └── testing │ │ ├── AsyncRetries.scala │ │ ├── BackendStubTests2.scala │ │ ├── HttpTestExtensions.scala │ │ ├── Platform.scala │ │ ├── RetryTests.scala │ │ └── streaming │ │ └── StreamingTestExtensions.scala │ └── scalanative │ ├── org │ └── scalatest │ │ └── freespec │ │ ├── AsyncFreeSpec.scala │ │ └── AsyncFreeSpecLike.scala │ └── sttp │ └── client4 │ ├── CurlBackendHttpTest.scala │ ├── TestPlatform.scala │ ├── ToCurlConverterTestExtension.scala │ └── testing │ ├── AsyncExecutionContext.scala │ ├── AsyncRetries.scala │ ├── HttpTestExtensions.scala │ ├── Platform.scala │ ├── SyncHttpTest.scala │ ├── SyncHttpTestExtensions.scala │ └── streaming │ └── StreamingTestExtensions.scala ├── docs ├── .gitignore ├── .python-version ├── Makefile ├── _static │ └── css │ │ └── custom.css ├── adr │ ├── 0001-extract-stream-from-http4s.md │ ├── 0002-http-model-conventions.md │ ├── 0003-separate-backend-types.md │ └── 0004-separate-request-types.md ├── backends │ ├── akka.md │ ├── catseffect.md │ ├── finagle.md │ ├── fs2.md │ ├── future.md │ ├── http4s.md │ ├── javascript │ │ └── fetch.md │ ├── monix.md │ ├── native │ │ └── curl.md │ ├── pekko.md │ ├── scalaz.md │ ├── start_stop.md │ ├── summary.md │ ├── synchronous.md │ ├── wrappers │ │ ├── cache.md │ │ ├── custom.md │ │ ├── logging.md │ │ ├── opentelemetry.md │ │ └── prometheus.md │ └── zio.md ├── community.md ├── conf.py ├── conf │ ├── proxy.md │ ├── redirects.md │ ├── ssl.md │ └── timeouts.md ├── examples.md ├── goals.md ├── how.md ├── index.md ├── make.bat ├── migrate_v3_v4.md ├── model │ ├── model.md │ └── uri.md ├── other.md ├── other │ ├── body_callbacks.md │ ├── json.md │ ├── openapi.md │ ├── resilience.md │ ├── sse.md │ ├── websockets.md │ └── xml.md ├── quickstart.md ├── requests │ ├── authentication.md │ ├── basics.md │ ├── body.md │ ├── cookies.md │ ├── headers.md │ ├── multipart.md │ ├── streaming.md │ └── type.md ├── requirements.txt ├── responses │ ├── basics.md │ ├── body.md │ └── exceptions.md ├── support.md ├── testing │ ├── curl.md │ └── stub.md └── watch.sh ├── effects ├── cats-ce2 │ └── src │ │ ├── main │ │ ├── scala │ │ │ └── sttp │ │ │ │ └── client4 │ │ │ │ └── impl │ │ │ │ └── cats │ │ │ │ ├── CatsMonadAsyncError.scala │ │ │ │ ├── CatsMonadError.scala │ │ │ │ └── implicits.scala │ │ └── scalajs │ │ │ └── sttp │ │ │ └── client4 │ │ │ └── impl │ │ │ └── cats │ │ │ └── FetchCatsBackend.scala │ │ └── test │ │ ├── scala │ │ └── sttp │ │ │ └── client4 │ │ │ └── impl │ │ │ └── cats │ │ │ ├── CatsTestBase.scala │ │ │ └── package.scala │ │ └── scalajs │ │ └── sttp │ │ └── client4 │ │ └── impl │ │ └── cats │ │ ├── FetchCatsHttpTest.scala │ │ └── FetchCatsWebSocketTest.scala ├── cats │ └── src │ │ ├── main │ │ ├── scala │ │ │ └── sttp │ │ │ │ └── client4 │ │ │ │ └── impl │ │ │ │ └── cats │ │ │ │ ├── CatsMonadAsyncError.scala │ │ │ │ ├── CatsMonadError.scala │ │ │ │ └── implicits.scala │ │ ├── scalajs │ │ │ └── sttp │ │ │ │ └── client4 │ │ │ │ └── impl │ │ │ │ └── cats │ │ │ │ └── FetchCatsBackend.scala │ │ └── scalajvm │ │ │ └── sttp │ │ │ └── client4 │ │ │ └── httpclient │ │ │ └── cats │ │ │ ├── CatsSequencer.scala │ │ │ ├── CatsSimpleQueue.scala │ │ │ └── HttpClientCatsBackend.scala │ │ └── test │ │ ├── scala │ │ └── sttp │ │ │ └── client4 │ │ │ └── impl │ │ │ └── cats │ │ │ ├── CatsRetryTest.scala │ │ │ ├── CatsTestBase.scala │ │ │ └── package.scala │ │ ├── scalajs │ │ └── sttp │ │ │ └── client4 │ │ │ └── impl │ │ │ └── cats │ │ │ ├── FetchCatsHttpTest.scala │ │ │ └── FetchCatsWebSocketTest.scala │ │ └── scalajvm │ │ └── sttp │ │ └── client4 │ │ ├── httpclient │ │ └── cats │ │ │ ├── HttpClientCatsCloseTest.scala │ │ │ ├── HttpClientCatsHttpTest.scala │ │ │ ├── HttpClientCatsTestBase.scala │ │ │ └── HttpClientCatsWebSocketTest.scala │ │ └── impl │ │ └── cats │ │ ├── CatsMonadErrorTest.scala │ │ └── TestIODispatcher.scala ├── fs2-ce2 │ └── src │ │ ├── main │ │ ├── scala │ │ │ └── sttp │ │ │ │ └── client4 │ │ │ │ └── impl │ │ │ │ └── fs2 │ │ │ │ ├── Fs2ServerSentEvents.scala │ │ │ │ ├── Fs2SimpleQueue.scala │ │ │ │ ├── Fs2WebSockets.scala │ │ │ │ └── fs2Decompressors.scala │ │ └── scalajvm │ │ │ └── sttp │ │ │ └── client4 │ │ │ └── httpclient │ │ │ └── fs2 │ │ │ ├── Fs2BodyFromHttpClient.scala │ │ │ ├── Fs2Sequencer.scala │ │ │ └── HttpClientFs2Backend.scala │ │ └── test │ │ ├── scala │ │ └── sttp │ │ │ └── client4 │ │ │ └── impl │ │ │ └── fs2 │ │ │ └── Fs2StreamingTest.scala │ │ └── scalajvm │ │ └── sttp │ │ └── client4 │ │ └── httpclient │ │ └── fs2 │ │ ├── HttpClientFs2HttpTest.scala │ │ ├── HttpClientFs2StreamingTest.scala │ │ ├── HttpClientFs2TestBase.scala │ │ └── HttpClientFs2WebSocketTest.scala ├── fs2 │ └── src │ │ ├── main │ │ ├── scala │ │ │ └── sttp │ │ │ │ └── client4 │ │ │ │ └── impl │ │ │ │ └── fs2 │ │ │ │ ├── Fs2ServerSentEvents.scala │ │ │ │ └── Fs2WebSockets.scala │ │ ├── scalajs │ │ │ └── sttp.client4.impl.fs2 │ │ │ │ └── Fs2SimpleQueue.scala │ │ └── scalajvm │ │ │ └── sttp │ │ │ └── client4 │ │ │ ├── httpclient │ │ │ └── fs2 │ │ │ │ ├── Fs2BodyFromHttpClient.scala │ │ │ │ ├── Fs2Sequencer.scala │ │ │ │ └── HttpClientFs2Backend.scala │ │ │ └── impl │ │ │ └── fs2 │ │ │ ├── Fs2SimpleQueue.scala │ │ │ ├── fs2Compressor.scala │ │ │ └── fs2Decompressors.scala │ │ └── test │ │ ├── scala │ │ └── sttp │ │ │ └── client4 │ │ │ └── impl │ │ │ └── fs2 │ │ │ └── Fs2StreamingTest.scala │ │ └── scalajvm │ │ └── sttp │ │ └── client4 │ │ └── httpclient │ │ └── fs2 │ │ ├── HttpClientFs2HttpTest.scala │ │ ├── HttpClientFs2StreamingTest.scala │ │ ├── HttpClientFs2TestBase.scala │ │ └── HttpClientFs2WebSocketTest.scala ├── monix │ └── src │ │ ├── main │ │ ├── scala │ │ │ └── sttp │ │ │ │ └── client4 │ │ │ │ └── impl │ │ │ │ └── monix │ │ │ │ ├── MonixServerSentEvents.scala │ │ │ │ ├── MonixSimpleQueue.scala │ │ │ │ ├── MonixWebSockets.scala │ │ │ │ └── TaskMonadAsyncError.scala │ │ ├── scalajs │ │ │ └── sttp │ │ │ │ └── client4 │ │ │ │ └── impl │ │ │ │ └── monix │ │ │ │ └── FetchMonixBackend.scala │ │ └── scalajvm │ │ │ └── sttp │ │ │ └── client4 │ │ │ └── httpclient │ │ │ └── monix │ │ │ ├── HttpClientMonixBackend.scala │ │ │ ├── MonixBodyFromHttpClient.scala │ │ │ ├── MonixSequencer.scala │ │ │ └── monixDecompressors.scala │ │ └── test │ │ ├── scala │ │ └── sttp │ │ │ └── client4 │ │ │ └── impl │ │ │ └── monix │ │ │ ├── MonixStreamingTest.scala │ │ │ └── package.scala │ │ ├── scalajs │ │ └── sttp │ │ │ └── client4 │ │ │ └── impl │ │ │ └── monix │ │ │ ├── FetchMonixHttpTest.scala │ │ │ ├── FetchMonixStreamingTest.scala │ │ │ └── FetchMonixWebSocketTest.scala │ │ └── scalajvm │ │ └── sttp │ │ └── client4 │ │ └── impl │ │ └── monix │ │ ├── HttpClientMonixHttpTest.scala │ │ ├── HttpClientMonixStreamingTest.scala │ │ └── HttpClientMonixWebSocketTest.scala ├── ox │ └── src │ │ ├── main │ │ └── scala │ │ │ └── sttp │ │ │ └── client4 │ │ │ └── impl │ │ │ └── ox │ │ │ ├── sse │ │ │ └── OxServerSentEvents.scala │ │ │ └── ws │ │ │ └── OxWebSockets.scala │ │ └── test │ │ └── scala │ │ └── sttp │ │ └── client4 │ │ └── impl │ │ └── ox │ │ └── ws │ │ ├── OxSseTest.scala │ │ └── OxWebSocketsTest.scala ├── scalaz │ └── src │ │ ├── main │ │ └── scala │ │ │ └── sttp │ │ │ └── client4 │ │ │ └── impl │ │ │ └── scalaz │ │ │ ├── TaskMonadAsyncError.scala │ │ │ └── implicits.scala │ │ └── test │ │ └── scala │ │ └── sttp │ │ └── client4 │ │ └── impl │ │ └── scalaz │ │ └── package.scala ├── zio │ └── src │ │ ├── main │ │ ├── scala │ │ │ └── sttp │ │ │ │ └── client4 │ │ │ │ └── impl │ │ │ │ └── zio │ │ │ │ ├── ExtendedEnvBackend.scala │ │ │ │ ├── RIOMonadAsyncError.scala │ │ │ │ ├── ZioServerSentEvents.scala │ │ │ │ ├── ZioSimpleQueue.scala │ │ │ │ └── ZioWebSockets.scala │ │ ├── scalajs │ │ │ └── sttp │ │ │ │ └── client4 │ │ │ │ └── impl │ │ │ │ └── zio │ │ │ │ ├── FetchZioBackend.scala │ │ │ │ └── package.scala │ │ └── scalajvm │ │ │ └── sttp │ │ │ └── client4 │ │ │ ├── httpclient │ │ │ └── zio │ │ │ │ ├── HttpClientZioBackend.scala │ │ │ │ ├── ZioBodyFromHttpClient.scala │ │ │ │ ├── ZioSequencer.scala │ │ │ │ └── package.scala │ │ │ └── impl │ │ │ └── zio │ │ │ ├── package.scala │ │ │ ├── zioCompressor.scala │ │ │ └── zioDecompressors.scala │ │ └── test │ │ ├── scala │ │ └── sttp │ │ │ └── client4 │ │ │ └── impl │ │ │ └── zio │ │ │ └── ZioTestBase.scala │ │ ├── scalajs │ │ └── sttp │ │ │ └── client4 │ │ │ └── impl │ │ │ └── zio │ │ │ ├── FetchZioHttpTest.scala │ │ │ ├── FetchZioStreamingTest.scala │ │ │ └── FetchZioWebSocketTest.scala │ │ └── scalajvm │ │ └── sttp │ │ └── client4 │ │ └── httpclient │ │ └── zio │ │ ├── BackendStubZioTests.scala │ │ ├── HttpClientZioHttpTest.scala │ │ ├── HttpClientZioStreamingTest.scala │ │ └── HttpClientZioWebSocketTest.scala └── zio1 │ └── src │ ├── main │ ├── scala │ │ └── sttp │ │ │ └── client4 │ │ │ └── impl │ │ │ └── zio │ │ │ ├── ExtendedEnvBackend.scala │ │ │ ├── RIOMonadAsyncError.scala │ │ │ ├── SttpClientStubbingBase.scala │ │ │ ├── ZioServerSentEvents.scala │ │ │ ├── ZioSimpleQueue.scala │ │ │ ├── ZioWebSockets.scala │ │ │ └── package.scala │ ├── scalajs │ │ └── sttp │ │ │ └── client4 │ │ │ └── impl │ │ │ └── zio │ │ │ └── FetchZioBackend.scala │ └── scalajvm │ │ └── sttp │ │ └── client4 │ │ └── httpclient │ │ └── zio │ │ ├── HttpClientZioBackend.scala │ │ ├── ZioBodyFromHttpClient.scala │ │ ├── ZioSequencer.scala │ │ ├── package.scala │ │ └── zioDecompressors.scala │ └── test │ ├── scala │ └── sttp │ │ └── client4 │ │ └── impl │ │ └── zio │ │ └── ZioTestBase.scala │ ├── scalajs │ └── sttp │ │ └── client4 │ │ └── impl │ │ └── zio │ │ ├── FetchZioHttpTest.scala │ │ ├── FetchZioStreamingTest.scala │ │ └── FetchZioWebSocketTest.scala │ └── scalajvm │ └── sttp │ └── client4 │ └── httpclient │ └── zio │ ├── BackendStubZioTests.scala │ ├── HttpClientZioHttpTest.scala │ ├── HttpClientZioStreamingTest.scala │ └── HttpClientZioWebSocketTest.scala ├── examples-ce2 └── src │ └── main │ ├── resources │ └── logback.xml │ └── scala │ └── sttp │ └── client4 │ └── examples │ ├── GetAndParseJsonOrFailMonixCirce.scala │ ├── PostSerializeJsonMonixHttpClientCirce.scala │ └── WebSocketMonix.scala ├── examples └── src │ └── main │ ├── resources │ └── logback.xml │ └── scala │ └── sttp │ └── client4 │ └── examples │ ├── PostFormSynchronous.scala │ ├── dynamicUriSynchronous.scala │ ├── errors │ ├── httpErrorHandlingAdjustResponse.scala │ ├── httpErrorHandlingJson.scala │ └── httpErrorHandlingUsingBasicRequest.scala │ ├── fileUploadSynchronous.scala │ ├── json │ ├── GetAndParseJsonCatsEffectCirce.scala │ ├── GetAndParseJsonZioJson.scala │ ├── getAndParseJsonPekkoHttpJson4s.scala │ └── getAndParseJsonSynchronousJsoniter.scala │ ├── logging │ └── logRequestsSlf4j.scala │ ├── observability │ ├── metricsWrapperPekkoHttp.scala │ └── openTelemetryTracingAndMetrics.scala │ ├── other │ ├── GetRawResponseBodySynchronous.scala │ ├── cmdOutputStreamingWithOsLib.scala │ ├── downloadFileWitOsLib.scala │ └── uploadFileWithOsLib.scala │ ├── postMultipartFormSynchronous.scala │ ├── resilience │ ├── RateLimitOx.scala │ ├── RetryOx.scala │ └── RetryZio.scala │ ├── streaming │ ├── StreamFs2.scala │ └── StreamZio.scala │ ├── testing │ ├── TestEndpointMultipleQueryParameters.scala │ └── WebSocketTesting.scala │ ├── wrapper │ ├── CircuitBreakerCatsEffect.scala │ ├── addHeaderBackend.scala │ ├── rateLimiterFuture.scala │ ├── redisCachingBackend.scala │ └── retryingBackend.scala │ └── ws │ ├── WebSocketPekko.scala │ ├── WebSocketStreamFs2.scala │ ├── WebSocketSynchronous.scala │ ├── WebSocketZio.scala │ └── wsOxExample.scala ├── finagle-backend └── src │ ├── main │ └── scala │ │ └── sttp │ │ └── client4 │ │ └── finagle │ │ └── FinagleBackend.scala │ └── test │ └── scala │ └── sttp │ └── client4 │ └── finagle │ └── FinagleBackendTest.scala ├── generated-docs └── out │ ├── .gitignore │ ├── .python-version │ ├── Makefile │ ├── _static │ └── css │ │ └── custom.css │ ├── adr │ ├── 0001-extract-stream-from-http4s.md │ ├── 0002-http-model-conventions.md │ ├── 0003-separate-backend-types.md │ └── 0004-separate-request-types.md │ ├── backends │ ├── akka.md │ ├── catseffect.md │ ├── finagle.md │ ├── fs2.md │ ├── future.md │ ├── http4s.md │ ├── javascript │ │ └── fetch.md │ ├── monix.md │ ├── native │ │ └── curl.md │ ├── pekko.md │ ├── scalaz.md │ ├── start_stop.md │ ├── summary.md │ ├── synchronous.md │ ├── wrappers │ │ ├── cache.md │ │ ├── custom.md │ │ ├── logging.md │ │ ├── opentelemetry.md │ │ └── prometheus.md │ └── zio.md │ ├── community.md │ ├── conf.py │ ├── conf │ ├── proxy.md │ ├── redirects.md │ ├── ssl.md │ └── timeouts.md │ ├── examples.md │ ├── goals.md │ ├── how.md │ ├── includes │ └── examples_list.md │ ├── index.md │ ├── make.bat │ ├── migrate_v3_v4.md │ ├── model │ ├── model.md │ └── uri.md │ ├── other.md │ ├── other │ ├── body_callbacks.md │ ├── json.md │ ├── openapi.md │ ├── resilience.md │ ├── sse.md │ ├── websockets.md │ └── xml.md │ ├── quickstart.md │ ├── requests │ ├── authentication.md │ ├── basics.md │ ├── body.md │ ├── cookies.md │ ├── headers.md │ ├── multipart.md │ ├── streaming.md │ └── type.md │ ├── requirements.txt │ ├── responses │ ├── basics.md │ ├── body.md │ └── exceptions.md │ ├── support.md │ ├── testing │ ├── curl.md │ └── stub.md │ └── watch.sh ├── http4s-backend └── src │ ├── main │ └── scala │ │ └── sttp │ │ └── client4 │ │ └── http4s │ │ └── Http4sBackend.scala │ └── test │ └── scala │ └── sttp │ └── client4 │ └── http4s │ ├── Http4sHttpStreamingTest.scala │ └── Http4sHttpTest.scala ├── http4s-ce2-backend └── src │ ├── main │ └── scala │ │ └── sttp │ │ └── client4 │ │ └── http4s │ │ └── Http4sBackend.scala │ └── test │ └── scala │ └── sttp │ └── client4 │ └── http4s │ ├── Http4sHttpStreamingTest.scala │ └── Http4sHttpTest.scala ├── json ├── circe │ └── src │ │ ├── main │ │ └── scala │ │ │ └── sttp │ │ │ └── client4 │ │ │ └── circe │ │ │ ├── SttpCirceApi.scala │ │ │ └── package.scala │ │ └── test │ │ └── scala │ │ └── sttp │ │ └── client4 │ │ └── circe │ │ ├── BackendStubCirceTests.scala │ │ └── CirceTests.scala ├── common │ └── src │ │ ├── main │ │ └── scala │ │ │ └── sttp │ │ │ └── client4 │ │ │ ├── IsOption.scala │ │ │ ├── JsonInput.scala │ │ │ └── json │ │ │ └── package.scala │ │ └── test │ │ └── scala │ │ └── sttp │ │ └── client4 │ │ └── json │ │ └── RunResponseAs.scala ├── json4s │ └── src │ │ ├── main │ │ └── scala │ │ │ └── sttp │ │ │ └── client4 │ │ │ └── json4s │ │ │ ├── SttpJson4sApi.scala │ │ │ └── package.scala │ │ └── test │ │ └── scala │ │ └── sttp │ │ └── client4 │ │ ├── BackendStubJson4sTests.scala │ │ └── Json4sTests.scala ├── jsoniter │ └── src │ │ ├── main │ │ └── scala │ │ │ └── sttp │ │ │ └── client4 │ │ │ └── jsoniter │ │ │ ├── SttpJsoniterJsonApi.scala │ │ │ └── jsoniter.scala │ │ └── test │ │ └── scala │ │ └── sttp │ │ └── client4 │ │ └── jsoniter │ │ ├── BackendStubJsoniterTests.scala │ │ └── JsoniterJsonTests.scala ├── play-json │ └── src │ │ ├── main │ │ └── scala │ │ │ └── sttp │ │ │ └── client4 │ │ │ └── playJson │ │ │ ├── SttpPlayJsonApi.scala │ │ │ └── package.scala │ │ └── test │ │ └── scala │ │ └── sttp │ │ └── client4 │ │ ├── BackendStubPlayJsonTests.scala │ │ └── PlayJsonTests.scala ├── spray-json │ └── src │ │ ├── main │ │ └── scala │ │ │ └── sttp │ │ │ └── client4 │ │ │ └── sprayJson │ │ │ ├── SttpSprayJsonApi.scala │ │ │ └── package.scala │ │ └── test │ │ └── scala │ │ └── sttp │ │ └── client4 │ │ └── sprayJson │ │ ├── BackendStubSprayJsonTests.scala │ │ └── SprayJsonTests.scala ├── tethys-json │ └── src │ │ ├── main │ │ └── scala │ │ │ └── sttp │ │ │ └── client4 │ │ │ └── tethysJson │ │ │ ├── SttpTethysApi.scala │ │ │ └── package.scala │ │ └── test │ │ └── scala │ │ └── sttp │ │ └── client4 │ │ └── tethysJson │ │ ├── BackendStubTethysTests.scala │ │ └── TethysTests.scala ├── upickle │ └── src │ │ ├── main │ │ └── scala │ │ │ └── sttp │ │ │ └── client4 │ │ │ └── upicklejson │ │ │ ├── SttpUpickleApi.scala │ │ │ └── package.scala │ │ └── test │ │ └── scala │ │ └── sttp │ │ └── client4 │ │ └── upicklejson │ │ ├── BackendStubUpickleTests.scala │ │ └── UpickleTests.scala ├── zio-json │ └── src │ │ ├── main │ │ ├── scala │ │ │ └── sttp │ │ │ │ └── client4 │ │ │ │ └── ziojson │ │ │ │ ├── SttpZioJsonApi.scala │ │ │ │ └── ziojson.scala │ │ ├── scalajs │ │ │ └── sttp │ │ │ │ └── client4 │ │ │ │ └── ziojson │ │ │ │ └── SttpZioJsonApiExtensions.scala │ │ └── scalajvm │ │ │ └── sttp │ │ │ └── client4 │ │ │ └── ziojson │ │ │ └── SttpZioJsonApiExtensions.scala │ │ └── test │ │ └── scala │ │ └── sttp │ │ └── client4 │ │ └── ziojson │ │ ├── BackendStubZioJsonTests.scala │ │ └── ZioJsonTests.scala └── zio1-json │ └── src │ ├── main │ ├── scala │ │ └── sttp │ │ │ └── client4 │ │ │ └── ziojson │ │ │ ├── SttpZioJsonApi.scala │ │ │ └── ziojson.scala │ ├── scalajs │ │ └── sttp │ │ │ └── client4 │ │ │ └── ziojson │ │ │ └── SttpZioJsonApiExtensions.scala │ └── scalajvm │ │ └── sttp │ │ └── client4 │ │ └── ziojson │ │ └── SttpZioJsonApiExtensions.scala │ └── test │ └── scala │ └── sttp │ └── client4 │ └── ziojson │ ├── BackendStubZioJsonTests.scala │ └── ZioJsonTests.scala ├── logging ├── scribe │ └── src │ │ └── main │ │ └── scala │ │ └── sttp │ │ └── client4 │ │ └── logging │ │ └── scribe │ │ ├── ScribeLogger.scala │ │ └── ScribeLoggingBackend.scala └── slf4j │ └── src │ └── main │ └── scala │ └── sttp │ └── client4 │ └── logging │ └── slf4j │ ├── Slf4jLogger.scala │ └── Slf4jLoggingBackend.scala ├── manual-tests └── proxy-digest-test.md ├── observability ├── opentelemetry-backend │ └── src │ │ ├── main │ │ └── scala │ │ │ └── sttp │ │ │ └── client4 │ │ │ └── opentelemetry │ │ │ ├── OpenTelemetryDefaults.scala │ │ │ ├── OpenTelemetryMetricsBackend.scala │ │ │ ├── OpenTelemetryMetricsConfig.scala │ │ │ ├── OpenTelemetryTracingBackend.scala │ │ │ └── OpenTelemetryTracingConfig.scala │ │ └── test │ │ └── scala │ │ └── sttp │ │ └── client4 │ │ └── opentelemetry │ │ ├── OpenTelemetryMetricsBackendTest.scala │ │ └── OpenTelemetryTracingBackendTest.scala ├── opentelemetry-tracing-zio-backend │ └── src │ │ ├── main │ │ └── scala │ │ │ └── sttp │ │ │ └── client4 │ │ │ └── opentelemetry │ │ │ └── zio │ │ │ └── OpenTelemetryTracingZioBackend.scala │ │ └── test │ │ └── scala │ │ └── sttp │ │ └── client4 │ │ └── opentelemetry │ │ └── zio │ │ └── OpenTelemetryTracingZioBackendTest.scala ├── otel4s-metrics-backend │ └── src │ │ ├── main │ │ └── scala │ │ │ └── sttp │ │ │ └── client4 │ │ │ └── opentelemetry │ │ │ └── otel4s │ │ │ ├── Otel4sMetricsBackend.scala │ │ │ └── Otel4sMetricsConfig.scala │ │ └── test │ │ └── scala │ │ └── sttp │ │ └── client4 │ │ └── opentelemetry │ │ └── otel4s │ │ └── Otel4sMetricsBackendTest.scala ├── otel4s-tracing-backend │ └── src │ │ ├── main │ │ └── scala │ │ │ └── sttp │ │ │ └── client4 │ │ │ └── opentelemetry │ │ │ └── otel4s │ │ │ ├── Otel4sTracingBackend.scala │ │ │ ├── Otel4sTracingConfig.scala │ │ │ └── Otel4sTracingDefaults.scala │ │ └── test │ │ └── scala │ │ └── sttp │ │ └── client4 │ │ └── opentelemetry │ │ └── otel4s │ │ └── Otel4sTracingBackendTest.scala └── prometheus-backend │ └── src │ ├── main │ └── scala │ │ └── sttp │ │ └── client4 │ │ └── prometheus │ │ ├── PrometheusBackend.scala │ │ └── PrometheusConfig.scala │ └── test │ └── scala │ └── sttp │ └── client4 │ └── prometheus │ └── PrometheusBackendTest.scala ├── okhttp-backend ├── monix │ └── src │ │ ├── main │ │ └── scala │ │ │ └── sttp │ │ │ └── client4 │ │ │ └── okhttp │ │ │ └── monix │ │ │ └── OkHttpMonixBackend.scala │ │ └── test │ │ └── scala │ │ └── sttp │ │ └── client4 │ │ └── okhttp │ │ └── monix │ │ ├── OkHttpMonixHttpTest.scala │ │ ├── OkHttpMonixStreamingTest.scala │ │ └── OkHttpMonixWebSocketTest.scala └── src │ ├── main │ └── scala │ │ └── sttp │ │ └── client4 │ │ └── okhttp │ │ ├── BodyFromOkHttp.scala │ │ ├── BodyToOkHttp.scala │ │ ├── OkHttpAsyncBackend.scala │ │ ├── OkHttpBackend.scala │ │ ├── OkHttpFutureBackend.scala │ │ ├── OkHttpSyncBackend.scala │ │ ├── WebSocketImpl.scala │ │ └── quick.scala │ └── test │ └── scala │ └── sttp │ └── client4 │ └── okhttp │ ├── OkHttpFutureHttpTest.scala │ ├── OkHttpFutureWebsocketTest.scala │ ├── OkHttpHttpTest.scala │ ├── OkHttpSyncDigestAuthProxyManualTest.scala │ ├── OkHttpSyncHttpTest.scala │ └── OkHttpSyncWebSocketTest.scala ├── pekko-http-backend └── src │ ├── main │ └── scala │ │ └── sttp │ │ └── client4 │ │ └── pekkohttp │ │ ├── BodyFromPekko.scala │ │ ├── BodyToPekko.scala │ │ ├── FromPekko.scala │ │ ├── PekkoCompressor.scala │ │ ├── PekkoHttpBackend.scala │ │ ├── PekkoHttpClient.scala │ │ ├── PekkoHttpServerSentEvents.scala │ │ ├── ToPekko.scala │ │ ├── Util.scala │ │ └── pekkoDecompressors.scala │ └── test │ └── scala │ └── sttp │ └── client4 │ └── pekkohttp │ ├── BackendStubPekkoTests.scala │ ├── PekkoHttpClientHttpTest.scala │ ├── PekkoHttpRouteBackendTest.scala │ ├── PekkoHttpStreamingTest.scala │ └── PekkoHttpWebSocketTest.scala ├── project ├── FileUtils.scala ├── GenerateListOfExamples.scala ├── PollingUtils.scala ├── VerifyExamplesCompileUsingScalaCli.scala ├── build.properties └── plugins.sbt └── testing ├── compile └── src │ └── test │ └── scala │ └── sttp │ └── client4 │ └── testing │ └── compile │ ├── EvalScala.scala │ └── IllTypedTests.scala └── server └── src └── main ├── resources ├── binaryfile.jpg ├── r3.gz └── textfile.txt └── scala ├── akka └── http │ └── scaladsl │ └── coding │ └── DeflateNoWrap.scala └── sttp └── client4 └── testing └── server └── HttpServer.scala /.adr-dir: -------------------------------------------------------------------------------- 1 | docs/adr 2 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Scala Steward: Reformat with scalafmt 3.5.9 2 | 23c9f679564435a81c84c3a5ccf0534feb5285d0 3 | 4 | # Scala Steward: Reformat with scalafmt 3.7.1 5 | 12beac3c7d3307d04dbf066117d1042b7c931dbb 6 | 7 | # Scala Steward: Reformat with scalafmt 3.7.2 8 | aa1d709eba0652b405782a226fe4a73406dc192f 9 | 10 | # Reformat using new scalafmt rules 11 | e7ed7720a3ae4389c4b755b476e94bafb26ba694 12 | # Scala Steward: Reformat with scalafmt 3.7.3 13 | b364558c7d44d6c21f1532d618707cc7ca1e4148 14 | 15 | # Scala Steward: Reformat with scalafmt 3.7.5 16 | 5c45ddcfbb2db36c3f9ad3dd9468d4089bca7d3b 17 | 18 | # Scala Steward: Reformat with scalafmt 3.7.14 19 | a5e39d1f4d49c7e32a9b834ebbf429f8f5cab5ee 20 | 21 | # Scala Steward: Reformat with scalafmt 3.8.1 22 | 19891560efe4b9262b4b4079e5ca95dbbea1a906 23 | 24 | # Scala Steward: Reformat with scalafmt 3.8.2 25 | be3cd01c8ea70ee2383d0d05ab8d129f67b70c28 26 | 27 | # Scala Steward: Reformat with scalafmt 3.8.5 28 | 744672208f8e4e501218f22fcd8589841aee7ad2 29 | 30 | # Scala Steward: Reformat with scalafmt 3.9.4 31 | 264d88d48c3b49dbe215946056fadef0ad20e293 32 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | labels: 3 | - label: "automerge" 4 | authors: ["softwaremill-ci"] 5 | files: 6 | - "build.sbt" 7 | - "project/plugins.sbt" 8 | - "project/build.properties" 9 | - label: "dependency" 10 | authors: ["softwaremill-ci"] 11 | files: 12 | - "build.sbt" 13 | - "project/plugins.sbt" 14 | - "project/build.properties" 15 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Before submitting pull request: 2 | - [ ] Check if the project compiles by running `sbt compile` 3 | - [ ] Verify docs compilation by running `sbt compileDocs` 4 | - [ ] Check if tests pass by running `sbt test` 5 | - [ ] Format code by running `sbt scalafmt` 6 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | categories: 2 | - title: 'Dependency updates' 3 | labels: 4 | - 'dependency' 5 | template: | 6 | ## What’s Changed 7 | 8 | $CHANGES -------------------------------------------------------------------------------- /.github/workflows/scala-steward.yml: -------------------------------------------------------------------------------- 1 | name: Scala Steward 2 | 3 | # This workflow will launch at 00:00 every day 4 | on: 5 | schedule: 6 | - cron: '0 0 * * *' 7 | workflow_dispatch: 8 | 9 | jobs: 10 | scala-steward: 11 | uses: softwaremill/github-actions-workflows/.github/workflows/scala-steward.yml@main 12 | secrets: 13 | repo-github-token: ${{secrets.REPO_GITHUB_TOKEN}} 14 | with: 15 | java-opts: '-Xmx3500M' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # sbt specific 5 | .cache 6 | .history 7 | .lib/ 8 | dist/* 9 | target/ 10 | lib_managed/ 11 | src_managed/ 12 | project/boot/ 13 | project/plugins/project/ 14 | 15 | # Scala-IDE specific 16 | .scala_dependencies 17 | .worksheet 18 | 19 | .idea* 20 | 21 | .keys* 22 | 23 | core/native/local.sbt 24 | 25 | .metals/ 26 | .bloop/ 27 | .bsp/ 28 | .java-version 29 | metals.sbt 30 | .scala-build 31 | 32 | .vscode 33 | 34 | # scala-native 35 | lowered.hnir 36 | 37 | work.txt -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | sphinx: 4 | configuration: generated-docs/out/conf.py 5 | 6 | python: 7 | install: 8 | - requirements: generated-docs/out/requirements.txt 9 | 10 | build: 11 | os: ubuntu-22.04 12 | tools: 13 | python: "3.12" 14 | -------------------------------------------------------------------------------- /.sbtopts: -------------------------------------------------------------------------------- 1 | -J-Xmx3000M 2 | -J-Xss2M 3 | -Dsbt.task.timings=false 4 | -------------------------------------------------------------------------------- /.scala-steward.conf: -------------------------------------------------------------------------------- 1 | updates.ignore = [ 2 | {groupId = "org.scala-lang", artifactId = "scala-compiler", version = "2.12."}, 3 | {groupId = "org.scala-lang", artifactId = "scala-compiler", version = "2.13."}, 4 | {groupId = "org.scala-lang", artifactId = "scala-compiler", version = "3."} 5 | ] 6 | updates.pin = [ 7 | {groupId = "com.typesafe.akka", version = "2.6."}, 8 | {groupId = "org.slf4j", artifactId = "slf4j-api", version = "1."}, 9 | {groupId = "org.scala-lang", artifactId = "scala3-library", version = "3.3."}, 10 | {groupId = "org.scala-lang", artifactId = "scala3-library_sjs1", version = "3.3."} 11 | ] 12 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = 3.9.4 2 | maxColumn = 120 3 | runner.dialect = scala213 4 | fileOverride { 5 | "glob:**/scala-3/**" { 6 | runner.dialect = scala3 7 | } 8 | "glob:**/examples/**" { 9 | runner.dialect = scala3 10 | } 11 | "glob:**/effects/ox/**" { 12 | runner.dialect = scala3 13 | } 14 | } -------------------------------------------------------------------------------- /akka-http-backend/src/main/scala/sttp/client4/akkahttp/AkkaHttpServerSentEvents.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.akkahttp 2 | 3 | import akka.NotUsed 4 | import akka.stream.scaladsl.{Flow, Framing} 5 | import akka.util.ByteString 6 | import sttp.model.sse.ServerSentEvent 7 | 8 | object AkkaHttpServerSentEvents { 9 | val parse: Flow[ByteString, ServerSentEvent, NotUsed] = 10 | Framing 11 | .delimiter(ByteString("\n\n"), maximumFrameLength = Int.MaxValue, allowTruncation = true) 12 | .map(_.utf8String) 13 | .map(_.split("\n").toList) 14 | .map(ServerSentEvent.parse) 15 | } 16 | -------------------------------------------------------------------------------- /akka-http-backend/src/main/scala/sttp/client4/akkahttp/akkaDecompressors.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.akkahttp 2 | 3 | import sttp.client4.compression.Decompressor 4 | import sttp.model.Encodings 5 | import akka.http.scaladsl.coding.Coders 6 | import akka.http.scaladsl.model.HttpResponse 7 | 8 | object GZipAkkaDecompressor extends Decompressor[HttpResponse] { 9 | override val encoding: String = Encodings.Gzip 10 | override def apply(body: HttpResponse): HttpResponse = Coders.Gzip.decodeMessage(body) 11 | } 12 | 13 | object DeflateAkkaDecompressor extends Decompressor[HttpResponse] { 14 | override val encoding: String = Encodings.Deflate 15 | override def apply(body: HttpResponse): HttpResponse = Coders.Deflate.decodeMessage(body) 16 | } 17 | -------------------------------------------------------------------------------- /akka-http-backend/src/test/scala/sttp/client4/akkahttp/AkkaHttpClientHttpTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.akkahttp 2 | 3 | import sttp.client4.Backend 4 | import sttp.client4.testing.{ConvertToFuture, HttpTest} 5 | 6 | import scala.concurrent.Future 7 | 8 | class AkkaHttpClientHttpTest extends HttpTest[Future] { 9 | override val backend: Backend[Future] = AkkaHttpBackend() 10 | override implicit val convertToFuture: ConvertToFuture[Future] = ConvertToFuture.future 11 | 12 | override def supportsCancellation: Boolean = false 13 | override def supportsResponseAsInputStream = false 14 | 15 | override def timeoutToNone[T](t: Future[T], timeoutMillis: Int): Future[Option[T]] = t.map(Some(_)) 16 | } 17 | -------------------------------------------------------------------------------- /armeria-backend/cats-ce2/src/test/scala/sttp/client4/armeria/cats/ArmeriaCatsHttpTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.armeria.cats 2 | 3 | import cats.effect.IO 4 | import sttp.client4._ 5 | import sttp.client4.impl.cats.CatsTestBase 6 | import sttp.client4.testing.HttpTest 7 | 8 | class ArmeriaCatsHttpTest extends HttpTest[IO] with CatsTestBase { 9 | override val backend: Backend[IO] = ArmeriaCatsBackend[IO]() 10 | 11 | "illegal url exceptions" - { 12 | "should be wrapped in the effect wrapper" in { 13 | basicRequest.get(uri"ps://sth.com").send(backend).toFuture().failed.map { e => 14 | e shouldBe a[IllegalArgumentException] 15 | } 16 | } 17 | } 18 | 19 | override def supportsHostHeaderOverride = false 20 | override def supportsCancellation = false 21 | override def supportsAutoDecompressionDisabling = false 22 | override def supportsDeflateWrapperChecking = false // armeria hangs 23 | override def supportsEmptyContentEncoding = false 24 | override def supportsResponseAsInputStream = false 25 | } 26 | -------------------------------------------------------------------------------- /armeria-backend/cats/src/test/scala/sttp/client4/armeria/cats/ArmeriaCatsHttpTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.armeria.cats 2 | 3 | import cats.effect.IO 4 | import sttp.client4._ 5 | import sttp.client4.impl.cats.{CatsRetryTest, CatsTestBase} 6 | import sttp.client4.testing.HttpTest 7 | 8 | class ArmeriaCatsHttpTest extends HttpTest[IO] with CatsRetryTest with CatsTestBase { 9 | override val backend: Backend[IO] = ArmeriaCatsBackend[IO]() 10 | 11 | "illegal url exceptions" - { 12 | "should be wrapped in the effect wrapper" in { 13 | basicRequest.get(uri"ps://sth.com").send(backend).toFuture().failed.map { e => 14 | e shouldBe a[IllegalArgumentException] 15 | } 16 | } 17 | } 18 | 19 | override def supportsHostHeaderOverride = false 20 | override def supportsCancellation = false 21 | override def supportsAutoDecompressionDisabling = false 22 | override def supportsDeflateWrapperChecking = false // armeria hangs 23 | override def supportsEmptyContentEncoding = false 24 | override def supportsResponseAsInputStream = false 25 | } 26 | -------------------------------------------------------------------------------- /armeria-backend/fs2-ce2/src/test/scala/sttp/client4/armeria/fs2/ArmeriaFs2HttpTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.armeria.fs2 2 | 3 | import cats.effect.IO 4 | import sttp.client4.Backend 5 | import sttp.client4.impl.cats.CatsTestBase 6 | import sttp.client4.testing.HttpTest 7 | 8 | class ArmeriaFs2HttpTest extends HttpTest[IO] with CatsTestBase { 9 | override val backend: Backend[IO] = ArmeriaFs2Backend() 10 | 11 | override def supportsHostHeaderOverride = false 12 | override def supportsCancellation = false 13 | override def supportsAutoDecompressionDisabling = false 14 | override def supportsDeflateWrapperChecking = false // armeria hangs 15 | override def supportsEmptyContentEncoding = false 16 | override def supportsResponseAsInputStream = false 17 | } 18 | -------------------------------------------------------------------------------- /armeria-backend/fs2-ce2/src/test/scala/sttp/client4/armeria/fs2/ArmeriaFs2StreamingTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.armeria.fs2 2 | 3 | import cats.effect.IO 4 | import sttp.capabilities.fs2.Fs2Streams 5 | import sttp.client4.{BackendOptions, StreamBackend} 6 | import sttp.client4.armeria.ArmeriaWebClient 7 | import sttp.client4.impl.fs2.Fs2StreamingTest 8 | import sttp.client4.testing.RetryTests 9 | 10 | import java.time.Duration 11 | 12 | // streaming tests often fail with a ClosedSessionException, see https://github.com/line/armeria/issues/1754 13 | class ArmeriaFs2StreamingTest extends Fs2StreamingTest with RetryTests { 14 | override val backend: StreamBackend[IO, Fs2Streams[IO]] = 15 | ArmeriaFs2Backend.usingClient( 16 | // the default caused timeouts in SSE tests 17 | ArmeriaWebClient.newClient(BackendOptions.Default, _.writeTimeout(Duration.ofMillis(0))) 18 | ) 19 | 20 | override protected def supportsStreamingMultipartParts: Boolean = false 21 | } 22 | -------------------------------------------------------------------------------- /armeria-backend/fs2/src/test/scala/sttp/client4/armeria/fs2/ArmeriaFs2HttpTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.armeria.fs2 2 | 3 | import cats.effect.IO 4 | import sttp.client4.Backend 5 | import sttp.client4.impl.cats.{CatsTestBase, TestIODispatcher} 6 | import sttp.client4.testing.HttpTest 7 | 8 | class ArmeriaFs2HttpTest extends HttpTest[IO] with CatsTestBase with TestIODispatcher { 9 | override val backend: Backend[IO] = ArmeriaFs2Backend(dispatcher = dispatcher) 10 | 11 | override def supportsHostHeaderOverride = false 12 | override def supportsCancellation = false 13 | override def supportsAutoDecompressionDisabling = false 14 | override def supportsDeflateWrapperChecking = false // armeria hangs 15 | override def supportsEmptyContentEncoding = false 16 | override def supportsResponseAsInputStream = false 17 | } 18 | -------------------------------------------------------------------------------- /armeria-backend/fs2/src/test/scala/sttp/client4/armeria/fs2/ArmeriaFs2StreamingTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.armeria.fs2 2 | 3 | import cats.effect.IO 4 | import sttp.capabilities.fs2.Fs2Streams 5 | import sttp.client4.{BackendOptions, StreamBackend} 6 | import sttp.client4.armeria.ArmeriaWebClient 7 | import sttp.client4.impl.cats.TestIODispatcher 8 | import sttp.client4.impl.fs2.Fs2StreamingTest 9 | import sttp.client4.testing.RetryTests 10 | 11 | import java.time.Duration 12 | 13 | // streaming tests often fail with a ClosedSessionException, see https://github.com/line/armeria/issues/1754 14 | class ArmeriaFs2StreamingTest extends Fs2StreamingTest with TestIODispatcher with RetryTests { 15 | override val backend: StreamBackend[IO, Fs2Streams[IO]] = 16 | ArmeriaFs2Backend.usingClient( 17 | // the default caused timeouts in SSE tests 18 | ArmeriaWebClient.newClient(BackendOptions.Default, _.writeTimeout(Duration.ofMillis(0))), 19 | dispatcher 20 | ) 21 | 22 | override protected def supportsStreamingMultipartParts: Boolean = false 23 | } 24 | -------------------------------------------------------------------------------- /armeria-backend/monix/src/test/scala/sttp/client4/armeria/monix/ArmeriaMonixHttpTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.armeria.monix 2 | 3 | import java.util.concurrent.TimeoutException 4 | import monix.eval.Task 5 | import sttp.client4.Backend 6 | import sttp.client4.impl.monix.convertMonixTaskToFuture 7 | import sttp.client4.testing.{ConvertToFuture, HttpTest} 8 | import monix.execution.Scheduler.Implicits.global 9 | import scala.concurrent.duration.DurationInt 10 | 11 | class ArmeriaMonixHttpTest extends HttpTest[Task] { 12 | override val backend: Backend[Task] = ArmeriaMonixBackend() 13 | override implicit val convertToFuture: ConvertToFuture[Task] = convertMonixTaskToFuture 14 | 15 | override def timeoutToNone[T](t: Task[T], timeoutMillis: Int): Task[Option[T]] = 16 | t.map(Some(_)) 17 | .timeout(timeoutMillis.milliseconds) 18 | .onErrorRecover { case _: TimeoutException => 19 | None 20 | } 21 | 22 | override def supportsHostHeaderOverride = false 23 | override def supportsAutoDecompressionDisabling = false 24 | override def supportsDeflateWrapperChecking = false // armeria hangs 25 | override def supportsEmptyContentEncoding = false 26 | override def supportsResponseAsInputStream = false 27 | } 28 | -------------------------------------------------------------------------------- /armeria-backend/monix/src/test/scala/sttp/client4/armeria/monix/ArmeriaMonixStreamingTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.armeria.monix 2 | 3 | import monix.eval.Task 4 | import sttp.capabilities.monix.MonixStreams 5 | import sttp.client4.{BackendOptions, StreamBackend} 6 | import sttp.client4.impl.monix.MonixStreamingTest 7 | import monix.execution.Scheduler.Implicits.global 8 | import sttp.client4.armeria.ArmeriaWebClient 9 | import sttp.client4.testing.RetryTests 10 | 11 | import java.time.Duration 12 | 13 | // streaming tests often fail with a ClosedSessionException, see https://github.com/line/armeria/issues/1754 14 | class ArmeriaMonixStreamingTest extends MonixStreamingTest with RetryTests { 15 | override val backend: StreamBackend[Task, MonixStreams] = 16 | ArmeriaMonixBackend.usingClient( 17 | // the default caused timeouts in SSE tests 18 | ArmeriaWebClient.newClient(BackendOptions.Default, _.writeTimeout(Duration.ofMillis(0))) 19 | ) 20 | 21 | override protected def supportsStreamingMultipartParts: Boolean = false 22 | } 23 | -------------------------------------------------------------------------------- /armeria-backend/scalaz/src/test/scala/sttp/client4/armeria/scalaz/ArmeriaScalazHttpTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.armeria.scalaz 2 | 3 | import scalaz.concurrent.Task 4 | import sttp.client4._ 5 | import sttp.client4.impl.scalaz.convertScalazTaskToFuture 6 | import sttp.client4.testing.{ConvertToFuture, HttpTest} 7 | 8 | class ArmeriaScalazHttpTest extends HttpTest[Task] { 9 | 10 | override val backend: Backend[Task] = ArmeriaScalazBackend() 11 | override implicit val convertToFuture: ConvertToFuture[Task] = convertScalazTaskToFuture 12 | 13 | override def supportsHostHeaderOverride = false 14 | override def supportsCancellation = false 15 | override def supportsAutoDecompressionDisabling = false 16 | override def supportsDeflateWrapperChecking = false // armeria hangs 17 | override def supportsEmptyContentEncoding = false 18 | override def supportsResponseAsInputStream = false 19 | 20 | override def timeoutToNone[T](t: Task[T], timeoutMillis: Int): Task[Option[T]] = t.map(Some(_)) 21 | } 22 | -------------------------------------------------------------------------------- /armeria-backend/src/main/scala/sttp/client4/armeria/UnknownStatusException.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.armeria 2 | 3 | /** A `RuntimeException` raised when an `HttpStatus.UnknownStatus` received from Armeria backend. */ 4 | class UnknownStatusException(message: String) extends RuntimeException(message) 5 | -------------------------------------------------------------------------------- /armeria-backend/src/test/scala/sttp/client4/armeria/future/ArmeriaFutureHttpTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.armeria.future 2 | 3 | import sttp.client4.Backend 4 | import sttp.client4.testing.{ConvertToFuture, HttpTest} 5 | 6 | import scala.concurrent.Future 7 | 8 | class ArmeriaFutureHttpTest extends HttpTest[Future] { 9 | 10 | override val backend: Backend[Future] = ArmeriaFutureBackend() 11 | override implicit val convertToFuture: ConvertToFuture[Future] = ConvertToFuture.future 12 | 13 | override def supportsHostHeaderOverride = false 14 | override def supportsCancellation = false 15 | override def supportsAutoDecompressionDisabling = false 16 | override def supportsDeflateWrapperChecking = false // armeria hangs 17 | override def supportsEmptyContentEncoding = false 18 | override def supportsResponseAsInputStream = false 19 | 20 | override def timeoutToNone[T](t: Future[T], timeoutMillis: Int): Future[Option[T]] = t.map(Some(_)) 21 | } 22 | -------------------------------------------------------------------------------- /banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/softwaremill/sttp/89790e72bbc3b5d5042f33fac49e15f006cd9978/banner.png -------------------------------------------------------------------------------- /caching/src/main/scala/sttp/client4/caching/Cache.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.caching 2 | 3 | import scala.concurrent.duration.FiniteDuration 4 | 5 | /** A cache interface to be used with [[CachingBackend]]. 6 | * 7 | * @tparam f 8 | * The effect type, [[sttp.shared.Identity]] for direct-style (synchronous). Must be the same as used by the backend, 9 | * which is being wrapped. 10 | */ 11 | trait Cache[F[_]] { 12 | def get(key: Array[Byte]): F[Option[Array[Byte]]] 13 | def delete(key: Array[Byte]): F[Unit] 14 | def set(key: Array[Byte], value: Array[Byte], ttl: FiniteDuration): F[Unit] 15 | def close(): F[Unit] 16 | } 17 | -------------------------------------------------------------------------------- /caching/src/main/scala/sttp/client4/caching/CachedResponse.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.caching 2 | 3 | import sttp.model.StatusCode 4 | import sttp.model.Header 5 | import sttp.client4.Response 6 | import java.util.Base64 7 | import sttp.model.RequestMetadata 8 | import com.github.plokhotnyuk.jsoniter_scala.core.JsonValueCodec 9 | import com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker 10 | 11 | case class CachedResponse( 12 | body: String, 13 | code: StatusCode, 14 | statusText: String, 15 | headers: List[Header] 16 | ) { 17 | def toResponse(request: RequestMetadata): Response[Array[Byte]] = Response( 18 | Base64.getDecoder.decode(body), 19 | code, 20 | statusText, 21 | headers, 22 | Nil, 23 | request 24 | ) 25 | } 26 | 27 | object CachedResponse { 28 | def apply(response: Response[Array[Byte]]): CachedResponse = CachedResponse( 29 | Base64.getEncoder.encodeToString(response.body), 30 | response.code, 31 | response.statusText, 32 | response.headers.toList 33 | ) 34 | 35 | implicit val cachedResponseCodec: JsonValueCodec[CachedResponse] = JsonCodecMaker.make 36 | } 37 | -------------------------------------------------------------------------------- /core/src/main/resources/scala-native/ffi.c: -------------------------------------------------------------------------------- 1 | #if defined(STTP_CURL_FFI) 2 | #include 3 | 4 | int sttp_curl_setopt_int(CURL *curl, CURLoption opt, int arg) {return curl_easy_setopt(curl, opt, arg); } 5 | int sttp_curl_setopt_long(CURL *curl, CURLoption opt, long arg) {return curl_easy_setopt(curl, opt, arg); } 6 | int sttp_curl_setopt_pointer(CURL *curl, CURLoption opt, void* arg) {return curl_easy_setopt(curl, opt, arg); } 7 | const char* sttp_curl_get_version() { 8 | return curl_version_info(CURLVERSION_NOW)->version; 9 | } 10 | int sttp_curl_getinfo_pointer(CURL *curl, CURLINFO info, void* arg) {return curl_easy_getinfo(curl, info, arg); } 11 | #endif 12 | -------------------------------------------------------------------------------- /core/src/main/scala/sttp/client4/SpecifyAuthScheme.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | 3 | import sttp.client4.internal.DigestAuthenticator 4 | import sttp.client4.internal.Utf8 5 | import java.util.Base64 6 | import sttp.attributes.AttributeKey 7 | 8 | class SpecifyAuthScheme[+R <: PartialRequestBuilder[R, _]]( 9 | hn: String, 10 | req: R, 11 | digestAttributeKey: AttributeKey[DigestAuthenticator.DigestAuthData] 12 | ) { 13 | def basic(user: String, password: String): R = { 14 | val c = new String(Base64.getEncoder.encode(s"$user:$password".getBytes(Utf8)), Utf8) 15 | req.header(hn, s"Basic $c") 16 | } 17 | 18 | def basicToken(token: String): R = 19 | req.header(hn, s"Basic $token") 20 | 21 | def bearer(token: String): R = 22 | req.header(hn, s"Bearer $token") 23 | 24 | def digest(user: String, password: String): R = 25 | req.attribute(digestAttributeKey, DigestAuthenticator.DigestAuthData(user, password)) 26 | } 27 | -------------------------------------------------------------------------------- /core/src/main/scala/sttp/client4/compression/CompressionHandlers.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.compression 2 | 3 | /** Defines the compressors that might be used to compress request bodies, and decompressors that might be used to 4 | * decompress response bodies. 5 | * 6 | * @tparam R 7 | * The capabilities that the bodyies (both to compress & compressed) might use (e.g. streams). 8 | * @tparam B 9 | * The type of the raw body (as used by the backend) that is decompressed. 10 | * @see 11 | * [[sttp.client4.RequestOptions.decompressResponseBody]] 12 | * @see 13 | * [[sttp.client4.RequestOptions.compressRequestBody]] 14 | */ 15 | case class CompressionHandlers[-R, B]( 16 | compressors: List[Compressor[R]], 17 | decompressors: List[Decompressor[B]] 18 | ) { 19 | def addCompressor[R2 <: R](compressors: Compressor[R2]): CompressionHandlers[R2, B] = 20 | copy(compressors = this.compressors :+ compressors) 21 | 22 | def addDecompressor(decompressors: Decompressor[B]): CompressionHandlers[R, B] = 23 | copy(decompressors = this.decompressors :+ decompressors) 24 | } 25 | -------------------------------------------------------------------------------- /core/src/main/scala/sttp/client4/compression/Decompressor.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.compression 2 | 3 | import java.io.UnsupportedEncodingException 4 | 5 | /** Allows decompressing bodies, using the supported encoding. */ 6 | trait Decompressor[B] { 7 | def encoding: String 8 | def apply(body: B): B 9 | } 10 | 11 | object Decompressor extends DecompressorExtensions { 12 | def decompressIfPossible[B](b: B, encoding: String, decompressors: List[Decompressor[B]]): B = 13 | decompressors.find(_.encoding.equalsIgnoreCase(encoding)) match { 14 | case Some(decompressor) => decompressor(b) 15 | case None => throw new UnsupportedEncodingException(s"Unsupported encoding: $encoding") 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /core/src/main/scala/sttp/client4/internal/NoStreams.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.internal 2 | 3 | import sttp.capabilities.Streams 4 | 5 | private[client4] trait NoStreams extends Streams[Nothing] { 6 | override type BinaryStream = Nothing 7 | override type Pipe[A, B] = Nothing 8 | } 9 | private[client4] object NoStreams extends NoStreams 10 | -------------------------------------------------------------------------------- /core/src/main/scala/sttp/client4/internal/OnEndInputStream.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.internal 2 | 3 | import java.io.InputStream 4 | 5 | private[client4] class OnEndInputStream(delegate: InputStream, callback: () => Unit) extends InputStream { 6 | private var callbackCalled = false 7 | 8 | override def read(): Int = { 9 | val result = delegate.read() 10 | if (result == -1) onEnd() 11 | result 12 | } 13 | 14 | override def read(b: Array[Byte]): Int = { 15 | val result = delegate.read(b) 16 | if (result == -1) onEnd() 17 | result 18 | } 19 | 20 | override def read(b: Array[Byte], off: Int, len: Int): Int = { 21 | val result = delegate.read(b, off, len) 22 | if (result == -1) onEnd() 23 | result 24 | } 25 | 26 | override def close(): Unit = { 27 | onEnd() 28 | delegate.close() 29 | } 30 | 31 | private def onEnd(): Unit = if (!callbackCalled) { 32 | callbackCalled = true 33 | callback() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /core/src/main/scala/sttp/client4/internal/SttpFile.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.internal 2 | 3 | /** A platform agnostic file abstraction. 4 | * 5 | * Different platforms have different file representations. Each platform should provide conversions in the 6 | * `FileCompanionExtensions` trait to convert between their supported representations and the `File` abstraction. 7 | */ 8 | abstract class SttpFile private[internal] (val underlying: Any) extends SttpFileExtensions { 9 | def name: String 10 | def size: Long 11 | } 12 | 13 | object SttpFile extends SttpFileCompanionExtensions {} 14 | -------------------------------------------------------------------------------- /core/src/main/scala/sttp/client4/internal/ws/FutureSimpleQueue.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.internal.ws 2 | 3 | import java.util.concurrent.{ArrayBlockingQueue, BlockingQueue, LinkedBlockingQueue} 4 | 5 | import sttp.ws.WebSocketBufferFull 6 | 7 | import scala.concurrent.{blocking, ExecutionContext, Future} 8 | 9 | private[client4] class FutureSimpleQueue[T](capacity: Option[Int])(implicit ec: ExecutionContext) 10 | extends SimpleQueue[Future, T] { 11 | 12 | private val queue: BlockingQueue[T] = capacity match { 13 | case Some(value) => new ArrayBlockingQueue[T](value) 14 | case None => new LinkedBlockingQueue[T]() 15 | } 16 | 17 | /** Eagerly adds the given item to the queue. 18 | */ 19 | override def offer(t: T): Unit = 20 | if (!queue.offer(t)) { 21 | throw WebSocketBufferFull(capacity.getOrElse(Int.MaxValue)) 22 | } 23 | 24 | /** Takes an element from the queue or suspends, until one is available. May be eager or lazy, depending on `F`. 25 | */ 26 | override def poll: Future[T] = Future(blocking(queue.take())) 27 | } 28 | -------------------------------------------------------------------------------- /core/src/main/scala/sttp/client4/internal/ws/SimpleQueue.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.internal.ws 2 | 3 | private[client4] trait SimpleQueue[F[_], T] { 4 | 5 | /** Eagerly adds the given item to the queue. 6 | */ 7 | def offer(t: T): Unit 8 | 9 | /** Takes an element from the queue or suspends, until one is available. May be eager or lazy, depending on `F`. 10 | */ 11 | def poll: F[T] 12 | } 13 | -------------------------------------------------------------------------------- /core/src/main/scala/sttp/client4/internal/ws/SyncQueue.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.internal.ws 2 | 3 | import java.util.concurrent.{ArrayBlockingQueue, BlockingQueue, LinkedBlockingQueue} 4 | import sttp.shared.Identity 5 | import sttp.ws.WebSocketBufferFull 6 | 7 | private[client4] class SyncQueue[T](capacity: Option[Int]) extends SimpleQueue[Identity, T] { 8 | 9 | private val queue: BlockingQueue[T] = capacity match { 10 | case Some(value) => new ArrayBlockingQueue[T](value) 11 | case None => new LinkedBlockingQueue[T]() 12 | } 13 | 14 | /** Eagerly adds the given item to the queue. 15 | */ 16 | override def offer(t: T): Unit = 17 | if (!queue.offer(t)) { 18 | throw WebSocketBufferFull(capacity.getOrElse(Int.MaxValue)) 19 | } 20 | 21 | /** Takes an element from the queue or suspends, until one is available. May be eager or lazy, depending on `F`. 22 | */ 23 | override def poll: Identity[T] = queue.take() 24 | } 25 | -------------------------------------------------------------------------------- /core/src/main/scala/sttp/client4/internal/ws/WebSocketEvent.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.internal.ws 2 | 3 | import sttp.ws.WebSocketFrame 4 | 5 | private[client4] sealed trait WebSocketEvent 6 | private[client4] object WebSocketEvent { 7 | case class Open() extends WebSocketEvent 8 | case class Error(t: Throwable) extends WebSocketEvent 9 | case class Frame(f: WebSocketFrame) extends WebSocketEvent 10 | } 11 | -------------------------------------------------------------------------------- /core/src/main/scala/sttp/client4/logging/Logger.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.logging 2 | 3 | /** Interfaces with a logger system. */ 4 | trait Logger[F[_]] { 5 | def apply(level: LogLevel, message: => String, exception: Option[Throwable], context: Map[String, Any]): F[Unit] 6 | } 7 | 8 | sealed trait LogLevel 9 | object LogLevel { 10 | case object Trace extends LogLevel 11 | case object Debug extends LogLevel 12 | case object Info extends LogLevel 13 | case object Warn extends LogLevel 14 | case object Error extends LogLevel 15 | } 16 | -------------------------------------------------------------------------------- /core/src/main/scala/sttp/client4/logging/LoggingOptions.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.logging 2 | 3 | /** Logging configuration that can be set for individual requests, overriding what's set in [[LogConfig]] when creating 4 | * the log backend. 5 | */ 6 | case class LoggingOptions( 7 | log: Boolean = true, 8 | includeTimings: Option[Boolean] = None, 9 | logRequestBody: Option[Boolean] = None, 10 | logResponseBody: Option[Boolean] = None, 11 | logRequestHeaders: Option[Boolean] = None, 12 | logResponseHeaders: Option[Boolean] = None 13 | ) 14 | -------------------------------------------------------------------------------- /core/src/main/scala/sttp/client4/logging/ResponseTimings.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.logging 2 | 3 | import scala.concurrent.duration.Duration 4 | 5 | /** Contains the timings of different parts of the response processing. 6 | * 7 | * @param bodyReceived 8 | * The time it took from sending the request, to receiving the entire response body. This value is not available for 9 | * WebSocket requests, or when using `...Unsafe` streaming response descriptions (e.g. obtaining the response body 10 | * using `asInputStreamUnsafe`). 11 | * @param bodyHandled 12 | * The time it took from sending the request, to handling the entire response body (e.g. including parsing). 13 | */ 14 | case class ResponseTimings(bodyReceived: Option[Duration], bodyHandled: Duration) 15 | -------------------------------------------------------------------------------- /core/src/main/scala/sttp/client4/monad/FunctionK.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.monad 2 | 3 | trait FunctionK[F[_], G[_]] { 4 | def apply[A](fa: => F[A]): G[A] 5 | } 6 | -------------------------------------------------------------------------------- /core/src/main/scala/sttp/client4/package.scala: -------------------------------------------------------------------------------- 1 | package sttp 2 | 3 | package object client4 extends SttpApi { 4 | 5 | /** The type of a predicate that can be used to determine, if a request should be retries. 6 | * @see 7 | * [[RetryWhen.Default]] 8 | */ 9 | type RetryWhen = (GenericRequest[_, _], Either[Throwable, Response[_]]) => Boolean 10 | } 11 | -------------------------------------------------------------------------------- /core/src/main/scala/sttp/client4/testing/AtomicCyclicIterator.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.testing 2 | 3 | import java.util.concurrent.atomic.AtomicInteger 4 | import scala.util.{Failure, Success, Try} 5 | 6 | private[testing] final class AtomicCyclicIterator[+T] private (val elements: Seq[T]) { 7 | private val vector = elements.toVector 8 | private val length = elements.length 9 | private val currentIndex = new AtomicInteger(0) 10 | 11 | def next(): T = { 12 | val index = currentIndex.getAndIncrement % length 13 | vector(index) 14 | } 15 | } 16 | 17 | private[testing] object AtomicCyclicIterator { 18 | 19 | def tryFrom[T](elements: Seq[T]): Try[AtomicCyclicIterator[T]] = 20 | if (elements.nonEmpty) 21 | Success(new AtomicCyclicIterator(elements)) 22 | else 23 | Failure(new IllegalArgumentException("Argument must be a non-empty collection.")) 24 | 25 | def unsafeFrom[T](elements: Seq[T]): AtomicCyclicIterator[T] = tryFrom(elements).get 26 | 27 | def apply[T](head: T, tail: Seq[T]): AtomicCyclicIterator[T] = unsafeFrom(head +: tail) 28 | 29 | def of[T](head: T, tail: T*): AtomicCyclicIterator[T] = apply(head, tail) 30 | } 31 | -------------------------------------------------------------------------------- /core/src/main/scala/sttp/client4/testing/WebSocketStreamConsumer.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.testing 2 | 3 | import sttp.capabilities.Streams 4 | import sttp.ws.WebSocketFrame 5 | 6 | /** Body which can be used to provide behavior for [[sttp.client4.ws.stream.asWebSocketStream]] response descriptions, 7 | * when creating a [[BackendStub]]. 8 | */ 9 | case class WebSocketStreamConsumer[S, Pipe[_, _], F[_]] private ( 10 | consume: Pipe[WebSocketFrame.Data[_], WebSocketFrame] => F[Unit] 11 | ) 12 | 13 | object WebSocketStreamConsumer { 14 | def apply[F[_]]: WebSocketStreamConsumerCreator[F] = new WebSocketStreamConsumerCreator[F] {} 15 | 16 | trait WebSocketStreamConsumerCreator[F[_]] { 17 | def apply[S <: Streams[S]](s: Streams[S])( 18 | consume: s.Pipe[WebSocketFrame.Data[_], WebSocketFrame] => F[Unit] 19 | ): WebSocketStreamConsumer[S, s.Pipe, F] = new WebSocketStreamConsumer(consume) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/src/main/scala/sttp/client4/wrappers/DelegateBackend.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.wrappers 2 | 3 | import sttp.client4.GenericBackend 4 | import sttp.monad.MonadError 5 | 6 | /** A base class for delegate backends, which includes delegating implementations for `close` and `monad`, so that only 7 | * `send` needs to be defined. 8 | */ 9 | abstract class DelegateBackend[F[_], +P](delegate: GenericBackend[F, P]) extends GenericBackend[F, P] { 10 | override def close(): F[Unit] = delegate.close() 11 | override implicit def monad: MonadError[F] = delegate.monad 12 | } 13 | -------------------------------------------------------------------------------- /core/src/main/scala/sttp/client4/ws/exceptions.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.ws 2 | 3 | import sttp.model.StatusCode 4 | 5 | class NotAWebSocketException(statusCode: StatusCode) 6 | extends Exception(s"Not a web socket; got response code: $statusCode") 7 | 8 | class GotAWebSocketException() extends Exception("Got a web socket, but expected normal content") 9 | -------------------------------------------------------------------------------- /core/src/main/scala/sttp/client4/ws/package.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | 3 | package object ws { 4 | object async extends SttpWebSocketAsyncApi 5 | object sync extends SttpWebSocketSyncApi 6 | object stream extends SttpWebSocketStreamApi 7 | } 8 | -------------------------------------------------------------------------------- /core/src/main/scalajs/sttp/client4/DefaultFutureBackend.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | 3 | import sttp.client4.fetch.FetchBackend 4 | import sttp.client4.testing.WebSocketBackendStub 5 | 6 | import scala.concurrent.{ExecutionContext, Future} 7 | 8 | object DefaultFutureBackend { 9 | 10 | /** Creates a default websocket-capable backend which uses [[Future]] to represent side effects, with the given 11 | * `options`. Currently based on [[FetchBackend]]. 12 | */ 13 | def apply()(implicit ec: ExecutionContext = ExecutionContext.global): WebSocketBackend[Future] = FetchBackend() 14 | 15 | /** Create a stub backend for testing, which uses [[Future]] to represent side effects, and doesn't support streaming. 16 | * 17 | * See [[WebSocketBackendStub]] for details on how to configure stub responses. 18 | */ 19 | def stub(implicit ec: ExecutionContext = ExecutionContext.global): WebSocketBackendStub[Future] = 20 | WebSocketBackendStub.asynchronousFuture 21 | } 22 | -------------------------------------------------------------------------------- /core/src/main/scalajs/sttp/client4/PartialRequestExtensions.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | 3 | import sttp.client4.internal.SttpFile 4 | import org.scalajs.dom.File 5 | 6 | trait PartialRequestExtensions[+R <: PartialRequestBuilder[R, _]] { self: R => 7 | 8 | /** If content type is not yet specified, will be set to `application/octet-stream`. 9 | * 10 | * If content length is not yet specified, will be set to the length of the given file. 11 | */ 12 | def body(file: File): R = body(SttpFile.fromDomFile(file)) 13 | } 14 | -------------------------------------------------------------------------------- /core/src/main/scalajs/sttp/client4/ToCurlConverterTestExtension.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | 3 | trait ToCurlConverterTestExtension {} 4 | -------------------------------------------------------------------------------- /core/src/main/scalajs/sttp/client4/compression/CompressorExtensions.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.compression 2 | 3 | trait CompressorExtensions { 4 | def default[R]: List[Compressor[R]] = Nil 5 | } 6 | -------------------------------------------------------------------------------- /core/src/main/scalajs/sttp/client4/compression/DecompressorExtensions.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.compression 2 | 3 | import java.io.InputStream 4 | 5 | trait DecompressorExtensions { 6 | def defaultInputStream: List[Decompressor[InputStream]] = Nil 7 | } 8 | -------------------------------------------------------------------------------- /core/src/main/scalajs/sttp/client4/internal/ConvertFromFuture.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.internal 2 | 3 | import scala.concurrent.Future 4 | 5 | trait ConvertFromFuture[F[_]] { 6 | def apply[T](f: Future[T]): F[T] 7 | } 8 | 9 | object ConvertFromFuture { 10 | lazy val future: ConvertFromFuture[Future] = new ConvertFromFuture[Future] { 11 | override def apply[T](f: Future[T]): Future[T] = f 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /core/src/main/scalajs/sttp/client4/internal/JSSimpleQueue.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.internal 2 | 3 | import sttp.client4.internal.ConvertFromFuture 4 | import sttp.client4.internal.ws.SimpleQueue 5 | 6 | import scala.concurrent.{Future, Promise} 7 | 8 | private[client4] class JSSimpleQueue[F[_], T](implicit fromFuture: ConvertFromFuture[F]) extends SimpleQueue[F, T] { 9 | 10 | private var state: Either[List[Promise[T]], List[T]] = Right(List()) 11 | 12 | def offer(t: T): Unit = state match { 13 | case Left(p :: promises) => 14 | p.success(t) 15 | state = Left(promises) 16 | case Left(Nil) => state = Right(List(t)) 17 | case Right(elems) => state = Right(elems :+ t) 18 | } 19 | 20 | def poll: F[T] = fromFuture { 21 | state match { 22 | case Right(t :: elems) => 23 | state = Right(elems) 24 | Future.successful(t) 25 | case Right(Nil) => 26 | val p = Promise[T]() 27 | state = Left(List(p)) 28 | p.future 29 | case Left(promises) => 30 | val p = Promise[T]() 31 | state = Left(promises :+ p) 32 | p.future 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /core/src/main/scalajs/sttp/client4/internal/MessageDigestCompatibility.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.internal 2 | 3 | import scala.scalajs.js.JSConverters._ 4 | 5 | private[client4] class MessageDigestCompatibility(algorithm: String) { 6 | private lazy val md: scala.scalajs.js.typedarray.ArrayBuffer => String = algorithm match { 7 | case "MD5" => SparkMD5.ArrayBuffer.hash(_) 8 | case _ => throw new IllegalArgumentException(s"Unsupported algorithm: $algorithm") 9 | } 10 | 11 | def digest(input: Array[Byte]): Array[Byte] = 12 | md(input.toJSArray.asInstanceOf[scala.scalajs.js.typedarray.ArrayBuffer]).getBytes("UTF-8") 13 | } 14 | -------------------------------------------------------------------------------- /core/src/main/scalajs/sttp/client4/internal/SparkMD5.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.internal 2 | 3 | import scala.scalajs.js 4 | import scala.scalajs.js.annotation.JSGlobal 5 | 6 | @js.native 7 | @JSGlobal 8 | private[client4] object SparkMD5 extends js.Object { 9 | @js.native 10 | object ArrayBuffer extends js.Object { 11 | def hash(arr: scala.scalajs.js.typedarray.ArrayBuffer, raw: Boolean = false): String = js.native 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /core/src/main/scalajs/sttp/client4/internal/SttpFileExtensions.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.internal 2 | 3 | import org.scalajs.dom.File 4 | 5 | import java.io.FileInputStream 6 | import java.io.InputStream 7 | 8 | // wrap a DomFile 9 | trait SttpFileExtensions { self: SttpFile => 10 | 11 | def toDomFile: File = underlying.asInstanceOf[File] 12 | 13 | def readAsString(): String = throw new UnsupportedOperationException() 14 | def readAsByteArray(): Array[Byte] = throw new UnsupportedOperationException() 15 | def openStream(): InputStream = throw new UnsupportedOperationException() 16 | def length(): Long = throw new UnsupportedOperationException() 17 | } 18 | 19 | trait SttpFileCompanionExtensions { 20 | def fromDomFile(file: File): SttpFile = 21 | new SttpFile(file) { 22 | val name: String = file.name 23 | val size: Long = file.size.toLong 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /core/src/main/scalajs/sttp/client4/quick.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | 3 | import scala.concurrent.Future 4 | 5 | object quick extends SttpApi { 6 | lazy val backend: Backend[Future] = DefaultFutureBackend() 7 | 8 | implicit class RichRequest[T](val request: Request[T]) { 9 | def send(): Future[Response[T]] = backend.send(request) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /core/src/main/scalajvm/sttp/client4/DefaultFutureBackend.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | 3 | import sttp.client4.httpclient.HttpClientFutureBackend 4 | import sttp.client4.testing.WebSocketBackendStub 5 | 6 | import scala.concurrent.{ExecutionContext, Future} 7 | 8 | object DefaultFutureBackend { 9 | 10 | /** Creates a default websocket-capable backend which uses [[Future]] to represent side effects, with the given 11 | * `options`. Currently based on [[HttpClientFutureBackend]]. 12 | */ 13 | def apply( 14 | options: BackendOptions = BackendOptions.Default 15 | )(implicit ec: ExecutionContext = ExecutionContext.global): WebSocketBackend[Future] = 16 | HttpClientFutureBackend(options) 17 | 18 | /** Create a stub backend for testing, which uses [[Future]] to represent side effects, and doesn't support streaming. 19 | * 20 | * See [[WebSocketBackendStub]] for details on how to configure stub responses. 21 | */ 22 | def stub(implicit ec: ExecutionContext = ExecutionContext.global): WebSocketBackendStub[Future] = 23 | WebSocketBackendStub.asynchronousFuture 24 | } 25 | -------------------------------------------------------------------------------- /core/src/main/scalajvm/sttp/client4/DefaultSyncBackend.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | 3 | import sttp.client4.httpclient.HttpClientSyncBackend 4 | import sttp.client4.testing.WebSocketSyncBackendStub 5 | 6 | object DefaultSyncBackend { 7 | 8 | /** Creates a default synchronous backend with the given `options`, which is currently based on 9 | * [[HttpClientSyncBackend]]. 10 | */ 11 | def apply(options: BackendOptions = BackendOptions.Default): WebSocketSyncBackend = HttpClientSyncBackend(options) 12 | 13 | /** Create a stub backend for testing. See [[WebSocketSyncBackendStub]] for details on how to configure stub 14 | * responses. 15 | */ 16 | def stub: WebSocketSyncBackendStub = WebSocketSyncBackendStub 17 | } 18 | -------------------------------------------------------------------------------- /core/src/main/scalajvm/sttp/client4/PartialRequestExtensions.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | 3 | import java.io.File 4 | import java.nio.file.Path 5 | 6 | import sttp.client4.internal.SttpFile 7 | 8 | trait PartialRequestExtensions[+R <: PartialRequestBuilder[R, _]] { self: R => 9 | 10 | /** If content type is not yet specified, will be set to `application/octet-stream`. 11 | * 12 | * If content length is not yet specified, will be set to the length of the given file. 13 | */ 14 | def body(file: File): R = body(SttpFile.fromFile(file)) 15 | 16 | /** If content type is not yet specified, will be set to `application/octet-stream`. 17 | * 18 | * If content length is not yet specified, will be set to the length of the given file. 19 | */ 20 | def body(path: Path): R = body(SttpFile.fromPath(path)) 21 | } 22 | -------------------------------------------------------------------------------- /core/src/main/scalajvm/sttp/client4/compression/CompressorExtensions.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.compression 2 | 3 | trait CompressorExtensions { 4 | def default[R]: List[Compressor[R]] = List(new GZipDefaultCompressor[R](), new DeflateDefaultCompressor[R]()) 5 | } 6 | -------------------------------------------------------------------------------- /core/src/main/scalajvm/sttp/client4/compression/DecompressorExtensions.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.compression 2 | 3 | import java.io.InputStream 4 | 5 | trait DecompressorExtensions { 6 | def defaultInputStream: List[Decompressor[InputStream]] = 7 | List(GZipInputStreamDecompressor, DeflateInputStreamDecompressor) 8 | } 9 | -------------------------------------------------------------------------------- /core/src/main/scalajvm/sttp/client4/compression/defaultDecompressors.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.compression 2 | 3 | import sttp.client4.internal.SafeGZIPInputStream 4 | import sttp.model.Encodings 5 | 6 | import java.io.InputStream 7 | import java.util.zip.InflaterInputStream 8 | 9 | object GZipInputStreamDecompressor extends Decompressor[InputStream] { 10 | override val encoding: String = Encodings.Gzip 11 | override def apply(body: InputStream): InputStream = SafeGZIPInputStream.apply(body) 12 | } 13 | 14 | object DeflateInputStreamDecompressor extends Decompressor[InputStream] { 15 | override val encoding: String = Encodings.Deflate 16 | override def apply(body: InputStream): InputStream = new InflaterInputStream(body) 17 | } 18 | -------------------------------------------------------------------------------- /core/src/main/scalajvm/sttp/client4/internal/MessageDigestCompatibility.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.internal 2 | 3 | import java.security.MessageDigest 4 | 5 | private[client4] class MessageDigestCompatibility(algorithm: String) { 6 | private lazy val md = MessageDigest.getInstance(algorithm) 7 | 8 | def digest(input: Array[Byte]): Array[Byte] = md.digest(input) 9 | } 10 | -------------------------------------------------------------------------------- /core/src/main/scalajvm/sttp/client4/internal/SttpFileExtensions.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.internal 2 | 3 | import java.nio.file.Files 4 | import java.nio.file.Path 5 | 6 | import scala.io.Source 7 | import java.io.FileInputStream 8 | import java.io.InputStream 9 | 10 | // wrap a Path 11 | trait SttpFileExtensions { self: SttpFile => 12 | 13 | def toPath: Path = underlying.asInstanceOf[Path] 14 | def toFile: java.io.File = toPath.toFile 15 | 16 | def readAsString(): String = { 17 | val s = Source.fromFile(toFile, "UTF-8"); 18 | try s.getLines().mkString("\n") 19 | finally s.close() 20 | } 21 | def readAsByteArray(): Array[Byte] = Files.readAllBytes(toPath) 22 | def openStream(): InputStream = new FileInputStream(toFile) 23 | def length(): Long = toFile.length() 24 | } 25 | 26 | trait SttpFileCompanionExtensions { 27 | def fromPath(path: Path): SttpFile = 28 | new SttpFile(path) { 29 | val name: String = path.getFileName.toString 30 | def size: Long = Files.size(path) 31 | } 32 | def fromFile(file: java.io.File): SttpFile = fromPath(file.toPath) 33 | } 34 | -------------------------------------------------------------------------------- /core/src/main/scalajvm/sttp/client4/internal/SttpToJavaConverters.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.internal 2 | 3 | private[client4] object SttpToJavaConverters { 4 | 5 | def toJavaFunction[U, V](f: Function1[U, V]): java.util.function.Function[U, V] = 6 | new java.util.function.Function[U, V] { 7 | override def apply(t: U): V = f(t) 8 | } 9 | 10 | def toJavaBiConsumer[U, R](f: Function2[U, R, Unit]): java.util.function.BiConsumer[U, R] = 11 | new java.util.function.BiConsumer[U, R] { 12 | override def accept(t: U, u: R): Unit = f(t, u) 13 | } 14 | 15 | def toJavaSupplier[U](f: Function0[U]): java.util.function.Supplier[U] = 16 | new java.util.function.Supplier[U] { 17 | override def get(): U = f() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /core/src/main/scalajvm/sttp/client4/internal/httpclient/FutureSequencer.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.internal.httpclient 2 | 3 | import java.util.concurrent.Semaphore 4 | import scala.concurrent.{blocking, ExecutionContext, Future} 5 | 6 | private[client4] class FutureSequencer(implicit ec: ExecutionContext) extends Sequencer[Future] { 7 | private val semaphore = new Semaphore(1) 8 | 9 | def apply[T](t: => Future[T]): Future[T] = { 10 | blocking { 11 | semaphore.acquire() 12 | } 13 | t.andThen { case _ => semaphore.release() } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /core/src/main/scalajvm/sttp/client4/internal/httpclient/IdSequencer.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.internal.httpclient 2 | 3 | import sttp.shared.Identity 4 | 5 | import java.util.concurrent.Semaphore 6 | import scala.concurrent.blocking 7 | 8 | private[client4] class IdSequencer extends Sequencer[Identity] { 9 | private val semaphore = new Semaphore(1) 10 | 11 | def apply[T](t: => T): T = { 12 | blocking(semaphore.acquire()) 13 | try t 14 | finally semaphore.release() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /core/src/main/scalajvm/sttp/client4/internal/httpclient/Sequencer.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.internal.httpclient 2 | 3 | /** Ensures that given effects are always run in sequence. */ 4 | private[client4] trait Sequencer[F[_]] { 5 | def apply[T](t: => F[T]): F[T] 6 | } 7 | -------------------------------------------------------------------------------- /core/src/main/scalajvm/sttp/client4/internal/httpclient/package.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.internal 2 | 3 | import java.util.concurrent.Flow.Publisher 4 | 5 | package object httpclient { 6 | private[client4] def cancelPublisher[T](p: Publisher[T]): Unit = 7 | p.subscribe(new java.util.concurrent.Flow.Subscriber[T] { 8 | override def onSubscribe(s: java.util.concurrent.Flow.Subscription): Unit = s.cancel() 9 | override def onNext(t: T): Unit = () 10 | override def onError(t: Throwable): Unit = () 11 | override def onComplete(): Unit = () 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /core/src/main/scalajvm/sttp/client4/quick.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | 3 | object quick extends SttpApi { 4 | lazy val backend: WebSocketSyncBackend = DefaultSyncBackend() 5 | 6 | implicit class RichRequest[T](val request: Request[T]) { 7 | def send(): Response[T] = backend.send(request) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /core/src/main/scalanative/sttp/client4/DefaultSyncBackend.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | 3 | import sttp.client4.curl.CurlBackend 4 | import sttp.client4.testing.SyncBackendStub 5 | 6 | object DefaultSyncBackend { 7 | 8 | /** Creates a default synchronous backend, which is currently based on [[CurlBackend]]. */ 9 | def apply(): SyncBackend = CurlBackend() 10 | 11 | /** Create a stub backend for testing. See [[SyncBackendStub]] for details on how to configure stub responses. */ 12 | def stub: SyncBackendStub = SyncBackendStub 13 | } 14 | -------------------------------------------------------------------------------- /core/src/main/scalanative/sttp/client4/FileHelpers.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | 3 | import sttp.client4.internal._ 4 | 5 | import java.io.{File, FileOutputStream, IOException, InputStream} 6 | import java.nio.file.AccessDeniedException 7 | 8 | object FileHelpers { 9 | private[client4] def saveFile(file: File, is: InputStream): File = { 10 | if (!file.exists()) { 11 | if (file.getParentFile != null) { 12 | file.getParentFile.mkdirs() 13 | } 14 | try 15 | file.createNewFile() 16 | catch { 17 | case e: AccessDeniedException => throw new IOException("Permission denied", e) // aligns SN bahavior with Java 18 | } 19 | } 20 | 21 | val os = new FileOutputStream(file) 22 | 23 | transfer(is, os) 24 | file 25 | } 26 | 27 | private[client4] def getFilePath[T, R](response: GenericResponseAs[T, R]): Option[SttpFile] = 28 | response match { 29 | case MappedResponseAs(raw, g, _) => getFilePath(raw) 30 | case rfm: ResponseAsFromMetadata[T, _] => 31 | rfm.conditions 32 | .flatMap(c => getFilePath(c.responseAs)) 33 | .headOption 34 | case ResponseAsFile(file) => Some(file) 35 | case _ => None 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /core/src/main/scalanative/sttp/client4/PartialRequestExtensions.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | 3 | import java.io.File 4 | import java.nio.file.Path 5 | 6 | import sttp.client4.internal._ 7 | 8 | trait PartialRequestExtensions[+R <: PartialRequestBuilder[R, _]] { self: R => 9 | 10 | /** If content type is not yet specified, will be set to `application/octet-stream`. 11 | * 12 | * If content length is not yet specified, will be set to the length of the given file. 13 | */ 14 | def body(file: File): R = body(SttpFile.fromFile(file)) 15 | 16 | /** If content type is not yet specified, will be set to `application/octet-stream`. 17 | * 18 | * If content length is not yet specified, will be set to the length of the given file. 19 | */ 20 | def body(path: Path): R = body(SttpFile.fromPath(path)) 21 | } 22 | -------------------------------------------------------------------------------- /core/src/main/scalanative/sttp/client4/compression/CompressorExtensions.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.compression 2 | 3 | trait CompressorExtensions { 4 | def default[R]: List[Compressor[R]] = Nil 5 | } 6 | -------------------------------------------------------------------------------- /core/src/main/scalanative/sttp/client4/compression/DecompressorExtensions.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.compression 2 | 3 | import java.io.InputStream 4 | 5 | trait DecompressorExtensions { 6 | def defaultInputStream: List[Decompressor[InputStream]] = Nil 7 | } 8 | -------------------------------------------------------------------------------- /core/src/main/scalanative/sttp/client4/curl/CurlBackends.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.curl 2 | 3 | import sttp.client4._ 4 | import sttp.client4.wrappers.FollowRedirectsBackend 5 | import sttp.monad.{IdentityMonad, TryMonad} 6 | 7 | import scala.util.Try 8 | 9 | // Curl supports redirects, but it doesn't store the history, so using FollowRedirectsBackend is more convenient 10 | 11 | private class CurlBackend(verbose: Boolean) extends AbstractSyncCurlBackend(IdentityMonad, verbose) with SyncBackend {} 12 | 13 | object CurlBackend { 14 | def apply(verbose: Boolean = false): SyncBackend = FollowRedirectsBackend(new CurlBackend(verbose)) 15 | } 16 | 17 | private class CurlTryBackend(verbose: Boolean) extends AbstractSyncCurlBackend(TryMonad, verbose) with Backend[Try] {} 18 | 19 | object CurlTryBackend { 20 | def apply(verbose: Boolean = false): Backend[Try] = FollowRedirectsBackend(new CurlTryBackend(verbose)) 21 | } 22 | -------------------------------------------------------------------------------- /core/src/main/scalanative/sttp/client4/curl/internal/CurlList.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.curl.internal 2 | 3 | import sttp.client4.curl.internal.CurlApi.SlistHandle 4 | 5 | class CurlList(val ptr: SlistHandle) extends AnyVal {} 6 | -------------------------------------------------------------------------------- /core/src/main/scalanative/sttp/client4/curl/internal/CurlSpaces.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.curl.internal 2 | 3 | import scala.scalanative.unsafe.Ptr 4 | 5 | class CurlSpaces(val bodyResp: Ptr[CurlFetch], val headersResp: Ptr[CurlFetch], val httpCode: Ptr[Long]) 6 | -------------------------------------------------------------------------------- /core/src/main/scalanative/sttp/client4/curl/internal/package.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.curl 2 | 3 | import scala.scalanative.unsafe.{CSize, CString, CStruct2, Ptr} 4 | 5 | package object internal { 6 | type CurlSlist = CStruct2[CString, Ptr[_]] 7 | type CurlFetch = CStruct2[CString, CSize] 8 | val CurlZeroTerminated = -1L 9 | 10 | private[curl] final val CCurl = libcurlPlatformCompat.instance 11 | } 12 | -------------------------------------------------------------------------------- /core/src/main/scalanative/sttp/client4/internal/CryptoMd5.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.internal 2 | 3 | import scala.scalanative.unsafe._ 4 | import scala.scalanative.libc.string.strlen 5 | import scala.scalanative.runtime.ByteArray 6 | import scala.scalanative.unsigned._ 7 | 8 | object CryptoMd5 { 9 | @link("crypto") 10 | @extern 11 | private object C { 12 | def MD5(string: CString, size: CSize, result: CString): CString = extern 13 | } 14 | 15 | def digest(input: Array[Byte]): Array[Byte] = { 16 | val result = ByteArray.alloc(16) 17 | C.MD5(input.asInstanceOf[ByteArray].at(0), input.length.toUInt, result.at(0)) 18 | result.asInstanceOf[Array[Byte]] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/scalanative/sttp/client4/internal/MessageDigestCompatibility.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.internal 2 | 3 | private[client4] class MessageDigestCompatibility(algorithm: String) { 4 | require(algorithm == "MD5", s"Unsupported algorithm: $algorithm") 5 | 6 | def digest(input: Array[Byte]): Array[Byte] = 7 | CryptoMd5.digest(input) 8 | } 9 | -------------------------------------------------------------------------------- /core/src/main/scalanative/sttp/client4/internal/SttpFileExtensions.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.internal 2 | 3 | import java.nio.file.Files 4 | import java.nio.file.Path 5 | 6 | import scala.io.Source 7 | 8 | import java.io.FileInputStream 9 | import java.io.InputStream 10 | 11 | trait SttpFileExtensions { self: SttpFile => 12 | def toPath: Path = underlying.asInstanceOf[Path] 13 | def toFile: java.io.File = toPath.toFile 14 | 15 | def readAsString(): String = { 16 | val s = Source.fromFile(toFile, "UTF-8"); 17 | try s.getLines().mkString("\n") 18 | finally s.close() 19 | } 20 | def readAsByteArray(): Array[Byte] = Files.readAllBytes(toPath) 21 | def openStream(): InputStream = new FileInputStream(toFile) 22 | def length(): Long = toFile.length() 23 | } 24 | 25 | trait SttpFileCompanionExtensions { 26 | def fromPath(path: Path): SttpFile = 27 | new SttpFile(path) { 28 | val name: String = path.getFileName.toString 29 | def size: Long = Files.size(path) 30 | } 31 | def fromFile(file: java.io.File): SttpFile = fromPath(file.toPath) 32 | } 33 | -------------------------------------------------------------------------------- /core/src/main/scalanative/sttp/client4/quick.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | 3 | object quick extends SttpApi { 4 | lazy val backend: SyncBackend = DefaultSyncBackend() 5 | 6 | implicit class RichRequest[T](val request: Request[T]) { 7 | def send(): Response[T] = backend.send(request) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /core/src/test/scala/sttp/client4/testing/ConvertToFuture.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.testing 2 | 3 | import sttp.shared.Identity 4 | 5 | import scala.concurrent.Future 6 | import scala.util.Try 7 | 8 | trait ConvertToFuture[F[_]] { 9 | def toFuture[T](value: F[T]): Future[T] 10 | } 11 | 12 | object ConvertToFuture { 13 | 14 | lazy val id: ConvertToFuture[Identity] = new ConvertToFuture[Identity] { 15 | override def toFuture[T](value: Identity[T]): Future[T] = 16 | Future.successful(value) 17 | } 18 | 19 | lazy val future: ConvertToFuture[Future] = new ConvertToFuture[Future] { 20 | override def toFuture[T](value: Future[T]): Future[T] = value 21 | } 22 | 23 | lazy val scalaTry: ConvertToFuture[Try] = new ConvertToFuture[Try] { 24 | override def toFuture[T](value: Try[T]): Future[T] = Future.fromTry(value) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /core/src/test/scala/sttp/client4/testing/TestStreams.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.testing 2 | 3 | import sttp.capabilities.Streams 4 | 5 | trait TestStreams extends Streams[TestStreams] { 6 | override type BinaryStream = List[Byte] 7 | override type Pipe[A, B] = A => B 8 | } 9 | 10 | object TestStreams extends TestStreams 11 | -------------------------------------------------------------------------------- /core/src/test/scala/sttp/client4/testing/ToFutureWrapper.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.testing 2 | 3 | import scala.concurrent.Future 4 | 5 | import org.scalatest.exceptions.TestFailedException 6 | 7 | trait ToFutureWrapper { 8 | 9 | implicit final class ConvertToFutureDecorator[F[_], T](wrapped: => F[T]) { 10 | def toFuture()(implicit ctf: ConvertToFuture[F]): Future[T] = 11 | try 12 | ctf.toFuture(wrapped) 13 | catch { 14 | case e: TestFailedException if e.getCause != null => Future.failed(e.getCause) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /core/src/test/scalajs/sttp/client4/FetchBackendHttpTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | 3 | import sttp.client4.fetch.FetchBackend 4 | import sttp.client4.testing.{AbstractFetchHttpTest, ConvertToFuture} 5 | 6 | import scala.concurrent.Future 7 | 8 | class FetchBackendHttpTest extends AbstractFetchHttpTest[Future, Nothing] { 9 | 10 | override val backend: Backend[Future] = FetchBackend() 11 | override implicit val convertToFuture: ConvertToFuture[Future] = ConvertToFuture.future 12 | 13 | override protected def supportsCustomMultipartContentType = false 14 | 15 | override protected def supportsCustomMultipartEncoding = false 16 | } 17 | -------------------------------------------------------------------------------- /core/src/test/scalajs/sttp/client4/FetchBackendWebSocketTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | 3 | import sttp.client4.fetch.FetchBackend 4 | import sttp.client4.testing.ConvertToFuture 5 | import sttp.client4.testing.websocket.WebSocketTest 6 | import sttp.monad.{FutureMonad, MonadError} 7 | 8 | import scala.concurrent.{ExecutionContext, Future} 9 | import scala.scalajs.concurrent.JSExecutionContext.Implicits.queue 10 | 11 | class FetchBackendWebSocketTest extends WebSocketTest[Future] { 12 | 13 | implicit override def executionContext: ExecutionContext = queue 14 | override def throwsWhenNotAWebSocket: Boolean = true 15 | override def supportsReadingWebSocketResponseHeaders: Boolean = false 16 | override def supportsReadingSubprotocolWebSocketResponseHeader: Boolean = false 17 | 18 | override val backend: WebSocketBackend[Future] = FetchBackend() 19 | override implicit val convertToFuture: ConvertToFuture[Future] = ConvertToFuture.future 20 | 21 | override implicit def monad: MonadError[Future] = new FutureMonad() 22 | } 23 | -------------------------------------------------------------------------------- /core/src/test/scalajs/sttp/client4/TestPlatform.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | 3 | sealed trait TestPlatform 4 | object TestPlatform { 5 | case object JVM extends TestPlatform 6 | case object JS extends TestPlatform 7 | case object Native extends TestPlatform 8 | 9 | val Current = JS 10 | } 11 | -------------------------------------------------------------------------------- /core/src/test/scalajs/sttp/client4/testing/AsyncExecutionContext.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.testing 2 | 3 | import org.scalatest.AsyncTestSuite 4 | 5 | import scala.concurrent.ExecutionContext 6 | 7 | /** When running JS tests, the default ScalaTest async execution context uses 8 | * `scala.scalajs.concurrent.JSExecutionContext.Implicits.queue`, which causes async tests to fail with: Queue is empty 9 | * while future is not completed, this means you're probably using a wrong ExecutionContext for your task, please 10 | * double check your Future. 11 | */ 12 | trait AsyncExecutionContext { self: AsyncTestSuite => 13 | implicit override def executionContext: ExecutionContext = ExecutionContext.global 14 | } 15 | -------------------------------------------------------------------------------- /core/src/test/scalajs/sttp/client4/testing/AsyncRetries.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.testing 2 | 3 | // see the jvm impl 4 | trait AsyncRetries 5 | -------------------------------------------------------------------------------- /core/src/test/scalajs/sttp/client4/testing/Platform.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.testing 2 | 3 | import scala.concurrent.Future 4 | import scala.concurrent.Promise 5 | import scala.concurrent.duration.FiniteDuration 6 | import scala.scalajs.js.timers.setTimeout 7 | 8 | object Platform { 9 | 10 | def delayedFuture[T](delay: FiniteDuration)(result: => T): Future[T] = { 11 | val p = Promise[T]() 12 | 13 | setTimeout(delay)(p.success(result)) 14 | p.future 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /core/src/test/scalajs/sttp/client4/testing/streaming/StreamingTestExtensions.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.testing.streaming 2 | 3 | import scala.language.higherKinds 4 | import sttp.client4.testing.AsyncExecutionContext 5 | 6 | trait StreamingTestExtensions[F[_], S] extends AsyncExecutionContext { self: StreamingTest[F, S] => } 7 | -------------------------------------------------------------------------------- /core/src/test/scalajvm/sttp/client4/BackendOptionsProxyTest2.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | 3 | import org.scalatest.flatspec.AnyFlatSpec 4 | import org.scalatest.matchers.should.Matchers 5 | 6 | import java.io.IOException 7 | import java.net.URI 8 | 9 | class BackendOptionsProxyTest2 extends AnyFlatSpec with Matchers { 10 | it should "throw UnsupportedOperationException with reason" in { 11 | val proxySetting = BackendOptions.Proxy( 12 | "fakeproxyserverhost", 13 | 8080, 14 | BackendOptions.ProxyType.Http, 15 | nonProxyHosts = Nil, 16 | onlyProxyHosts = Nil 17 | ) 18 | 19 | val proxySelector = proxySetting.asJavaProxySelector 20 | val ex = intercept[UnsupportedOperationException] { 21 | val uri = new URI("foo") 22 | val ioe = new IOException("bar") 23 | proxySelector.connectFailed(uri, proxySetting.inetSocketAddress, ioe) 24 | } 25 | ex.getMessage should startWith("Couldn't connect to the proxy server, uri: foo, socket: fakeproxyserverhost") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /core/src/test/scalajvm/sttp/client4/HttpClientFutureHttpTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | 3 | import sttp.client4.httpclient.HttpClientFutureBackend 4 | import sttp.client4.testing.{ConvertToFuture, HttpTest} 5 | 6 | import scala.concurrent.Future 7 | 8 | class HttpClientFutureHttpTest extends HttpTest[Future] { 9 | override val backend: Backend[Future] = HttpClientFutureBackend() 10 | override implicit val convertToFuture: ConvertToFuture[Future] = ConvertToFuture.future 11 | 12 | override def supportsHostHeaderOverride = false 13 | override def supportsCancellation: Boolean = false 14 | override def supportsDeflateWrapperChecking = false 15 | 16 | override def timeoutToNone[T](t: Future[T], timeoutMillis: Int): Future[Option[T]] = t.map(Some(_)) 17 | } 18 | -------------------------------------------------------------------------------- /core/src/test/scalajvm/sttp/client4/HttpClientFutureWebSocketTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | 3 | import sttp.client4.httpclient.HttpClientFutureBackend 4 | import sttp.client4.testing.ConvertToFuture 5 | import sttp.client4.testing.websocket.{WebSocketConcurrentTest, WebSocketTest} 6 | import sttp.monad.{FutureMonad, MonadError} 7 | 8 | import scala.concurrent.Future 9 | 10 | class HttpClientFutureWebSocketTest[F[_]] extends WebSocketTest[Future] with WebSocketConcurrentTest[Future] { 11 | override val backend: WebSocketBackend[Future] = HttpClientFutureBackend() 12 | override implicit val convertToFuture: ConvertToFuture[Future] = ConvertToFuture.future 13 | override implicit val monad: MonadError[Future] = new FutureMonad() 14 | 15 | override def concurrently[T](fs: List[() => Future[T]]): Future[List[T]] = Future.sequence(fs.map(_())) 16 | 17 | override def supportsReadingWebSocketResponseHeaders: Boolean = false 18 | } 19 | -------------------------------------------------------------------------------- /core/src/test/scalajvm/sttp/client4/HttpClientSyncWebSocketTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | 3 | import sttp.client4.httpclient.HttpClientSyncBackend 4 | import sttp.client4.testing.websocket.WebSocketTest 5 | import sttp.client4.testing.ConvertToFuture 6 | import sttp.monad.{IdentityMonad, MonadError} 7 | import sttp.shared.Identity 8 | 9 | class HttpClientSyncWebSocketTest extends WebSocketTest[Identity] { 10 | override val backend: WebSocketSyncBackend = HttpClientSyncBackend() 11 | override implicit val convertToFuture: ConvertToFuture[Identity] = ConvertToFuture.id 12 | override implicit val monad: MonadError[Identity] = IdentityMonad 13 | 14 | override def throwsWhenNotAWebSocket: Boolean = true 15 | // HttpClient doesn't expose the response headers for web sockets in any way 16 | override def supportsReadingWebSocketResponseHeaders: Boolean = false 17 | } 18 | -------------------------------------------------------------------------------- /core/src/test/scalajvm/sttp/client4/TestPlatform.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | 3 | sealed trait TestPlatform 4 | object TestPlatform { 5 | case object JVM extends TestPlatform 6 | case object JS extends TestPlatform 7 | case object Native extends TestPlatform 8 | 9 | val Current = JVM 10 | } 11 | -------------------------------------------------------------------------------- /core/src/test/scalajvm/sttp/client4/ToCurlConverterTestExtension.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | 3 | import java.io.File 4 | 5 | import org.scalatest.Suite 6 | import sttp.client4.internal.SttpFile 7 | import org.scalatest.flatspec.AnyFlatSpec 8 | import org.scalatest.matchers.should.Matchers 9 | 10 | trait ToCurlConverterTestExtension { suit: Suite with AnyFlatSpec with Matchers => 11 | it should "render multipart form data if content is a file" in { 12 | basicRequest 13 | .multipartBody(multipartSttpFile("upload", SttpFile.fromPath(new File("myDataSet").toPath))) 14 | .post(uri"http://localhost") 15 | .toCurl should include( 16 | """--form 'upload=@myDataSet'""" 17 | ) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /core/src/test/scalajvm/sttp/client4/TryBackendTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | 3 | import sttp.client4.httpclient.HttpClientSyncBackend 4 | import sttp.client4.testing.{ConvertToFuture, HttpTest} 5 | import sttp.client4.wrappers.TryBackend 6 | 7 | import scala.util.Try 8 | 9 | class TryBackendTest extends HttpTest[Try] { 10 | 11 | override val backend: Backend[Try] = TryBackend(HttpClientSyncBackend()) 12 | override implicit val convertToFuture: ConvertToFuture[Try] = ConvertToFuture.scalaTry 13 | 14 | override def supportsCancellation = false 15 | override def supportsHostHeaderOverride = false 16 | override def supportsDeflateWrapperChecking = false 17 | 18 | override def timeoutToNone[T](t: Try[T], timeoutMillis: Int): Try[Option[T]] = t.map(Some(_)) 19 | } 20 | -------------------------------------------------------------------------------- /core/src/test/scalajvm/sttp/client4/testing/AsyncRetries.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.testing 2 | 3 | import org.scalatest.freespec.AsyncFreeSpecLike 4 | import org.scalatest.{Failed, FutureOutcome} 5 | 6 | import scala.concurrent.Future 7 | 8 | // this needs to be added here, as it doesn't compile on native 9 | trait AsyncRetries extends AsyncFreeSpecLike { 10 | // TODO: on GH Actions some tests sometimes timeout. For lack of a better solution, retrying them, but this needs proper fixing one day. 11 | override def withFixture(test: NoArgAsyncTest): FutureOutcome = 12 | new FutureOutcome(super.withFixture(test).toFuture.flatMap { 13 | case Failed(e) => 14 | info(s"Test: ${test.name}, failed with: ${e.getMessage}, retrying.", Some(e)) 15 | super.withFixture(test).toFuture 16 | case o => Future.successful(o) 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /core/src/test/scalajvm/sttp/client4/testing/BackendStubTests2.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.testing 2 | 3 | import org.scalatest.concurrent.ScalaFutures 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should.Matchers 6 | import sttp.client4.SttpClientException.ReadException 7 | import sttp.client4.{basicRequest, UriContext} 8 | 9 | import java.util.concurrent.TimeoutException 10 | import scala.concurrent.ExecutionContext.Implicits.global 11 | import scala.concurrent.Future 12 | 13 | class BackendStubTests2 extends AnyFlatSpec with Matchers with ScalaFutures { 14 | it should "handle exceptions thrown instead of a response (asynchronous)" in { 15 | val backend: BackendStub[Future] = BackendStub.asynchronousFuture 16 | .whenRequestMatches(_ => true) 17 | .thenRespond(throw new TimeoutException()) 18 | 19 | val result = basicRequest.get(uri"http://example.org").send(backend) 20 | result.failed.map(_ shouldBe a[ReadException]).futureValue 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/src/test/scalajvm/sttp/client4/testing/Platform.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.testing 2 | 3 | import scala.concurrent.ExecutionContext 4 | import scala.concurrent.Future 5 | import scala.concurrent.duration.FiniteDuration 6 | 7 | object Platform { 8 | 9 | def delayedFuture[T](delay: FiniteDuration)(result: => T)(implicit ec: ExecutionContext): Future[T] = 10 | Future { 11 | Thread.sleep(delay.toMillis) 12 | result 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /core/src/test/scalajvm/sttp/client4/testing/RetryTests.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.testing 2 | 3 | import org.scalatest.freespec.AsyncFreeSpec 4 | import org.scalatest.{Canceled, Failed, FutureOutcome} 5 | 6 | import scala.concurrent.Future 7 | 8 | /** Retries all tests up to the given number of attempts. */ 9 | trait RetryTests extends AsyncFreeSpec { 10 | val retries = 10 11 | 12 | override def withFixture(test: NoArgAsyncTest): FutureOutcome = withFixture(test, retries) 13 | 14 | def withFixture(test: NoArgAsyncTest, count: Int): FutureOutcome = { 15 | val outcome = super.withFixture(test) 16 | new FutureOutcome(outcome.toFuture.flatMap { 17 | case Failed(_) | Canceled(_) => 18 | if (count == 1) super.withFixture(test).toFuture 19 | else { 20 | info("Retrying a failed test: " + test.name) 21 | withFixture(test, count - 1).toFuture 22 | } 23 | case other => Future.successful(other) 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /core/src/test/scalajvm/sttp/client4/testing/streaming/StreamingTestExtensions.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.testing.streaming 2 | 3 | trait StreamingTestExtensions[F[_], S] { self: StreamingTest[F, S] => } 4 | -------------------------------------------------------------------------------- /core/src/test/scalanative/org/scalatest/freespec/AsyncFreeSpec.scala: -------------------------------------------------------------------------------- 1 | package org.scalatest.freespec 2 | 3 | // added only to make the tests compile, since it's used in shared tests 4 | trait AsyncFreeSpec extends AnyFreeSpec 5 | -------------------------------------------------------------------------------- /core/src/test/scalanative/org/scalatest/freespec/AsyncFreeSpecLike.scala: -------------------------------------------------------------------------------- 1 | package org.scalatest.freespec 2 | 3 | // added only to make the tests compile, since it's used in shared tests 4 | trait AsyncFreeSpecLike extends AnyFreeSpecLike 5 | -------------------------------------------------------------------------------- /core/src/test/scalanative/sttp/client4/CurlBackendHttpTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | package curl 3 | 4 | import sttp.client4.curl.CurlBackend 5 | import sttp.client4.curl.internal.CCurl 6 | import sttp.client4.testing.SyncHttpTest 7 | import scalanative.unsafe.fromCString 8 | 9 | class CurlBackendHttpTest extends SyncHttpTest { 10 | override implicit val backend: SyncBackend = CurlBackend(verbose = true) 11 | "curl backend" - { 12 | "set user agent in requests" in { 13 | val response = basicRequest 14 | .get(uri"$endpoint/echo/headers") 15 | .send(backend) 16 | 17 | val requestUserAgent = response.body 18 | .fold(sys.error(_), identity) 19 | .split(",") 20 | .map(_.toLowerCase) 21 | .find(_.startsWith("user-agent->")) 22 | .map(_.stripPrefix("user-agent->")) 23 | 24 | requestUserAgent should be(Some(s"sttp-curl/${fromCString(CCurl.getVersion())}")) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /core/src/test/scalanative/sttp/client4/TestPlatform.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | 3 | sealed trait TestPlatform 4 | object TestPlatform { 5 | case object JVM extends TestPlatform 6 | case object JS extends TestPlatform 7 | case object Native extends TestPlatform 8 | 9 | val Current = Native 10 | } 11 | -------------------------------------------------------------------------------- /core/src/test/scalanative/sttp/client4/ToCurlConverterTestExtension.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | 3 | trait ToCurlConverterTestExtension {} 4 | -------------------------------------------------------------------------------- /core/src/test/scalanative/sttp/client4/testing/AsyncExecutionContext.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.testing 2 | 3 | import scala.concurrent.ExecutionContext 4 | 5 | trait AsyncExecutionContext { 6 | implicit def executionContext: ExecutionContext = ExecutionContext.global 7 | } 8 | -------------------------------------------------------------------------------- /core/src/test/scalanative/sttp/client4/testing/AsyncRetries.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.testing 2 | 3 | // see the jvm impl 4 | trait AsyncRetries 5 | -------------------------------------------------------------------------------- /core/src/test/scalanative/sttp/client4/testing/HttpTestExtensions.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.testing 2 | 3 | import scala.language.higherKinds 4 | 5 | trait HttpTestExtensions[F[_]] extends AsyncExecutionContext 6 | -------------------------------------------------------------------------------- /core/src/test/scalanative/sttp/client4/testing/Platform.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.testing 2 | 3 | import scala.concurrent.{ExecutionContext, Future} 4 | import scala.concurrent.duration.FiniteDuration 5 | 6 | object Platform { 7 | 8 | def delayedFuture[T](delay: FiniteDuration)(result: => T)(implicit ec: ExecutionContext): Future[T] = 9 | Future { 10 | Thread.sleep(delay.toMillis) 11 | result 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /core/src/test/scalanative/sttp/client4/testing/streaming/StreamingTestExtensions.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.testing.streaming 2 | 3 | import sttp.client4.testing.AsyncExecutionContext 4 | 5 | trait StreamingTestExtensions[F[_], S] extends AsyncExecutionContext {} 6 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | _build_html -------------------------------------------------------------------------------- /docs/.python-version: -------------------------------------------------------------------------------- 1 | 3.12 2 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python -msphinx 7 | SPHINXPROJ = tapir 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_static/css/custom.css: -------------------------------------------------------------------------------- 1 | /* general style for all example tags */ 2 | .example-tag { 3 | border-width: 1px; 4 | border-radius: 9999px; 5 | border-style: solid; 6 | padding-left: 0.5rem; 7 | padding-right: 0.5rem; 8 | margin-right: 0.25rem; 9 | margin-top: 0.25rem; 10 | margin-bottom: 0.25rem; 11 | } 12 | 13 | /* different colors for specific tags */ 14 | .example-effects { 15 | color: rgb(193 21 116); 16 | background-color: rgb(253 242 250); 17 | border-color: rgb(252 206 238); 18 | } 19 | 20 | .example-json { 21 | color: rgb(185 56 21); 22 | background-color: rgb(254 246 238); 23 | border-color: rgb(249 219 175); 24 | } 25 | 26 | .example-backend { 27 | color: rgb(6 118 71); 28 | background-color: rgb(236 253 243); 29 | border-color: rgb(169 239 197); 30 | } 31 | 32 | .example-docs { 33 | color: rgb(52 64 84); 34 | background-color: rgb(249 250 251); 35 | border-color: rgb(234 236 240); 36 | } 37 | 38 | .example-client { 39 | color: rgb(6 89 134); 40 | background-color: rgb(240 249 255); 41 | border-color: rgb(185 230 254); 42 | } -------------------------------------------------------------------------------- /docs/backends/finagle.md: -------------------------------------------------------------------------------- 1 | # Twitter future (Finagle) backend 2 | 3 | To use, add the following dependency to your project: 4 | 5 | ``` 6 | "com.softwaremill.sttp.client4" %% "finagle-backend" % "@VERSION@" 7 | ``` 8 | 9 | Next you'll need to add an implicit value: 10 | 11 | ```scala 12 | import sttp.client4.finagle.FinagleBackend 13 | val backend = FinagleBackend() 14 | ``` 15 | 16 | This backend depends on [finagle](https://twitter.github.io/finagle/), and offers an asynchronous backend, which wraps results in Twitter's `Future`. 17 | 18 | Please note that: 19 | 20 | * the backend does not support non-blocking [streaming](../requests/streaming.md) or [websockets](../other/websockets.md). 21 | -------------------------------------------------------------------------------- /docs/backends/native/curl.md: -------------------------------------------------------------------------------- 1 | # Scala Native (curl) backend 2 | 3 | A Scala Native (0.5.x) backend implemented using [Curl](https://github.com/curl/curl/blob/master/include/curl/curl.h). 4 | 5 | To use, add the following dependency to your project: 6 | 7 | ``` 8 | "com.softwaremill.sttp.client4" %%% "core" % "@VERSION@" 9 | ``` 10 | 11 | and initialize one of the backends: 12 | 13 | ```scala 14 | import sttp.client4.curl.* 15 | 16 | val backend = CurlBackend() 17 | val tryBackend = CurlTryBackend() 18 | ``` 19 | 20 | You need to have an environment with Scala Native [setup](https://scala-native.readthedocs.io/en/latest/user/setup.html) 21 | with additionally installed `libcrypto` (included in OpenSSL) and `curl` in version `7.56.0` or newer. 22 | 23 | ## scala-cli example 24 | 25 | Try the following example: 26 | 27 | ```scala 28 | // hello.scala 29 | 30 | //> using platform native 31 | //> using dep com.softwaremill.sttp.client4::core_native0.5:@VERSION@ 32 | 33 | import sttp.client4.* 34 | import sttp.client4.curl.CurlBackend 35 | 36 | @main def run(): Unit = 37 | val backend = CurlBackend() 38 | println(basicRequest.get(uri"http://httpbin.org/ip").send(backend)) 39 | ``` 40 | 41 | -------------------------------------------------------------------------------- /docs/backends/start_stop.md: -------------------------------------------------------------------------------- 1 | # Starting & cleaning up 2 | 3 | In case of most backends, you should only instantiate a backend once per application, as a backend typically allocates resources such as thread or connection pools. 4 | 5 | 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! 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /docs/community.md: -------------------------------------------------------------------------------- 1 | # Community 2 | 3 | If you have a question, suggestion, or hit a problem, feel free to ask on our [discourse forum](https://softwaremill.community/c/sttp-client)! 4 | 5 | Or, if you encounter a bug, something is unclear in the code or documentation, don't hesitate and [open an issue](https://github.com/softwaremill/sttp/issues) on GitHub. 6 | 7 | We are also always looking for contributions and new ideas, so if you'd like to get into the project, check out the open issues, or post your own suggestions! 8 | -------------------------------------------------------------------------------- /docs/conf/timeouts.md: -------------------------------------------------------------------------------- 1 | # Timeouts 2 | 3 | sttp supports read and connection timeouts: 4 | 5 | * Connection timeout - can be set globally (30 seconds by default) 6 | * Read timeout - can be set per request (1 minute by default) 7 | 8 | How to use: 9 | 10 | ```scala mdoc:compile-only 11 | import sttp.client4.* 12 | import scala.concurrent.duration.* 13 | 14 | // all backends provide a constructor that allows to specify backend options 15 | val backend = DefaultSyncBackend( 16 | options = BackendOptions.connectionTimeout(1.minute)) 17 | 18 | basicRequest 19 | .get(uri"...") 20 | .readTimeout(5.minutes) // or Duration.Inf to turn read timeout off 21 | .send(backend) 22 | ``` 23 | -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- 1 | # Examples by category 2 | 3 | The sttp client repository contains a number of how-to guides. If you're missing an example for your use-case, please let us 4 | know by [reporting an issue](https://github.com/softwaremill/sttp)! 5 | 6 | Each example is fully self-contained and can be run using [scala-cli](https://scala-cli.virtuslab.org) (you just need 7 | to copy the content of the file, apart from scala-cli, no additional setup is required!). Hopefully this will make 8 | experimenting with sttp client as frictionless as possible! 9 | 10 | Examples are tagged with the stack being used (direct-style, cats-effect, ZIO, Future) and backend implementation 11 | 12 | ```{eval-rst} 13 | .. include:: includes/examples_list.md 14 | :parser: markdown 15 | ``` 16 | 17 | 18 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=python -msphinx 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=sttp 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The Sphinx module was not found. Make sure you have Sphinx installed, 20 | echo.then set the SPHINXBUILD environment variable to point to the full 21 | echo.path of the 'sphinx-build' executable. Alternatively you may add the 22 | echo.Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/other.md: -------------------------------------------------------------------------------- 1 | # Other Scala HTTP clients 2 | 3 | * [akka-http client](http://doc.akka.io/docs/akka-http/current/scala/http/client-side/index.html) 4 | * [play ws](https://github.com/playframework/play-ws) 5 | * [http4s](http://http4s.org/v0.17/client/) 6 | * [Gigahorse](http://eed3si9n.com/gigahorse/) 7 | * [Requests-Scala](https://github.com/lihaoyi/requests-scala) 8 | 9 | Also, check the [comparison by Marco Firrincieli](https://github.com/mfirry/scala-http-clients) on how to implement a simple request using a number of Scala HTTP libraries. 10 | -------------------------------------------------------------------------------- /docs/other/sse.md: -------------------------------------------------------------------------------- 1 | # Server-sent events 2 | 3 | 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). 4 | 5 | Refer to the documentation of individual backends, for more information on how to use SSE with a given backend, as well as usage examples. -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx_rtd_theme==2.0.0 2 | sphinx==7.3.7 3 | sphinx-autobuild==2024.4.16 4 | myst-parser==2.0.0 5 | -------------------------------------------------------------------------------- /docs/support.md: -------------------------------------------------------------------------------- 1 | # Support & sponsorship 2 | 3 | ## Sponsors 4 | 5 | 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. 6 | 7 | [![](https://files.softwaremill.com/logo/logo.png "SoftwareMill")](https://softwaremill.com) 8 | 9 | ## Commercial Support 10 | 11 | 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! 12 | -------------------------------------------------------------------------------- /docs/testing/curl.md: -------------------------------------------------------------------------------- 1 | # Converting requests to CURL commands 2 | 3 | sttp comes with builtin request to curl converter. To convert request to curl invocation use `.toCurl` method. 4 | 5 | For example: 6 | 7 | ```scala mdoc 8 | import sttp.client4.* 9 | 10 | basicRequest.get(uri"http://httpbin.org/ip").toCurl 11 | ``` 12 | 13 | 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. -------------------------------------------------------------------------------- /docs/watch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | sphinx-autobuild . _build/html 3 | -------------------------------------------------------------------------------- /effects/cats-ce2/src/main/scala/sttp/client4/impl/cats/CatsMonadAsyncError.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.impl.cats 2 | 3 | import cats.effect.Concurrent 4 | import sttp.monad.{Canceler, MonadAsyncError} 5 | 6 | class CatsMonadAsyncError[F[_]](implicit F: Concurrent[F]) extends CatsMonadError[F] with MonadAsyncError[F] { 7 | override def async[T](register: ((Either[Throwable, T]) => Unit) => Canceler): F[T] = 8 | F.cancelable(register.andThen(c => F.delay(c.cancel()))) 9 | } 10 | -------------------------------------------------------------------------------- /effects/cats-ce2/src/main/scala/sttp/client4/impl/cats/CatsMonadError.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.impl.cats 2 | 3 | import cats.effect.Sync 4 | import sttp.monad.MonadError 5 | 6 | class CatsMonadError[F[_]](implicit F: Sync[F]) extends MonadError[F] { 7 | override def unit[T](t: T): F[T] = F.pure(t) 8 | 9 | override def map[T, T2](fa: F[T])(f: T => T2): F[T2] = F.map(fa)(f) 10 | 11 | override def flatMap[T, T2](fa: F[T])(f: T => F[T2]): F[T2] = 12 | F.flatMap(fa)(f) 13 | 14 | override def error[T](t: Throwable): F[T] = F.raiseError(t) 15 | 16 | override protected def handleWrappedError[T](rt: F[T])(h: PartialFunction[Throwable, F[T]]): F[T] = 17 | F.recoverWith(rt)(h) 18 | 19 | override def eval[T](t: => T): F[T] = F.delay(t) 20 | 21 | override def suspend[T](t: => F[T]): F[T] = F.defer(t) 22 | 23 | override def flatten[T](ffa: F[F[T]]): F[T] = F.flatten(ffa) 24 | 25 | override def ensure[T](f: F[T], e: => F[Unit]): F[T] = F.guaranteeCase(f)(_ => e) 26 | override def ensure2[T](f: => F[T], e: => F[Unit]): F[T] = F.guaranteeCase(F.defer(f))(_ => e) 27 | } 28 | -------------------------------------------------------------------------------- /effects/cats-ce2/src/test/scala/sttp/client4/impl/cats/CatsTestBase.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.impl.cats 2 | 3 | import cats.effect.{Blocker, ContextShift, IO, Timer} 4 | import sttp.client4.testing.ConvertToFuture 5 | import sttp.monad.MonadError 6 | 7 | import java.util.concurrent.TimeoutException 8 | import scala.concurrent.ExecutionContext 9 | import scala.concurrent.duration.DurationInt 10 | 11 | trait CatsTestBase { 12 | 13 | implicit def executionContext: ExecutionContext 14 | 15 | implicit lazy val monad: MonadError[IO] = new CatsMonadAsyncError[IO] 16 | implicit val contextShift: ContextShift[IO] = IO.contextShift(implicitly) 17 | implicit lazy val timer: Timer[IO] = IO.timer(implicitly) 18 | lazy val blocker: Blocker = Blocker.liftExecutionContext(implicitly) 19 | 20 | implicit val convertToFuture: ConvertToFuture[IO] = convertCatsIOToFuture 21 | 22 | def timeoutToNone[T](t: IO[T], timeoutMillis: Int): IO[Option[T]] = 23 | t.map(Some(_)) 24 | .timeout(timeoutMillis.milliseconds) 25 | .handleErrorWith { 26 | case _: TimeoutException => IO(None) 27 | case e => throw e 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /effects/cats-ce2/src/test/scala/sttp/client4/impl/cats/package.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.impl 2 | 3 | import _root_.cats.effect.IO 4 | import sttp.client4.testing.ConvertToFuture 5 | 6 | import scala.concurrent.Future 7 | 8 | package object cats { 9 | 10 | val convertCatsIOToFuture: ConvertToFuture[IO] = new ConvertToFuture[IO] { 11 | override def toFuture[T](value: IO[T]): Future[T] = value.unsafeToFuture() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /effects/cats-ce2/src/test/scalajs/sttp/client4/impl/cats/FetchCatsHttpTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.impl.cats 2 | 3 | import cats.effect.IO 4 | import sttp.client4.Backend 5 | import sttp.client4.testing.AbstractFetchHttpTest 6 | 7 | import scala.concurrent.ExecutionContext 8 | import scala.scalajs.concurrent.JSExecutionContext.Implicits 9 | 10 | class FetchCatsHttpTest extends AbstractFetchHttpTest[IO, Any] with CatsTestBase { 11 | implicit override def executionContext: ExecutionContext = Implicits.queue 12 | 13 | override val backend: Backend[IO] = FetchCatsBackend() 14 | 15 | override protected def supportsCustomMultipartContentType = false 16 | 17 | override def timeoutToNone[T](t: IO[T], timeoutMillis: Int): IO[Option[T]] = 18 | super[CatsTestBase].timeoutToNone(t, timeoutMillis) 19 | } 20 | -------------------------------------------------------------------------------- /effects/cats-ce2/src/test/scalajs/sttp/client4/impl/cats/FetchCatsWebSocketTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.impl.cats 2 | 3 | import cats.effect.IO 4 | import sttp.client4.WebSocketBackend 5 | import sttp.client4.testing.websocket.WebSocketTest 6 | 7 | import scala.concurrent.ExecutionContext 8 | import scala.scalajs.concurrent.JSExecutionContext.queue 9 | 10 | class FetchCatsWebSocketTest extends WebSocketTest[IO] with CatsTestBase { 11 | implicit override def executionContext: ExecutionContext = queue 12 | override def throwsWhenNotAWebSocket: Boolean = true 13 | override def supportsReadingWebSocketResponseHeaders: Boolean = false 14 | override def supportsReadingSubprotocolWebSocketResponseHeader: Boolean = false 15 | 16 | override val backend: WebSocketBackend[IO] = FetchCatsBackend() 17 | } 18 | -------------------------------------------------------------------------------- /effects/cats/src/main/scala/sttp/client4/impl/cats/CatsMonadAsyncError.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.impl.cats 2 | 3 | import cats.effect.kernel.Async 4 | import cats.syntax.functor._ 5 | import cats.syntax.option._ 6 | import sttp.monad.{Canceler, MonadAsyncError} 7 | 8 | class CatsMonadAsyncError[F[_]](implicit F: Async[F]) extends CatsMonadError[F] with MonadAsyncError[F] { 9 | override def async[T](register: ((Either[Throwable, T]) => Unit) => Canceler): F[T] = 10 | F.async(cb => F.delay(register(cb)).map(c => F.delay(c.cancel()).some)) 11 | } 12 | -------------------------------------------------------------------------------- /effects/cats/src/main/scala/sttp/client4/impl/cats/CatsMonadError.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.impl.cats 2 | 3 | import cats.effect.kernel.Sync 4 | import sttp.monad.MonadError 5 | 6 | class CatsMonadError[F[_]](implicit F: Sync[F]) extends MonadError[F] { 7 | override def unit[T](t: T): F[T] = F.pure(t) 8 | 9 | override def map[T, T2](fa: F[T])(f: T => T2): F[T2] = F.map(fa)(f) 10 | 11 | override def flatMap[T, T2](fa: F[T])(f: T => F[T2]): F[T2] = 12 | F.flatMap(fa)(f) 13 | 14 | override def error[T](t: Throwable): F[T] = F.raiseError(t) 15 | 16 | override protected def handleWrappedError[T](rt: F[T])(h: PartialFunction[Throwable, F[T]]): F[T] = 17 | F.recoverWith(rt)(h) 18 | 19 | override def eval[T](t: => T): F[T] = F.delay(t) 20 | 21 | override def flatten[T](ffa: F[F[T]]): F[T] = F.flatten(ffa) 22 | 23 | override def ensure[T](f: F[T], e: => F[Unit]): F[T] = F.guaranteeCase(f)(_ => e) 24 | override def ensure2[T](f: => F[T], e: => F[Unit]): F[T] = F.guaranteeCase(F.defer(f))(_ => e) 25 | 26 | override def blocking[T](t: => T): F[T] = F.blocking(t) 27 | } 28 | -------------------------------------------------------------------------------- /effects/cats/src/main/scalajvm/sttp/client4/httpclient/cats/CatsSequencer.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.httpclient.cats 2 | 3 | import cats.effect.kernel.{Async, MonadCancel} 4 | import cats.effect.std.Semaphore 5 | import cats.syntax.all._ 6 | import sttp.client4.internal.httpclient.Sequencer 7 | 8 | private[cats] class CatsSequencer[F[_]](s: Semaphore[F])(implicit m: MonadCancel[F, Throwable]) extends Sequencer[F] { 9 | override def apply[T](t: => F[T]): F[T] = s.permit.use(_ => t) 10 | } 11 | 12 | private[cats] object CatsSequencer { 13 | def create[F[_]: Async]: F[Sequencer[F]] = Semaphore(1).map(new CatsSequencer(_)) 14 | } 15 | -------------------------------------------------------------------------------- /effects/cats/src/main/scalajvm/sttp/client4/httpclient/cats/CatsSimpleQueue.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.httpclient.cats 2 | 3 | import cats.MonadError 4 | import cats.effect.std.{Dispatcher, Queue} 5 | import cats.syntax.flatMap._ 6 | import sttp.client4.internal.ws.SimpleQueue 7 | import sttp.ws.WebSocketBufferFull 8 | 9 | class CatsSimpleQueue[F[_], A](queue: Queue[F, A], capacity: Option[Int], dispatcher: Dispatcher[F])(implicit 10 | F: MonadError[F, Throwable] 11 | ) extends SimpleQueue[F, A] { 12 | override def offer(t: A): Unit = 13 | dispatcher.unsafeRunSync( 14 | queue 15 | .tryOffer(t) 16 | .flatMap[Unit] { 17 | case true => F.unit 18 | case false => F.raiseError(new WebSocketBufferFull(capacity.getOrElse(Int.MaxValue))) 19 | } 20 | ) 21 | 22 | override def poll: F[A] = queue.take 23 | } 24 | -------------------------------------------------------------------------------- /effects/cats/src/test/scala/sttp/client4/impl/cats/CatsTestBase.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.impl.cats 2 | 3 | import java.util.concurrent.TimeoutException 4 | 5 | import cats.effect.IO 6 | import cats.effect.unsafe.IORuntime 7 | import sttp.client4.testing.ConvertToFuture 8 | import sttp.monad.MonadError 9 | 10 | import scala.concurrent.ExecutionContext 11 | import scala.concurrent.duration.DurationInt 12 | 13 | trait CatsTestBase { 14 | 15 | implicit def executionContext: ExecutionContext 16 | 17 | implicit lazy val monad: MonadError[IO] = new CatsMonadAsyncError[IO] 18 | implicit val ioRuntime: IORuntime = IORuntime.global 19 | 20 | implicit val convertToFuture: ConvertToFuture[IO] = convertCatsIOToFuture() 21 | 22 | def timeoutToNone[T](t: IO[T], timeoutMillis: Int): IO[Option[T]] = 23 | t.map(Some(_)) 24 | .timeout(timeoutMillis.milliseconds) 25 | .handleErrorWith { 26 | case _: TimeoutException => IO(None) 27 | case e => throw e 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /effects/cats/src/test/scala/sttp/client4/impl/cats/package.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.impl 2 | 3 | import _root_.cats.effect.{unsafe, IO} 4 | import sttp.client4.testing.ConvertToFuture 5 | 6 | import scala.concurrent.Future 7 | 8 | package object cats { 9 | 10 | def convertCatsIOToFuture()(implicit runtime: unsafe.IORuntime): ConvertToFuture[IO] = new ConvertToFuture[IO] { 11 | override def toFuture[T](value: IO[T]): Future[T] = value.unsafeToFuture() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /effects/cats/src/test/scalajs/sttp/client4/impl/cats/FetchCatsHttpTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.impl.cats 2 | 3 | import cats.effect.IO 4 | import sttp.client4.Backend 5 | import sttp.client4.testing.AbstractFetchHttpTest 6 | 7 | import scala.concurrent.ExecutionContext 8 | import scala.scalajs.concurrent.JSExecutionContext.Implicits 9 | 10 | class FetchCatsHttpTest extends AbstractFetchHttpTest[IO, Any] with CatsTestBase { 11 | implicit override def executionContext: ExecutionContext = Implicits.queue 12 | 13 | override val backend: Backend[IO] = FetchCatsBackend() 14 | 15 | override protected def supportsCustomMultipartContentType = false 16 | 17 | override def timeoutToNone[T](t: IO[T], timeoutMillis: Int): IO[Option[T]] = 18 | super[CatsTestBase].timeoutToNone(t, timeoutMillis) 19 | } 20 | -------------------------------------------------------------------------------- /effects/cats/src/test/scalajs/sttp/client4/impl/cats/FetchCatsWebSocketTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.impl.cats 2 | 3 | import cats.effect.IO 4 | import sttp.client4.WebSocketBackend 5 | import sttp.client4.testing.websocket.WebSocketTest 6 | 7 | import scala.concurrent.ExecutionContext 8 | import scala.scalajs.concurrent.JSExecutionContext.queue 9 | 10 | class FetchCatsWebSocketTest extends WebSocketTest[IO] with CatsTestBase { 11 | implicit override def executionContext: ExecutionContext = queue 12 | override def throwsWhenNotAWebSocket: Boolean = true 13 | override def supportsReadingWebSocketResponseHeaders: Boolean = false 14 | override def supportsReadingSubprotocolWebSocketResponseHeader: Boolean = false 15 | 16 | override val backend: WebSocketBackend[IO] = FetchCatsBackend() 17 | } 18 | -------------------------------------------------------------------------------- /effects/cats/src/test/scalajvm/sttp/client4/httpclient/cats/HttpClientCatsCloseTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.httpclient.cats 2 | 3 | import cats.effect.IO 4 | import cats.effect.unsafe.implicits.global 5 | import org.scalatest.freespec.AsyncFreeSpec 6 | import org.scalatest.matchers.should.Matchers 7 | 8 | class HttpClientCatsCloseTest extends AsyncFreeSpec with Matchers { 9 | "cats-effect" - { 10 | "continue working after the backend is closed" in { 11 | HttpClientCatsBackend 12 | .resource[IO]() 13 | .use(_ => IO.unit) 14 | .flatMap(_ => IO(succeed).start.flatMap(_.joinWith(IO(fail())))) // the cats executor should still be open 15 | .unsafeToFuture() 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /effects/cats/src/test/scalajvm/sttp/client4/httpclient/cats/HttpClientCatsHttpTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.httpclient.cats 2 | 3 | import cats.effect.IO 4 | import sttp.client4.impl.cats.CatsRetryTest 5 | import sttp.client4.testing.HttpTest 6 | 7 | class HttpClientCatsHttpTest extends HttpTest[IO] with CatsRetryTest with HttpClientCatsTestBase { 8 | override def supportsHostHeaderOverride = false 9 | 10 | override def supportsDeflateWrapperChecking = false 11 | } 12 | -------------------------------------------------------------------------------- /effects/cats/src/test/scalajvm/sttp/client4/httpclient/cats/HttpClientCatsTestBase.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.httpclient.cats 2 | 3 | import cats.effect.IO 4 | import org.scalatest.Suite 5 | import sttp.client4._ 6 | import sttp.client4.impl.cats.{CatsTestBase, TestIODispatcher} 7 | 8 | trait HttpClientCatsTestBase extends CatsTestBase with TestIODispatcher { this: Suite => 9 | implicit val backend: WebSocketBackend[IO] = 10 | HttpClientCatsBackend[IO](dispatcher).unsafeRunSync() 11 | } 12 | -------------------------------------------------------------------------------- /effects/cats/src/test/scalajvm/sttp/client4/httpclient/cats/HttpClientCatsWebSocketTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.httpclient.cats 2 | 3 | import cats.effect.IO 4 | import cats.implicits._ 5 | import sttp.client4.testing.websocket.{WebSocketConcurrentTest, WebSocketTest} 6 | 7 | class HttpClientCatsWebSocketTest 8 | extends WebSocketTest[IO] 9 | with WebSocketConcurrentTest[IO] 10 | with HttpClientCatsTestBase { 11 | 12 | override def concurrently[T](fs: List[() => IO[T]]): IO[List[T]] = fs.map(_()).parSequence 13 | 14 | override def supportsReadingWebSocketResponseHeaders: Boolean = false 15 | } 16 | -------------------------------------------------------------------------------- /effects/cats/src/test/scalajvm/sttp/client4/impl/cats/CatsMonadErrorTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.impl.cats 2 | 3 | import cats.effect.IO 4 | import cats.effect.unsafe.{IORuntime, IORuntimeConfig} 5 | import org.scalatest.freespec.AsyncFreeSpec 6 | 7 | class CatsMonadErrorTest extends AsyncFreeSpec { 8 | "blocking" - { 9 | "should shift to blocking execution context" in { 10 | implicit val ioRuntime: IORuntime = createIORuntime(computePoolSize = 1) 11 | val monad = new CatsMonadError[IO] 12 | 13 | val program = monad 14 | .blocking(Thread.sleep(100)) 15 | .background 16 | .use(getOutcome => IO.race(getOutcome, IO.unit)) 17 | 18 | program 19 | .flatMap(either => IO.delay(assert(either.isRight))) 20 | .unsafeToFuture() 21 | } 22 | } 23 | 24 | private def createIORuntime(computePoolSize: Int): IORuntime = { 25 | val (compute, _, _) = IORuntime.createWorkStealingComputeThreadPool(computePoolSize) 26 | val (blocking, _) = IORuntime.createDefaultBlockingExecutionContext() 27 | 28 | IORuntime(compute, blocking, compute, () => (), IORuntimeConfig()) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /effects/cats/src/test/scalajvm/sttp/client4/impl/cats/TestIODispatcher.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.impl.cats 2 | 3 | import cats.effect.IO 4 | import cats.effect.std.Dispatcher 5 | import cats.effect.unsafe.implicits.global 6 | import org.scalatest.{BeforeAndAfterAll, Suite} 7 | 8 | trait TestIODispatcher extends BeforeAndAfterAll { this: Suite => 9 | 10 | // use a var to avoid initialization error `scala.UninitializedFieldError` 11 | protected var dispatcher: Dispatcher[IO] = _ 12 | 13 | private val (d, shutdownDispatcher) = Dispatcher.parallel[IO].allocated.unsafeRunSync() 14 | dispatcher = d 15 | 16 | override protected def afterAll(): Unit = { 17 | shutdownDispatcher.unsafeRunSync() 18 | super.afterAll() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /effects/fs2-ce2/src/main/scala/sttp/client4/impl/fs2/Fs2ServerSentEvents.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.impl.fs2 2 | 3 | import fs2.text 4 | import sttp.model.sse.ServerSentEvent 5 | 6 | object Fs2ServerSentEvents { 7 | def parse[F[_]]: fs2.Pipe[F, Byte, ServerSentEvent] = { response => 8 | response 9 | .through(text.utf8Decode[F]) 10 | .through(text.lines[F]) 11 | .split(_.isEmpty) 12 | .filter(_.nonEmpty) 13 | .map(_.toList) 14 | .map(ServerSentEvent.parse) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /effects/fs2-ce2/src/main/scala/sttp/client4/impl/fs2/Fs2SimpleQueue.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.impl.fs2 2 | 3 | import cats.effect.{Effect, IO} 4 | import fs2.concurrent.InspectableQueue 5 | import sttp.client4.internal.ws.SimpleQueue 6 | import sttp.ws.WebSocketBufferFull 7 | 8 | class Fs2SimpleQueue[F[_], A](queue: InspectableQueue[F, A], capacity: Option[Int])(implicit F: Effect[F]) 9 | extends SimpleQueue[F, A] { 10 | override def offer(t: A): Unit = 11 | F.toIO(queue.offer1(t)) 12 | .flatMap { 13 | case true => IO.unit 14 | case false => IO.raiseError(WebSocketBufferFull(capacity.getOrElse(Int.MaxValue))) 15 | } 16 | .unsafeRunSync() 17 | 18 | override def poll: F[A] = queue.dequeue1 19 | } 20 | -------------------------------------------------------------------------------- /effects/fs2-ce2/src/main/scalajvm/sttp/client4/httpclient/fs2/Fs2Sequencer.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.httpclient.fs2 2 | 3 | import cats.syntax.all._ 4 | import cats.effect.Concurrent 5 | import cats.effect.concurrent.Semaphore 6 | import sttp.client4.internal.httpclient.Sequencer 7 | 8 | private[fs2] class Fs2Sequencer[F[_]](s: Semaphore[F]) extends Sequencer[F] { 9 | override def apply[T](t: => F[T]): F[T] = s.withPermit(t) 10 | } 11 | 12 | private[fs2] object Fs2Sequencer { 13 | def create[F[_]: Concurrent]: F[Sequencer[F]] = Semaphore(1).map(new Fs2Sequencer(_)) 14 | } 15 | -------------------------------------------------------------------------------- /effects/fs2-ce2/src/test/scala/sttp/client4/impl/fs2/Fs2StreamingTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.impl.fs2 2 | 3 | import cats.effect.IO 4 | import cats.instances.string._ 5 | import fs2.{Chunk, Stream} 6 | import sttp.capabilities.fs2.Fs2Streams 7 | import sttp.client4.impl.cats.CatsTestBase 8 | import sttp.model.sse.ServerSentEvent 9 | import sttp.client4.testing.streaming.StreamingTest 10 | 11 | trait Fs2StreamingTest extends StreamingTest[IO, Fs2Streams[IO]] with CatsTestBase { 12 | override val streams: Fs2Streams[IO] = new Fs2Streams[IO] {} 13 | 14 | override def bodyProducer(chunks: Iterable[Array[Byte]]): Stream[IO, Byte] = 15 | Stream.fromIterator[IO](chunks.iterator).flatMap(arr => Stream.chunk(Chunk.array(arr))) 16 | 17 | override def bodyConsumer(stream: fs2.Stream[IO, Byte]): IO[String] = 18 | stream 19 | .through(fs2.text.utf8Decode) 20 | .compile 21 | .foldMonoid 22 | 23 | def sseConsumer(stream: streams.BinaryStream): IO[List[ServerSentEvent]] = 24 | stream.through(Fs2ServerSentEvents.parse).compile.toList 25 | } 26 | -------------------------------------------------------------------------------- /effects/fs2-ce2/src/test/scalajvm/sttp/client4/httpclient/fs2/HttpClientFs2HttpTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.httpclient.fs2 2 | 3 | import cats.effect.IO 4 | import sttp.client4.testing.HttpTest 5 | 6 | class HttpClientFs2HttpTest extends HttpTest[IO] with HttpClientFs2TestBase { 7 | override def supportsHostHeaderOverride = false 8 | override def supportsResponseAsInputStream = false 9 | } 10 | -------------------------------------------------------------------------------- /effects/fs2-ce2/src/test/scalajvm/sttp/client4/httpclient/fs2/HttpClientFs2StreamingTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.httpclient.fs2 2 | 3 | import sttp.client4.impl.fs2.Fs2StreamingTest 4 | 5 | class HttpClientFs2StreamingTest extends Fs2StreamingTest with HttpClientFs2TestBase { 6 | override protected def supportsStreamingMultipartParts: Boolean = false 7 | } 8 | -------------------------------------------------------------------------------- /effects/fs2-ce2/src/test/scalajvm/sttp/client4/httpclient/fs2/HttpClientFs2TestBase.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.httpclient.fs2 2 | 3 | import cats.effect.IO 4 | import sttp.capabilities.fs2.Fs2Streams 5 | import sttp.client4._ 6 | import sttp.client4.impl.cats.CatsTestBase 7 | 8 | trait HttpClientFs2TestBase extends CatsTestBase { 9 | implicit val backend: WebSocketStreamBackend[IO, Fs2Streams[IO]] = 10 | HttpClientFs2Backend[IO](blocker).unsafeRunSync() 11 | } 12 | -------------------------------------------------------------------------------- /effects/fs2/src/main/scala/sttp/client4/impl/fs2/Fs2ServerSentEvents.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.impl.fs2 2 | 3 | import fs2.text 4 | import sttp.model.sse.ServerSentEvent 5 | 6 | object Fs2ServerSentEvents { 7 | def parse[F[_]]: fs2.Pipe[F, Byte, ServerSentEvent] = { response => 8 | response 9 | .through(text.utf8Decode[F]) 10 | .through(text.lines[F]) 11 | .split(_.isEmpty) 12 | .filter(_.nonEmpty) 13 | .map(_.toList) 14 | .map(ServerSentEvent.parse) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /effects/fs2/src/main/scalajs/sttp.client4.impl.fs2/Fs2SimpleQueue.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.impl.fs2 2 | 3 | import cats.MonadError 4 | import cats.effect.std.{Dispatcher, Queue} 5 | import cats.syntax.flatMap._ 6 | import sttp.client4.internal.ws.SimpleQueue 7 | import sttp.ws.WebSocketBufferFull 8 | 9 | class Fs2SimpleQueue[F[_], A](queue: Queue[F, A], capacity: Option[Int], dispatcher: Dispatcher[F])(implicit 10 | F: MonadError[F, Throwable] 11 | ) extends SimpleQueue[F, A] { 12 | override def offer(t: A): Unit = 13 | // On the JVM, we do unsafeRunSync. Here this is not possible, so just starting a future and leaving it running, 14 | // without waiting for it to complete (the `offer` contract allows that). 15 | dispatcher.unsafeToFuture( 16 | queue 17 | .tryOffer(t) 18 | .flatMap[Unit] { 19 | case true => F.unit 20 | case false => F.raiseError(new WebSocketBufferFull(capacity.getOrElse(Int.MaxValue))) 21 | } 22 | ) 23 | 24 | override def poll: F[A] = queue.take 25 | } 26 | -------------------------------------------------------------------------------- /effects/fs2/src/main/scalajvm/sttp/client4/httpclient/fs2/Fs2Sequencer.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.httpclient.fs2 2 | 3 | import cats.syntax.all._ 4 | import cats.effect.{Concurrent, MonadCancel} 5 | import cats.effect.std.Semaphore 6 | import sttp.client4.internal.httpclient.Sequencer 7 | 8 | private[fs2] class Fs2Sequencer[F[_]](s: Semaphore[F])(implicit m: MonadCancel[F, Throwable]) extends Sequencer[F] { 9 | override def apply[T](t: => F[T]): F[T] = s.permit.use(_ => t) 10 | } 11 | 12 | private[fs2] object Fs2Sequencer { 13 | def create[F[_]: Concurrent]: F[Sequencer[F]] = Semaphore(1).map(new Fs2Sequencer(_)) 14 | } 15 | -------------------------------------------------------------------------------- /effects/fs2/src/main/scalajvm/sttp/client4/impl/fs2/Fs2SimpleQueue.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.impl.fs2 2 | 3 | import cats.MonadError 4 | import cats.effect.std.{Dispatcher, Queue} 5 | import cats.syntax.flatMap._ 6 | import sttp.client4.internal.ws.SimpleQueue 7 | import sttp.ws.WebSocketBufferFull 8 | 9 | class Fs2SimpleQueue[F[_], A](queue: Queue[F, A], capacity: Option[Int], dispatcher: Dispatcher[F])(implicit 10 | F: MonadError[F, Throwable] 11 | ) extends SimpleQueue[F, A] { 12 | override def offer(t: A): Unit = 13 | dispatcher.unsafeRunSync( 14 | queue 15 | .tryOffer(t) 16 | .flatMap[Unit] { 17 | case true => F.unit 18 | case false => F.raiseError(new WebSocketBufferFull(capacity.getOrElse(Int.MaxValue))) 19 | } 20 | ) 21 | 22 | override def poll: F[A] = queue.take 23 | } 24 | -------------------------------------------------------------------------------- /effects/fs2/src/test/scala/sttp/client4/impl/fs2/Fs2StreamingTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.impl.fs2 2 | 3 | import cats.effect.IO 4 | import cats.instances.string._ 5 | import fs2.{Chunk, Stream} 6 | import sttp.capabilities.fs2.Fs2Streams 7 | import sttp.client4.impl.cats.CatsTestBase 8 | import sttp.model.sse.ServerSentEvent 9 | import sttp.client4.testing.streaming.StreamingTest 10 | 11 | trait Fs2StreamingTest extends StreamingTest[IO, Fs2Streams[IO]] with CatsTestBase { 12 | override val streams: Fs2Streams[IO] = new Fs2Streams[IO] {} 13 | 14 | override def bodyProducer(chunks: Iterable[Array[Byte]]): Stream[IO, Byte] = 15 | Stream 16 | .fromIterator[IO](chunks.iterator, chunks.size) 17 | .map(Chunk.array(_)) 18 | .flatMap(Stream.chunk) 19 | 20 | override def bodyConsumer(stream: fs2.Stream[IO, Byte]): IO[String] = 21 | stream 22 | .through(fs2.text.utf8Decode) 23 | .compile 24 | .foldMonoid 25 | 26 | def sseConsumer(stream: streams.BinaryStream): IO[List[ServerSentEvent]] = 27 | stream.through(Fs2ServerSentEvents.parse).compile.toList 28 | } 29 | -------------------------------------------------------------------------------- /effects/fs2/src/test/scalajvm/sttp/client4/httpclient/fs2/HttpClientFs2HttpTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.httpclient.fs2 2 | 3 | import cats.effect.IO 4 | import sttp.client4.testing.HttpTest 5 | 6 | class HttpClientFs2HttpTest extends HttpTest[IO] with HttpClientFs2TestBase { 7 | override def supportsHostHeaderOverride = false 8 | override def supportsResponseAsInputStream = false 9 | } 10 | -------------------------------------------------------------------------------- /effects/fs2/src/test/scalajvm/sttp/client4/httpclient/fs2/HttpClientFs2StreamingTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.httpclient.fs2 2 | 3 | import sttp.client4.impl.fs2.Fs2StreamingTest 4 | 5 | class HttpClientFs2StreamingTest extends Fs2StreamingTest with HttpClientFs2TestBase 6 | -------------------------------------------------------------------------------- /effects/fs2/src/test/scalajvm/sttp/client4/httpclient/fs2/HttpClientFs2TestBase.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.httpclient.fs2 2 | 3 | import cats.effect.IO 4 | import org.scalatest.Suite 5 | import sttp.capabilities.fs2.Fs2Streams 6 | import sttp.client4._ 7 | import sttp.client4.impl.cats.{CatsTestBase, TestIODispatcher} 8 | 9 | trait HttpClientFs2TestBase extends CatsTestBase with TestIODispatcher { this: Suite => 10 | implicit val backend: WebSocketStreamBackend[IO, Fs2Streams[IO]] = 11 | HttpClientFs2Backend[IO](dispatcher).unsafeRunSync() 12 | } 13 | -------------------------------------------------------------------------------- /effects/monix/src/main/scala/sttp/client4/impl/monix/MonixSimpleQueue.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.impl.monix 2 | 3 | import monix.eval.Task 4 | import monix.execution.{AsyncQueue => MAsyncQueue, Scheduler} 5 | import sttp.client4.internal.ws.SimpleQueue 6 | import sttp.ws.WebSocketBufferFull 7 | 8 | class MonixSimpleQueue[A](bufferCapacity: Option[Int])(implicit s: Scheduler) extends SimpleQueue[Task, A] { 9 | private val queue = bufferCapacity match { 10 | case Some(capacity) => MAsyncQueue.bounded[A](capacity) 11 | case None => MAsyncQueue.unbounded[A]() 12 | } 13 | 14 | override def offer(t: A): Unit = 15 | if (!queue.tryOffer(t)) { 16 | throw WebSocketBufferFull(bufferCapacity.getOrElse(Int.MaxValue)) 17 | } 18 | override def poll: Task[A] = Task.deferFuture(queue.poll()) 19 | } 20 | -------------------------------------------------------------------------------- /effects/monix/src/main/scalajvm/sttp/client4/httpclient/monix/MonixSequencer.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.httpclient.monix 2 | 3 | import cats.effect.Concurrent 4 | import cats.effect.concurrent.Semaphore 5 | import monix.eval.Task 6 | import sttp.client4.internal.httpclient.Sequencer 7 | 8 | private[monix] class MonixSequencer(s: Semaphore[Task]) extends Sequencer[Task] { 9 | override def apply[T](t: => Task[T]): Task[T] = s.withPermit(t) 10 | } 11 | 12 | private[monix] object MonixSequencer { 13 | def create(implicit c: Concurrent[Task]): Task[MonixSequencer] = Semaphore(1).map(new MonixSequencer(_)) 14 | } 15 | -------------------------------------------------------------------------------- /effects/monix/src/main/scalajvm/sttp/client4/httpclient/monix/monixDecompressors.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.httpclient.monix 2 | 3 | import sttp.capabilities.monix.MonixStreams 4 | import sttp.model.Encodings 5 | import sttp.client4.compression.Decompressor 6 | import monix.reactive.compression._ 7 | 8 | object GZipMonixDecompressor extends Decompressor[MonixStreams.BinaryStream] { 9 | override val encoding: String = Encodings.Gzip 10 | override def apply(body: MonixStreams.BinaryStream): MonixStreams.BinaryStream = body.transform(gunzip()) 11 | } 12 | 13 | object DeflateMonixDecompressor extends Decompressor[MonixStreams.BinaryStream] { 14 | override val encoding: String = Encodings.Deflate 15 | override def apply(body: MonixStreams.BinaryStream): MonixStreams.BinaryStream = body.transform(inflate()) 16 | } 17 | -------------------------------------------------------------------------------- /effects/monix/src/test/scala/sttp/client4/impl/monix/MonixStreamingTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.impl.monix 2 | 3 | import monix.eval.Task 4 | import monix.reactive.Observable 5 | import sttp.capabilities.monix.MonixStreams 6 | import sttp.model.sse.ServerSentEvent 7 | import sttp.client4.testing.ConvertToFuture 8 | import sttp.client4.testing.streaming.StreamingTest 9 | 10 | abstract class MonixStreamingTest extends StreamingTest[Task, MonixStreams] { 11 | override val streams: MonixStreams = MonixStreams 12 | 13 | override implicit val convertToFuture: ConvertToFuture[Task] = convertMonixTaskToFuture 14 | 15 | override def bodyProducer(chunks: Iterable[Array[Byte]]): Observable[Array[Byte]] = 16 | Observable.fromIterable(chunks) 17 | 18 | override def bodyConsumer(stream: Observable[Array[Byte]]): Task[String] = 19 | stream.toListL 20 | .map(bs => new String(bs.toArray.flatten, "utf8")) 21 | 22 | override def sseConsumer(stream: Observable[Array[Byte]]): Task[List[ServerSentEvent]] = 23 | stream.transform(MonixServerSentEvents.parse).foldLeftL(List.empty[ServerSentEvent]) { case (list, event) => 24 | list :+ event 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /effects/monix/src/test/scala/sttp/client4/impl/monix/package.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.impl 2 | 3 | import scala.concurrent.Future 4 | 5 | import _root_.monix.eval.Task 6 | import sttp.client4.testing.ConvertToFuture 7 | 8 | package object monix { 9 | 10 | val convertMonixTaskToFuture: ConvertToFuture[Task] = new ConvertToFuture[Task] { 11 | import _root_.monix.execution.Scheduler.Implicits.global 12 | 13 | override def toFuture[T](value: Task[T]): Future[T] = value.runToFuture 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /effects/monix/src/test/scalajs/sttp/client4/impl/monix/FetchMonixHttpTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.impl.monix 2 | 3 | import monix.eval.Task 4 | import sttp.capabilities.monix.MonixStreams 5 | import sttp.client4.StreamBackend 6 | import sttp.client4.testing.{AbstractFetchHttpTest, ConvertToFuture} 7 | 8 | class FetchMonixHttpTest extends AbstractFetchHttpTest[Task, MonixStreams] { 9 | 10 | override val backend: StreamBackend[Task, MonixStreams] = FetchMonixBackend() 11 | override implicit val convertToFuture: ConvertToFuture[Task] = convertMonixTaskToFuture 12 | 13 | override protected def supportsCustomMultipartContentType = false 14 | 15 | override protected def supportsCustomMultipartEncoding = false 16 | } 17 | -------------------------------------------------------------------------------- /effects/monix/src/test/scalajs/sttp/client4/impl/monix/FetchMonixStreamingTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.impl.monix 2 | 3 | import monix.eval.Task 4 | import sttp.capabilities.monix.MonixStreams 5 | import sttp.client4.StreamBackend 6 | 7 | class FetchMonixStreamingTest extends MonixStreamingTest { 8 | override val backend: StreamBackend[Task, MonixStreams] = FetchMonixBackend() 9 | 10 | override protected def supportsStreamingMultipartParts: Boolean = false 11 | } 12 | -------------------------------------------------------------------------------- /effects/monix/src/test/scalajvm/sttp/client4/impl/monix/HttpClientMonixHttpTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.impl.monix 2 | 3 | import monix.eval.Task 4 | import monix.execution.Scheduler.Implicits.global 5 | import sttp.client4.Backend 6 | import sttp.client4.httpclient.monix.HttpClientMonixBackend 7 | import sttp.client4.testing.{ConvertToFuture, HttpTest} 8 | 9 | import java.util.concurrent.TimeoutException 10 | import scala.concurrent.duration.DurationInt 11 | 12 | class HttpClientMonixHttpTest extends HttpTest[Task] { 13 | override val backend: Backend[Task] = HttpClientMonixBackend().runSyncUnsafe() 14 | override implicit val convertToFuture: ConvertToFuture[Task] = convertMonixTaskToFuture 15 | 16 | override def supportsHostHeaderOverride = false 17 | override def supportsDeflateWrapperChecking = false 18 | override def supportsResponseAsInputStream = false 19 | 20 | override def timeoutToNone[T](t: Task[T], timeoutMillis: Int): Task[Option[T]] = 21 | t.map(Some(_)) 22 | .timeout(timeoutMillis.milliseconds) 23 | .onErrorRecover { case _: TimeoutException => 24 | None 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /effects/monix/src/test/scalajvm/sttp/client4/impl/monix/HttpClientMonixStreamingTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.impl.monix 2 | 3 | import monix.eval.Task 4 | import monix.execution.Scheduler.Implicits.global 5 | import sttp.capabilities.monix.MonixStreams 6 | import sttp.client4.StreamBackend 7 | import sttp.client4.httpclient.monix.HttpClientMonixBackend 8 | 9 | class HttpClientMonixStreamingTest extends MonixStreamingTest { 10 | override val backend: StreamBackend[Task, MonixStreams] = 11 | HttpClientMonixBackend().runSyncUnsafe() 12 | 13 | override protected def supportsStreamingMultipartParts: Boolean = false 14 | } 15 | -------------------------------------------------------------------------------- /effects/ox/src/main/scala/sttp/client4/impl/ox/sse/OxServerSentEvents.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.impl.ox.sse 2 | 3 | import ox.flow.Flow 4 | import sttp.model.sse.ServerSentEvent 5 | 6 | import java.io.InputStream 7 | 8 | object OxServerSentEvents: 9 | def parse(is: InputStream): Flow[ServerSentEvent] = 10 | Flow 11 | .fromInputStream(is) 12 | .linesUtf8 13 | .mapStatefulConcat(List.empty[String])( 14 | (lines, str) => if str.isEmpty then (Nil, List(lines)) else (lines :+ str, Nil), 15 | onComplete = { lines => 16 | if lines.nonEmpty then Some(lines) 17 | else None 18 | } 19 | ) 20 | .filter(_.nonEmpty) 21 | .map(ServerSentEvent.parse) 22 | -------------------------------------------------------------------------------- /effects/scalaz/src/main/scala/sttp/client4/impl/scalaz/TaskMonadAsyncError.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.impl.scalaz 2 | 3 | import scalaz.concurrent.Task 4 | import scalaz.{-\/, \/-} 5 | import sttp.monad.{Canceler, MonadAsyncError} 6 | 7 | object TaskMonadAsyncError extends MonadAsyncError[Task] { 8 | override def unit[T](t: T): Task[T] = Task.point(t) 9 | 10 | override def map[T, T2](fa: Task[T])(f: (T) => T2): Task[T2] = fa.map(f) 11 | 12 | override def flatMap[T, T2](fa: Task[T])(f: (T) => Task[T2]): Task[T2] = 13 | fa.flatMap(f) 14 | 15 | override def async[T](register: (Either[Throwable, T] => Unit) => Canceler): Task[T] = 16 | Task.async { cb => 17 | register { 18 | case Left(t) => cb(-\/(t)) 19 | case Right(t) => cb(\/-(t)) 20 | } 21 | } 22 | 23 | override def error[T](t: Throwable): Task[T] = Task.fail(t) 24 | 25 | override protected def handleWrappedError[T](rt: Task[T])(h: PartialFunction[Throwable, Task[T]]): Task[T] = 26 | rt.handleWith(h) 27 | 28 | override def eval[T](t: => T): Task[T] = Task(t) 29 | 30 | override def ensure[T](f: Task[T], e: => Task[Unit]): Task[T] = f.onFinish(_ => e) 31 | } 32 | -------------------------------------------------------------------------------- /effects/scalaz/src/test/scala/sttp/client4/impl/scalaz/package.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.impl 2 | 3 | import sttp.client4.testing.ConvertToFuture 4 | 5 | import _root_.scalaz.concurrent.Task 6 | import _root_.scalaz.{-\/, \/-} 7 | import scala.concurrent.{Future, Promise} 8 | import scala.util.{Failure, Success} 9 | 10 | package object scalaz { 11 | 12 | val convertScalazTaskToFuture: ConvertToFuture[Task] = new ConvertToFuture[Task] { 13 | // from https://github.com/Verizon/delorean 14 | override def toFuture[T](value: Task[T]): Future[T] = { 15 | val p = Promise[T]() 16 | 17 | value.unsafePerformAsync { 18 | case \/-(a) => p.complete(Success(a)); () 19 | case -\/(t) => p.complete(Failure(t)); () 20 | } 21 | 22 | p.future 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /effects/zio/src/main/scala/sttp/client4/impl/zio/ZioServerSentEvents.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.impl.zio 2 | 3 | import sttp.capabilities.zio.ZioStreams 4 | import sttp.model.sse.ServerSentEvent 5 | import zio.stream.ZPipeline 6 | 7 | object ZioServerSentEvents { 8 | val parse: ZioStreams.Pipe[Byte, ServerSentEvent] = { stream => 9 | stream 10 | .via(ZPipeline.utf8Decode) 11 | .via(ZPipeline.splitLines) 12 | .split(_.isEmpty) 13 | .map(c => ServerSentEvent.parse(c.toList)) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /effects/zio/src/main/scala/sttp/client4/impl/zio/ZioSimpleQueue.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.impl.zio 2 | 3 | import sttp.client4.internal.ws.SimpleQueue 4 | import sttp.ws.WebSocketBufferFull 5 | import zio.{Queue, RIO, Runtime, Unsafe} 6 | 7 | private[client4] class ZioSimpleQueue[R, A](queue: Queue[A], runtime: Runtime[Any]) extends SimpleQueue[RIO[R, *], A] { 8 | override def offer(t: A): Unit = 9 | Unsafe.unsafeCompat { implicit u => 10 | if (!runtime.unsafe.run(queue.offer(t)).getOrThrowFiberFailure()) { 11 | throw WebSocketBufferFull(queue.capacity) 12 | } 13 | } 14 | override def poll: RIO[R, A] = 15 | queue.take 16 | } 17 | -------------------------------------------------------------------------------- /effects/zio/src/main/scalajvm/sttp/client4/httpclient/zio/ZioSequencer.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.httpclient.zio 2 | 3 | import sttp.client4.internal.httpclient.Sequencer 4 | import zio.{Semaphore, Task, UIO} 5 | 6 | private[zio] class ZioSequencer(s: Semaphore) extends Sequencer[Task] { 7 | override def apply[T](t: => Task[T]): Task[T] = s.withPermit(t) 8 | } 9 | 10 | private[zio] object ZioSequencer { 11 | def create: UIO[ZioSequencer] = Semaphore.make(1).map(new ZioSequencer(_)) 12 | } 13 | -------------------------------------------------------------------------------- /effects/zio/src/main/scalajvm/sttp/client4/impl/zio/zioDecompressors.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.impl.zio 2 | 3 | import sttp.client4.compression.Decompressor 4 | import sttp.model.Encodings 5 | import sttp.capabilities.zio.ZioStreams 6 | import zio.stream.ZPipeline 7 | import zio.stream.ZStream 8 | import zio.stream.ZSink 9 | 10 | object GZipZioDecompressor extends Decompressor[ZioStreams.BinaryStream] { 11 | override val encoding: String = Encodings.Gzip 12 | override def apply(body: ZioStreams.BinaryStream): ZioStreams.BinaryStream = body.via(ZPipeline.gunzip()) 13 | } 14 | 15 | object DeflateZioDecompressor extends Decompressor[ZioStreams.BinaryStream] { 16 | override val encoding: String = Encodings.Deflate 17 | override def apply(body: ZioStreams.BinaryStream): ZioStreams.BinaryStream = 18 | ZStream.scoped[Any](body.peel(ZSink.take[Byte](1))).flatMap { case (chunk, stream) => 19 | val wrapped = chunk.headOption.exists(byte => (byte & 0x0f) == 0x08) 20 | (ZStream.fromChunk(chunk) ++ stream).via(ZPipeline.inflate(noWrap = !wrapped)) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /effects/zio/src/test/scala/sttp/client4/impl/zio/ZioTestBase.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.impl.zio 2 | 3 | import scala.concurrent.Future 4 | 5 | import zio._ 6 | 7 | import sttp.client4.testing.ConvertToFuture 8 | 9 | trait ZioTestBase { 10 | 11 | private val runtime: Runtime[Any] = Runtime.default 12 | 13 | val convertZioTaskToFuture: ConvertToFuture[Task] = new ConvertToFuture[Task] { 14 | override def toFuture[T](value: Task[T]): Future[T] = 15 | Unsafe.unsafe { implicit u => 16 | Runtime.default.unsafe.runToFuture(value.tapError { e => 17 | e.printStackTrace(); ZIO.unit 18 | }) 19 | } 20 | } 21 | 22 | def unsafeRunSync[T](task: Task[T]): Exit[Throwable, T] = 23 | Unsafe.unsafe { implicit u => 24 | runtime.unsafe.run(task) 25 | } 26 | 27 | def unsafeRunSyncOrThrow[T](task: Task[T]): T = 28 | Unsafe.unsafe { implicit u => 29 | runtime.unsafe.run(task).getOrThrowFiberFailure() 30 | } 31 | 32 | def timeoutToNone[T](t: Task[T], timeoutMillis: Int): Task[Option[T]] = 33 | t.timeout(timeoutMillis.milliseconds) 34 | 35 | } 36 | -------------------------------------------------------------------------------- /effects/zio/src/test/scalajs/sttp/client4/impl/zio/FetchZioHttpTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.impl.zio 2 | 3 | import zio.Task 4 | import sttp.capabilities.zio.ZioStreams 5 | import sttp.client4.StreamBackend 6 | import sttp.client4.testing.{AbstractFetchHttpTest, ConvertToFuture} 7 | 8 | class FetchZioHttpTest extends AbstractFetchHttpTest[Task, ZioStreams] with ZioTestBase { 9 | 10 | override val backend: StreamBackend[Task, ZioStreams] = FetchZioBackend() 11 | override implicit val convertToFuture: ConvertToFuture[Task] = convertZioTaskToFuture 12 | 13 | override protected def supportsCustomMultipartContentType = false 14 | 15 | override protected def supportsCustomMultipartEncoding = false 16 | 17 | override def timeoutToNone[T](t: Task[T], timeoutMillis: Int): Task[Option[T]] = super.timeoutToNone(t, timeoutMillis) 18 | } 19 | -------------------------------------------------------------------------------- /effects/zio1/src/main/scala/sttp/client4/impl/zio/ZioServerSentEvents.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.impl.zio 2 | 3 | import sttp.capabilities.zio.ZioStreams 4 | import sttp.model.sse.ServerSentEvent 5 | import zio.stream.ZTransducer 6 | 7 | object ZioServerSentEvents { 8 | val parse: ZioStreams.Pipe[Byte, ServerSentEvent] = { stream => 9 | stream 10 | .aggregate(ZTransducer.utf8Decode) 11 | .aggregate(ZTransducer.splitLines) 12 | .aggregate(ZTransducer.collectAllWhile[String](_.nonEmpty)) 13 | .map(ServerSentEvent.parse) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /effects/zio1/src/main/scala/sttp/client4/impl/zio/ZioSimpleQueue.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.impl.zio 2 | 3 | import sttp.client4.internal.ws.SimpleQueue 4 | import sttp.ws.WebSocketBufferFull 5 | import zio.{Queue, RIO, Runtime} 6 | 7 | class ZioSimpleQueue[R, A](queue: Queue[A], runtime: Runtime[Any]) extends SimpleQueue[RIO[R, *], A] { 8 | override def offer(t: A): Unit = 9 | if (!runtime.unsafeRun(queue.offer(t))) { 10 | throw WebSocketBufferFull(queue.capacity) 11 | } 12 | override def poll: RIO[R, A] = 13 | queue.take 14 | } 15 | -------------------------------------------------------------------------------- /effects/zio1/src/main/scalajvm/sttp/client4/httpclient/zio/ZioSequencer.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.httpclient.zio 2 | 3 | import sttp.client4.internal.httpclient.Sequencer 4 | import zio.{Semaphore, Task, UIO} 5 | 6 | private[zio] class ZioSequencer(s: Semaphore) extends Sequencer[Task] { 7 | override def apply[T](t: => Task[T]): Task[T] = s.withPermit(t) 8 | } 9 | 10 | private[zio] object ZioSequencer { 11 | def create: UIO[ZioSequencer] = Semaphore.make(1).map(new ZioSequencer(_)) 12 | } 13 | -------------------------------------------------------------------------------- /effects/zio1/src/main/scalajvm/sttp/client4/httpclient/zio/zioDecompressors.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.httpclient.zio 2 | 3 | import sttp.client4.compression.Decompressor 4 | import sttp.model.Encodings 5 | import sttp.capabilities.zio.ZioStreams 6 | import zio.stream.ZStream 7 | import zio.stream.ZSink 8 | import zio.stream.ZTransducer 9 | 10 | object GZipZioDecompressor extends Decompressor[ZioStreams.BinaryStream] { 11 | override val encoding: String = Encodings.Gzip 12 | override def apply(body: ZioStreams.BinaryStream): ZioStreams.BinaryStream = body.transduce(ZTransducer.gunzip()) 13 | } 14 | 15 | object DeflateZioDecompressor extends Decompressor[ZioStreams.BinaryStream] { 16 | override val encoding: String = Encodings.Deflate 17 | override def apply(body: ZioStreams.BinaryStream): ZioStreams.BinaryStream = 18 | ZStream.managed(body.peel(ZSink.take[Byte](1))).flatMap { case (chunk, stream) => 19 | val wrapped = chunk.headOption.exists(byte => (byte & 0x0f) == 0x08) 20 | (ZStream.fromChunk(chunk) ++ stream).transduce(ZTransducer.inflate(noWrap = !wrapped)) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /effects/zio1/src/test/scala/sttp/client4/impl/zio/ZioTestBase.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.impl.zio 2 | 3 | import sttp.client4.testing.ConvertToFuture 4 | import zio._ 5 | import zio.clock.Clock 6 | import zio.duration.durationInt 7 | 8 | import scala.concurrent.Future 9 | 10 | trait ZioTestBase { 11 | 12 | val runtime: Runtime[ZEnv] = Runtime.default 13 | 14 | val convertZioTaskToFuture: ConvertToFuture[Task] = new ConvertToFuture[Task] { 15 | override def toFuture[T](value: Task[T]): Future[T] = 16 | runtime.unsafeRunToFuture(value.tapError { e => 17 | e.printStackTrace(); ZIO.unit 18 | }) 19 | } 20 | 21 | def timeoutToNone[T](t: Task[T], timeoutMillis: Int): Task[Option[T]] = 22 | t.timeout(timeoutMillis.milliseconds).provideLayer(Clock.live) 23 | } 24 | -------------------------------------------------------------------------------- /effects/zio1/src/test/scalajs/sttp/client4/impl/zio/FetchZioHttpTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.impl.zio 2 | 3 | import zio.Task 4 | import sttp.capabilities.zio.ZioStreams 5 | import sttp.client4.StreamBackend 6 | import sttp.client4.testing.{AbstractFetchHttpTest, ConvertToFuture} 7 | 8 | class FetchZioHttpTest extends AbstractFetchHttpTest[Task, ZioStreams] with ZioTestBase { 9 | 10 | override val backend: StreamBackend[Task, ZioStreams] = FetchZioBackend() 11 | override implicit val convertToFuture: ConvertToFuture[Task] = convertZioTaskToFuture 12 | 13 | override protected def supportsCustomMultipartContentType = false 14 | 15 | override protected def supportsCustomMultipartEncoding = false 16 | 17 | override def timeoutToNone[T](t: Task[T], timeoutMillis: Int): Task[Option[T]] = super.timeoutToNone(t, timeoutMillis) 18 | } 19 | -------------------------------------------------------------------------------- /examples-ce2/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %d{HH:mm:ss.SSS}%boldYellow(%replace( [%X{cid}] ){' \[\] ', ' '})[%thread] %-5level %logger{5} - %msg%n%rEx 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples-ce2/src/main/scala/sttp/client4/examples/GetAndParseJsonOrFailMonixCirce.scala: -------------------------------------------------------------------------------- 1 | // {cat=JSON; effects=Monix; backend=HttpClient}: Receive & parse JSON using circe 2 | 3 | //> using scala 2.13 4 | //> using dep com.softwaremill.sttp.client4::monix:4.0.8 5 | //> using dep com.softwaremill.sttp.client4::circe:4.0.8 6 | //> using dep io.circe::circe-generic:0.14.13 7 | 8 | package sttp.client4.examples 9 | 10 | import io.circe.generic.auto._ 11 | import sttp.client4._ 12 | import sttp.client4.httpclient.monix.HttpClientMonixBackend 13 | import sttp.client4.circe._ 14 | 15 | object GetAndParseJsonOrFailMonixCirce extends App { 16 | import monix.execution.Scheduler.Implicits.global 17 | 18 | case class HttpBinResponse(origin: String, headers: Map[String, String]) 19 | 20 | val request: Request[HttpBinResponse] = basicRequest 21 | .get(uri"https://httpbin.org/get") 22 | .response(asJson[HttpBinResponse].orFail) 23 | 24 | HttpClientMonixBackend 25 | .resource() 26 | .use { backend => 27 | request.send(backend).map { response: Response[HttpBinResponse] => 28 | println(s"Got response code: ${response.code}") 29 | println(response.body) 30 | } 31 | } 32 | .runSyncUnsafe() 33 | } 34 | -------------------------------------------------------------------------------- /examples-ce2/src/main/scala/sttp/client4/examples/PostSerializeJsonMonixHttpClientCirce.scala: -------------------------------------------------------------------------------- 1 | // {cat=Hello, World!; effects=Monix; backend=HttpClient}: Post JSON data 2 | 3 | //> using scala 2.13 4 | //> using dep com.softwaremill.sttp.client4::monix:4.0.8 5 | //> using dep com.softwaremill.sttp.client4::circe:4.0.8 6 | //> using dep io.circe::circe-generic:0.14.13 7 | 8 | package sttp.client4.examples 9 | 10 | object PostSerializeJsonMonixHttpClientCirce extends App { 11 | import sttp.client4._ 12 | import sttp.client4.circe._ 13 | import sttp.client4.httpclient.monix.HttpClientMonixBackend 14 | import io.circe.generic.auto._ 15 | import monix.eval.Task 16 | 17 | case class Info(x: Int, y: String) 18 | 19 | val postTask = HttpClientMonixBackend().flatMap { backend => 20 | val r = basicRequest 21 | .body(asJson(Info(91, "abc"))) 22 | .post(uri"https://httpbin.org/post") 23 | 24 | r.send(backend) 25 | .flatMap(response => Task(println(s"""Got ${response.code} response, body:\n${response.body}"""))) 26 | .guarantee(backend.close()) 27 | } 28 | 29 | import monix.execution.Scheduler.Implicits.global 30 | postTask.runSyncUnsafe() 31 | } 32 | -------------------------------------------------------------------------------- /examples-ce2/src/main/scala/sttp/client4/examples/WebSocketMonix.scala: -------------------------------------------------------------------------------- 1 | // {cat=WebSocket; effects=Monix; backend=HttpClient}: Connect to & interact with a WebSocket 2 | 3 | //> using scala 2.13 4 | //> using dep com.softwaremill.sttp.client4::monix:4.0.8 5 | 6 | package sttp.client4.examples 7 | 8 | import monix.eval.Task 9 | import sttp.client4._ 10 | import sttp.client4.ws.async._ 11 | import sttp.client4.httpclient.monix.HttpClientMonixBackend 12 | import sttp.ws.WebSocket 13 | 14 | object WebSocketMonix extends App { 15 | import monix.execution.Scheduler.Implicits.global 16 | 17 | def useWebSocket(ws: WebSocket[Task]): Task[Unit] = { 18 | def send(i: Int) = ws.sendText(s"Hello $i!") 19 | val receive = ws.receiveText().flatMap(t => Task(println(s"RECEIVED: $t"))) 20 | send(1) *> send(2) *> receive *> receive 21 | } 22 | 23 | HttpClientMonixBackend 24 | .resource() 25 | .use { backend => 26 | basicRequest 27 | .get(uri"wss://ws.postman-echo.com/raw") 28 | .response(asWebSocket(useWebSocket)) 29 | .send(backend) 30 | .void 31 | } 32 | .runSyncUnsafe() 33 | } 34 | -------------------------------------------------------------------------------- /examples/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %d{HH:mm:ss.SSS}%boldYellow(%replace( [%X{cid}] ){' \[\] ', ' '})[%thread] %-5level %logger{5} - %msg%n%rEx 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/src/main/scala/sttp/client4/examples/PostFormSynchronous.scala: -------------------------------------------------------------------------------- 1 | // {cat=Hello, World!; effects=Direct; backend=HttpClient}: POST form data 2 | 3 | //> using dep com.softwaremill.sttp.client4::core:4.0.8 4 | 5 | package sttp.client4.examples 6 | 7 | import sttp.client4.* 8 | 9 | @main def postFormSynchronous(): Unit = 10 | val signup = Some("yes") 11 | 12 | val request = basicRequest 13 | // send the body as form data (x-www-form-urlencoded) 14 | .body(Map("name" -> "John", "surname" -> "doe")) 15 | // use an optional parameter in the URI 16 | .post(uri"https://httpbin.org/post?signup=$signup") 17 | 18 | val backend = DefaultSyncBackend() 19 | val response = request.send(backend) 20 | 21 | // the response body should contain a "form" field with the uploaded form data 22 | println(response.body) 23 | -------------------------------------------------------------------------------- /examples/src/main/scala/sttp/client4/examples/dynamicUriSynchronous.scala: -------------------------------------------------------------------------------- 1 | // {cat=Hello, World!; effects=Direct; backend=HttpClient}: Dynamic URI components 2 | 3 | //> using dep com.softwaremill.sttp.client4::core:4.0.8 4 | 5 | package sttp.client4.examples 6 | 7 | import sttp.client4.* 8 | 9 | import scala.util.Random 10 | 11 | @main def dynamicUriSynchronous(): Unit = 12 | val dynamicPath = List("Hello", Random().nextInt(100).toString, "World!") 13 | val dynamicQuery = Map("p1" -> Random().nextInt(100).toString, "p2" -> "special ąęść characters") 14 | val optionalQuery = if Random().nextBoolean() then Some("yes") else None 15 | 16 | val requestUri = uri"https://httpbin.org/anything/$dynamicPath?$dynamicQuery&optional=$optionalQuery" 17 | 18 | val backend = DefaultSyncBackend() 19 | val response = basicRequest.get(requestUri).send(backend) 20 | 21 | println("Sending request to: " + requestUri) 22 | println("Got response:") 23 | println(response.body) 24 | -------------------------------------------------------------------------------- /examples/src/main/scala/sttp/client4/examples/errors/httpErrorHandlingAdjustResponse.scala: -------------------------------------------------------------------------------- 1 | // {cat=Error handling; effects=Direct; backend=HttpClient}: HTTP error handling, adjusting the response description 2 | 3 | //> using dep com.softwaremill.sttp.client4::core:4.0.8 4 | 5 | package sttp.client4.examples 6 | 7 | import sttp.client4.* 8 | 9 | @main def httpErrorHandlingAdjustResponse(): Unit = 10 | val backend = DefaultSyncBackend() 11 | 12 | // by default the response is read into an Either[String, String], to indicate HTTP success or error 13 | def httpErrorDefault(): Unit = 14 | val response = basicRequest.get(uri"https://httpbin.org/status/400").send(backend) 15 | val body: Either[String, String] = response.body 16 | println(s"HTTP error, response code ${response.code}, body: ${body}") 17 | 18 | // using asStringOrFail, any HTTP-level errors (successful connection, but a non-2xx status code) 19 | // are translated into exceptions 20 | def httpErrorOrFail(): Unit = 21 | try 22 | val _ = basicRequest.get(uri"https://httpbin.org/status/400").response(asStringOrFail).send(backend) 23 | catch case e: SttpClientException.ReadException => println(s"Connection exception: $e") 24 | 25 | httpErrorDefault() 26 | httpErrorOrFail() 27 | -------------------------------------------------------------------------------- /examples/src/main/scala/sttp/client4/examples/fileUploadSynchronous.scala: -------------------------------------------------------------------------------- 1 | // {cat=Hello, World!; effects=Direct; backend=HttpClient}: Upload file 2 | 3 | //> using dep com.softwaremill.sttp.client4::core:4.0.8 4 | 5 | package sttp.client4.examples 6 | 7 | import sttp.client4.* 8 | 9 | import java.nio.file.Files 10 | import java.nio.file.Path 11 | 12 | @main def fileUploadSynchronous(): Unit = 13 | withTemporaryFile("Hello, World!".getBytes) { file => 14 | val request = basicRequest 15 | .body(file) 16 | .post(uri"https://httpbin.org/post") 17 | 18 | val backend: SyncBackend = DefaultSyncBackend() 19 | val response: Response[Either[String, String]] = request.send(backend) 20 | 21 | // the uploaded data should be echoed in the "data" field of the response body 22 | println(response.body) 23 | } 24 | 25 | private def withTemporaryFile[T](data: Array[Byte])(f: Path => T): T = { 26 | val file = Files.createTempFile("sttp", "demo") 27 | try 28 | Files.write(file, data) 29 | f(file) 30 | finally 31 | val _ = Files.deleteIfExists(file) 32 | } 33 | -------------------------------------------------------------------------------- /examples/src/main/scala/sttp/client4/examples/json/GetAndParseJsonZioJson.scala: -------------------------------------------------------------------------------- 1 | // {cat=JSON; effects=ZIO; backend=HttpClient}: Receive & parse JSON using ZIO Json 2 | 3 | //> using dep com.softwaremill.sttp.client4::zio:4.0.8 4 | //> using dep com.softwaremill.sttp.client4::zio-json:4.0.8 5 | 6 | package sttp.client4.examples.json 7 | 8 | import sttp.client4.* 9 | import sttp.client4.ziojson.* 10 | import sttp.client4.httpclient.zio.HttpClientZioBackend 11 | import sttp.client4.httpclient.zio.send 12 | import zio.* 13 | import zio.json.JsonDecoder 14 | import zio.json.DeriveJsonDecoder 15 | 16 | object GetAndParseJsonZioJson extends ZIOAppDefault: 17 | 18 | case class HttpBinResponse(origin: String, headers: Map[String, String]) 19 | 20 | object HttpBinResponse: 21 | given JsonDecoder[HttpBinResponse] = DeriveJsonDecoder.gen[HttpBinResponse] 22 | 23 | override def run = { 24 | val request = basicRequest 25 | .get(uri"https://httpbin.org/get") 26 | .response(asJson[HttpBinResponse]) 27 | 28 | for 29 | response <- send(request) 30 | _ <- Console.printLine(s"Got response code: ${response.code}") 31 | _ <- Console.printLine(response.body.toString) 32 | yield () 33 | }.provideLayer(HttpClientZioBackend.layer()) 34 | -------------------------------------------------------------------------------- /examples/src/main/scala/sttp/client4/examples/other/GetRawResponseBodySynchronous.scala: -------------------------------------------------------------------------------- 1 | // {cat=Other; effects=Direct; backend=HttpClient}: Handle the body by both parsing it to JSON and returning the raw string 2 | 3 | //> using dep com.softwaremill.sttp.client4::circe:4.0.8 4 | //> using dep io.circe::circe-generic:0.14.13 5 | 6 | package sttp.client4.examples.other 7 | 8 | import io.circe 9 | import io.circe.generic.auto.* 10 | import sttp.client4.* 11 | import sttp.client4.circe.* 12 | 13 | @main def getRawResponseBodySynchronous(): Unit = 14 | case class HttpBinResponse(origin: String, headers: Map[String, String]) 15 | 16 | val request = basicRequest 17 | .get(uri"https://httpbin.org/get") 18 | .response(asBoth(asJson[HttpBinResponse], asStringAlways)) 19 | 20 | val backend: SyncBackend = DefaultSyncBackend() 21 | 22 | try 23 | val response: Response[(Either[ResponseException[String], HttpBinResponse], String)] = 24 | request.send(backend) 25 | 26 | val (parsed, raw) = response.body 27 | 28 | println("Got response - parsed: " + parsed) 29 | println("Got response - raw: " + raw) 30 | finally backend.close() 31 | -------------------------------------------------------------------------------- /examples/src/main/scala/sttp/client4/examples/other/cmdOutputStreamingWithOsLib.scala: -------------------------------------------------------------------------------- 1 | // {cat=Other; effects=Direct; backend=HttpClient}: Command output streaming with os-lib support 2 | 3 | //> using dep com.lihaoyi::os-lib:0.11.3 4 | //> using dep com.softwaremill.sttp.client4::core:4.0.8 5 | 6 | package sttp.client4.examples.other 7 | 8 | import sttp.client4.* 9 | import os.* 10 | 11 | private val backend: SyncBackend = DefaultSyncBackend() 12 | private val path: os.Path = os.Path("/tmp/example-file.txt") 13 | 14 | @main def cmdOutputStreamingWithOsLib(): Unit = { 15 | val _ = os.remove(path) 16 | os.write(path, "CONTENT OF THE SIMPLE FILE USED IN THIS EXAMPLE") 17 | val process = os.proc("cat", path.toString).spawn() 18 | val request = basicRequest 19 | .post(uri"http://httpbin.org/post") 20 | .body(process.stdout.wrapped) 21 | .response(asString) 22 | val response = request.send(backend) 23 | println(response) 24 | } 25 | -------------------------------------------------------------------------------- /examples/src/main/scala/sttp/client4/examples/other/downloadFileWitOsLib.scala: -------------------------------------------------------------------------------- 1 | // {cat=Other; effects=Direct; backend=HttpClient}: Download file with os-lib support 2 | 3 | //> using dep com.lihaoyi::os-lib:0.11.3 4 | //> using dep com.softwaremill.sttp.client4::core:4.0.8 5 | 6 | package sttp.client4.examples.other 7 | 8 | import sttp.client4.* 9 | import os.* 10 | 11 | private val fileSize = 8192 12 | private val dest: os.Path = os.Path(s"/tmp/file-example-$fileSize-bytes") 13 | private val backend: SyncBackend = DefaultSyncBackend() 14 | 15 | @main def downloadFileWithOsLib(): Unit = { 16 | val _ = os.remove(dest) 17 | val request = basicRequest 18 | .get(uri"https://httpbin.org/bytes/$fileSize") 19 | .response(asInputStream(i => os.write(dest, i))) 20 | val response = request.send(backend) 21 | println(response.headers) 22 | } 23 | -------------------------------------------------------------------------------- /examples/src/main/scala/sttp/client4/examples/other/uploadFileWithOsLib.scala: -------------------------------------------------------------------------------- 1 | // {cat=Other; effects=Direct; backend=HttpClient}: Download file with os-lib support 2 | 3 | //> using dep com.lihaoyi::os-lib:0.11.3 4 | //> using dep com.softwaremill.sttp.client4::core:4.0.8 5 | 6 | package sttp.client4.examples.other 7 | 8 | import sttp.client4.* 9 | import os.* 10 | 11 | private val path: os.Path = os.Path("/tmp/example-file.txt") 12 | private val backend: SyncBackend = DefaultSyncBackend() 13 | 14 | @main def uploadFileWithOsLib(): Unit = { 15 | val _ = os.remove(path) 16 | os.write(path, "THIS IS CONTENT OF TEST FILE") 17 | val request = basicRequest 18 | .post(uri"http://httpbin.org/post") 19 | .body(os.read.inputStream(path)) 20 | .response(asString) 21 | val response = request.send(backend) 22 | println(response) 23 | } 24 | -------------------------------------------------------------------------------- /examples/src/main/scala/sttp/client4/examples/resilience/RateLimitOx.scala: -------------------------------------------------------------------------------- 1 | // {cat=Resilience; effects=Direct; backend=HttpClient}: Rate limit sending requests using Ox 2 | 3 | //> using dep com.softwaremill.sttp.client4::ox:4.0.8 4 | 5 | package sttp.client4.examples.resilience 6 | 7 | import ox.Ox 8 | import ox.OxApp 9 | import sttp.client4.* 10 | 11 | import scala.concurrent.duration.* 12 | import ox.resilience.RateLimiter 13 | import java.time.Instant 14 | 15 | object RateLimitOx extends OxApp.Simple: 16 | override def run(using Ox): Unit = 17 | val backend = DefaultSyncBackend() 18 | 19 | val rateLimiter = RateLimiter.fixedWindowWithDuration(1, 3.seconds) 20 | for (_ <- 1 to 5) 21 | rateLimiter.runBlocking { 22 | println(s"${Instant.now()} Sending request ...") 23 | basicRequest.get(uri"https://httpbin.org/status/500").send(backend) 24 | } 25 | -------------------------------------------------------------------------------- /examples/src/main/scala/sttp/client4/examples/resilience/RetryZio.scala: -------------------------------------------------------------------------------- 1 | // {cat=Resilience; effects=ZIO; backend=HttpClient}: Retry sending a request using ZIO's retries 2 | 3 | //> using dep com.softwaremill.sttp.client4::zio:4.0.8 4 | 5 | package sttp.client4.examples.resilience 6 | 7 | import sttp.client4.* 8 | import sttp.client4.httpclient.zio.HttpClientZioBackend 9 | import zio.Schedule 10 | import zio.Task 11 | import zio.ZIO 12 | import zio.ZIOAppDefault 13 | import zio.durationInt 14 | 15 | object RetryZio extends ZIOAppDefault: 16 | override def run: ZIO[Any, Throwable, Response[String]] = 17 | HttpClientZioBackend() 18 | .flatMap: backend => 19 | val localhostRequest = basicRequest 20 | .get(uri"http://localhost/test") 21 | .response(asStringAlways) 22 | 23 | val sendWithRetries: Task[Response[String]] = localhostRequest 24 | .send(backend) 25 | .either 26 | .repeat( 27 | Schedule.spaced(1.second) *> 28 | Schedule.recurs(10) *> 29 | Schedule.recurWhile(result => RetryWhen.Default(localhostRequest, result)) 30 | ) 31 | .absolve 32 | 33 | sendWithRetries.ensuring(backend.close().ignore) 34 | -------------------------------------------------------------------------------- /examples/src/main/scala/sttp/client4/examples/testing/TestEndpointMultipleQueryParameters.scala: -------------------------------------------------------------------------------- 1 | // {cat=Testing}: Create a backend stub which simulates interactions using multiple query parameters 2 | 3 | //> using dep com.softwaremill.sttp.client4::core:4.0.8 4 | 5 | package sttp.client4.examples.testing 6 | 7 | import sttp.client4.* 8 | import sttp.client4.testing.* 9 | 10 | @main def testEndpointMultipleQueryParameters(): Unit = 11 | val backend = SyncBackendStub 12 | .whenRequestMatches(_.uri.paramsMap.contains("filter")) 13 | .thenRespondAdjust("Filtered") 14 | .whenRequestMatches(_.uri.path.contains("secret")) 15 | .thenRespondAdjust("42") 16 | 17 | val parameters1 = Map("filter" -> "name=mary", "sort" -> "asc") 18 | println( 19 | basicRequest 20 | .get(uri"http://example.org?search=true&$parameters1") 21 | .send(backend) 22 | .body 23 | ) 24 | 25 | val parameters2 = Map("sort" -> "desc") 26 | println( 27 | basicRequest 28 | .get(uri"http://example.org/secret/read?$parameters2") 29 | .send(backend) 30 | .body 31 | ) 32 | -------------------------------------------------------------------------------- /examples/src/main/scala/sttp/client4/examples/ws/WebSocketPekko.scala: -------------------------------------------------------------------------------- 1 | // {cat=WebSocket; effects=Future; backend=Pekko}: Connect to & interact with a WebSocket 2 | 3 | //> using dep com.softwaremill.sttp.client4::pekko-http-backend:4.0.8 4 | //> using dep org.apache.pekko::pekko-stream:1.1.2 5 | 6 | package sttp.client4.examples.ws 7 | 8 | import sttp.client4._ 9 | import sttp.client4.ws.async._ 10 | import sttp.client4.pekkohttp.PekkoHttpBackend 11 | import sttp.ws.WebSocket 12 | 13 | import scala.concurrent.ExecutionContext.Implicits.global 14 | import scala.concurrent.Future 15 | 16 | @main def webSocketPekko(): Unit = 17 | def useWebSocket(ws: WebSocket[Future]): Future[Unit] = 18 | def send(i: Int) = ws.sendText(s"Hello $i!") 19 | def receive() = ws.receiveText().map(t => println(s"RECEIVED: $t")) 20 | for 21 | _ <- send(1) 22 | _ <- send(2) 23 | _ <- receive() 24 | _ <- receive() 25 | yield () 26 | 27 | val backend = PekkoHttpBackend() 28 | 29 | basicRequest 30 | .get(uri"wss://ws.postman-echo.com/raw") 31 | .response(asWebSocket(useWebSocket)) 32 | .send(backend) 33 | .onComplete(_ => backend.close()) 34 | -------------------------------------------------------------------------------- /examples/src/main/scala/sttp/client4/examples/ws/WebSocketSynchronous.scala: -------------------------------------------------------------------------------- 1 | // {cat=WebSocket; effects=Direct; backend=HttpClient}: Connect to & interact with a WebSocket 2 | 3 | //> using dep com.softwaremill.sttp.client4::core:4.0.8 4 | 5 | package sttp.client4.examples.ws 6 | 7 | import sttp.client4.* 8 | import sttp.client4.ws.SyncWebSocket 9 | import sttp.client4.ws.sync.* 10 | 11 | @main def webSocketSynchronous(): Unit = 12 | def useWebSocket(ws: SyncWebSocket): Unit = 13 | def send(i: Int): Unit = ws.sendText(s"Hello $i!") 14 | def receive(): Unit = { 15 | val t = ws.receiveText() 16 | println(s"RECEIVED: $t") 17 | } 18 | send(1) 19 | send(2) 20 | receive() 21 | receive() 22 | 23 | val backend = DefaultSyncBackend() 24 | 25 | try 26 | println( 27 | basicRequest 28 | .get(uri"wss://ws.postman-echo.com/raw") 29 | .response(asWebSocket(useWebSocket)) 30 | .send(backend) 31 | ) 32 | finally backend.close() 33 | -------------------------------------------------------------------------------- /examples/src/main/scala/sttp/client4/examples/ws/WebSocketZio.scala: -------------------------------------------------------------------------------- 1 | // {cat=WebSocket; effects=ZIO; backend=HttpClient}: Connect to & interact with a WebSocket 2 | 3 | //> using dep com.softwaremill.sttp.client4::zio:4.0.8 4 | 5 | package sttp.client4.examples.ws 6 | 7 | import sttp.client4.* 8 | import sttp.client4.httpclient.zio.HttpClientZioBackend 9 | import sttp.client4.ws.async.* 10 | import sttp.ws.WebSocket 11 | import zio.* 12 | import zio.Console 13 | 14 | object WebSocketZio extends ZIOAppDefault: 15 | def useWebSocket(ws: WebSocket[Task]): Task[Unit] = 16 | def send(i: Int) = ws.sendText(s"Hello $i!") 17 | val receive = ws.receiveText().flatMap(t => Console.printLine(s"RECEIVED: $t")) 18 | send(1) *> send(2) *> receive *> receive 19 | 20 | // create a description of a program, which requires SttpClient dependency in the environment 21 | def sendAndPrint(backend: WebSocketBackend[Task]): Task[Response[Unit]] = 22 | basicRequest.get(uri"wss://ws.postman-echo.com/raw").response(asWebSocketAlways(useWebSocket)).send(backend) 23 | 24 | override def run = 25 | // provide an implementation for the SttpClient dependency 26 | HttpClientZioBackend.scoped().flatMap(sendAndPrint) 27 | -------------------------------------------------------------------------------- /examples/src/main/scala/sttp/client4/examples/ws/wsOxExample.scala: -------------------------------------------------------------------------------- 1 | // {cat=WebSocket; effects=Direct; backend=HttpClient}: Connect to & interact with a WebSocket, using Ox channels for streaming 2 | 3 | //> using dep com.softwaremill.sttp.client4::ox:4.0.8 4 | 5 | package sttp.client4.examples.ws 6 | 7 | import ox.* 8 | import ox.channels.Source 9 | import sttp.client4.* 10 | import sttp.client4.impl.ox.ws.* 11 | import sttp.client4.ws.SyncWebSocket 12 | import sttp.client4.ws.sync.* 13 | import sttp.ws.WebSocketFrame 14 | 15 | @main def wsOxExample = 16 | def useWebSocket(ws: SyncWebSocket): Unit = 17 | supervised: 18 | val inputs = Source.fromValues(1, 2, 3).map(i => WebSocketFrame.text(s"Frame no $i")) 19 | val (wsSource, wsSink) = asSourceAndSink(ws) 20 | forkDiscard: 21 | inputs.pipeTo(wsSink, propagateDone = true) 22 | wsSource.foreach: frame => 23 | println(s"RECEIVED: $frame") 24 | 25 | val backend = DefaultSyncBackend() 26 | try 27 | basicRequest 28 | .get(uri"wss://ws.postman-echo.com/raw") 29 | .response(asWebSocket(useWebSocket)) 30 | .send(backend) 31 | .discard 32 | finally 33 | backend.close() 34 | -------------------------------------------------------------------------------- /finagle-backend/src/test/scala/sttp/client4/finagle/FinagleBackendTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.finagle 2 | 3 | import com.twitter.util.{Future => TFuture, Return, Throw} 4 | import sttp.client4.testing.{ConvertToFuture, HttpTest} 5 | 6 | import sttp.client4.Backend 7 | import scala.concurrent.{Future, Promise} 8 | 9 | class FinagleBackendTest extends HttpTest[TFuture] { 10 | 11 | override val backend: Backend[TFuture] = FinagleBackend() 12 | override implicit val convertToFuture: ConvertToFuture[TFuture] = new ConvertToFuture[TFuture] { 13 | override def toFuture[T](value: TFuture[T]): Future[T] = { 14 | val promise: Promise[T] = Promise() 15 | value.respond { 16 | case Return(value) => promise.success(value) 17 | case Throw(exception) => promise.failure(exception) 18 | } 19 | promise.future 20 | } 21 | } 22 | override def throwsExceptionOnUnsupportedEncoding = false 23 | override def supportsCustomMultipartContentType = false 24 | override def supportsAutoDecompressionDisabling = false 25 | override def supportsResponseAsInputStream = false 26 | 27 | override def supportsCancellation: Boolean = false 28 | override def timeoutToNone[T](t: TFuture[T], timeoutMillis: Int): TFuture[Option[T]] = t.map(Some(_)) 29 | } 30 | -------------------------------------------------------------------------------- /generated-docs/out/.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | _build_html -------------------------------------------------------------------------------- /generated-docs/out/.python-version: -------------------------------------------------------------------------------- 1 | 3.12 2 | -------------------------------------------------------------------------------- /generated-docs/out/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python -msphinx 7 | SPHINXPROJ = tapir 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /generated-docs/out/_static/css/custom.css: -------------------------------------------------------------------------------- 1 | /* general style for all example tags */ 2 | .example-tag { 3 | border-width: 1px; 4 | border-radius: 9999px; 5 | border-style: solid; 6 | padding-left: 0.5rem; 7 | padding-right: 0.5rem; 8 | margin-right: 0.25rem; 9 | margin-top: 0.25rem; 10 | margin-bottom: 0.25rem; 11 | } 12 | 13 | /* different colors for specific tags */ 14 | .example-effects { 15 | color: rgb(193 21 116); 16 | background-color: rgb(253 242 250); 17 | border-color: rgb(252 206 238); 18 | } 19 | 20 | .example-json { 21 | color: rgb(185 56 21); 22 | background-color: rgb(254 246 238); 23 | border-color: rgb(249 219 175); 24 | } 25 | 26 | .example-backend { 27 | color: rgb(6 118 71); 28 | background-color: rgb(236 253 243); 29 | border-color: rgb(169 239 197); 30 | } 31 | 32 | .example-docs { 33 | color: rgb(52 64 84); 34 | background-color: rgb(249 250 251); 35 | border-color: rgb(234 236 240); 36 | } 37 | 38 | .example-client { 39 | color: rgb(6 89 134); 40 | background-color: rgb(240 249 255); 41 | border-color: rgb(185 230 254); 42 | } -------------------------------------------------------------------------------- /generated-docs/out/backends/finagle.md: -------------------------------------------------------------------------------- 1 | # Twitter future (Finagle) backend 2 | 3 | To use, add the following dependency to your project: 4 | 5 | ``` 6 | "com.softwaremill.sttp.client4" %% "finagle-backend" % "4.0.8" 7 | ``` 8 | 9 | Next you'll need to add an implicit value: 10 | 11 | ```scala 12 | import sttp.client4.finagle.FinagleBackend 13 | val backend = FinagleBackend() 14 | ``` 15 | 16 | This backend depends on [finagle](https://twitter.github.io/finagle/), and offers an asynchronous backend, which wraps results in Twitter's `Future`. 17 | 18 | Please note that: 19 | 20 | * the backend does not support non-blocking [streaming](../requests/streaming.md) or [websockets](../other/websockets.md). 21 | -------------------------------------------------------------------------------- /generated-docs/out/backends/native/curl.md: -------------------------------------------------------------------------------- 1 | # Scala Native (curl) backend 2 | 3 | A Scala Native (0.5.x) backend implemented using [Curl](https://github.com/curl/curl/blob/master/include/curl/curl.h). 4 | 5 | To use, add the following dependency to your project: 6 | 7 | ``` 8 | "com.softwaremill.sttp.client4" %%% "core" % "4.0.8" 9 | ``` 10 | 11 | and initialize one of the backends: 12 | 13 | ```scala 14 | import sttp.client4.curl.* 15 | 16 | val backend = CurlBackend() 17 | val tryBackend = CurlTryBackend() 18 | ``` 19 | 20 | You need to have an environment with Scala Native [setup](https://scala-native.readthedocs.io/en/latest/user/setup.html) 21 | with additionally installed `libcrypto` (included in OpenSSL) and `curl` in version `7.56.0` or newer. 22 | 23 | ## scala-cli example 24 | 25 | Try the following example: 26 | 27 | ```scala 28 | // hello.scala 29 | 30 | //> using platform native 31 | //> using dep com.softwaremill.sttp.client4::core_native0.5:4.0.8 32 | 33 | import sttp.client4.* 34 | import sttp.client4.curl.CurlBackend 35 | 36 | @main def run(): Unit = 37 | val backend = CurlBackend() 38 | println(basicRequest.get(uri"http://httpbin.org/ip").send(backend)) 39 | ``` 40 | 41 | -------------------------------------------------------------------------------- /generated-docs/out/backends/start_stop.md: -------------------------------------------------------------------------------- 1 | # Starting & cleaning up 2 | 3 | In case of most backends, you should only instantiate a backend once per application, as a backend typically allocates resources such as thread or connection pools. 4 | 5 | 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! 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /generated-docs/out/community.md: -------------------------------------------------------------------------------- 1 | # Community 2 | 3 | If you have a question, suggestion, or hit a problem, feel free to ask on our [discourse forum](https://softwaremill.community/c/sttp-client)! 4 | 5 | Or, if you encounter a bug, something is unclear in the code or documentation, don't hesitate and [open an issue](https://github.com/softwaremill/sttp/issues) on GitHub. 6 | 7 | We are also always looking for contributions and new ideas, so if you'd like to get into the project, check out the open issues, or post your own suggestions! 8 | -------------------------------------------------------------------------------- /generated-docs/out/conf/timeouts.md: -------------------------------------------------------------------------------- 1 | # Timeouts 2 | 3 | sttp supports read and connection timeouts: 4 | 5 | * Connection timeout - can be set globally (30 seconds by default) 6 | * Read timeout - can be set per request (1 minute by default) 7 | 8 | How to use: 9 | 10 | ```scala 11 | import sttp.client4.* 12 | import scala.concurrent.duration.* 13 | 14 | // all backends provide a constructor that allows to specify backend options 15 | val backend = DefaultSyncBackend( 16 | options = BackendOptions.connectionTimeout(1.minute)) 17 | 18 | basicRequest 19 | .get(uri"...") 20 | .readTimeout(5.minutes) // or Duration.Inf to turn read timeout off 21 | .send(backend) 22 | ``` 23 | -------------------------------------------------------------------------------- /generated-docs/out/examples.md: -------------------------------------------------------------------------------- 1 | # Examples by category 2 | 3 | The sttp client repository contains a number of how-to guides. If you're missing an example for your use-case, please let us 4 | know by [reporting an issue](https://github.com/softwaremill/sttp)! 5 | 6 | Each example is fully self-contained and can be run using [scala-cli](https://scala-cli.virtuslab.org) (you just need 7 | to copy the content of the file, apart from scala-cli, no additional setup is required!). Hopefully this will make 8 | experimenting with sttp client as frictionless as possible! 9 | 10 | Examples are tagged with the stack being used (direct-style, cats-effect, ZIO, Future) and backend implementation 11 | 12 | ```{eval-rst} 13 | .. include:: includes/examples_list.md 14 | :parser: markdown 15 | ``` 16 | 17 | 18 | -------------------------------------------------------------------------------- /generated-docs/out/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=python -msphinx 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=sttp 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The Sphinx module was not found. Make sure you have Sphinx installed, 20 | echo.then set the SPHINXBUILD environment variable to point to the full 21 | echo.path of the 'sphinx-build' executable. Alternatively you may add the 22 | echo.Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /generated-docs/out/other.md: -------------------------------------------------------------------------------- 1 | # Other Scala HTTP clients 2 | 3 | * [akka-http client](http://doc.akka.io/docs/akka-http/current/scala/http/client-side/index.html) 4 | * [play ws](https://github.com/playframework/play-ws) 5 | * [http4s](http://http4s.org/v0.17/client/) 6 | * [Gigahorse](http://eed3si9n.com/gigahorse/) 7 | * [Requests-Scala](https://github.com/lihaoyi/requests-scala) 8 | 9 | Also, check the [comparison by Marco Firrincieli](https://github.com/mfirry/scala-http-clients) on how to implement a simple request using a number of Scala HTTP libraries. 10 | -------------------------------------------------------------------------------- /generated-docs/out/other/sse.md: -------------------------------------------------------------------------------- 1 | # Server-sent events 2 | 3 | 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). 4 | 5 | Refer to the documentation of individual backends, for more information on how to use SSE with a given backend, as well as usage examples. -------------------------------------------------------------------------------- /generated-docs/out/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx_rtd_theme==2.0.0 2 | sphinx==7.3.7 3 | sphinx-autobuild==2024.4.16 4 | myst-parser==2.0.0 5 | -------------------------------------------------------------------------------- /generated-docs/out/support.md: -------------------------------------------------------------------------------- 1 | # Support & sponsorship 2 | 3 | ## Sponsors 4 | 5 | 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. 6 | 7 | [![](https://files.softwaremill.com/logo/logo.png "SoftwareMill")](https://softwaremill.com) 8 | 9 | ## Commercial Support 10 | 11 | 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! 12 | -------------------------------------------------------------------------------- /generated-docs/out/testing/curl.md: -------------------------------------------------------------------------------- 1 | # Converting requests to CURL commands 2 | 3 | sttp comes with builtin request to curl converter. To convert request to curl invocation use `.toCurl` method. 4 | 5 | For example: 6 | 7 | ```scala 8 | import sttp.client4.* 9 | 10 | basicRequest.get(uri"http://httpbin.org/ip").toCurl 11 | // res0: String = """curl \ 12 | // --request GET \ 13 | // --url 'http://httpbin.org/ip' \ 14 | // --header 'Accept-Encoding: gzip, deflate' \ 15 | // --location \ 16 | // --max-redirs 32""" 17 | ``` 18 | 19 | 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. -------------------------------------------------------------------------------- /generated-docs/out/watch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | sphinx-autobuild . _build/html 3 | -------------------------------------------------------------------------------- /http4s-backend/src/test/scala/sttp/client4/http4s/Http4sHttpStreamingTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.http4s 2 | 3 | import cats.effect.IO 4 | import sttp.client4.StreamBackend 5 | import sttp.client4.impl.fs2.Fs2StreamingTest 6 | 7 | import scala.concurrent.ExecutionContext 8 | import org.http4s.blaze.client.BlazeClientBuilder 9 | import sttp.capabilities.fs2.Fs2Streams 10 | 11 | class Http4sHttpStreamingTest extends Fs2StreamingTest { 12 | 13 | private val blazeClientBuilder = BlazeClientBuilder[IO].withExecutionContext(ExecutionContext.global) 14 | override val backend: StreamBackend[IO, Fs2Streams[IO]] = 15 | Http4sBackend.usingBlazeClientBuilder(blazeClientBuilder).allocated.unsafeRunSync()._1 16 | 17 | } 18 | -------------------------------------------------------------------------------- /http4s-backend/src/test/scala/sttp/client4/http4s/Http4sHttpTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.http4s 2 | 3 | import cats.effect.IO 4 | import org.http4s.blaze.client.BlazeClientBuilder 5 | import sttp.client4.Backend 6 | import sttp.client4.impl.cats.{CatsRetryTest, CatsTestBase} 7 | import sttp.client4.testing.HttpTest 8 | 9 | import scala.concurrent.ExecutionContext 10 | 11 | class Http4sHttpTest extends HttpTest[IO] with CatsRetryTest with CatsTestBase { 12 | private val blazeClientBuilder = BlazeClientBuilder[IO] 13 | 14 | override val backend: Backend[IO] = 15 | Http4sBackend.usingBlazeClientBuilder(blazeClientBuilder).allocated.unsafeRunSync()._1 16 | 17 | override protected def supportsRequestTimeout = false 18 | override protected def supportsCustomMultipartContentType = false 19 | override protected def supportsResponseAsInputStream = false 20 | } 21 | -------------------------------------------------------------------------------- /http4s-ce2-backend/src/test/scala/sttp/client4/http4s/Http4sHttpStreamingTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.http4s 2 | 3 | import cats.effect.IO 4 | import org.http4s.blaze.client.BlazeClientBuilder 5 | import sttp.client4.StreamBackend 6 | import sttp.client4.impl.fs2.Fs2StreamingTest 7 | 8 | import scala.concurrent.ExecutionContext 9 | import sttp.capabilities.fs2.Fs2Streams 10 | 11 | class Http4sHttpStreamingTest extends Fs2StreamingTest { 12 | 13 | private val blazeClientBuilder = BlazeClientBuilder[IO](ExecutionContext.global) 14 | override val backend: StreamBackend[IO, Fs2Streams[IO]] = 15 | Http4sBackend.usingBlazeClientBuilder(blazeClientBuilder, blocker).allocated.unsafeRunSync()._1 16 | 17 | } 18 | -------------------------------------------------------------------------------- /http4s-ce2-backend/src/test/scala/sttp/client4/http4s/Http4sHttpTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.http4s 2 | 3 | import cats.effect.IO 4 | import org.http4s.blaze.client.BlazeClientBuilder 5 | import sttp.client4.Backend 6 | import sttp.client4.impl.cats.CatsTestBase 7 | import sttp.client4.testing.HttpTest 8 | 9 | import scala.concurrent.ExecutionContext 10 | 11 | class Http4sHttpTest extends HttpTest[IO] with CatsTestBase { 12 | private val blazeClientBuilder = BlazeClientBuilder[IO](ExecutionContext.global) 13 | 14 | override val backend: Backend[IO] = 15 | Http4sBackend.usingBlazeClientBuilder(blazeClientBuilder, blocker).allocated.unsafeRunSync()._1 16 | 17 | override protected def supportsRequestTimeout = false 18 | override protected def supportsCustomMultipartContentType = false 19 | override protected def supportsResponseAsInputStream = false 20 | } 21 | -------------------------------------------------------------------------------- /json/circe/src/main/scala/sttp/client4/circe/package.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | 3 | package object circe extends SttpCirceApi 4 | -------------------------------------------------------------------------------- /json/circe/src/test/scala/sttp/client4/circe/BackendStubCirceTests.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.circe 2 | 3 | import org.scalatest.concurrent.ScalaFutures 4 | import sttp.client4._ 5 | import sttp.client4.testing.SyncBackendStub 6 | import io.circe.generic.auto._ 7 | import org.scalatest.flatspec.AnyFlatSpec 8 | import org.scalatest.matchers.should.Matchers 9 | 10 | class BackendStubCirceTests extends AnyFlatSpec with Matchers with ScalaFutures { 11 | 12 | it should "deserialize to json using a string stub" in { 13 | val backend = SyncBackendStub.whenAnyRequest.thenRespondAdjust("""{"name": "John"}""") 14 | val r = basicRequest.get(uri"http://example.org").response(asJson[Person]).send(backend) 15 | r.is200 should be(true) 16 | r.body should be(Right(Person("John"))) 17 | } 18 | 19 | case class Person(name: String) 20 | } 21 | -------------------------------------------------------------------------------- /json/common/src/main/scala/sttp/client4/IsOption.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | 3 | trait IsOption[-T] { 4 | def isOption: Boolean 5 | } 6 | 7 | object IsOption { 8 | private object True extends IsOption[Any] { 9 | override val isOption: Boolean = true 10 | } 11 | 12 | private object False extends IsOption[Any] { 13 | override val isOption: Boolean = false 14 | } 15 | 16 | implicit def optionIsOption[T]: IsOption[Option[T]] = True 17 | implicit def leftOptionIsOption[T]: IsOption[Either[Option[T], _]] = True 18 | implicit def rightOptionIsOption[T]: IsOption[Either[_, Option[T]]] = True 19 | implicit def otherIsNotOption[T]: IsOption[T] = False 20 | } 21 | -------------------------------------------------------------------------------- /json/common/src/main/scala/sttp/client4/JsonInput.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | 3 | protected[sttp] object JsonInput { 4 | def sanitize[T: IsOption]: String => String = { s => 5 | if (implicitly[IsOption[T]].isOption && s.trim.isEmpty) "null" else s 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /json/common/src/main/scala/sttp/client4/json/package.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | 3 | package object json { 4 | implicit class RichResponseAs[T](ra: ResponseAs[T]) { 5 | def showAsJson: ResponseAs[T] = ra.showAs("either(as string, as json)") 6 | def showAsJsonAlways: ResponseAs[T] = ra.showAs("as json") 7 | def showAsJsonOrFail: ResponseAs[T] = ra.showAs("as json or fail") 8 | def showAsJsonEither: ResponseAs[T] = ra.showAs("either(as json, as json)") 9 | def showAsJsonEitherOrFail: ResponseAs[T] = ra.showAs("either(as json, as json) or fail") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /json/common/src/test/scala/sttp/client4/json/RunResponseAs.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.json 2 | 3 | import sttp.client4.ResponseAs 4 | import sttp.client4.MappedResponseAs 5 | import sttp.client4.ResponseAsByteArray 6 | import sttp.client4.internal.Utf8 7 | import sttp.model.ResponseMetadata 8 | import sttp.model.StatusCode 9 | import org.scalatest.Assertions.fail 10 | 11 | object RunResponseAs { 12 | def apply[A]( 13 | responseAs: ResponseAs[A], 14 | responseMetadata: ResponseMetadata = ResponseMetadata(StatusCode.Ok, "", Nil) 15 | ): String => A = 16 | responseAs.delegate match { 17 | case responseAs: MappedResponseAs[_, A, Nothing] @unchecked => 18 | responseAs.raw match { 19 | case ResponseAsByteArray => 20 | s => responseAs.g(s.getBytes(Utf8), responseMetadata) 21 | case _ => 22 | fail("MappedResponseAs does not wrap a ResponseAsByteArray") 23 | } 24 | case _ => fail("ResponseAs is not a MappedResponseAs") 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /json/json4s/src/main/scala/sttp/client4/json4s/package.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | 3 | package object json4s extends SttpJson4sApi 4 | -------------------------------------------------------------------------------- /json/json4s/src/test/scala/sttp/client4/BackendStubJson4sTests.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | 3 | import org.json4s.native.Serialization 4 | import org.scalatest.concurrent.ScalaFutures 5 | import org.scalatest.flatspec.AnyFlatSpec 6 | import org.scalatest.matchers.should.Matchers 7 | import sttp.client4.testing.SyncBackendStub 8 | import sttp.model.Uri 9 | import org.json4s.{native, DefaultFormats} 10 | 11 | case class Person(name: String) 12 | 13 | class BackendStubJson4sTests extends AnyFlatSpec with Matchers with ScalaFutures { 14 | 15 | implicit val serialization: Serialization.type = native.Serialization 16 | implicit val formats: DefaultFormats.type = DefaultFormats 17 | 18 | import json4s._ 19 | 20 | it should "deserialize to json using a string stub" in { 21 | val backend = SyncBackendStub.whenAnyRequest.thenRespondAdjust("""{"name": "John"}""") 22 | val r = basicRequest.get(Uri("http://example.org")).response(asJson[Person]).send(backend) 23 | 24 | r.is200 should be(true) 25 | r.body should be(Right(Person("John"))) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /json/jsoniter/src/main/scala/sttp/client4/jsoniter/jsoniter.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | 3 | package object jsoniter extends SttpJsoniterJsonApi 4 | -------------------------------------------------------------------------------- /json/jsoniter/src/test/scala/sttp/client4/jsoniter/BackendStubJsoniterTests.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.jsoniter 2 | 3 | import org.scalatest.concurrent.ScalaFutures 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should.Matchers 6 | import sttp.client4.testing.SyncBackendStub 7 | import sttp.model.Uri 8 | import com.github.plokhotnyuk.jsoniter_scala.core.JsonValueCodec 9 | import com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker 10 | 11 | case class Person(name: String) 12 | 13 | object Person { 14 | implicit val personJsonValueCodec: JsonValueCodec[Person] = JsonCodecMaker.make 15 | } 16 | 17 | class BackendStubJsoniterTests extends AnyFlatSpec with Matchers with ScalaFutures { 18 | 19 | import sttp.client4.basicRequest 20 | 21 | it should "deserialize to json using a string stub" in { 22 | val backend = SyncBackendStub.whenAnyRequest.thenRespondAdjust("""{"name": "John"}""") 23 | val r = basicRequest.get(Uri("http://example.org")).response(asJson[Person]).send(backend) 24 | 25 | r.is200 should be(true) 26 | r.body should be(Right(Person("John"))) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /json/play-json/src/main/scala/sttp/client4/playJson/package.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | 3 | package object playJson extends SttpPlayJsonApi 4 | -------------------------------------------------------------------------------- /json/play-json/src/test/scala/sttp/client4/BackendStubPlayJsonTests.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | 3 | import org.scalatest.concurrent.ScalaFutures 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should.Matchers 6 | import sttp.client4.testing.SyncBackendStub 7 | import sttp.model.Uri 8 | import play.api.libs.json.Json 9 | import play.api.libs.json.OFormat 10 | import playJson._ 11 | 12 | case class Person(name: String) 13 | 14 | object Person { 15 | implicit val personFormat: OFormat[Person] = Json.format[Person] 16 | } 17 | 18 | class BackendStubPlayJsonTests extends AnyFlatSpec with Matchers with ScalaFutures { 19 | 20 | it should "deserialize to json using a string stub" in { 21 | val backend = SyncBackendStub.whenAnyRequest.thenRespondAdjust("""{"name": "John"}""") 22 | val r = basicRequest.get(Uri("http://example.org")).response(asJson[Person]).send(backend) 23 | 24 | r.is200 should be(true) 25 | r.body should be(Right(Person("John"))) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /json/spray-json/src/main/scala/sttp/client4/sprayJson/package.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | 3 | package object sprayJson extends SttpSprayJsonApi 4 | -------------------------------------------------------------------------------- /json/spray-json/src/test/scala/sttp/client4/sprayJson/BackendStubSprayJsonTests.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.sprayJson 2 | 3 | import org.scalatest.concurrent.ScalaFutures 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should.Matchers 6 | import sttp.client4.testing.SyncBackendStub 7 | import sttp.model.Uri 8 | import spray.json.DefaultJsonProtocol.{jsonFormat1, StringJsonFormat} 9 | import spray.json.RootJsonFormat 10 | import sttp.client4.basicRequest 11 | 12 | case class Person(name: String) 13 | 14 | object Person { 15 | implicit val personRootJsonFormat: RootJsonFormat[Person] = jsonFormat1(Person.apply) 16 | } 17 | 18 | class BackendStubSprayJsonTests extends AnyFlatSpec with Matchers with ScalaFutures { 19 | 20 | it should "deserialize to json using a string stub" in { 21 | val backend = SyncBackendStub.whenAnyRequest.thenRespondAdjust("""{"name": "John"}""") 22 | val r = basicRequest.get(Uri("http://example.org")).response(asJson[Person]).send(backend) 23 | 24 | r.is200 should be(true) 25 | r.body should be(Right(Person("John"))) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /json/tethys-json/src/main/scala/sttp/client4/tethysJson/package.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | 3 | package object tethysJson extends SttpTethysApi 4 | -------------------------------------------------------------------------------- /json/tethys-json/src/test/scala/sttp/client4/tethysJson/BackendStubTethysTests.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.tethysJson 2 | 3 | import org.scalatest.concurrent.ScalaFutures 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should.Matchers 6 | import sttp.client4._ 7 | import sttp.client4.testing.SyncBackendStub 8 | import tethys.derivation.semiauto.{jsonReader, jsonWriter} 9 | import tethys.jackson.jacksonTokenIteratorProducer 10 | import tethys.{JsonReader, JsonWriter} 11 | 12 | class BackendStubTethysTests extends AnyFlatSpec with Matchers with ScalaFutures { 13 | 14 | it should "deserialize to json using a string stub" in { 15 | val backend = SyncBackendStub.whenAnyRequest.thenRespondAdjust("""{"name": "John"}""") 16 | val r = basicRequest.get(uri"http://example.org").response(asJson[Person]).send(backend) 17 | r.is200 should be(true) 18 | r.body should be(Right(Person("John"))) 19 | } 20 | 21 | case class Person(name: String) 22 | 23 | object Person { 24 | implicit val encoder: JsonWriter[Person] = jsonWriter 25 | implicit val decoder: JsonReader[Person] = jsonReader 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /json/upickle/src/main/scala/sttp/client4/upicklejson/package.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | 3 | package object upicklejson { 4 | object default extends SttpUpickleApi { 5 | override val upickleApi: upickle.default.type = upickle.default 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /json/upickle/src/test/scala/sttp/client4/upicklejson/BackendStubUpickleTests.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.upicklejson 2 | 3 | import upickle.default._ 4 | import sttp.client4.upicklejson.default._ 5 | import org.scalatest.concurrent.ScalaFutures 6 | import sttp.client4.basicRequest 7 | import sttp.client4.testing.SyncBackendStub 8 | import sttp.model.Uri 9 | import org.scalatest.flatspec.AnyFlatSpec 10 | import org.scalatest.matchers.should.Matchers 11 | 12 | case class Person(name: String) 13 | object Person { 14 | implicit val personRW: ReadWriter[Person] = macroRW[Person] 15 | } 16 | 17 | class BackendStubUpickleTests extends AnyFlatSpec with Matchers with ScalaFutures { 18 | 19 | it should "deserialize to json using a string stub" in { 20 | val backend = SyncBackendStub.whenAnyRequest.thenRespondAdjust("""{"name": "John"}""") 21 | val r = basicRequest.get(Uri("http://example.org")).response(asJson[Person]).send(backend) 22 | r.is200 should be(true) 23 | r.body should be(Right(Person("John"))) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /json/zio-json/src/main/scala/sttp/client4/ziojson/ziojson.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | 3 | package object ziojson extends SttpZioJsonApi 4 | -------------------------------------------------------------------------------- /json/zio-json/src/main/scalajs/sttp/client4/ziojson/SttpZioJsonApiExtensions.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.ziojson 2 | 3 | trait SttpZioJsonApiExtensions {} 4 | -------------------------------------------------------------------------------- /json/zio-json/src/main/scalajvm/sttp/client4/ziojson/SttpZioJsonApiExtensions.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.ziojson 2 | 3 | import sttp.capabilities.Effect 4 | import sttp.capabilities.zio.ZioStreams 5 | import sttp.client4.ResponseException 6 | import sttp.client4.StreamResponseAs 7 | import sttp.client4.asStreamWithMetadata 8 | import zio.Task 9 | import zio.ZIO 10 | import zio.json.JsonDecoder 11 | import zio.stream.ZPipeline 12 | import sttp.client4.ResponseException.UnexpectedStatusCode 13 | import sttp.client4.ResponseException.DeserializationException 14 | 15 | trait SttpZioJsonApiExtensions { this: SttpZioJsonApi => 16 | def asJsonStream[B: JsonDecoder] 17 | : StreamResponseAs[Either[ResponseException[String], B], ZioStreams with Effect[Task]] = 18 | asStreamWithMetadata(ZioStreams)((s, meta) => 19 | JsonDecoder[B] 20 | .decodeJsonStream(ZPipeline.utf8Decode.apply(s).mapChunks(_.flatMap(_.toCharArray))) 21 | .map(Right(_)) 22 | .catchSome { case e: Exception => ZIO.left(DeserializationException("", e, meta)) } 23 | ).mapWithMetadata { 24 | case (Left(s), meta) => Left(UnexpectedStatusCode(s, meta)) 25 | case (Right(s), _) => s 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /json/zio-json/src/test/scala/sttp/client4/ziojson/BackendStubZioJsonTests.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.ziojson 2 | 3 | import org.scalatest.concurrent.ScalaFutures 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should.Matchers 6 | import sttp.client4.basicRequest 7 | import sttp.client4.testing.SyncBackendStub 8 | import sttp.model.Uri 9 | import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} 10 | 11 | case class Person(name: String) 12 | 13 | object Person { 14 | implicit val encoder: JsonEncoder[Person] = DeriveJsonEncoder.gen[Person] 15 | implicit val codec: JsonDecoder[Person] = DeriveJsonDecoder.gen[Person] 16 | } 17 | 18 | class BackendStubJson4sTests extends AnyFlatSpec with Matchers with ScalaFutures { 19 | 20 | it should "deserialize to json using a string stub" in { 21 | val backend = SyncBackendStub.whenAnyRequest.thenRespondAdjust("""{"name": "John"}""") 22 | val r = basicRequest.get(Uri("http://example.org")).response(asJson[Person]).send(backend) 23 | 24 | r.is200 should be(true) 25 | r.body should be(Right(Person("John"))) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /json/zio1-json/src/main/scala/sttp/client4/ziojson/ziojson.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4 2 | 3 | package object ziojson extends SttpZioJsonApi 4 | -------------------------------------------------------------------------------- /json/zio1-json/src/main/scalajs/sttp/client4/ziojson/SttpZioJsonApiExtensions.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.ziojson 2 | 3 | trait SttpZioJsonApiExtensions {} 4 | -------------------------------------------------------------------------------- /json/zio1-json/src/main/scalajvm/sttp/client4/ziojson/SttpZioJsonApiExtensions.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.ziojson 2 | 3 | import sttp.capabilities.Effect 4 | import sttp.capabilities.zio.ZioStreams 5 | import sttp.client4.ResponseException 6 | import sttp.client4.StreamResponseAs 7 | import sttp.client4.asStreamWithMetadata 8 | import zio.RIO 9 | import zio.ZIO 10 | import zio.blocking.Blocking 11 | import zio.json.JsonDecoder 12 | import zio.stream.ZTransducer 13 | import sttp.client4.ResponseException.DeserializationException 14 | import sttp.client4.ResponseException.UnexpectedStatusCode 15 | 16 | trait SttpZioJsonApiExtensions { this: SttpZioJsonApi => 17 | def asJsonStream[B: JsonDecoder] 18 | : StreamResponseAs[Either[ResponseException[String], B], ZioStreams with Effect[RIO[Blocking, *]]] = 19 | asStreamWithMetadata(ZioStreams)((s, meta) => 20 | JsonDecoder[B] 21 | .decodeJsonStream(s >>> ZTransducer.utf8Decode.mapChunks(_.flatMap(_.toCharArray))) 22 | .map(Right(_)) 23 | .catchSome { case e: Exception => ZIO.left(DeserializationException("", e, meta)) } 24 | ).mapWithMetadata { 25 | case (Left(s), meta) => Left(UnexpectedStatusCode(s, meta)) 26 | case (Right(s), _) => s 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /json/zio1-json/src/test/scala/sttp/client4/ziojson/BackendStubZioJsonTests.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.ziojson 2 | 3 | import org.scalatest.concurrent.ScalaFutures 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should.Matchers 6 | import sttp.client4.testing.SyncBackendStub 7 | import sttp.model.Uri 8 | import sttp.client4.basicRequest 9 | import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} 10 | 11 | case class Person(name: String) 12 | 13 | object Person { 14 | implicit val encoder: JsonEncoder[Person] = DeriveJsonEncoder.gen[Person] 15 | implicit val codec: JsonDecoder[Person] = DeriveJsonDecoder.gen[Person] 16 | } 17 | 18 | class BackendStubJson4sTests extends AnyFlatSpec with Matchers with ScalaFutures { 19 | 20 | it should "deserialize to json using a string stub" in { 21 | val backend = SyncBackendStub.whenAnyRequest.thenRespondAdjust("""{"name": "John"}""") 22 | val r = basicRequest.get(Uri("http://example.org")).response(asJson[Person]).send(backend) 23 | 24 | r.is200 should be(true) 25 | r.body should be(Right(Person("John"))) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /logging/scribe/src/main/scala/sttp/client4/logging/scribe/ScribeLogger.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.logging.scribe 2 | 3 | import scribe.data 4 | import scribe.mdc.MDC 5 | import sttp.client4.logging.{LogLevel, Logger} 6 | import sttp.monad.MonadError 7 | 8 | case class ScribeLogger[F[_]](monad: MonadError[F]) extends Logger[F] { 9 | private val levelMap: Map[LogLevel, scribe.Level] = Map( 10 | LogLevel.Trace -> scribe.Level.Trace, 11 | LogLevel.Debug -> scribe.Level.Debug, 12 | LogLevel.Info -> scribe.Level.Info, 13 | LogLevel.Warn -> scribe.Level.Warn, 14 | LogLevel.Error -> scribe.Level.Error 15 | ) 16 | 17 | override def apply( 18 | level: LogLevel, 19 | message: => String, 20 | throwable: Option[Throwable], 21 | context: Map[String, Any] 22 | ): F[Unit] = 23 | throwable match { 24 | case Some(t) => monad.eval(scribe.log(levelMap(level), MDC.global, message, data(context), t)) 25 | case None => monad.eval(scribe.log(levelMap(level), MDC.global, message, data(context))) 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /observability/otel4s-metrics-backend/src/main/scala/sttp/client4/opentelemetry/otel4s/Otel4sMetricsConfig.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.opentelemetry.otel4s 2 | 3 | import org.typelevel.otel4s.metrics.BucketBoundaries 4 | 5 | final case class Otel4sMetricsConfig( 6 | requestDurationHistogramBuckets: BucketBoundaries, 7 | requestBodySizeHistogramBuckets: Option[BucketBoundaries], 8 | responseBodySizeHistogramBuckets: Option[BucketBoundaries] 9 | ) 10 | 11 | object Otel4sMetricsConfig { 12 | val DefaultDurationBuckets: BucketBoundaries = BucketBoundaries( 13 | 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10 14 | ) 15 | 16 | val default: Otel4sMetricsConfig = Otel4sMetricsConfig( 17 | requestDurationHistogramBuckets = DefaultDurationBuckets, 18 | requestBodySizeHistogramBuckets = None, 19 | responseBodySizeHistogramBuckets = None 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /observability/otel4s-tracing-backend/src/main/scala/sttp/client4/opentelemetry/otel4s/Otel4sTracingConfig.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.opentelemetry.otel4s 2 | 3 | import org.typelevel.otel4s.Attributes 4 | import sttp.client4.{GenericRequest, Response} 5 | 6 | final case class Otel4sTracingConfig( 7 | spanName: GenericRequest[_, _] => String, 8 | requestAttributes: GenericRequest[_, _] => Attributes, 9 | responseAttributes: Response[_] => Attributes, 10 | errorAttributes: Throwable => Attributes 11 | ) 12 | 13 | object Otel4sTracingConfig { 14 | val default: Otel4sTracingConfig = Otel4sTracingConfig( 15 | request => Otel4sTracingDefaults.spanName(request), 16 | request => Otel4sTracingDefaults.requestAttributes(request), 17 | response => Otel4sTracingDefaults.responseAttributes(response), 18 | error => Otel4sTracingDefaults.errorAttributes(error) 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /okhttp-backend/monix/src/test/scala/sttp/client4/okhttp/monix/OkHttpMonixHttpTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.okhttp.monix 2 | 3 | import monix.eval.Task 4 | import monix.execution.Scheduler.Implicits.global 5 | import sttp.capabilities.monix.MonixStreams 6 | import sttp.client4.StreamBackend 7 | import sttp.client4.impl.monix.convertMonixTaskToFuture 8 | import sttp.client4.okhttp.OkHttpHttpTest 9 | import sttp.client4.testing.ConvertToFuture 10 | 11 | import java.util.concurrent.TimeoutException 12 | import scala.concurrent.duration._ 13 | 14 | class OkHttpMonixHttpTest extends OkHttpHttpTest[Task] { 15 | 16 | override val backend: StreamBackend[Task, MonixStreams] = OkHttpMonixBackend().runSyncUnsafe() 17 | override implicit val convertToFuture: ConvertToFuture[Task] = convertMonixTaskToFuture 18 | 19 | override def timeoutToNone[T](t: Task[T], timeoutMillis: Int): Task[Option[T]] = 20 | t.map(Some(_)) 21 | .timeout(timeoutMillis.milliseconds) 22 | .onErrorRecover { case _: TimeoutException => 23 | None 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /okhttp-backend/monix/src/test/scala/sttp/client4/okhttp/monix/OkHttpMonixStreamingTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.okhttp.monix 2 | 3 | import monix.eval.Task 4 | import monix.execution.Scheduler.Implicits.global 5 | import sttp.capabilities.monix.MonixStreams 6 | import sttp.client4.StreamBackend 7 | import sttp.client4.impl.monix.MonixStreamingTest 8 | 9 | class OkHttpMonixStreamingTest extends MonixStreamingTest { 10 | override val backend: StreamBackend[Task, MonixStreams] = 11 | OkHttpMonixBackend().runSyncUnsafe() 12 | } 13 | -------------------------------------------------------------------------------- /okhttp-backend/src/main/scala/sttp/client4/okhttp/quick.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.okhttp 2 | 3 | import sttp.client4._ 4 | 5 | object quick extends SttpApi { 6 | lazy val backend: WebSocketSyncBackend = OkHttpSyncBackend() 7 | } 8 | -------------------------------------------------------------------------------- /okhttp-backend/src/test/scala/sttp/client4/okhttp/OkHttpFutureHttpTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.okhttp 2 | 3 | import sttp.client4.WebSocketBackend 4 | import sttp.client4.testing.{ConvertToFuture, HttpTest} 5 | 6 | import scala.concurrent.Future 7 | 8 | class OkHttpFutureHttpTest extends OkHttpHttpTest[Future] { 9 | 10 | override val backend: WebSocketBackend[Future] = OkHttpFutureBackend() 11 | override implicit val convertToFuture: ConvertToFuture[Future] = ConvertToFuture.future 12 | 13 | override def supportsCancellation: Boolean = false 14 | override def timeoutToNone[T](t: Future[T], timeoutMillis: Int): Future[Option[T]] = t.map(Some(_)) 15 | } 16 | -------------------------------------------------------------------------------- /okhttp-backend/src/test/scala/sttp/client4/okhttp/OkHttpHttpTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.okhttp 2 | 3 | import sttp.client4.testing.HttpTest 4 | 5 | abstract class OkHttpHttpTest[F[_]] extends HttpTest[F] { 6 | override protected def supportsDeflateWrapperChecking = false 7 | override protected def supportsNonAsciiHeaderValues = false 8 | } 9 | -------------------------------------------------------------------------------- /okhttp-backend/src/test/scala/sttp/client4/okhttp/OkHttpSyncDigestAuthProxyManualTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.okhttp 2 | 3 | import org.scalatest.Ignore 4 | import sttp.client4._ 5 | import sttp.client4.testing.{ConvertToFuture, ToFutureWrapper} 6 | import org.scalatest.freespec.AsyncFreeSpec 7 | import org.scalatest.matchers.should.Matchers 8 | import sttp.client4.wrappers.DigestAuthenticationBackend 9 | import sttp.shared.Identity 10 | 11 | @Ignore 12 | class OkHttpSyncDigestAuthProxyManualTest extends AsyncFreeSpec with Matchers with ToFutureWrapper { 13 | val backend: WebSocketBackend[Identity] = 14 | DigestAuthenticationBackend(OkHttpSyncBackend(options = BackendOptions.httpProxy("localhost", 3128))) 15 | 16 | implicit val convertToFuture: ConvertToFuture[Identity] = ConvertToFuture.id 17 | 18 | "complex proxy auth with digest" in { 19 | val response = basicRequest 20 | .get(uri"http://httpbin.org/digest-auth/auth/andrzej/test/SHA-512") 21 | .auth 22 | .digest("andrzej", "test") 23 | .proxyAuth 24 | .digest("kasper", "qweqwe") 25 | .send(backend) 26 | response.code.code shouldBe 200 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /okhttp-backend/src/test/scala/sttp/client4/okhttp/OkHttpSyncHttpTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.okhttp 2 | 3 | import sttp.client4.testing.ConvertToFuture 4 | import sttp.client4.WebSocketBackend 5 | import sttp.shared.Identity 6 | 7 | class OkHttpSyncHttpTest extends OkHttpHttpTest[Identity] { 8 | override val backend: WebSocketBackend[Identity] = OkHttpSyncBackend() 9 | 10 | override implicit val convertToFuture: ConvertToFuture[Identity] = ConvertToFuture.id 11 | 12 | override def supportsCancellation: Boolean = false 13 | override def timeoutToNone[T](t: Identity[T], timeoutMillis: Int): Identity[Option[T]] = Some(t) 14 | } 15 | -------------------------------------------------------------------------------- /pekko-http-backend/src/main/scala/sttp/client4/pekkohttp/PekkoHttpServerSentEvents.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.pekkohttp 2 | 3 | import org.apache.pekko 4 | import pekko.NotUsed 5 | import pekko.stream.scaladsl.{Flow, Framing} 6 | import pekko.util.ByteString 7 | import sttp.model.sse.ServerSentEvent 8 | 9 | object PekkoHttpServerSentEvents { 10 | val parse: Flow[ByteString, ServerSentEvent, NotUsed] = 11 | Framing 12 | .delimiter(ByteString("\n\n"), maximumFrameLength = Int.MaxValue, allowTruncation = true) 13 | .map(_.utf8String) 14 | .map(_.split("\n").toList) 15 | .map(ServerSentEvent.parse) 16 | } 17 | -------------------------------------------------------------------------------- /pekko-http-backend/src/main/scala/sttp/client4/pekkohttp/pekkoDecompressors.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.pekkohttp 2 | 3 | import sttp.client4.compression.Decompressor 4 | import sttp.model.Encodings 5 | import org.apache.pekko.http.scaladsl.model.HttpResponse 6 | import org.apache.pekko.http.scaladsl.coding.Coders 7 | 8 | object GZipPekkoDecompressor extends Decompressor[HttpResponse] { 9 | override val encoding: String = Encodings.Gzip 10 | override def apply(body: HttpResponse): HttpResponse = Coders.Gzip.decodeMessage(body) 11 | } 12 | 13 | object DeflatePekkoDecompressor extends Decompressor[HttpResponse] { 14 | override val encoding: String = Encodings.Deflate 15 | override def apply(body: HttpResponse): HttpResponse = Coders.Deflate.decodeMessage(body) 16 | } 17 | -------------------------------------------------------------------------------- /pekko-http-backend/src/test/scala/sttp/client4/pekkohttp/PekkoHttpClientHttpTest.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.pekkohttp 2 | 3 | import sttp.client4.Backend 4 | import sttp.client4.testing.{ConvertToFuture, HttpTest} 5 | 6 | import scala.concurrent.Future 7 | 8 | class PekkoHttpClientHttpTest extends HttpTest[Future] { 9 | override val backend: Backend[Future] = PekkoHttpBackend() 10 | override implicit val convertToFuture: ConvertToFuture[Future] = ConvertToFuture.future 11 | 12 | override def supportsCancellation: Boolean = false 13 | override def supportsResponseAsInputStream: Boolean = false 14 | override def timeoutToNone[T](t: Future[T], timeoutMillis: Int): Future[Option[T]] = t.map(Some(_)) 15 | } 16 | -------------------------------------------------------------------------------- /project/FileUtils.scala: -------------------------------------------------------------------------------- 1 | import java.io.File 2 | import java.nio.file.{FileVisitResult, Files, Path, SimpleFileVisitor} 3 | import java.nio.file.attribute.BasicFileAttributes 4 | 5 | object FileUtils { 6 | def listScalaFiles(basePath: File): Seq[Path] = { 7 | val dirPath = basePath.toPath 8 | var result = Vector.empty[Path] 9 | 10 | val fileVisitor = new SimpleFileVisitor[Path] { 11 | override def visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult = { 12 | if (file.toString.endsWith(".scala")) { 13 | result = result :+ file 14 | } 15 | FileVisitResult.CONTINUE 16 | } 17 | } 18 | 19 | Files.walkFileTree(dirPath, fileVisitor) 20 | result 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /project/PollingUtils.scala: -------------------------------------------------------------------------------- 1 | import java.io.FileNotFoundException 2 | import java.net.{ConnectException, URL} 3 | 4 | import scala.concurrent.TimeoutException 5 | import scala.concurrent.duration._ 6 | 7 | object PollingUtils { 8 | 9 | def waitUntilServerAvailable(url: URL): Unit = { 10 | val connected = poll(5.seconds, 250.milliseconds) { 11 | urlConnectionAvailable(url) 12 | } 13 | if (!connected) { 14 | throw new TimeoutException(s"Failed to connect to $url") 15 | } 16 | } 17 | 18 | def poll(timeout: FiniteDuration, interval: FiniteDuration)(poll: => Boolean): Boolean = { 19 | val start = System.nanoTime() 20 | 21 | def go(): Boolean = 22 | if (poll) { 23 | true 24 | } else if ((System.nanoTime() - start) > timeout.toNanos) { 25 | false 26 | } else { 27 | Thread.sleep(interval.toMillis) 28 | go() 29 | } 30 | go() 31 | } 32 | 33 | def urlConnectionAvailable(url: URL): Boolean = 34 | try { 35 | url 36 | .openConnection() 37 | .getInputStream 38 | .close() 39 | true 40 | } catch { 41 | case _: ConnectException => false 42 | case _: FileNotFoundException => true // on 404 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /project/VerifyExamplesCompileUsingScalaCli.scala: -------------------------------------------------------------------------------- 1 | import java.io.File 2 | import sbt.Logger 3 | import scala.sys.process.{Process, ProcessLogger} 4 | 5 | object VerifyExamplesCompileUsingScalaCli { 6 | def apply(log: Logger, examplesSrcPath: File): Unit = { 7 | val examples = FileUtils.listScalaFiles(examplesSrcPath) 8 | log.info(s"Found ${examples.size} examples") 9 | 10 | for (example <- examples) { 11 | log.info(s"Compiling: $example") 12 | val errorOutput = new StringBuilder 13 | val logger = ProcessLogger((o: String) => (), (e: String) => errorOutput.append(e + "\n")) 14 | try { 15 | val result = Process(List("scala-cli", "compile", example.toFile.getAbsolutePath), examplesSrcPath).!(logger) 16 | if (result != 0) { 17 | throw new Exception(s"""Compiling $example failed.\n$errorOutput""".stripMargin) 18 | } 19 | } finally { 20 | Process(List("scala-cli", "clean", example.toFile.getAbsolutePath), examplesSrcPath).! 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.11.1 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.19.0") 2 | addSbtPlugin("org.scala-js" % "sbt-jsdependencies" % "1.0.2") 3 | addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.7") 4 | addSbtPlugin("io.spray" % "sbt-revolver" % "0.10.0") 5 | addSbtPlugin("com.eed3si9n" % "sbt-projectmatrix" % "0.11.0") 6 | 7 | val sbtSoftwareMillVersion = "2.0.25" 8 | addSbtPlugin("com.softwaremill.sbt-softwaremill" % "sbt-softwaremill-common" % sbtSoftwareMillVersion) 9 | addSbtPlugin("com.softwaremill.sbt-softwaremill" % "sbt-softwaremill-publish" % sbtSoftwareMillVersion) 10 | addSbtPlugin("com.softwaremill.sbt-softwaremill" % "sbt-softwaremill-browser-test-js" % sbtSoftwareMillVersion) 11 | 12 | addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.7.1") 13 | 14 | addSbtPlugin("org.jetbrains.scala" % "sbt-ide-settings" % "1.1.2") 15 | addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.4") 16 | -------------------------------------------------------------------------------- /testing/compile/src/test/scala/sttp/client4/testing/compile/EvalScala.scala: -------------------------------------------------------------------------------- 1 | package sttp.client4.testing.compile 2 | 3 | object EvalScala { 4 | import scala.tools.reflect.ToolBox 5 | 6 | def apply(code: String): Any = { 7 | val m = scala.reflect.runtime.currentMirror 8 | val tb = m.mkToolBox() 9 | tb.eval(tb.parse(code)) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /testing/server/src/main/resources/binaryfile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/softwaremill/sttp/89790e72bbc3b5d5042f33fac49e15f006cd9978/testing/server/src/main/resources/binaryfile.jpg -------------------------------------------------------------------------------- /testing/server/src/main/resources/r3.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/softwaremill/sttp/89790e72bbc3b5d5042f33fac49e15f006cd9978/testing/server/src/main/resources/r3.gz -------------------------------------------------------------------------------- /testing/server/src/main/scala/akka/http/scaladsl/coding/DeflateNoWrap.scala: -------------------------------------------------------------------------------- 1 | package akka.http.scaladsl.coding 2 | 3 | import akka.http.scaladsl.model._ 4 | import java.util.zip.Deflater 5 | 6 | /** [[Deflate]] but sets the `nowrap` flag to true instead of false. 7 | */ 8 | class DeflateNoWrap private[http] ( 9 | compressionLevel: Int, 10 | override val messageFilter: HttpMessage => Boolean 11 | ) extends Deflate(compressionLevel, messageFilter) { 12 | def this(messageFilter: HttpMessage => Boolean) = 13 | this(DeflateCompressor.DefaultCompressionLevel, messageFilter) 14 | 15 | override def newCompressor = new DeflateCompressor(compressionLevel) { 16 | override protected lazy val deflater = new Deflater(compressionLevel, true) 17 | } 18 | } 19 | object DeflateNoWrap extends DeflateNoWrap(Encoder.DefaultFilter) 20 | --------------------------------------------------------------------------------