Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make AbstractCurlBackend async friendly #2012

Merged
merged 4 commits into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -25,35 +25,59 @@ import scala.scalanative.unsigned._
abstract class AbstractCurlBackend[F[_]](_monad: MonadError[F], verbose: Boolean) extends GenericBackend[F, Any] {
override implicit def monad: MonadError[F] = _monad

/** Given a [[CurlHandle]], perform the request and return a [[CurlCode]]. */
protected def performCurl(c: CurlHandle): F[CurlCode]

/** Same as [[performCurl]], but also checks and throws runtime exceptions on bad [[CurlCode]]s. */
final def perform(c: CurlHandle) = performCurl(c).flatMap(lift)
natsukagami marked this conversation as resolved.
Show resolved Hide resolved

type R = Any with Effect[F]

override def close(): F[Unit] = monad.unit(())

private var headers: CurlList = _
private var multiPartHeaders: Seq[CurlList] = Seq()
/** A request-specific context, with allocated zones and headers. */
private class Context() {
val zone: Zone = Zone.open()
var headers: CurlList = _
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of vars, mabye we can prepare the headers/multiPartHeaders parameters in send, and then create an (immutable) instance of Context? E.g. case class Context(zone: Zone, headers: Option[CurlList], multiPartHeaders: Seq[CurlList])

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit tricky to do, since setRequestBody is setting up the multi part headers as it goes through the multipart body. I can on the other hand just exposes a addHeader function to create a header that gets cleared when the context is closed.

var multiPartHeaders: Seq[CurlList] = Seq()

def close() = {
zone.close()
if (headers.ptr != null) headers.ptr.free()
natsukagami marked this conversation as resolved.
Show resolved Hide resolved
multiPartHeaders.foreach(_.ptr.free())
}
}

private object Context {
def apply[T](body: Context => F[T]): F[T] = {
implicit val ctx = new Context()
body(ctx).ensure(monad.unit(ctx.close()))
}
}

override def send[T](request: GenericRequest[T, R]): F[Response[T]] =
adjustExceptions(request) {
unsafe.Zone { implicit z =>
def perform(implicit ctx: Context): F[Response[T]] = {
implicit val z = ctx.zone
val curl = CurlApi.init
if (verbose) {
curl.option(Verbose, parameter = true)
}
if (request.tags.nonEmpty) {
monad.error(new UnsupportedOperationException("Tags are not supported"))
return monad.error(new UnsupportedOperationException("Tags are not supported"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice catch! :)

}
val reqHeaders = request.headers
if (reqHeaders.nonEmpty) {
reqHeaders.find(_.name == "Accept-Encoding").foreach(h => curl.option(AcceptEncoding, h.value))
request.body match {
case _: MultipartBody[_] =>
headers = transformHeaders(
ctx.headers = transformHeaders(
reqHeaders :+ Header.contentType(MediaType.MultipartFormData)
)
case _ =>
headers = transformHeaders(reqHeaders)
ctx.headers = transformHeaders(reqHeaders)
}
curl.option(HttpHeader, headers.ptr)
curl.option(HttpHeader, ctx.headers.ptr)
}

val spaces = responseSpace
Expand All @@ -62,6 +86,8 @@ abstract class AbstractCurlBackend[F[_]](_monad: MonadError[F], verbose: Boolean
case None => handleBase(request, curl, spaces)
}
}

Context(ctx => perform(ctx))
natsukagami marked this conversation as resolved.
Show resolved Hide resolved
}

private def adjustExceptions[T](request: GenericRequest[_, _])(t: => F[T]): F[T] =
Expand All @@ -70,22 +96,21 @@ abstract class AbstractCurlBackend[F[_]](_monad: MonadError[F], verbose: Boolean
)

private def handleBase[T](request: GenericRequest[T, R], curl: CurlHandle, spaces: CurlSpaces)(implicit
z: unsafe.Zone
ctx: Context
) = {
implicit val z = ctx.zone
curl.option(WriteFunction, AbstractCurlBackend.wdFunc)
curl.option(WriteData, spaces.bodyResp)
curl.option(TimeoutMs, request.options.readTimeout.toMillis)
curl.option(HeaderData, spaces.headersResp)
curl.option(Url, request.uri.toString)
setMethod(curl, request.method)
setRequestBody(curl, request.body)
monad.flatMap(lift(curl.perform)) { _ =>
monad.flatMap(perform(curl)) { _ =>
curl.info(ResponseCode, spaces.httpCode)
val responseBody = fromCString((!spaces.bodyResp)._1)
val responseHeaders_ = parseHeaders(fromCString((!spaces.headersResp)._1))
val httpCode = StatusCode((!spaces.httpCode).toInt)
if (headers.ptr != null) headers.ptr.free()
multiPartHeaders.foreach(_.ptr.free())
free((!spaces.bodyResp)._1)
free((!spaces.headersResp)._1)
free(spaces.bodyResp.asInstanceOf[Ptr[CSignedChar]])
Expand All @@ -112,19 +137,18 @@ abstract class AbstractCurlBackend[F[_]](_monad: MonadError[F], verbose: Boolean
}

private def handleFile[T](request: GenericRequest[T, R], curl: CurlHandle, file: SttpFile, spaces: CurlSpaces)(
implicit z: unsafe.Zone
implicit ctx: Context
) = {
implicit val z = ctx.zone
val outputPath = file.toPath.toString
val outputFilePtr: Ptr[FILE] = fopen(toCString(outputPath), toCString("wb"))
curl.option(WriteData, outputFilePtr)
curl.option(Url, request.uri.toString)
setMethod(curl, request.method)
setRequestBody(curl, request.body)
monad.flatMap(lift(curl.perform)) { _ =>
monad.flatMap(perform(curl)) { _ =>
curl.info(ResponseCode, spaces.httpCode)
val httpCode = StatusCode((!spaces.httpCode).toInt)
if (headers.ptr != null) headers.ptr.free()
multiPartHeaders.foreach(_.ptr.free())
free(spaces.httpCode.asInstanceOf[Ptr[CSignedChar]])
fclose(outputFilePtr)
curl.cleanup()
Expand Down Expand Up @@ -159,7 +183,10 @@ abstract class AbstractCurlBackend[F[_]](_monad: MonadError[F], verbose: Boolean
lift(m)
}

private def setRequestBody(curl: CurlHandle, body: GenericRequestBody[R])(implicit zone: Zone): F[CurlCode] =
private def setRequestBody(curl: CurlHandle, body: GenericRequestBody[R])(implicit
ctx: Context
): F[CurlCode] = {
implicit val z = ctx.zone
body match { // todo: assign to monad object
case b: BasicBodyPart =>
val str = basicBodyToString(b)
Expand All @@ -178,7 +205,7 @@ abstract class AbstractCurlBackend[F[_]](_monad: MonadError[F], verbose: Boolean
if (otherHeaders.nonEmpty) {
val curlList = transformHeaders(otherHeaders)
part.withHeaders(curlList.ptr)
multiPartHeaders = multiPartHeaders :+ curlList
ctx.multiPartHeaders = ctx.multiPartHeaders :+ curlList
}
}
lift(curl.option(Mimepost, mime))
Expand All @@ -187,6 +214,7 @@ abstract class AbstractCurlBackend[F[_]](_monad: MonadError[F], verbose: Boolean
case NoBody =>
monad.unit(CurlCode.Ok)
}
}

private def basicBodyToString(body: BodyPart[_]): String =
body match {
Expand Down Expand Up @@ -269,6 +297,12 @@ abstract class AbstractCurlBackend[F[_]](_monad: MonadError[F], verbose: Boolean
}
}

/** Curl backends that performs the curl operation with a simple `curl_easy_perform`. */
abstract class AbstractSyncCurlBackend[F[_]](_monad: MonadError[F], verbose: Boolean)
extends AbstractCurlBackend[F](_monad, verbose) {
override def performCurl(c: CurlHandle): F[CurlCode.CurlCode] = monad.unit(c.perform)
}

object AbstractCurlBackend {
val wdFunc: CFuncPtr4[Ptr[Byte], CSize, CSize, Ptr[CurlFetch], CSize] = {
(ptr: Ptr[CChar], size: CSize, nmemb: CSize, data: Ptr[CurlFetch]) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import scala.util.Try

// Curl supports redirects, but it doesn't store the history, so using FollowRedirectsBackend is more convenient

private class CurlBackend(verbose: Boolean) extends AbstractCurlBackend(IdMonad, verbose) with SyncBackend {}
private class CurlBackend(verbose: Boolean) extends AbstractSyncCurlBackend(IdMonad, verbose) with SyncBackend {}

object CurlBackend {
def apply(verbose: Boolean = false): SyncBackend = FollowRedirectsBackend(new CurlBackend(verbose))
}

private class CurlTryBackend(verbose: Boolean) extends AbstractCurlBackend(TryMonad, verbose) with Backend[Try] {}
private class CurlTryBackend(verbose: Boolean) extends AbstractSyncCurlBackend(TryMonad, verbose) with Backend[Try] {}

object CurlTryBackend {
def apply(verbose: Boolean = false): Backend[Try] = FollowRedirectsBackend(new CurlTryBackend(verbose))
Expand Down
Loading