Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
package sttp.client3
import sttp.capabilities.{Effect, Streams}
import sttp.client3.internal.DigestAuthenticator.DigestAuthData
import sttp.client3.internal.{SttpFile, ToCurlConverter, ToRfc2616Converter, _}
import sttp.client3.logging.LoggingOptions
import sttp.model._
import sttp.model.headers.CookieWithMeta
import java.io.InputStream
import java.nio.ByteBuffer
import java.util.Base64
import scala.collection.immutable.Seq
import scala.concurrent.duration.Duration
/** Describes a HTTP request, along with a description of how the response body should be handled.
*
* The request can be sent:
*
* - synchronously, using [[SimpleHttpClient.send()]]
* - using the [[send(SttpBackend)]] methods, which support any effect. The backend must provide a superset of the
* capabilities required by the 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. An exception to this are unsafe
* streaming and websocket responses, which need to be consumed/closed by the client.
* @param tags
* Request-specific tags which can be used by backends for logging, metrics, etc. Not used by default.
* @tparam U
* Specifies if the method & uri are specified. By default can be either: * [[Empty]], which is a type constructor
* which always resolves to [[None]]. This type of request is aliased to [[PartialRequest]]: there's no method and
* uri specified, and the request cannot be sent. * [[Identity]], which is an identity type constructor. This type of
* request is aliased to [[Request]]: the method and uri are specified, and the request can be sent.
* @tparam T
* The target type, to which the response body should be read.
* @tparam R
* The backend capabilities required by the request or response description. This might be `Any` (no requirements),
* [[Effect]] (the backend must support the given effect type), [[Streams]] (the ability to send and receive
* streaming bodies) or [[sttp.capabilities.WebSockets]] (the ability to handle websocket requests).
*/
case class RequestT[U[_], T, -R](
method: U[Method],
uri: U[Uri],
body: RequestBody[R],
headers: Seq[Header],
response: ResponseAs[T, R],
options: RequestOptions,
tags: Map[String, Any]
) extends HasHeaders
with RequestTExtensions[U, T, R] {
def get(uri: Uri): Request[T, R] =
this.copy[Identity, T, R](uri = uri, method = Method.GET)
def head(uri: Uri): Request[T, R] =
this.copy[Identity, T, R](uri = uri, method = Method.HEAD)
def post(uri: Uri): Request[T, R] =
this.copy[Identity, T, R](uri = uri, method = Method.POST)
def put(uri: Uri): Request[T, R] =
this.copy[Identity, T, R](uri = uri, method = Method.PUT)
def delete(uri: Uri): Request[T, R] =
this.copy[Identity, T, R](uri = uri, method = Method.DELETE)
def options(uri: Uri): Request[T, R] =
this.copy[Identity, T, R](uri = uri, method = Method.OPTIONS)
def patch(uri: Uri): Request[T, R] =
this.copy[Identity, T, R](uri = uri, method = Method.PATCH)
def method(method: Method, uri: Uri): Request[T, R] =
this.copy[Identity, T, R](uri = uri, method = method)
def contentType(ct: String): RequestT[U, T, R] =
header(HeaderNames.ContentType, ct, replaceExisting = true)
def contentType(mt: MediaType): RequestT[U, T, R] =
header(HeaderNames.ContentType, mt.toString, replaceExisting = true)
def contentType(ct: String, encoding: String): RequestT[U, T, R] =
header(HeaderNames.ContentType, contentTypeWithCharset(ct, encoding), replaceExisting = true)
def contentLength(l: Long): RequestT[U, T, R] =
header(HeaderNames.ContentLength, l.toString, replaceExisting = true)
/** Adds the given header to the end of the headers sequence.
* @param replaceExisting
* If there's already a header with the same name, should it be replaced?
*/
def header(h: Header, replaceExisting: Boolean = false): RequestT[U, T, R] = {
val current = if (replaceExisting) headers.filterNot(_.is(h.name)) else headers
this.copy(headers = current :+ h)
}
/** Adds the given header to the end of the headers sequence.
* @param replaceExisting
* If there's already a header with the same name, should it be replaced?
*/
def header(k: String, v: String, replaceExisting: Boolean): RequestT[U, T, R] =
header(Header(k, v), replaceExisting)
/** Adds the given header to the end of the headers sequence. */
def header(k: String, v: String): RequestT[U, T, R] = header(Header(k, v))
/** Adds the given header to the end of the headers sequence, if the value is defined. Otherwise has no effect. */
def header(k: String, ov: Option[String]): RequestT[U, T, R] = ov.fold(this)(header(k, _))
/** Adds the given headers to the end of the headers sequence. */
def headers(hs: Map[String, String]): RequestT[U, T, R] = headers(hs.map(t => Header(t._1, t._2)).toSeq: _*)
/** Adds the given headers to the end of the headers sequence.
* @param replaceExisting
* If there's already a header with the same name, should it be replaced?
*/
def headers(hs: Map[String, String], replaceExisting: Boolean): RequestT[U, T, R] =
if (replaceExisting) hs.foldLeft(this) { (s, h) => s.header(h._1, h._2, replaceExisting) }
else headers(hs)
/** Adds the given headers to the end of the headers sequence. */
def headers(hs: Header*): RequestT[U, T, R] = this.copy(headers = headers ++ hs)
/** Adds the given headers to the end of the headers sequence. */
def headers(hs: Seq[Header], replaceExisting: Boolean): RequestT[U, T, R] =
if (replaceExisting) hs.foldLeft(this) { (s, h) => s.header(h, replaceExisting) }
else headers(hs: _*)
def auth: SpecifyAuthScheme[U, T, R] =
new SpecifyAuthScheme[U, T, R](HeaderNames.Authorization, this, DigestAuthenticationBackend.DigestAuthTag)
def proxyAuth: SpecifyAuthScheme[U, T, R] =
new SpecifyAuthScheme[U, T, R](HeaderNames.ProxyAuthorization, this, DigestAuthenticationBackend.ProxyDigestAuthTag)
def acceptEncoding(encoding: String): RequestT[U, T, R] =
header(HeaderNames.AcceptEncoding, encoding, replaceExisting = true)
def cookie(nv: (String, String)): RequestT[U, T, R] = cookies(nv)
def cookie(n: String, v: String): RequestT[U, T, R] = cookies((n, v))
def cookies(r: Response[_]): RequestT[U, T, R] = cookies(
r.cookies.collect { case Right(c) => c }.map(c => (c.name, c.value)): _*
)
def cookies(cs: Iterable[CookieWithMeta]): RequestT[U, T, R] = cookies(cs.map(c => (c.name, c.value)).toSeq: _*)
def cookies(nvs: (String, String)*): RequestT[U, T, R] = {
header(
HeaderNames.Cookie,
(headers.find(_.is(HeaderNames.Cookie)).map(_.value).toSeq ++ nvs.map(p => p._1 + "=" + p._2)).mkString("; "),
replaceExisting = true
)
}
/** Uses the `utf-8` encoding.
*
* If content type is not yet specified, will be set to `text/plain` with `utf-8` encoding.
*
* If content length is not yet specified, will be set to the number of bytes in the string using the `utf-8`
* encoding.
*/
def body(b: String): RequestT[U, T, R] = body(b, Utf8)
/** If content type is not yet specified, will be set to `text/plain` with the given encoding.
*
* If content length is not yet specified, will be set to the number of bytes in the string using the given encoding.
*/
def body(b: String, encoding: String): RequestT[U, T, R] =
withBody(StringBody(b, encoding))
.setContentLengthIfMissing(b.getBytes(encoding).length.toLong)
/** If content type is not yet specified, will be set to `application/octet-stream`.
*
* If content length is not yet specified, will be set to the length of the given array.
*/
def body(b: Array[Byte]): RequestT[U, T, R] =
withBody(ByteArrayBody(b))
.setContentLengthIfMissing(b.length.toLong)
/** If content type is not yet specified, will be set to `application/octet-stream`.
*/
def body(b: ByteBuffer): RequestT[U, T, R] =
withBody(ByteBufferBody(b))
/** If content type is not yet specified, will be set to `application/octet-stream`.
*/
def body(b: InputStream): RequestT[U, T, R] =
withBody(InputStreamBody(b))
/** If content type is not yet specified, will be set to `application/octet-stream`.
*
* If content length is not yet specified, will be set to the length of the given file.
*/
private[client3] def body(f: SttpFile): RequestT[U, T, R] =
withBody(FileBody(f)).setContentLengthIfMissing(f.size)
/** Encodes the given parameters as form data using `utf-8`. If content type is not yet specified, will be set to
* `application/x-www-form-urlencoded`.
*
* If content length is not yet specified, will be set to the length of the number of bytes in the url-encoded
* parameter string.
*/
def body(fs: Map[String, String]): RequestT[U, T, R] =
formDataBody(fs.toList, Utf8)
/** Encodes the given parameters as form data. If content type is not yet specified, will be set to
* `application/x-www-form-urlencoded`.
*
* If content length is not yet specified, will be set to the length of the number of bytes in the url-encoded
* parameter string.
*/
def body(fs: Map[String, String], encoding: String): RequestT[U, T, R] =
formDataBody(fs.toList, encoding)
/** Encodes the given parameters as form data using `utf-8`. If content type is not yet specified, will be set to
* `application/x-www-form-urlencoded`.
*
* If content length is not yet specified, will be set to the length of the number of bytes in the url-encoded
* parameter string.
*/
def body(fs: (String, String)*): RequestT[U, T, R] =
formDataBody(fs.toList, Utf8)
/** Encodes the given parameters as form data. If content type is not yet specified, will be set to
* `application/x-www-form-urlencoded`.
*
* If content length is not yet specified, will be set to the length of the number of bytes in the url-encoded
* parameter string.
*/
def body(fs: Seq[(String, String)], encoding: String): RequestT[U, T, R] =
formDataBody(fs, encoding)
def multipartBody[R2](ps: Seq[Part[RequestBody[R2]]]): RequestT[U, T, R with R2] =
copy(body = MultipartBody(ps))
def multipartBody[R2](p1: Part[RequestBody[R2]], ps: Part[RequestBody[R2]]*): RequestT[U, T, R with R2] =
copy(body = MultipartBody(p1 :: ps.toList))
def streamBody[S](s: Streams[S])(b: s.BinaryStream): RequestT[U, T, R with S] =
withBody(StreamBody(s)(b))
/** When the request is sent, if reading the response times out (there's no activity for the given period of time), a
* failed effect will be returned, or an exception will be thrown
*/
def readTimeout(t: Duration): RequestT[U, T, R] =
this.copy(options = options.copy(readTimeout = t))
/** 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.
*/
def response[T2, R2](ra: ResponseAs[T2, R2]): RequestT[U, T2, R with R2] =
this.copy(response = ra)
def mapResponse[T2](f: T => T2): RequestT[U, T2, R] =
this.copy(response = response.map(f))
def isWebSocket: Boolean = ResponseAs.isWebSocket(response)
def followRedirects(fr: Boolean): RequestT[U, T, R] =
this.copy(options = options.copy(followRedirects = fr))
def maxRedirects(n: Int): RequestT[U, T, R] =
if (n <= 0)
this.copy(options = options.copy(followRedirects = false))
else
this.copy(options = options.copy(followRedirects = true, maxRedirects = n))
def tag(k: String, v: Any): RequestT[U, T, R] =
this.copy(tags = 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: RequestT[U, T, R] = 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
/** Allows setting HTTP version per request. Supported only is a few backends
*
* @param version:
* one of values from [[HttpVersion]] enum.
* @return
* request with version tag
*/
def httpVersion(version: HttpVersion): RequestT[U, T, R] = tag(httpVersionKey, version)
/** Get[[HttpVersion]] from tags in request. Supported only is a few backends
*
* @return
* one of values form [[HttpVersion]] enum or [[None]]
*/
def httpVersion: Option[HttpVersion] = tags.get(httpVersionKey).map(_.asInstanceOf[HttpVersion])
private val loggingOptionsTagKey = "loggingOptions"
/** Will only have effect when using the `LoggingBackend` */
def logSettings(
logRequestBody: Option[Boolean] = None,
logResponseBody: Option[Boolean] = None,
logRequestHeaders: Option[Boolean] = None,
logResponseHeaders: Option[Boolean] = None
): RequestT[U, T, R] = {
val loggingOptions = LoggingOptions(
logRequestBody = logRequestBody,
logResponseBody = logResponseBody,
logRequestHeaders = logRequestHeaders,
logResponseHeaders = logResponseHeaders
)
this.tag(loggingOptionsTagKey, loggingOptions)
}
def loggingOptions: Option[LoggingOptions] = tag(loggingOptionsTagKey).asInstanceOf[Option[LoggingOptions]]
/** When a POST or PUT request is redirected, should the redirect be a POST/PUT as well (with the original body), or
* should the request be converted to a GET without a body.
*
* Note that this only affects 301 and 302 redirects. 303 redirects are always converted, while 307 and 308 redirects
* always keep the same method.
*
* See https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections for details.
*/
def redirectToGet(r: Boolean): RequestT[U, T, R] =
this.copy(options = options.copy(redirectToGet = r))
/** Sends the request, using the backend from the implicit scope. Only requests for which the method & URI are
* specified can be sent.
*
* The required capabilities must be a subset of the capabilities provided by the backend.
*
* @return
* For synchronous backends (when the effect type is [[Identity]]), [[Response]] is returned directly and
* exceptions are thrown. For asynchronous backends (when the effect type is e.g. [[scala.concurrent.Future]]), an
* effect containing the [[Response]] is returned. Exceptions are represented as failed effects (e.g. failed
* futures).
*
* The response body is deserialized as specified by this request (see [[RequestT.response]]).
*
* Known exceptions are converted by backends to one of [[SttpClientException]]. Other exceptions are thrown
* unchanged.
*/
@deprecated(message = "use request.send(backend), providing the backend explicitly", since = "3.0.0")
def send[F[_], P]()(implicit
backend: SttpBackend[F, P],
pEffectFIsR: P with Effect[F] <:< R,
isIdInRequest: IsIdInRequest[U]
): F[Response[T]] =
send(backend)(
isIdInRequest,
pEffectFIsR
) // the order of implicits must be different so that the signatures are different
/** Sends the request, using the given backend. Only requests for which the method & URI are specified can be sent.
*
* The required capabilities must be a subset of the capabilities provided by the backend.
*
* @return
* For synchronous backends (when the effect type is [[Identity]]), [[Response]] is returned directly and
* exceptions are thrown. For asynchronous backends (when the effect type is e.g. [[scala.concurrent.Future]]), an
* effect containing the [[Response]] is returned. Exceptions are represented as failed effects (e.g. failed
* futures).
*
* The response body is deserialized as specified by this request (see [[RequestT.response]]).
*
* Known exceptions are converted by backends to one of [[SttpClientException]]. Other exceptions are thrown
* unchanged.
*/
def send[F[_], P](backend: SttpBackend[F, P])(implicit
isIdInRequest: IsIdInRequest[U],
pEffectFIsR: P with Effect[F] <:< R
): F[Response[T]] = backend.send(asRequest.asInstanceOf[Request[T, P with Effect[F]]]) // as witnessed by pEffectFIsR
def toCurl(implicit isIdInRequest: IsIdInRequest[U]): String = ToCurlConverter.requestToCurl(asRequest)
def toCurl(sensitiveHeaders: Set[String])(implicit isIdInRequest: IsIdInRequest[U]): String =
ToCurlConverter.requestToCurl(asRequest, sensitiveHeaders)
def toRfc2616Format(implicit isIdInRequest: IsIdInRequest[U]): String = ToRfc2616Converter.requestToRfc2616(asRequest)
def toRfc2616Format(sensitiveHeaders: Set[String])(implicit isIdInRequest: IsIdInRequest[U]): String =
ToRfc2616Converter.requestToRfc2616(asRequest, sensitiveHeaders)
def showBasic: String =
(this.method, this.uri) match {
case (m: Method, u: Uri) =>
val ws = if (isWebSocket) " (web socket) " else ""
s"$m$ws $u"
case _ => "(no method & uri set)"
}
def show(
includeBody: Boolean = true,
includeHeaders: Boolean = true,
sensitiveHeaders: Set[String] = HeaderNames.SensitiveHeaders
): String = {
val headers =
if (includeHeaders) ", headers: " + this.headers.map(_.toStringSafe(sensitiveHeaders)).mkString(", ") else ""
val body = if (includeBody) s", body: ${this.body.show}" else ""
val methodAndUri = (this.method, this.uri) match {
case (m: Method, u: Uri) =>
val ws = if (isWebSocket) " (web socket) " else ""
s"$m$ws $u, "
case _ => ""
}
s"${methodAndUri}response as: ${response.show}$headers$body"
}
private def asRequest(implicit isIdInRequest: IsIdInRequest[U]): RequestT[Identity, T, R] = {
// we could avoid the asInstanceOf by creating an artificial copy
// changing the method & url fields using `isIdInRequest`, but that
// would be only to satisfy the type checker, and a needless copy at
// runtime.
this.asInstanceOf[RequestT[Identity, T, R]]
}
private def hasContentType: Boolean = headers.exists(_.is(HeaderNames.ContentType))
private def setContentTypeIfMissing(mt: MediaType): RequestT[U, T, R] =
if (hasContentType) this else contentType(mt)
private[client3] def withBody[R2](body: RequestBody[R2]): RequestT[U, T, R with R2] = {
val defaultCt = body match {
case StringBody(_, encoding, ct) =>
ct.copy(charset = Some(encoding))
case _ =>
body.defaultContentType
}
setContentTypeIfMissing(defaultCt).copy(body = body)
}
private def hasContentLength: Boolean = headers.exists(_.is(HeaderNames.ContentLength))
private def setContentLengthIfMissing(l: => Long): RequestT[U, T, R] =
if (hasContentLength) this else contentLength(l)
private def formDataBody(fs: Seq[(String, String)], encoding: String): RequestT[U, T, R] = {
val b = RequestBody.paramsToStringBody(fs, encoding)
setContentTypeIfMissing(MediaType.ApplicationXWwwFormUrlencoded)
.setContentLengthIfMissing(b.s.getBytes(encoding).length.toLong)
.copy(body = b)
}
private[client3] def onlyMetadata(implicit isIdInRequest: IsIdInRequest[U]): RequestMetadata = {
val r = asRequest
val m = r.method
val u = r.uri
val h = r.headers
new RequestMetadata {
override val method: Method = m
override val uri: Uri = u
override val headers: Seq[Header] = h
}
}
}
object RequestT {
implicit class RichRequestTEither[U[_], A, B, R](r: RequestT[U, Either[A, B], R]) {
def mapResponseRight[B2](f: B => B2): RequestT[U, Either[A, B2], R] = r.copy(response = r.response.mapRight(f))
def responseGetRight: RequestT[U, B, R] = r.copy(response = r.response.getRight)
}
implicit class RichRequestTEitherResponseException[U[_], HE, DE, B, R](
r: RequestT[U, Either[ResponseException[HE, DE], B], R]
) {
def responseGetEither: RequestT[U, Either[HE, B], R] = r.copy(response = r.response.getEither)
}
}
class SpecifyAuthScheme[U[_], T, -R](hn: String, rt: RequestT[U, T, R], digestTag: String) {
def basic(user: String, password: String): RequestT[U, T, R] = {
val c = new String(Base64.getEncoder.encode(s"$user:$password".getBytes(Utf8)), Utf8)
rt.header(hn, s"Basic $c")
}
def basicToken(token: String): RequestT[U, T, R] =
rt.header(hn, s"Basic $token")
def bearer(token: String): RequestT[U, T, R] =
rt.header(hn, s"Bearer $token")
def digest(user: String, password: String): RequestT[U, T, R] = {
rt.tag(digestTag, DigestAuthData(user, password))
}
}
case class RequestOptions(
followRedirects: Boolean,
readTimeout: Duration, // TODO: Use FiniteDuration while migrating to sttp-4
maxRedirects: Int,
redirectToGet: Boolean
)