Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .scala-steward.conf
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ updates.ignore = [
updates.pin = [
{groupId = "com.typesafe.akka", version = "2.6."},
{groupId = "org.slf4j", artifactId = "slf4j-api", version = "1."},
{groupId = "org.scala-lang", artifactId = "scala3-library", version = "3.3."},
{groupId = "org.scala-lang", artifactId = "scala3-library_sjs1", version = "3.3."},
{groupId = "org.scala-lang", artifactId = "scala3-library_sjs1_3", version = "3.3."}
{groupId = "org.scala-lang", artifactId = "scala3-library", version = "3.9."},
{groupId = "org.scala-lang", artifactId = "scala3-library_sjs1", version = "3.9."},
{groupId = "org.scala-lang", artifactId = "scala3-library_sjs1_3", version = "3.9."}
]
2 changes: 1 addition & 1 deletion .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
version = 3.11.5
maxColumn = 120
runner.dialect = scala213
runner.dialect = scala213source3
fileOverride {
"glob:**/scala-3/**" {
runner.dialect = scala3
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

sttp client is an open-source HTTP client for Scala, supporting various approaches to writing Scala code: synchronous (direct-style), `Future`-based, and using functional effect systems (cats-effect, ZIO, Monix, Kyo, scalaz).

The library is available for Scala 2.12, 2.13 and 3. Supported platforms are the JVM (Java 11+), Scala.JS and Scala Native.
The library is available for Scala 2.12, 2.13 and 3. Supported platforms are the JVM (Java 11+ for Scala 2, Java 17+ for Scala 3), Scala.JS and Scala Native.

Here's a quick example of sttp client in action, runnable using [scala-cli](https://scala-cli.virtuslab.org):

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ abstract class AbstractArmeriaBackend[F[_], S <: Streams[S]](

val streams: Streams[S]

type R = S with Effect[F]
type R = S & Effect[F]

protected def bodyFromStreamMessage: BodyFromStreamMessage[F, S]

Expand Down Expand Up @@ -108,7 +108,7 @@ abstract class AbstractArmeriaBackend[F[_], S <: Streams[S]](
} finally captor.close()
}

private def requestToArmeria(request: GenericRequest[_, R]): WebClientRequestPreparation = {
private def requestToArmeria(request: GenericRequest[?, R]): WebClientRequestPreparation = {
val requestPreparation = client
.prepare()
.disablePathParams()
Expand Down Expand Up @@ -159,7 +159,7 @@ abstract class AbstractArmeriaBackend[F[_], S <: Streams[S]](
case ByteBufferBody(b, _) =>
requestPreparation.content(contentType, HttpData.wrap(Unpooled.wrappedBuffer(b)))
case multipart: MultipartBody[_] =>
val armeriaMultipart = Multipart.of(multipart.parts.map(toArmeriaBodyPart): _*)
val armeriaMultipart = Multipart.of(multipart.parts.map(toArmeriaBodyPart)*)
requestPreparation.content(
contentType.withParameter("boundary", armeriaMultipart.boundary()),
armeriaMultipart.toStreamMessage
Expand All @@ -186,7 +186,7 @@ abstract class AbstractArmeriaBackend[F[_], S <: Streams[S]](
case _ => HttpMethod.UNKNOWN
}

private def toArmeriaBodyPart(bodyPart: Part[BodyPart[_]]): ArmeriaBodyPart = {
private def toArmeriaBodyPart(bodyPart: Part[BodyPart[?]]): ArmeriaBodyPart = {
val dispositionBuilder = ContentDisposition.builder("form-data")
dispositionBuilder.name(bodyPart.name)
bodyPart.fileName.foreach(dispositionBuilder.filename)
Expand Down Expand Up @@ -219,7 +219,7 @@ abstract class AbstractArmeriaBackend[F[_], S <: Streams[S]](
}).build()
}

private def adjustExceptions[T](request: GenericRequest[_, _])(execute: => F[T]): F[T] =
private def adjustExceptions[T](request: GenericRequest[?, ?])(execute: => F[T]): F[T] =
SttpClientException.adjustExceptions(monad)(execute) {
case ex: UnprocessedRequestException =>
// The cause of an UnprocessedRequestException is always not null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ object ArmeriaWebClient {
.build()

/** Create a new [[WebClient]] which is adjusted for sttp client's needs. */
def newClient(): WebClient = newClient(identity[WebClientBuilder] _)
def newClient(): WebClient = newClient(identity[WebClientBuilder](_))

/** Create a new [[WebClient]] which is adjusted for sttp client's needs. */
def newClient(customizeWebClient: WebClientBuilder => WebClientBuilder): WebClient =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ private[armeria] trait BodyFromStreamMessage[F[_], S] {
}

def bytesToPublisher(b: Array[Byte]): F[StreamMessage[HttpData]] =
StreamMessage.of(Array(HttpData.wrap(b)): _*).unit
StreamMessage.of(Array(HttpData.wrap(b))*).unit

def pathToPublisher(f: Path): F[StreamMessage[HttpData]] =
(StreamMessage.of(f): StreamMessage[HttpData]).unit
Expand Down Expand Up @@ -109,7 +109,7 @@ private[armeria] trait BodyFromStreamMessage[F[_], S] {
(publisherToStream(response), () => monad.eval(response.abort())).unit

override protected def handleWS[T](
responseAs: GenericWebSocketResponseAs[T, _],
responseAs: GenericWebSocketResponseAs[T, ?],
meta: ResponseMetadata,
ws: Nothing
): F[T] = ws
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import scala.collection.mutable
private final class StreamMessageAggregator extends Subscriber[HttpData] {
private val contentList: mutable.Buffer[HttpData] = new mutable.ArrayBuffer()
private var contentLength: Int = 0
private var subscription: Subscription = _
private var subscription: Subscription = null

val future: CompletableFuture[HttpData] = new CompletableFuture()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,17 @@ package object zio {
* [[Request.response]]). Or a failed effect, if an exception occurred when connecting to the target host, writing
* the request or reading the response.
*/
def send[T, C >: ZioStreams with Effect[Task]](
def send[T, C >: ZioStreams & Effect[Task]](
request: StreamRequest[T, C]
): ZIO[SttpClient, Throwable, Response[T]] =
ZIO.serviceWithZIO[SttpClient](request.send[Task, ZioStreams])

/** A variant of [[send]] which allows the effects that are part of the response handling specification (when using
* resource-safe streaming) to use an `R` environment.
*/
def sendR[T, C >: ZioStreams with Effect[RIO[R, *]], R](
def sendR[T, C >: ZioStreams & Effect[RIO[R, *]], R](
request: StreamRequest[T, C]
): ZIO[SttpClient with R, Throwable, Response[T]] = {
): ZIO[SttpClient & R, Throwable, Response[T]] = {
import sttp.client4.impl.zio.StreamBackendExtendEnv
ZIO.serviceWithZIO[SttpClient](b => request.send[RIO[R, *], ZioStreams](b.extendEnv[R]))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class ArmeriaZioStreamingTest extends StreamingTest[Task, ZioStreams] with ZioTe
override implicit val convertToFuture: ConvertToFuture[Task] = convertZioTaskToFuture

override def bodyProducer(arrays: Iterable[Array[Byte]]): Stream[Throwable, Byte] =
ZStream.fromChunks(arrays.map(Chunk.fromArray).toSeq: _*)
ZStream.fromChunks(arrays.map(Chunk.fromArray).toSeq*)

override def bodyConsumer(stream: Stream[Throwable, Byte]): Task[String] =
stream.runCollect.map(bytes => new String(bytes.toArray, Utf8))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,16 @@ package object zio {
* Known exceptions are converted to one of `SttpClientException`. Other exceptions are kept unchanged.
*/
def send[T](
request: GenericRequest[T, Effect[Task] with ZioStreams]
request: GenericRequest[T, Effect[Task] & ZioStreams]
): ZIO[SttpClient, Throwable, Response[T]] =
ZIO.accessM(env => env.get[SttpClient.Service].send(request))

/** A variant of `send` which allows the effects that are part of the response handling specification (when using
* websockets or resource-safe streaming) to use an `R` environment.
*/
def sendR[T, R](
request: GenericRequest[T, Effect[RIO[R, *]] with ZioStreams]
): ZIO[SttpClient with R, Throwable, Response[T]] =
request: GenericRequest[T, Effect[RIO[R, *]] & ZioStreams]
): ZIO[SttpClient & R, Throwable, Response[T]] =
ZIO.accessM(env => env.get[SttpClient.Service].extendEnv[R].send(request))

object SttpClientStubbing extends StreamClientStubbing[Any, ZioStreams] {
Expand All @@ -53,14 +53,14 @@ package object zio {
object stubbing {
import SttpClientStubbing.StubbingWhenRequest

def whenRequestMatches(p: GenericRequest[_, _] => Boolean): StubbingWhenRequest =
def whenRequestMatches(p: GenericRequest[?, ?] => Boolean): StubbingWhenRequest =
StubbingWhenRequest(p)

val whenAnyRequest: StubbingWhenRequest =
StubbingWhenRequest(_ => true)

def whenRequestMatchesPartial(
partial: PartialFunction[GenericRequest[_, _], Response[StubBody]]
partial: PartialFunction[GenericRequest[?, ?], Response[StubBody]]
): URIO[SttpClientStubbing, Unit] =
ZIO.accessM(_.get.whenRequestMatchesPartial(partial))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class ArmeriaZioStreamingTest extends StreamingTest[Task, ZioStreams] with ZioTe
override implicit val convertToFuture: ConvertToFuture[Task] = convertZioTaskToFuture

override def bodyProducer(arrays: Iterable[Array[Byte]]): Stream[Throwable, Byte] =
Stream.fromChunks(arrays.map(Chunk.fromArray).toSeq: _*)
Stream.fromChunks(arrays.map(Chunk.fromArray).toSeq*)

override def bodyConsumer(stream: Stream[Throwable, Byte]): Task[String] =
stream.runCollect.map(bytes => new String(bytes.toArray, Utf8))
Expand Down
28 changes: 23 additions & 5 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import com.softwaremill.SbtSoftwareMillBrowserTestJS._

val scala2_12 = "2.12.21"
val scala2_13 = "2.13.18"
val scala3 = "3.3.8"
val scala3 = "3.9.0"

val scala2 = List(scala2_12, scala2_13)
val scala2And3 = scala2 ++ List(scala3)
Expand Down Expand Up @@ -48,14 +48,30 @@ ideSkipProject := (scalaVersion.value != ideScalaVersion) ||
bspEnabled := !ideSkipProject.value
mimaPreviousArtifacts := Set.empty // we only use MiMa for `core` for now, using enableMimaSettings

val commonJvmSettings = Seq(
val commonSettings = Seq(
scalacOptions ++= {
if (ScalaArtifacts.isScala3(scalaVersion.value)) Nil
else if (scalaVersion.value.startsWith("2.12")) Seq("-Xsource:3")
else Seq("-Xsource:3", "-Wconf:cat=scala3-migration:w")
}
)

// scalac 2.12 overflows the stack when compiling the http4s backend tests with -Xsource:3
val http4sTestSettings = Seq(
Test / scalacOptions := {
val options = (Test / scalacOptions).value
if (scalaVersion.value.startsWith("2.12")) options.filterNot(_ == "-Xsource:3") else options
}
)

val commonJvmSettings = commonSettings ++ Seq(
scalacOptions ++=
(if (ScalaArtifacts.isScala3(scalaVersion.value)) Seq("-Yfuture-lazy-vals", "-java-output-version", "11")
(if (ScalaArtifacts.isScala3(scalaVersion.value)) Seq("-java-output-version", "17")
else Seq("-release", "11")),
Test / testOptions += Tests.Argument("-oD") // add test timings; js build specify other options which conflict
)

val commonJsSettings = Seq(
val commonJsSettings = commonSettings ++ Seq(
scalaJSLinkerConfig ~= {
_.withBatchMode(true).withParallel(false)
},
Expand Down Expand Up @@ -85,7 +101,7 @@ val commonJsBackendSettings = List(
)
)

val commonNativeSettings: Seq[Def.Setting[?]] = Seq.empty
val commonNativeSettings: Seq[Def.Setting[?]] = commonSettings

val versioningSchemeSettings = Seq(versionScheme := Some("early-semver"))

Expand Down Expand Up @@ -668,6 +684,7 @@ lazy val okhttpMonixBackend =
lazy val http4sCe2Backend = (projectMatrix in file("http4s-ce2-backend"))
.settings(commonJvmSettings)
.settings(testServerSettings)
.settings(http4sTestSettings)
.settings(
name := "http4s-ce2-backend",
libraryDependencies ++= Seq(
Expand All @@ -680,6 +697,7 @@ lazy val http4sCe2Backend = (projectMatrix in file("http4s-ce2-backend"))

lazy val http4sBackend = (projectMatrix in file("http4s-backend"))
.settings(testServerSettings)
.settings(http4sTestSettings)
.settings(
name := "http4s-backend",
libraryDependencies ++= Seq(
Expand Down
14 changes: 7 additions & 7 deletions caching/src/main/scala/sttp/client4/caching/CachingBackend.scala
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class CachingBackend[F[_], P](delegate: GenericBackend[F, P], cache: Cache[F], c

import sttp.monad.syntax._

override def send[T](request: GenericRequest[T, P with Effect[F]]): F[Response[T]] = {
override def send[T](request: GenericRequest[T, P & Effect[F]]): F[Response[T]] = {
val cacheableFromConfig = config.eligibleForCaching(request)

// Only requests with "cache-friendly" response-as descriptions can be cached, so that we can convert a cached
Expand Down Expand Up @@ -70,7 +70,7 @@ class CachingBackend[F[_], P](delegate: GenericBackend[F, P], cache: Cache[F], c

override def close(): F[Unit] = super.close().ensure(cache.close())

private def sendNotInCache[T](request: GenericRequest[T, P with Effect[F]], key: Array[Byte]): F[Response[T]] = {
private def sendNotInCache[T](request: GenericRequest[T, P & Effect[F]], key: Array[Byte]): F[Response[T]] = {
// Replacing the original response as with a byte array; we know that response-as is cache-friendly, so we'll be
// able to obtain a T-body later.
val byteArrayRequest = requestWithResponseAsByteArray(request)
Expand All @@ -93,14 +93,14 @@ class CachingBackend[F[_], P](delegate: GenericBackend[F, P], cache: Cache[F], c

private def adjustResponseReadFromCache[T](
responseFromCache: Response[Array[Byte]],
request: GenericRequest[T, _]
request: GenericRequest[T, ?]
): Response[T] = {
// We assume that it has been verified that responseAs is cache-friendly, so this won't throw an UOE.
val body: T = runResponseAs(request.response.delegate, responseFromCache.body, responseFromCache)
responseFromCache.copy(body = body)
}

private def responseAsCacheFriendly(responseAs: GenericResponseAs[_, _]): Boolean =
private def responseAsCacheFriendly(responseAs: GenericResponseAs[?, ?]): Boolean =
responseAs match {
case IgnoreResponse => true
case ResponseAsByteArray => true
Expand All @@ -119,7 +119,7 @@ class CachingBackend[F[_], P](delegate: GenericBackend[F, P], cache: Cache[F], c
}

private def runResponseAs[T](
responseAs: GenericResponseAs[T, _],
responseAs: GenericResponseAs[T, ?],
data: Array[Byte],
responseMetadata: ResponseMetadata
): T =
Expand All @@ -146,8 +146,8 @@ class CachingBackend[F[_], P](delegate: GenericBackend[F, P], cache: Cache[F], c
}

private def requestWithResponseAsByteArray[T](
request: GenericRequest[T, P with Effect[F]]
): GenericRequest[Array[Byte], P with Effect[F]] =
request: GenericRequest[T, P & Effect[F]]
): GenericRequest[Array[Byte], P & Effect[F]] =
request match {
case r: Request[T] @unchecked => r.response(asByteArrayAlways)
case _ => throw new IllegalStateException("WebSocket/streaming requests are not cacheable!")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class CachingBackendTest extends AnyFlatSpec with Matchers {
def timePassed(seconds: Int): Unit
}

def newInMemoryCache = new StubCache[Identity] {
def newInMemoryCache: StubCache[Identity] = new StubCache[Identity] {
var storage = new collection.mutable.HashMap[List[Byte], (Array[Byte], Long)]()
var now = 0L // how many seconds have passed till the dawn of time

Expand Down
23 changes: 11 additions & 12 deletions core/src/main/scala/sttp/client4/ResponseAs.scala
Original file line number Diff line number Diff line change
Expand Up @@ -264,8 +264,8 @@ case class StreamResponseAs[+T, S](delegate: GenericResponseAs[T, S]) extends Re
* @see
* [[ResponseAs]]
*/
case class WebSocketResponseAs[F[_], +T](delegate: GenericResponseAs[T, Effect[F] with WebSockets])
extends ResponseAsDelegate[T, Effect[F] with WebSockets] {
case class WebSocketResponseAs[F[_], +T](delegate: GenericResponseAs[T, Effect[F] & WebSockets])
extends ResponseAsDelegate[T, Effect[F] & WebSockets] {

/** Applies the given function `f` to the deserialized value `T`. */
def map[T2](f: T => T2): WebSocketResponseAs[F, T2] =
Expand Down Expand Up @@ -305,8 +305,8 @@ case class WebSocketResponseAs[F[_], +T](delegate: GenericResponseAs[T, Effect[F
* @see
* [[ResponseAs]]
*/
case class WebSocketStreamResponseAs[+T, S](delegate: GenericResponseAs[T, S with WebSockets])
extends ResponseAsDelegate[T, S with WebSockets] {
case class WebSocketStreamResponseAs[+T, S](delegate: GenericResponseAs[T, S & WebSockets])
extends ResponseAsDelegate[T, S & WebSockets] {

/** Applies the given function `f` to the deserialized value `T`. */
def map[T2](f: T => T2): WebSocketStreamResponseAs[T2, S] =
Expand Down Expand Up @@ -380,7 +380,7 @@ object GenericResponseAs {
}
)

def isWebSocket(ra: GenericResponseAs[_, _]): Boolean =
def isWebSocket(ra: GenericResponseAs[?, ?]): Boolean =
ra match {
case _: GenericWebSocketResponseAs[_, _] => true
case ResponseAsFromMetadata(conditions, default) =>
Expand All @@ -402,13 +402,13 @@ case object ResponseAsByteArray extends GenericResponseAs[Array[Byte], Any] {
// fact that `BinaryStream =:= s.BinaryStream`. We have to rely on correct construction via the companion object and
// perform typecasts when the request is deconstructed.
case class ResponseAsStream[F[_], T, Stream, S] private (s: Streams[S], f: (Stream, ResponseMetadata) => F[T])
extends GenericResponseAs[T, S with Effect[F]] {
extends GenericResponseAs[T, S & Effect[F]] {
override def show: String = "as stream"
}
object ResponseAsStream {
def apply[F[_], T, S](s: Streams[S])(
f: (s.BinaryStream, ResponseMetadata) => F[T]
): GenericResponseAs[T, S with Effect[F]] =
): GenericResponseAs[T, S & Effect[F]] =
new ResponseAsStream(s, f)
}

Expand All @@ -432,15 +432,14 @@ case class ResponseAsFile(output: SttpFile) extends GenericResponseAs[SttpFile,

sealed trait GenericWebSocketResponseAs[T, -R] extends GenericResponseAs[T, R]
case class ResponseAsWebSocket[F[_], T](f: (WebSocket[F], ResponseMetadata) => F[T])
extends GenericWebSocketResponseAs[T, WebSockets with Effect[F]] {
extends GenericWebSocketResponseAs[T, WebSockets & Effect[F]] {
override def show: String = "as web socket"
}
case class ResponseAsWebSocketUnsafe[F[_]]()
extends GenericWebSocketResponseAs[WebSocket[F], WebSockets with Effect[F]] {
case class ResponseAsWebSocketUnsafe[F[_]]() extends GenericWebSocketResponseAs[WebSocket[F], WebSockets & Effect[F]] {
override def show: String = "as web socket unsafe"
}
case class ResponseAsWebSocketStream[S, Pipe[_, _]](s: Streams[S], p: Pipe[WebSocketFrame.Data[_], WebSocketFrame])
extends GenericWebSocketResponseAs[Unit, S with WebSockets] {
case class ResponseAsWebSocketStream[S, Pipe[_, _]](s: Streams[S], p: Pipe[WebSocketFrame.Data[?], WebSocketFrame])
extends GenericWebSocketResponseAs[Unit, S & WebSockets] {
override def show: String = "as web socket stream"
}

Expand Down
2 changes: 1 addition & 1 deletion core/src/main/scala/sttp/client4/ResponseException.scala
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ object ResponseException {

//

@tailrec def find(exception: Throwable): Option[ResponseException[_]] =
@tailrec def find(exception: Throwable): Option[ResponseException[?]] =
Option(exception) match {
case Some(e: ResponseException[_]) => Some(e)
case Some(_) => find(exception.getCause)
Expand Down
Loading
Loading