Skip to content

Instantly share code, notes, and snippets.

@jelinski
Last active July 17, 2025 14:06
Show Gist options
  • Save jelinski/2f3e8c5098fd4b5a4b3897a2f872d7fd to your computer and use it in GitHub Desktop.
Save jelinski/2f3e8c5098fd4b5a4b3897a2f872d7fd to your computer and use it in GitHub Desktop.
An example showing how to dynamically set header value for Content-Disposition HTTP header using Tapir
import cats.effect.IO
import cats.syntax.all.*
import sttp.capabilities.fs2.Fs2Streams
import sttp.model.HeaderNames.ContentDisposition
import sttp.tapir.EndpointIO.Header
import sttp.tapir.*
import java.nio.charset.StandardCharsets
// This is an example showing how to dynamically set the Content-Disposition HTTP header in Tapir
object ContentDispositionTapirExample {
import ContentDispositionHeader.*
val route =
endpoint.get
.in("download")
.in(path[String].name("fileName"))
.out(contentDispositionHeader)
.out(streamBinaryBody(Fs2Streams[IO])(CodecFormat.OctetStream()))
val attachmentWithFileNameEndpoint =
route
.in("attachment-with-file-name")
.serverLogicSuccessPure { fileName =>
attachmentWithFileName(fileName) -> downloadFile(fileName)
}
val attachmentWithoutFileNameEndpoint =
route
.in("attachment")
.serverLogicSuccessPure { fileName =>
attachment -> downloadFile(fileName)
}
val inlineContentEndpoint =
route
.in("inline")
.serverLogicSuccessPure { fileName =>
inline -> downloadFile(fileName)
}
def downloadFile(fileName: String) =
fs2.Stream(s"Contents from file: $fileName".getBytes(StandardCharsets.UTF_8).toSeq*).covary[IO]
sealed trait ContentDispositionHeader
object ContentDispositionHeader {
def attachmentWithFileName(fileName: String): ContentDispositionHeader = ContentDispositionAttachment(fileName.some)
def attachment: ContentDispositionHeader = ContentDispositionAttachment(none)
def inline: ContentDispositionHeader = ContentDispositionInline()
private case class ContentDispositionAttachment(fileName: Option[String]) extends ContentDispositionHeader
private case class ContentDispositionInline() extends ContentDispositionHeader
val contentDispositionHeader: Header[ContentDispositionHeader] =
header[String](ContentDisposition)
.mapDecode(
DecodeResult
.Error(_, new IllegalArgumentException("Content-Disposition header is not expected in HTTP Request"))
) {
case ContentDispositionAttachment(Some(fileName)) => s"attachment; filename=\"$fileName\""
case ContentDispositionAttachment(None) => "attachment"
case ContentDispositionInline() => "inline"
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment