diff --git a/build.sbt b/build.sbt index faf321986f..6b60d14f6a 100644 --- a/build.sbt +++ b/build.sbt @@ -152,7 +152,7 @@ val zio2InteropRsVersion = "2.0.2" val oxVersion = "0.5.1" val sttpModelVersion = "1.7.11" -val sttpSharedVersion = "1.4.0" +val sttpSharedVersion = "1.4.2" val logback = "ch.qos.logback" % "logback-classic" % "1.5.12" diff --git a/core/src/main/scala/sttp/client4/RequestOptions.scala b/core/src/main/scala/sttp/client4/RequestOptions.scala index 32c3805e6c..78e3b46769 100644 --- a/core/src/main/scala/sttp/client4/RequestOptions.scala +++ b/core/src/main/scala/sttp/client4/RequestOptions.scala @@ -1,10 +1,16 @@ package sttp.client4 import scala.concurrent.duration.Duration +import sttp.model.HttpVersion +import sttp.client4.logging.LoggingOptions +/** Options for a [[Request]]. The defaults can be found on [[emptyRequest]]. */ case class RequestOptions( followRedirects: Boolean, - readTimeout: Duration, // TODO: Use FiniteDuration while migrating to sttp-4 + readTimeout: Duration, maxRedirects: Int, - redirectToGet: Boolean + redirectToGet: Boolean, + disableAutoDecompression: Boolean, + httpVersion: Option[HttpVersion], + loggingOptions: LoggingOptions ) diff --git a/core/src/main/scala/sttp/client4/SpecifyAuthScheme.scala b/core/src/main/scala/sttp/client4/SpecifyAuthScheme.scala index 81e8e3be2c..1acb3ab232 100644 --- a/core/src/main/scala/sttp/client4/SpecifyAuthScheme.scala +++ b/core/src/main/scala/sttp/client4/SpecifyAuthScheme.scala @@ -3,11 +3,12 @@ package sttp.client4 import sttp.client4.internal.DigestAuthenticator import sttp.client4.internal.Utf8 import java.util.Base64 +import sttp.attributes.AttributeKey class SpecifyAuthScheme[+R <: PartialRequestBuilder[R, _]]( hn: String, req: R, - digestTag: String + digestAttributeKey: AttributeKey[DigestAuthenticator.DigestAuthData] ) { def basic(user: String, password: String): R = { val c = new String(Base64.getEncoder.encode(s"$user:$password".getBytes(Utf8)), Utf8) @@ -21,5 +22,5 @@ class SpecifyAuthScheme[+R <: PartialRequestBuilder[R, _]]( req.header(hn, s"Bearer $token") def digest(user: String, password: String): R = - req.tag(digestTag, DigestAuthenticator.DigestAuthData(user, password)) + req.attribute(digestAttributeKey, DigestAuthenticator.DigestAuthData(user, password)) } diff --git a/core/src/main/scala/sttp/client4/SttpApi.scala b/core/src/main/scala/sttp/client4/SttpApi.scala index b6f588e1ac..35df0200f9 100644 --- a/core/src/main/scala/sttp/client4/SttpApi.scala +++ b/core/src/main/scala/sttp/client4/SttpApi.scala @@ -2,16 +2,16 @@ package sttp.client4 import sttp.client4.internal._ import sttp.model._ -import sttp.ws.WebSocket import java.io.InputStream import java.nio.ByteBuffer import scala.collection.immutable.Seq import scala.concurrent.duration._ import sttp.capabilities.Streams -import sttp.ws.WebSocketFrame import sttp.capabilities.Effect import sttp.client4.wrappers.FollowRedirectsBackend +import sttp.client4.logging.LoggingOptions +import sttp.attributes.AttributeMap trait SttpApi extends SttpExtensions with UriInterpolator { val DefaultReadTimeout: Duration = 1.minute @@ -30,9 +30,12 @@ trait SttpApi extends SttpExtensions with UriInterpolator { followRedirects = true, DefaultReadTimeout, FollowRedirectsBackend.MaxRedirects, - redirectToGet = false + redirectToGet = false, + disableAutoDecompression = false, + httpVersion = None, + loggingOptions = LoggingOptions() ), - Map() + AttributeMap.Empty ) /** A starting request, with the following modification comparing to [[emptyRequest]]: `Accept-Encoding` is set to diff --git a/core/src/main/scala/sttp/client4/logging/Log.scala b/core/src/main/scala/sttp/client4/logging/Log.scala index d697cdeb37..afcd7dfd31 100644 --- a/core/src/main/scala/sttp/client4/logging/Log.scala +++ b/core/src/main/scala/sttp/client4/logging/Log.scala @@ -57,15 +57,11 @@ class DefaultLog[F[_]]( ) extends Log[F] { def beforeRequestSend(request: GenericRequest[_, _]): F[Unit] = - request.loggingOptions match { - case Some(options) => - before( - request, - options.logRequestBody.getOrElse(logRequestBody), - options.logRequestHeaders.getOrElse(logRequestHeaders) - ) - case None => before(request, logRequestBody, logRequestHeaders) - } + before( + request, + request.loggingOptions.logRequestBody.getOrElse(logRequestBody), + request.loggingOptions.logRequestHeaders.getOrElse(logRequestHeaders) + ) private def before(request: GenericRequest[_, _], _logRequestBody: Boolean, _logRequestHeaders: Boolean): F[Unit] = logger( @@ -82,19 +78,14 @@ class DefaultLog[F[_]]( response: Response[_], responseBody: Option[String], elapsed: Option[Duration] - ): F[Unit] = request.loggingOptions match { - case Some(options) => - handleResponse( - request.showBasic, - response, - responseBody, - options.logResponseBody.getOrElse(responseBody.isDefined), - options.logResponseHeaders.getOrElse(logResponseHeaders), - elapsed - ) - case None => - handleResponse(request.showBasic, response, responseBody, responseBody.isDefined, logResponseHeaders, elapsed) - } + ): F[Unit] = handleResponse( + request.showBasic, + response, + responseBody, + request.loggingOptions.logResponseBody.getOrElse(responseBody.isDefined), + request.loggingOptions.logResponseHeaders.getOrElse(logResponseHeaders), + elapsed + ) private def handleResponse( showBasic: String, diff --git a/core/src/main/scala/sttp/client4/package.scala b/core/src/main/scala/sttp/client4/package.scala index e536215814..c8364db6de 100644 --- a/core/src/main/scala/sttp/client4/package.scala +++ b/core/src/main/scala/sttp/client4/package.scala @@ -1,11 +1,5 @@ package sttp package object client4 extends SttpApi { - - /** Provide an implicit value of this type to serialize arbitrary classes into a request body. Backends might also - * provide special logic for serializer instances which they define (e.g. to handle streaming). - */ - type BodySerializer[B] = B => BasicBodyPart - type RetryWhen = (GenericRequest[_, _], Either[Throwable, Response[_]]) => Boolean } diff --git a/core/src/main/scala/sttp/client4/request.scala b/core/src/main/scala/sttp/client4/request.scala index 1077b56d29..1cd627d8b1 100644 --- a/core/src/main/scala/sttp/client4/request.scala +++ b/core/src/main/scala/sttp/client4/request.scala @@ -6,6 +6,7 @@ import sttp.client4.internal.{ToCurlConverter, ToRfc2616Converter} import sttp.shared.Identity import scala.collection.immutable.Seq +import sttp.attributes.AttributeMap /** A generic description of an HTTP request, along with a description of how the response body should be handled. * @@ -67,8 +68,8 @@ trait GenericRequest[+T, -R] extends RequestBuilder[GenericRequest[T, R]] with R * @param response * Description of how the response body should be handled. Needs to be specified upfront so that the response is * always consumed and hence there are no requirements on client code to consume it. - * @param tags - * Request-specific tags which can be used by backends for logging, metrics, etc. Empty by default. + * @param attributes + * Request-specific attributes which can be used by backends for logging, metrics, etc. Empty by default. * @tparam T * The target type, to which the response body should be read. */ @@ -79,7 +80,7 @@ case class Request[T]( headers: Seq[Header], response: ResponseAs[T], options: RequestOptions, - tags: Map[String, Any] + attributes: AttributeMap ) extends GenericRequest[T, Any] with RequestBuilder[Request[T]] { @@ -88,11 +89,19 @@ case class Request[T]( override def method(method: Method, uri: Uri): Request[T] = copy(uri = uri, method = method) override def withHeaders(headers: Seq[Header]): Request[T] = copy(headers = headers) override def withOptions(options: RequestOptions): Request[T] = copy(options = options) - override def withTags(tags: Map[String, Any]): Request[T] = copy(tags = tags) + override def withAttributes(attributes: AttributeMap): Request[T] = copy(attributes = attributes) override protected def copyWithBody(body: BasicBody): Request[T] = copy(body = body) def multipartStreamBody[S](ps: Seq[Part[BodyPart[S]]]): StreamRequest[T, S] = - StreamRequest(method, uri, MultipartStreamBody(ps), headers, StreamResponseAs(response.delegate), options, tags) + StreamRequest( + method, + uri, + MultipartStreamBody(ps), + headers, + StreamResponseAs(response.delegate), + options, + attributes + ) def multipartStreamBody[S](p1: Part[BodyPart[S]], ps: Part[BodyPart[S]]*): StreamRequest[T, S] = StreamRequest( @@ -102,11 +111,11 @@ case class Request[T]( headers, StreamResponseAs(response.delegate), options, - tags + attributes ) def streamBody[S](s: Streams[S])(b: s.BinaryStream): StreamRequest[T, S] = - StreamRequest(method, uri, StreamBody(s)(b), headers, StreamResponseAs(response.delegate), options, tags) + StreamRequest(method, uri, StreamBody(s)(b), headers, StreamResponseAs(response.delegate), options, attributes) /** Specifies the target type to which the response body should be read. Note that this replaces any previous * specifications, which also includes any previous `mapResponse` invocations. @@ -117,19 +126,19 @@ case class Request[T]( /** Specifies that this is a WebSocket request. A [[WebSocketBackend]] will be required to send this request. */ def response[F[_], T2](ra: WebSocketResponseAs[F, T2]): WebSocketRequest[F, T2] = - WebSocketRequest(method, uri, body, headers, ra, options, tags) + WebSocketRequest(method, uri, body, headers, ra, options, attributes) /** Specifies that the response body should be processed using a non-blocking, asynchronous stream, as witnessed by * the `S` capability. A [[StreamBackend]] will be required to send this request. */ def response[T2, S](ra: StreamResponseAs[T2, S]): StreamRequest[T2, S] = - StreamRequest(method, uri, body, headers, ra, options, tags) + StreamRequest(method, uri, body, headers, ra, options, attributes) /** Specifies that this is a WebSocket request, and the WebSocket will be processed using a non-blocking, asynchronous * stream, as witnessed by the `S` capability. A [[WebSocketStreamBackend]] will be required to send this request. */ def response[T2, S](ra: WebSocketStreamResponseAs[T2, S]): WebSocketStreamRequest[T2, S] = - WebSocketStreamRequest(method, uri, body, headers, ra, options, tags) + WebSocketStreamRequest(method, uri, body, headers, ra, options, attributes) /** Sends the request, using the given backend. * @@ -182,8 +191,8 @@ object Request { * @param response * Description of how the response body should be handled. Needs to be specified upfront so that the response is * always consumed and hence there are no requirements on client code to consume it. - * @param tags - * Request-specific tags which can be used by backends for logging, metrics, etc. Empty by default. + * @param attributes + * Request-specific attributes which can be used by backends for logging, metrics, etc. Empty by default. * @tparam T * The target type, to which the response body should be read. If the response body is streamed, this might be the * value obtained by processing the entire stream. @@ -197,7 +206,7 @@ final case class StreamRequest[T, R]( headers: Seq[Header], response: StreamResponseAs[T, R], options: RequestOptions, - tags: Map[String, Any] + attributes: AttributeMap ) extends GenericRequest[T, R] with RequestBuilder[StreamRequest[T, R]] { @@ -206,7 +215,7 @@ final case class StreamRequest[T, R]( override def method(method: Method, uri: Uri): StreamRequest[T, R] = copy(method = method, uri = uri) override def withHeaders(headers: Seq[Header]): StreamRequest[T, R] = copy(headers = headers) override def withOptions(options: RequestOptions): StreamRequest[T, R] = copy(options = options) - override def withTags(tags: Map[String, Any]): StreamRequest[T, R] = copy(tags = tags) + override def withAttributes(attributes: AttributeMap): StreamRequest[T, R] = copy(attributes = attributes) override protected def copyWithBody(body: BasicBody): StreamRequest[T, R] = copy(body = body) /** Specifies the target type to which the response body should be read. Note that this replaces any previous @@ -229,7 +238,7 @@ final case class StreamRequest[T, R]( headers, WebSocketStreamResponseAs[T2, Effect[F] with R](ra.delegate), options, - tags + attributes ) def mapResponse[T2](f: T => T2): StreamRequest[T2, R] = copy(response = response.map(f)) @@ -263,8 +272,8 @@ final case class StreamRequest[T, R]( * @param response * Description of how the WebSocket should be handled. Needs to be specified upfront so that the response is always * consumed and hence there are no requirements on client code to consume it. - * @param tags - * Request-specific tags which can be used by backends for logging, metrics, etc. Empty by default. + * @param attributes + * Request-specific attributes which can be used by backends for logging, metrics, etc. Empty by default. * @tparam F * The effect type used to process the WebSocket. Might include asynchronous computations (e.g. * [[scala.concurrent.Future]]), pure effect descriptions (`IO`), or synchronous computations ([[Identity]]). @@ -279,7 +288,7 @@ final case class WebSocketRequest[F[_], T]( headers: Seq[Header], response: WebSocketResponseAs[F, T], options: RequestOptions, - tags: Map[String, Any] + attributes: AttributeMap ) extends GenericRequest[T, WebSockets with Effect[F]] with RequestBuilder[WebSocketRequest[F, T]] { @@ -288,7 +297,7 @@ final case class WebSocketRequest[F[_], T]( override def method(method: Method, uri: Uri): WebSocketRequest[F, T] = copy(method = method, uri = uri) override def withHeaders(headers: Seq[Header]): WebSocketRequest[F, T] = copy(headers = headers) override def withOptions(options: RequestOptions): WebSocketRequest[F, T] = copy(options = options) - override def withTags(tags: Map[String, Any]): WebSocketRequest[F, T] = copy(tags = tags) + override def withAttributes(attributes: AttributeMap): WebSocketRequest[F, T] = copy(attributes = attributes) override protected def copyWithBody(body: BasicBody): WebSocketRequest[F, T] = copy(body = body) def streamBody[S](s: Streams[S])(b: s.BinaryStream): WebSocketStreamRequest[T, Effect[F] with S] = @@ -299,7 +308,7 @@ final case class WebSocketRequest[F[_], T]( headers, WebSocketStreamResponseAs[T, Effect[F] with S](response.delegate), options, - tags + attributes ) def mapResponse[T2](f: T => T2): WebSocketRequest[F, T2] = copy(response = response.map(f)) @@ -343,8 +352,8 @@ final case class WebSocketRequest[F[_], T]( * @param response * Description of how the WebSocket should be handled. Needs to be specified upfront so that the response is always * consumed and hence there are no requirements on client code to consume it. - * @param tags - * Request-specific tags which can be used by backends for logging, metrics, etc. Empty by default. + * @param attributes + * Request-specific attributes which can be used by backends for logging, metrics, etc. Empty by default. * @tparam T * The target type, to which the response body should be read. If the WebSocket interactions are described entirely * by the response description, this might be `Unit`. Otherwise, this can be an `S` stream of frames or mapped @@ -359,7 +368,7 @@ final case class WebSocketStreamRequest[T, S]( headers: Seq[Header], response: WebSocketStreamResponseAs[T, S], options: RequestOptions, - tags: Map[String, Any] + attributes: AttributeMap ) extends GenericRequest[T, S with WebSockets] with RequestBuilder[WebSocketStreamRequest[T, S]] { @@ -368,7 +377,7 @@ final case class WebSocketStreamRequest[T, S]( override def method(method: Method, uri: Uri): WebSocketStreamRequest[T, S] = copy(method = method, uri = uri) override def withHeaders(headers: Seq[Header]): WebSocketStreamRequest[T, S] = copy(headers = headers) override def withOptions(options: RequestOptions): WebSocketStreamRequest[T, S] = copy(options = options) - override def withTags(tags: Map[String, Any]): WebSocketStreamRequest[T, S] = copy(tags = tags) + override def withAttributes(attributes: AttributeMap): WebSocketStreamRequest[T, S] = copy(attributes = attributes) override protected def copyWithBody(body: BasicBody): WebSocketStreamRequest[T, S] = copy(body = body) def mapResponse[T2](f: T => T2): WebSocketStreamRequest[T2, S] = copy(response = response.map(f)) diff --git a/core/src/main/scala/sttp/client4/requestBuilder.scala b/core/src/main/scala/sttp/client4/requestBuilder.scala index 143f70e517..753a227e10 100644 --- a/core/src/main/scala/sttp/client4/requestBuilder.scala +++ b/core/src/main/scala/sttp/client4/requestBuilder.scala @@ -19,6 +19,8 @@ import java.io.InputStream import java.nio.ByteBuffer import scala.concurrent.duration.Duration import scala.collection.immutable.Seq +import sttp.attributes.AttributeKey +import sttp.attributes.AttributeMap /** The builder methods of requests or partial requests of type `PR`. * @@ -43,8 +45,8 @@ trait PartialRequestBuilder[+PR <: PartialRequestBuilder[PR, R], +R] def response: ResponseAsDelegate[_, _] def options: RequestOptions - /** Request-specific tags which can be used by backends for logging, metrics, etc. Empty by default. */ - def tags: Map[String, Any] + /** Request-specific attributes which can be used by backends for logging, metrics, etc. Empty by default. */ + def attributes: AttributeMap /** Set the method & uri to the given ones. */ def method(method: Method, uri: Uri): R @@ -55,8 +57,8 @@ trait PartialRequestBuilder[+PR <: PartialRequestBuilder[PR, R], +R] /** Replace all options with the given ones. */ def withOptions(options: RequestOptions): PR - /** Replace all tags with the given ones. */ - def withTags(tags: Map[String, Any]): PR + /** Replace attributes with the given ones. */ + def withAttributes(attributes: AttributeMap): PR protected def copyWithBody(body: BasicBody): PR @@ -121,10 +123,21 @@ trait PartialRequestBuilder[+PR <: PartialRequestBuilder[PR, R], +R] */ def headers(hs: Header*): PR = hs.foldLeft(this)(_.header(_)) + /** Allows specifying basic, token, bearer (in the `Authorization` header) or digest authentication for this request. + */ def auth: SpecifyAuthScheme[PR] = - new SpecifyAuthScheme[PR](HeaderNames.Authorization, this, DigestAuthenticationBackend.DigestAuthTag) + new SpecifyAuthScheme[PR](HeaderNames.Authorization, this, DigestAuthenticationBackend.DigestAuthAttributeKey) + + /** Allows specifying basic, token, bearer (in the `Proxy-Authorization` header) or digest proxy authentication for + * this request. + */ def proxyAuth: SpecifyAuthScheme[PR] = - new SpecifyAuthScheme[PR](HeaderNames.ProxyAuthorization, this, DigestAuthenticationBackend.ProxyDigestAuthTag) + new SpecifyAuthScheme[PR]( + HeaderNames.ProxyAuthorization, + this, + DigestAuthenticationBackend.ProxyDigestAuthAttributeKey + ) + def acceptEncoding(encoding: String): PR = header(HeaderNames.AcceptEncoding, encoding) /** Adds the given cookie. Any previously defined cookies are left intact. */ @@ -288,64 +301,60 @@ trait PartialRequestBuilder[+PR <: PartialRequestBuilder[PR, R], +R] */ def redirectToGet(r: Boolean): PR = withOptions(options.copy(redirectToGet = r)) - def tag(k: String, v: Any): PR = withTags(tags + (k -> v)) - - def tag(k: String): Option[Any] = tags.get(k) - - private val disableAutoDecompressionKey = "disableAutoDecompression" - - // Used as a workaround to keep binary compatibility - // TODO: replace with additional parameter in RequestOptions when writing sttp4 - def disableAutoDecompression: PR = tag(disableAutoDecompressionKey, true) - - def autoDecompressionDisabled: Boolean = tags.getOrElse(disableAutoDecompressionKey, false).asInstanceOf[Boolean] - - private val httpVersionKey = "httpVersion" - - // Used as a workaround to keep binary compatibility - // TODO: replace with additional parameter in RequestOptions when writing sttp4 - // TODO: add similar functionality to Response + /** Disables auto-decompression of response bodies which are received with supported `Content-Encoding headers. */ + def disableAutoDecompression: PR = withOptions(options.copy(disableAutoDecompression = true)) - /** Allows setting HTTP version per request. Supported only is a few backends + /** True iff auto-decompression is disabled. * - * @param version: - * one of values from [[HttpVersion]] enum. - * @return - * request with version tag + * @see + * disableAutoDecompression */ - def httpVersion(version: HttpVersion): PR = tag(httpVersionKey, version) + def autoDecompressionDisabled: Boolean = options.disableAutoDecompression + + /** Set the HTTP version with which this request should be sent. Supported only in a few backends. */ + def httpVersion(version: HttpVersion): PR = withOptions(options.copy(httpVersion = Some(version))) - /** Get[[HttpVersion]] from tags in request. Supported only is a few backends + /** Get the [[HttpVersion]], with which this request should be sent, if any. Setting the HTTP version is supported + * only in a few backends. * * @return - * one of values form [[HttpVersion]] enum or [[None]] + * [[None]], if the request will be sent with the backend-default HTTP version. */ - def httpVersion: Option[HttpVersion] = tags.get(httpVersionKey).map(_.asInstanceOf[HttpVersion]) - - private val loggingOptionsTagKey = "loggingOptions" + def httpVersion: Option[HttpVersion] = options.httpVersion - /** Will only have effect when using the `LoggingBackend` */ - def logSettings( + /** Sets per-request logging options. Will only have effect when using the [[sttp.client4.logging.LoggingBackend]] + * wrapper. + */ + def loggingOptions( logRequestBody: Option[Boolean] = None, logResponseBody: Option[Boolean] = None, logRequestHeaders: Option[Boolean] = None, logResponseHeaders: Option[Boolean] = None - ): PR = { - val loggingOptions = LoggingOptions( - logRequestBody = logRequestBody, - logResponseBody = logResponseBody, - logRequestHeaders = logRequestHeaders, - logResponseHeaders = logResponseHeaders + ): PR = withOptions( + options.copy(loggingOptions = + LoggingOptions( + logRequestBody = logRequestBody, + logResponseBody = logResponseBody, + logRequestHeaders = logRequestHeaders, + logResponseHeaders = logResponseHeaders + ) ) - this.tag(loggingOptionsTagKey, loggingOptions) - } + ) + + /** Sets per-request logging options. Will only have effect when using the [[sttp.client4.logging.LoggingBackend]] + * wrapper. + */ + def loggingOptions(loggingOptions: LoggingOptions): PR = withOptions(options.copy(loggingOptions = loggingOptions)) + + /** The per-request logging options, which have effect when using the [[sttp.client4.logging.LoggingBackend]] wrapper. + */ + def loggingOptions: LoggingOptions = options.loggingOptions - def logSettings( - loggingOptions: Option[LoggingOptions] - ): PR = - this.tag(loggingOptionsTagKey, loggingOptions) + /** Reads a per-request attribute for the given key, if present. */ + def attribute[T](k: AttributeKey[T]): Option[T] = attributes.get(k) - def loggingOptions: Option[LoggingOptions] = tag(loggingOptionsTagKey).asInstanceOf[Option[LoggingOptions]] + /** Sets a per-request attribute for the given key, with the given value. */ + def attribute[T](k: AttributeKey[T], v: T): PR = withAttributes(attributes.put(k, v)) def show( includeBody: Boolean = true, @@ -365,8 +374,8 @@ trait PartialRequestBuilder[+PR <: PartialRequestBuilder[PR, R], +R] * @param response * Description of how the response body should be handled. Needs to be specified upfront so that the response is * always consumed and hence there are no requirements on client code to consume it. - * @param tags - * Request-specific tags which can be used by backends for logging, metrics, etc. Empty by default. + * @param attributes + * Request-specific attributes which can be used by backends for logging, metrics, etc. Empty by default. * @tparam T * The target type, to which the response body should be read. */ @@ -375,16 +384,16 @@ final case class PartialRequest[T]( headers: Seq[Header], response: ResponseAs[T], options: RequestOptions, - tags: Map[String, Any] + attributes: AttributeMap ) extends PartialRequestBuilder[PartialRequest[T], Request[T]] { override def showBasic: String = "(no method & uri set)" override def method(method: Method, uri: Uri): Request[T] = - Request(method, uri, body, headers, response, options, tags) + Request(method, uri, body, headers, response, options, attributes) override def withHeaders(headers: Seq[Header]): PartialRequest[T] = copy(headers = headers) override def withOptions(options: RequestOptions): PartialRequest[T] = copy(options = options) - override def withTags(tags: Map[String, Any]): PartialRequest[T] = copy(tags = tags) + override def withAttributes(attributes: AttributeMap): PartialRequest[T] = copy(attributes = attributes) override protected def copyWithBody(body: BasicBody): PartialRequest[T] = copy(body = body) def response[T2](ra: ResponseAs[T2]): PartialRequest[T2] = copy(response = ra) } diff --git a/core/src/main/scala/sttp/client4/wrappers/DigestAuthenticationBackend.scala b/core/src/main/scala/sttp/client4/wrappers/DigestAuthenticationBackend.scala index b008565000..f10954031a 100644 --- a/core/src/main/scala/sttp/client4/wrappers/DigestAuthenticationBackend.scala +++ b/core/src/main/scala/sttp/client4/wrappers/DigestAuthenticationBackend.scala @@ -17,6 +17,7 @@ import sttp.client4.{ } import sttp.model.Header import sttp.monad.syntax._ +import sttp.attributes.AttributeKey abstract class DigestAuthenticationBackend[F[_], P] private ( delegate: GenericBackend[F, P], @@ -27,13 +28,18 @@ abstract class DigestAuthenticationBackend[F[_], P] private ( delegate .send(request) .flatMap { firstResponse => - handleResponse(request, firstResponse, ProxyDigestAuthTag, DigestAuthenticator.proxy(_, clientNonceGenerator)) + handleResponse( + request, + firstResponse, + ProxyDigestAuthAttributeKey, + DigestAuthenticator.proxy(_, clientNonceGenerator) + ) } .flatMap { case (secondResponse, proxyAuthHeader) => handleResponse( proxyAuthHeader.map(h => request.header(h)).getOrElse(request), secondResponse, - DigestAuthTag, + DigestAuthAttributeKey, DigestAuthenticator.apply(_, clientNonceGenerator) ).map(_._1) } @@ -41,12 +47,11 @@ abstract class DigestAuthenticationBackend[F[_], P] private ( private def handleResponse[T]( request: GenericRequest[T, P with Effect[F]], response: Response[T], - digestTag: String, + digestAttributeKey: AttributeKey[DigestAuthenticator.DigestAuthData], digestAuthenticator: DigestAuthData => DigestAuthenticator ): F[(Response[T], Option[Header])] = request - .tag(digestTag) - .map(_.asInstanceOf[DigestAuthData]) + .attribute(digestAttributeKey) .flatMap { digestAuthData => val header = digestAuthenticator(digestAuthData).authenticate(request, response) header.map(h => delegate.send(request.header(h)).map(_ -> Option(h))) @@ -81,6 +86,10 @@ object DigestAuthenticationBackend { ): WebSocketStreamBackend[F, S] = new DigestAuthenticationBackend(delegate, clientNonceGenerator) with WebSocketStreamBackend[F, S] {} - private[client4] val DigestAuthTag = "__sttp_DigestAuth" - private[client4] val ProxyDigestAuthTag = "__sttp_ProxyDigestAuth" + private[client4] val DigestAuthAttributeKey = new AttributeKey[DigestAuthenticator.DigestAuthData]( + "sttp.client4.internal.DigestAuthenticator.DigestAuthData.direct" + ) + private[client4] val ProxyDigestAuthAttributeKey = new AttributeKey[DigestAuthenticator.DigestAuthData]( + "sttp.client4.internal.DigestAuthenticator.DigestAuthData.proxy" + ) } diff --git a/core/src/main/scalanative/sttp/client4/curl/AbstractCurlBackend.scala b/core/src/main/scalanative/sttp/client4/curl/AbstractCurlBackend.scala index d7e657bc7c..f223feabe8 100644 --- a/core/src/main/scalanative/sttp/client4/curl/AbstractCurlBackend.scala +++ b/core/src/main/scalanative/sttp/client4/curl/AbstractCurlBackend.scala @@ -75,8 +75,8 @@ abstract class AbstractCurlBackend[F[_]](_monad: MonadError[F], verbose: Boolean if (verbose) { curl.option(Verbose, parameter = true) } - if (request.tags.nonEmpty) { - return monad.error(new UnsupportedOperationException("Tags are not supported")) + if (request.attributes.nonEmpty) { + return monad.error(new UnsupportedOperationException("Attributes are not supported")) } val reqHeaders = request.headers if (reqHeaders.nonEmpty) { diff --git a/core/src/test/scalajvm/sttp/client4/BackendOptionsProxyTest2.scala b/core/src/test/scalajvm/sttp/client4/BackendOptionsProxyTest2.scala index bcec486d93..4859ef6bb7 100644 --- a/core/src/test/scalajvm/sttp/client4/BackendOptionsProxyTest2.scala +++ b/core/src/test/scalajvm/sttp/client4/BackendOptionsProxyTest2.scala @@ -22,6 +22,6 @@ class BackendOptionsProxyTest2 extends AnyFlatSpec with Matchers { val ioe = new IOException("bar") proxySelector.connectFailed(uri, proxySetting.inetSocketAddress, ioe) } - ex.getMessage should be("Couldn't connect to the proxy server, uri: foo, socket: fakeproxyserverhost:8080") + ex.getMessage should startWith("Couldn't connect to the proxy server, uri: foo, socket: fakeproxyserverhost") } } diff --git a/core/src/test/scalajvm/sttp/client4/testing/HttpTestExtensions.scala b/core/src/test/scalajvm/sttp/client4/testing/HttpTestExtensions.scala index a9a1a9d80e..9f82a32ac3 100644 --- a/core/src/test/scalajvm/sttp/client4/testing/HttpTestExtensions.scala +++ b/core/src/test/scalajvm/sttp/client4/testing/HttpTestExtensions.scala @@ -266,14 +266,7 @@ trait HttpTestExtensions[F[_]] extends AsyncFreeSpecLike { self: HttpTest[F] => if (self.supportsAutoDecompressionDisabling) { "should return compressed data" in { withTemporaryNonExistentFile { file => - val options = RequestOptions( - followRedirects = true, - DefaultReadTimeout, - FollowRedirectsBackend.MaxRedirects, - redirectToGet = false - ) val req = emptyRequest - .copy(options = options) .get(uri"$endpoint/raw-gzip-file") .response(asFile(file)) .acceptEncoding("gzip") diff --git a/docs/backends/wrappers/custom.md b/docs/backends/wrappers/custom.md index e248a14295..7c80f0fc0c 100644 --- a/docs/backends/wrappers/custom.md +++ b/docs/backends/wrappers/custom.md @@ -10,13 +10,13 @@ Possible use-cases for wrapper-backend include: See also the section on [resilience](../../resilience.md) which covers topics such as retries, circuit breaking and rate limiting. -## Request tagging +## Request attributes -Each request contains a `tags: Map[String, Any]` map. This map can be used to tag the request with any backend-specific information, and isn't used in any way by sttp itself. +Each request contains a `attributes: AttributeMap` type-safe map. This map can be used to tag the request with any backend-specific information, and isn't used in any way by sttp itself. -Tags can be added to a request using the `def tag(k: String, v: Any)` method, and read using the `def tag(k: String): Option[Any]` method. +Attributes can be added to a request using the `def attribute[T](k: AttributeKey[T], v: T)` method, and read using the `def attribute[T](k: Attribute[T]): Option[T]` method. -Backends, or backend wrappers can use tags e.g. for logging, passing a metric name, using different connection pools, or even different delegate backends. +Backends, or backend wrappers can use attributes e.g. for logging, passing a metric name, using different connection pools, or even different delegate backends. ## Listener backend @@ -42,6 +42,7 @@ Below is an example on how to implement a backend wrapper, which sends metrics for completed requests and wraps any `Future`-based backend: ```scala mdoc:compile-only +import sttp.attributes.AttributeKey import sttp.capabilities.Effect import sttp.client4._ import sttp.client4.akkahttp._ @@ -49,6 +50,7 @@ import sttp.client4.wrappers.DelegateBackend import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits.global import scala.util._ + // the metrics infrastructure trait MetricsServer { def reportDuration(name: String, duration: Long): Unit @@ -58,6 +60,9 @@ class CloudMetricsServer extends MetricsServer { override def reportDuration(name: String, duration: Long): Unit = ??? } +case class MetricPrefix(prefix: String) +val MetricPrefixAttributeKey = AttributeKey[MetricPrefix] + // the backend wrapper abstract class MetricWrapper[P](delegate: GenericBackend[Future, P], metrics: MetricsServer) @@ -67,7 +72,7 @@ abstract class MetricWrapper[P](delegate: GenericBackend[Future, P], val start = System.currentTimeMillis() def report(metricSuffix: String): Unit = { - val metricPrefix = request.tag("metric").getOrElse("?") + val metricPrefix = request.attribute(MetricPrefixAttributeKey).getOrElse(MetricPrefix("?")) val end = System.currentTimeMillis() metrics.reportDuration(metricPrefix + "-" + metricSuffix, end - start) } @@ -93,7 +98,7 @@ val backend = MetricWrapper(AkkaHttpBackend(), new CloudMetricsServer()) basicRequest .get(uri"http://company.com/api/service1") - .tag("metric", "service1") + .attribute(MetricPrefixAttributeKey, MetricPrefix("service1")) .send(backend) ```