tapir icon indicating copy to clipboard operation
tapir copied to clipboard

[BUG] All optional query parameters are required

Open pk1982r opened this issue 1 year ago • 0 comments

Tapir version: 1.10.6

Scala version: 2.13.11

Query parameters declared as optional are required by the endpoint. If any parameter is omitted the endpoint returns "Invalid value for: query parameter..." error. Problematic parameters are lists of entities. Akka interpreter is used. Akka version 2.6.21, Akka http/spray/ modules in 10.2.10 version. When lists of entities are replaced by Strings issue does not occur.

What is the problem?

If you skip any "optional" parameter HTTP 400 error is returned.

curl 'https://aaaa/names?surname=111' Invalid value for: query parameter car_name (missing)

curl 'https://aaaa/names?surname=111&bike_name=123' Invalid value for: query parameter car_name (missing)

curl 'https://aaaa/names?car_name=112313&surname=111' Invalid value for: query parameter bike_name (missing)

Only query with all parameters works.

curl 'https://aaaa/names?car_name=123&surname=111&bike_name=123' carName: CarName(123)

Maybe you can provide code to reproduce the problem?

import akka.http.scaladsl.server.Route
import cats.effect.IO
import cats.effect.unsafe.implicits.global
import cats.implicits.catsSyntaxApplyOps
import org.typelevel.log4cats.slf4j.Slf4jLogger
import sttp.tapir.CodecFormat.TextPlain
import sttp.tapir.server.akkahttp.AkkaHttpServerInterpreter
import sttp.tapir.{Codec, DecodeResult, EndpointInput, endpoint, query, stringBody}

import scala.concurrent.{ExecutionContext, Future}
import scala.language.postfixOps

class NamesRoute private (
)(implicit executionContext: ExecutionContext) {

  private lazy val logger = Slf4jLogger.getLogger[IO]

  def decode[T](s: String, fromString: String => T): DecodeResult[Option[List[T]]] = s.split(",", -1).toList match {
    case Nil => DecodeResult.Value(None)
    case l   => DecodeResult.Value(Some(l.map(fromString)))
  }

  def encode[T](list: Option[List[T]], asString: T => String): String =
    list.fold("")(l => l.map(asString).mkString(","))

  case class CarName(name: String)
  case class BikeName(name: String)

  implicit val carNameCodec: Codec[String, Option[List[CarName]], TextPlain] =
    Codec.string.mapDecode(h => decode[CarName](h, CarName))(encode[CarName](_, _.name))

  implicit val stringCodec: Codec[String, Option[List[String]], TextPlain] =
    Codec.string.mapDecode(h => decode[String](h, identity))(encode[String](_, identity))

  implicit val bikeNameCodec: Codec[String, Option[List[BikeName]], TextPlain] =
    Codec.string.mapDecode(h => decode[BikeName](h, BikeName))(encode[BikeName](_, _.name))

  val identifiers: EndpointInput[
    (Option[List[CarName]], Option[List[String]], Option[List[BikeName]])
  ] =
    query[Option[List[CarName]]]("car_name")
      .and(query[Option[List[String]]]("surname"))
      .and(query[Option[List[BikeName]]]("bike_name"))

  def route: Route = {

    val names =
      endpoint.get
        .in("names")
        .in(identifiers)
        .out(stringBody)

    AkkaHttpServerInterpreter().toRoute(names.serverLogicSuccess {
      case (Some(carName), _, _)        => Future.successful(s"carName: ${carName.mkString(",")}")
      case (None, Some(surname), _)     => Future.successful(s"surname: ${surname.mkString(",")}")
      case (None, None, Some(bikeName)) => Future.successful(s"bike_name: ${bikeName.mkString(",")}")
      case params =>
        logger.error(s"improper request parameters: $params").unsafeToFuture() *>
          Future.failed(GenericError(s"improper request parameters: $params"))
    })
  }
}

pk1982r avatar May 09 '24 12:05 pk1982r