Missing header I get 415

endpoint.post
  .in("any")
  .in(header(Header.contentType(MediaType.ApplicationXWwwFormUrlencoded)))

my endpoint looks like this, but when I make a request and I don’t specify the header I want to receive a 400 not a 415 as it does now, how could I map it?

I am using Http4sServerInterpreter

Thanks

You can use a custom DecodeFailureHandler, here’s a scala-cli example:

//> using lib "com.softwaremill.sttp.tapir::tapir-core:1.9.9"
//> using lib "com.softwaremill.sttp.tapir::tapir-http4s-server:1.9.9"
//> using lib "org.http4s::http4s-blaze-server:0.23.16"
//> using lib "org.http4s::http4s-dsl:0.23.25"

import cats.effect._
import org.http4s._
import org.http4s.dsl.io._
import org.http4s.server.blaze._
import org.http4s.implicits._
import scala.concurrent.ExecutionContext.{global => ecGlobal}
import sttp.tapir._
import sttp.tapir.server.http4s.Http4sServerInterpreter
import cats.effect.unsafe.implicits.global
import sttp.model.Header
import sttp.model.MediaType
import sttp.tapir.server.http4s.Http4sServerOptions
import sttp.tapir.server.interceptor.decodefailure.DecodeFailureHandler
import sttp.tapir.server.interceptor.DecodeFailureContext
import sttp.monad.MonadError
import sttp.tapir.server.model.ValuedEndpointOutput
import sttp.tapir.server.interceptor.decodefailure.DefaultDecodeFailureHandler
import sttp.tapir.DecodeResult.Mismatch
import sttp.tapir.DecodeResult.InvalidValue
import sttp.model.StatusCode
import sttp.tapir.EndpointIO.FixedHeader
import sttp.model.HeaderNames

val helloEndpoint = endpoint.post
  .in("hello")
  .in(header(Header.contentType(MediaType.ApplicationXWwwFormUrlencoded)))
  .out(stringBody)

//------------------------------------------------------------
val customDecodeFailureHandler: DecodeFailureHandler[IO] = new DecodeFailureHandler[IO] {
  val defaultHandler = DefaultDecodeFailureHandler[IO]
  override def apply(ctx: DecodeFailureContext)(implicit monad: MonadError[IO]): IO[Option[ValuedEndpointOutput[_]]] =
    ctx.failingInput match {
      case FixedHeader(Header(HeaderNames.ContentType, "application/x-www-form-urlencoded"), _, _) =>
        IO.pure(Some(ValuedEndpointOutput(statusCode.and(sttp.tapir.headers).and(stringBody), (StatusCode.BadRequest, List.empty, "Your bad request message"))))
      case other =>
        defaultHandler(ctx) 
    }
}
val options = Http4sServerOptions.customiseInterceptors[IO].decodeFailureHandler(customDecodeFailureHandler).options
//------------------------------------------------------------
val helloRoute = Http4sServerInterpreter[IO](options).toRoutes(helloEndpoint.serverLogic[IO](_ => IO(Right("Hello"))))
val httpApp: HttpApp[IO] = helloRoute.orNotFound
val server = BlazeServerBuilder[IO]
  .withExecutionContext(ecGlobal)
  .bindHttp(8080, "localhost")
  .withHttpApp(httpApp)
  .resource

def runServer: IO[Unit] = server.use(_ => IO.never)

def main(args: Array[String]): Unit = {
  runServer.unsafeRunSync()
}

Note that this handler will be applied gobally. If you want to limit it to certain endpoints and leave default handling in others, consider adding endpoint attributes to the condition.

@kciesielski however … the code only specified to return 415 when there’s a mismatch on the header values. If the header is missing (which I think should be represented as DecodeResult.Missing), then you would get a 400 I suppose?

@M_Perez are you sure you are not sending any Content-Type header in your request?

@adamw hmm I checked that in the morning and I was pretty sure it also returned 415 for missing header (which made me wonder too), but now I repeated the test and I’m getting 400 with message:

Invalid value for: header Content-Type: application/x-www-form-urlencoded (missing)

as expected.

@M_Perez please check if your header is indeed missing. Maybe it’s set with unexpected value by the client library or some entities between client and server (proxies, etc).