Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
100 commits
Select commit Hold shift + click to select a range
923b5c0
feat: add extractBodyFromRequest for server-side-only body inputs
magdzikk Aug 20, 2026
dd127cf
feat: render extracted bodies distinctly in show, add internal predicate
magdzikk Aug 20, 2026
bbf5628
feat: track extracted body inputs separately when decoding basic inputs
magdzikk Aug 20, 2026
e01ab10
feat: add CachingRequestBody, buffering the request body for repeated…
magdzikk Aug 20, 2026
a55c602
fix: copy cached bytes on handout in CachingRequestBody, correct fall…
magdzikk Aug 20, 2026
09897b1
feat: decode extracted bodies, buffering the request body per request
magdzikk Aug 20, 2026
9235564
docs: explain why decodeStreamingBody bypasses the caching request body
magdzikk Aug 24, 2026
e6c183f
feat: add EndpointBodyVerifier reporting body-definition errors and w…
magdzikk Aug 24, 2026
7d11bbd
chore: naming-consistency and formatting cleanup
magdzikk Aug 24, 2026
3d31b12
chore: format remaining serverCore files touched by this branch
magdzikk Aug 24, 2026
83a427a
feat: reject invalid body definitions when routes are constructed
magdzikk Aug 24, 2026
c283c58
feat: exclude extracted bodies from generated documentation
magdzikk Aug 24, 2026
7c23c68
feat: client interpreters ignore extracted bodies
magdzikk Aug 24, 2026
a858658
test: cross-backend regression test for reading the body twice (#4442)
magdzikk Aug 24, 2026
de0f833
docs: document extractBodyFromRequest and endpoint verification
magdzikk Aug 24, 2026
9205c42
fix: flatten oneOfBody variants in EndpointBodyVerifier, correct tooM…
magdzikk Aug 24, 2026
3780461
fix: Scala 2.12 type inference for primaryBodyAtoms flatMap
magdzikk Aug 24, 2026
f610a75
chore: fix compiler warnings introduced by extractBodyFromRequest tests
magdzikk Aug 24, 2026
c51a401
docs: document the two startup errors around extracted bodies
magdzikk Aug 24, 2026
374f41a
fix: minor correctness/clarity fixes for CachingRequestBody and grpc …
magdzikk Aug 24, 2026
9dc02e8
refactor: move throwOnErrors to EndpointBodyVerifier, fix multi-error…
magdzikk Aug 24, 2026
087e1a8
refactor: rename decodeBody's requestBody parameter to bodyReader
magdzikk Aug 24, 2026
2bf63a9
test: add missing maxBytes test for CachingRequestBody
magdzikk Aug 24, 2026
2f91e54
chore: drop redundant casts and tighten comments in CachingRequestBody
magdzikk Aug 24, 2026
7c31bee
chore: trim scaladoc in EndpointBodyVerifier
magdzikk Aug 25, 2026
d62a81d
docs: trim the body-restriction explanation in security.md
magdzikk Aug 25, 2026
2784357
Merge branch 'master' into fix/extract-body-from-request-4442
magdzikk Aug 28, 2026
ab1b081
docs: drop release-notes framing from the two-bodies warning
magdzikk Aug 31, 2026
90a7f13
Reuse existing verifier
magdzikk Aug 31, 2026
5ef8634
Change naming to "secondary"
magdzikk Aug 31, 2026
9aba4f0
refactor: move startup verification out of FilterServerEndpoints
magdzikk Aug 31, 2026
54fe795
fix: reject a secondary body marked inside oneOfBody
magdzikk Sep 1, 2026
e0f70cf
Shorten the scaladoc
magdzikk Sep 1, 2026
76ae8d3
Merge remote-tracking branch 'origin/master' into fix/extract-body-fr…
magdzikk Sep 1, 2026
c58ba8b
Merge remote-tracking branch 'origin/master' into fix/extract-body-fr…
magdzikk Sep 29, 2026
de10dfd
Decode using the request's charset
magdzikk Sep 29, 2026
46ab87c
Verify endpoints once, not per request
magdzikk Sep 29, 2026
f7f4bc6
Treat hidden bodies as secondary
magdzikk Sep 29, 2026
24c6e76
Reject secondary bodies in outputs
magdzikk Sep 30, 2026
63e80b4
Hide the secondary marker from the public API
magdzikk Sep 30, 2026
b4bbd3d
Mark which body definition errors are thrown at startup
magdzikk Sep 30, 2026
ac11ec4
Document the 400 for a secondary body failing to decode
magdzikk Sep 30, 2026
d8fa605
Drop the unneeded volatile
magdzikk Sep 30, 2026
4f8e075
Rely on the real-backend tests for secondary bodies
magdzikk Sep 30, 2026
b8b55cb
Move the body verifier to internal
magdzikk Sep 30, 2026
46fe017
Make replayable raw type matches exhaustive
magdzikk Sep 30, 2026
4cbcc19
Decode secondary bodies with decodeBody
magdzikk Sep 30, 2026
fedff7a
Simplify the body verifier
magdzikk Sep 30, 2026
7482e6d
Trim redundant secondary body tests
magdzikk Sep 30, 2026
0efcde4
Inline the secondary body check in the docs
magdzikk Sep 30, 2026
1d9bb66
Document the client value and the double-body backstop
magdzikk Sep 30, 2026
b7cf488
Support only asSecondary for reading the body twice
magdzikk Sep 30, 2026
08761e8
Decode secondary bodies in the stub matchers
magdzikk Sep 30, 2026
179b03d
Rename thrownAtStartup to rejectedByServers
magdzikk Sep 30, 2026
587469c
Merge remote-tracking branch 'origin/master' into fix/extract-body-fr…
magdzikk Oct 1, 2026
973ff73
Verify stub endpoints when the backend is created
magdzikk Oct 1, 2026
846ccb4
Drop unreachable verifier check, generic asSecondary hint, verify stu…
adamw Oct 5, 2026
c08efd0
Never document secondary bodies, also in responses and oneOfBody vari…
adamw Oct 5, 2026
66c215e
Decode cached string bodies with the charset each backend uses
magdzikk Oct 13, 2026
a4e192c
Tidy secondary body messages and docs
magdzikk Oct 13, 2026
3c89772
Test secondary bodies' size limit, decode failure and use in in
magdzikk Oct 13, 2026
4ecc311
Document that a secondary body buffers the whole request body
magdzikk Oct 14, 2026
b3c5fed
Merge remote-tracking branch 'origin/master' into fix/extract-body-fr…
magdzikk Oct 14, 2026
6754186
Decode cached string bodies exactly as each backend does
magdzikk Oct 14, 2026
c255dbc
Drop redundant secondary body tests
magdzikk Oct 14, 2026
9cf5ea1
Simplify the body verifier's entry point
magdzikk Oct 14, 2026
b4547de
Drop repeated comments in client interpreters
magdzikk Oct 14, 2026
72b6a9b
Keep the body length lookup where it was
magdzikk Oct 14, 2026
7209451
Correct the verifier docs and secondary body messages
magdzikk Oct 14, 2026
9360c0d
Document the security caveats of a secondary body
magdzikk Oct 14, 2026
c3758e7
Test secondary bodies across clients and requests
magdzikk Oct 14, 2026
7efbdd1
Fail the effect when decoding a cached string body throws
magdzikk Oct 14, 2026
10d1c5c
Decode cached string bodies on AWS and Vert.x without extra copies
magdzikk Oct 14, 2026
9e6e179
Describe decodeStringBody by what it returns
magdzikk Oct 14, 2026
1a978e9
Fix two inaccuracies in the secondary body docs
magdzikk Oct 14, 2026
01621f5
Drop unneeded braces in decodeBody
magdzikk Oct 14, 2026
9d94106
Name the file or multipart check for what it covers
magdzikk Oct 14, 2026
661b2bb
Tidy two server-core test names and cases
magdzikk Oct 14, 2026
2116e5d
Test a string body which fails to decode with a secondary body
magdzikk Oct 14, 2026
e170c97
Fail a cached string decode without scheduling another task
magdzikk Oct 14, 2026
9da7a7a
Explain why each string decoding override differs
magdzikk Oct 14, 2026
8a4467e
Test that every server rejects two ordinary bodies when routes are built
magdzikk Oct 14, 2026
402d46d
Compare status codes too when checking a secondary body changes nothing
magdzikk Oct 14, 2026
0d22994
Shorten the comment on decoding cached string bodies
magdzikk Oct 14, 2026
59d4eaa
Drop two redundant secondary body tests and fix a test name
magdzikk Oct 14, 2026
7523660
Test route rejection with the documented body in securityIn and in
magdzikk Oct 14, 2026
89e9603
Drop the PrepareServerEndpoints test covered by the cross-backend one
magdzikk Oct 14, 2026
e4f0f6d
Describe the secondary body restrictions wherever the bodies are decl…
magdzikk Oct 14, 2026
f7b322d
Make three secondary body tests fail when their behaviour breaks
magdzikk Oct 14, 2026
f676e64
Rename the verifier's ordinaryInputs to inInputs
magdzikk Oct 14, 2026
b0f0d29
Drop a redundant toList in the body verifier
magdzikk Oct 14, 2026
55f2b21
Drop the scaladoc repeating the verifier's on EndpointBodyProblems
magdzikk Oct 14, 2026
50622a5
Encode AWS Lambda string bodies as UTF-8 for byte reads
magdzikk Oct 15, 2026
fa2a872
Allow at most one body per input group
magdzikk Oct 15, 2026
8b27f05
Share the string charset rule between toRaw and decodeStringBody
magdzikk Oct 15, 2026
b3156aa
Explain rejectedByServers once and shorten the secondary body docs
magdzikk Oct 15, 2026
ca1ed3d
Decode cached string bodies in one way, varying only the charset
adamw Oct 17, 2026
6e4935a
Don't suggest that secondary bodies belong in securityIn
adamw Oct 17, 2026
c6df457
Decode string request bodies with the Content-Type charset on all ser…
adamw Oct 18, 2026
6a55bff
Merge remote-tracking branch 'origin/master' into fix/extract-body-fr…
adamw Oct 18, 2026
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 @@ -100,9 +100,10 @@ private[http4s] class EndpointToHttp4sClient(clientOptions: Http4sClientOptions)
currentUri.withQueryParam(key, values)
}
req.withUri(uri)
case EndpointIO.Empty(_, _) => req
case EndpointIO.Body(bodyType, codec, _) => setBody(value, bodyType, codec, req)
case ob: EndpointIO.OneOfBody[_, _] =>
case EndpointIO.Empty(_, _) => req
case b @ EndpointIO.Body(_, _, _) if b.isSecondary => req
case EndpointIO.Body(bodyType, codec, _) => setBody(value, bodyType, codec, req)
case ob: EndpointIO.OneOfBody[_, _] =>
ob.headVariantBodyWithAppliedMapping match {
case Some(Left(body)) => setInputParams(body, params, req)
case Some(Right(EndpointIO.StreamBodyWrapper(StreamBodyIO(streams, codec, _, _, _)))) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,9 @@ private[play] class EndpointToPlayClient(clientOptions: PlayClientOptions, ws: S
case EndpointInput.QueryParams(codec, _) =>
val mqp = codec.encode(value)
req.addQueryStringParameters(mqp.toSeq*)
case EndpointIO.Empty(_, _) => req
case EndpointIO.Body(bodyType, codec, _) =>
case EndpointIO.Empty(_, _) => req
case b @ EndpointIO.Body(_, _, _) if b.isSecondary => req
case EndpointIO.Body(bodyType, codec, _) =>
val req2 = setBody(value, bodyType, codec, req)
req2
case ob: EndpointIO.OneOfBody[_, _] =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,9 @@ private[play] class EndpointToPlayClient(clientOptions: PlayClientOptions, ws: S
case EndpointInput.QueryParams(codec, _) =>
val mqp = codec.encode(value)
req.addQueryStringParameters(mqp.toSeq*)
case EndpointIO.Empty(_, _) => req
case EndpointIO.Body(bodyType, codec, _) =>
case EndpointIO.Empty(_, _) => req
case b @ EndpointIO.Body(_, _, _) if b.isSecondary => req
case EndpointIO.Body(bodyType, codec, _) =>
val req2 = setBody(value, bodyType, codec, req)
req2
case ob: EndpointIO.OneOfBody[_, _] =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,9 @@ private[sttp] class EndpointToSttpClient[R](clientOptions: SttpClientOptions, ws
val mqp = codec.encode(value)
val uri2 = uri.addParams(mqp.toSeq*)
(uri2, req)
case EndpointIO.Empty(_, _) => (uri, req)
case EndpointIO.Body(bodyType, codec, _) =>
case EndpointIO.Empty(_, _) => (uri, req)
case b @ EndpointIO.Body(_, _, _) if b.isSecondary => (uri, req)
case EndpointIO.Body(bodyType, codec, _) =>
val req2 = setBody(value, bodyType, codec, req)
(uri, req2)
case ob: EndpointIO.OneOfBody[_, _] =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,9 @@ private[sttp4] trait EndpointToSttpClientBase {
val mqp = codec.encode(value)
val uri2 = uri.addParams(mqp.toSeq*)
(uri2, req, streamBody)
case EndpointIO.Empty(_, _) => (uri, req, streamBody)
case EndpointIO.Body(bodyType, codec, _) =>
case EndpointIO.Empty(_, _) => (uri, req, streamBody)
case b @ EndpointIO.Body(_, _, _) if b.isSecondary => (uri, req, streamBody)
case EndpointIO.Body(bodyType, codec, _) =>
val req2 = setBody(value, bodyType, codec, req)
(uri, req2, streamBody)
case ob: EndpointIO.OneOfBody[_, _] =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ trait ClientBasicTests { this: ClientTests[Any] =>
Right("orange/grapefruit 10 None")
)
testClient(in_string_out_string, (), "delicious", Right("delicious"))
testClient(in_security_string_secondary_string_out_string, "sent", "ignored", Right("sent"))
testClient(in_json_out_json, (), FruitAmount("orange", 11), Right(FruitAmount("orange", 11)))
testClient(in_byte_array_out_byte_array, (), "banana kiwi".getBytes(), Right("banana kiwi".getBytes()))
testClient(in_byte_buffer_out_byte_buffer, (), ByteBuffer.wrap("mango".getBytes), Right(ByteBuffer.wrap("mango".getBytes)))
Expand Down
44 changes: 42 additions & 2 deletions core/src/main/scala/sttp/tapir/EndpointIO.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ import sttp.tapir.model.{ServerRequest, StatusCodeRange}
import sttp.tapir.typelevel.ParamConcat
import sttp.ws.WebSocketFrame

import java.io.InputStream
import java.nio.ByteBuffer
import java.nio.charset.{Charset, StandardCharsets}
import scala.annotation.StaticAnnotation
import scala.annotation.{StaticAnnotation, implicitNotFound}
import scala.collection.immutable.{ListMap, Seq}
import scala.concurrent.duration.FiniteDuration

Expand Down Expand Up @@ -486,13 +488,51 @@ object EndpointIO {
override private[tapir] type L = R
override private[tapir] type CF = CodecFormat
override private[tapir] def copyWith[U](c: Codec[R, U, CodecFormat], i: Info[U]): Body[R, U] = copy(codec = c, info = i)

/** Marks this as a secondary body definition: still decoded on the server, but not part of the API contract - excluded from the
* documentation and ignored by client interpreters. Lets the request body be decoded a second time, e.g. once in `serverSecurityLogic`
* and once in the main logic. Only bodies which can be re-read from buffered bytes may be secondary.
*/
def asSecondary(implicit ev: Body.ReplayableRawBody[R]): Body[R, T] = {
val _ = ev
attribute(SecondaryBody.Attribute, SecondaryBody())
}

/** Whether this body is marked with [[asSecondary]]. */
def isSecondary: Boolean =
// attribute keys are equal by type name, so an unrelated key with the same name mustn't mark the body as secondary
info.attribute(SecondaryBody.Attribute).contains(SecondaryBody())

override def show: String = {
val charset = bodyType.asInstanceOf[RawBodyType[?]] match {
case RawBodyType.StringBody(charset) => s" (${charset.toString})"
case _ => ""
}
val format = codec.format.mediaType
s"{body as $format$charset}"
val secondary = if (isSecondary) "secondary " else ""
s"{${secondary}body as $format$charset}"
}
}

object Body {

/** Evidence that a raw body type can be re-read from buffered bytes, and is therefore usable as a secondary body. */
@implicitNotFound(
"Cannot use a body with raw type ${R} as a secondary body. Only bodies which can be re-read from buffered bytes " +
"are supported: string, byte array, byte buffer, input stream, input stream range. File and multipart bodies " +
"cannot be read twice."
)
sealed trait ReplayableRawBody[R]

object ReplayableRawBody {
private val instance: ReplayableRawBody[Any] = new ReplayableRawBody[Any] {}
private def of[R]: ReplayableRawBody[R] = instance.asInstanceOf[ReplayableRawBody[R]]

implicit val forString: ReplayableRawBody[String] = of
implicit val forByteArray: ReplayableRawBody[Array[Byte]] = of
implicit val forByteBuffer: ReplayableRawBody[ByteBuffer] = of
implicit val forInputStream: ReplayableRawBody[InputStream] = of
implicit val forInputStreamRange: ReplayableRawBody[InputStreamRange] = of
}
}

Expand Down
121 changes: 121 additions & 0 deletions core/src/main/scala/sttp/tapir/internal/EndpointBodyVerifier.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package sttp.tapir.internal

import sttp.tapir.{AnyEndpoint, EndpointIO, EndpointInput}

private[tapir] case class EndpointBodyProblems(errors: List[String], warnings: List[String])

/** Finds request body definitions which can't be served (errors), or which can but probably aren't what was intended (warnings). Errors are
* thrown by server interpreters when routes are constructed; both are reported by `EndpointVerifier`.
*/
private[tapir] object EndpointBodyVerifier {
def verifyOrThrow(endpoints: List[AnyEndpoint]): Unit = {
val errors = endpoints.flatMap(verifyOne(_).errors)
if (errors.nonEmpty) throw new IllegalArgumentException(errors.mkString("\n"))
}

def verifyOne(endpoint: AnyEndpoint): EndpointBodyProblems = {
val securityInputs = endpoint.securityInput.asVectorOfBasicInputs()
val inInputs = endpoint.input.asVectorOfBasicInputs()
val inputs = securityInputs ++ inInputs

val secondary = inputs.collect { case b: EndpointIO.Body[?, ?] if b.isSecondary => b }
def bodiesOf(basics: Vector[EndpointInput.Basic[?]]): Vector[EndpointInput.Basic[?]] = basics.collect {
case b: EndpointIO.Body[?, ?] => b
case b: EndpointIO.OneOfBody[?, ?] => b
case b: EndpointIO.StreamBodyWrapper[?, ?] => b
}
def isPrimary(body: EndpointInput.Basic[?]): Boolean = body match {
case b: EndpointIO.Body[?, ?] => !b.isSecondary
case _ => true
}
val securityBodies = bodiesOf(securityInputs)
val inBodies = bodiesOf(inInputs)
val securityPrimaryBodies = securityBodies.filter(isPrimary)
val inPrimaryBodies = inBodies.filter(isPrimary)
val primaryBodies = securityPrimaryBodies ++ inPrimaryBodies
def asAtoms(body: EndpointInput.Basic[?]): Vector[EndpointInput.Basic[?]] = body match {
case ob: EndpointIO.OneOfBody[?, ?] => ob.variants.map(_.bodyAsAtom).toVector
case other => Vector(other)
}
val primaryBodyAtoms: Vector[EndpointInput.Basic[?]] = primaryBodies.flatMap(asAtoms)
val streamingPrimary = primaryBodyAtoms.exists(_.isInstanceOf[EndpointIO.StreamBodyWrapper[?, ?]])
val fileOrMultipartPrimary = primaryBodyAtoms.exists {
case b: EndpointIO.Body[?, ?] => !isReplayable(b.bodyType)
case _ => false
}
lazy val shown = endpoint.showShort

// asSecondary can be called on a variant, as oneOfBody takes bodies, but the server interpreters only look for
// the marker on a top-level body input - so accepting it here would silently fall back to reading the body once
val secondaryInsideOneOfBody: List[String] =
inputs.collect {
case ob: EndpointIO.OneOfBody[?, ?] if asAtoms(ob).exists { case b: EndpointIO.Body[?, ?] => b.isSecondary; case _ => false } =>
s"Endpoint $shown marks a `oneOfBody` variant as secondary. Only a body input used on its own can be " +
s"secondary; a `oneOfBody` is always part of the API contract."
}.toList

val secondaryOutputs: List[String] =
List(endpoint.output, endpoint.errorOutput)
.flatMap(_.traverseOutputs[EndpointIO.Body[?, ?]] {
case b: EndpointIO.Body[?, ?] if b.isSecondary => Vector(b)
case ob: EndpointIO.OneOfBody[?, ?] =>
ob.variants.map(_.bodyAsAtom).collect { case b: EndpointIO.Body[?, ?] if b.isSecondary => b }.toVector
})
.map(b => s"Endpoint $shown marks the response body ${b.show} as secondary. Only request bodies can be secondary.")

val hiddenSchemaHint =
if (primaryBodies.exists { case b: EndpointIO.Body[?, ?] => b.codec.schema.hidden; case _ => false })
" A hidden schema doesn't make a body secondary."
else ""

val tooManyBodies: List[String] =
if (securityPrimaryBodies.nonEmpty && inPrimaryBodies.nonEmpty)
List(
s"Endpoint $shown declares a request body in both `securityIn` and `in`. Only one may be part of the API " +
s"contract. If both should decode the same request body, mark one of them with `.asSecondary`." +
hiddenSchemaHint
)
else if (securityBodies.size > 1)
List(
s"Endpoint $shown declares more than one request body in `securityIn`. Each of `securityIn` and `in` may " +
s"declare at most one body, secondary or not.$hiddenSchemaHint"
)
else if (inBodies.size > 1)
List(
s"Endpoint $shown declares more than one request body in `in`. Each of `securityIn` and `in` may declare at " +
s"most one body, secondary or not.$hiddenSchemaHint"
)
else Nil

val nonReplayablePrimaryKinds = List(streamingPrimary -> "streaming", fileOrMultipartPrimary -> "file or multipart").collect {
case (true, kind) => kind
}
val nonReplayablePrimaryWithSecondary =
if (secondary.nonEmpty)
nonReplayablePrimaryKinds.map { kind =>
s"Endpoint $shown combines a $kind body with a secondary body. The request body is buffered to decode the " +
s"secondary body, so it can't also be read as a $kind body."
}
else Nil

val secondaryWithoutPrimary =
if (secondary.nonEmpty && primaryBodies.isEmpty)
List(
s"Endpoint $shown reads a secondary request body, but no request body is part of the API contract: it " +
s"will be absent from the documentation and clients will not send it. Either declare an ordinary body " +
s"as well, or drop `.asSecondary` and use the body input directly."
)
else Nil

val uselessMetadata =
secondary.filter(b => b.info.description.isDefined || b.info.examples.nonEmpty).map { b =>
s"Endpoint $shown sets a description or example on the secondary body ${b.show}, which never reaches the " +
s"documentation, as secondary bodies are excluded from it."
}

EndpointBodyProblems(
errors = secondaryInsideOneOfBody ++ secondaryOutputs ++ tooManyBodies ++ nonReplayablePrimaryWithSecondary,
warnings = secondaryWithoutPrimary ++ uselessMetadata
)
}
}
9 changes: 9 additions & 0 deletions core/src/main/scala/sttp/tapir/internal/SecondaryBody.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package sttp.tapir.internal

import sttp.tapir.AttributeKey

private[tapir] case class SecondaryBody()

private[tapir] object SecondaryBody {
val Attribute: AttributeKey[SecondaryBody] = new AttributeKey[SecondaryBody]("sttp.tapir.internal.SecondaryBody")
}
7 changes: 7 additions & 0 deletions core/src/main/scala/sttp/tapir/internal/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -360,4 +360,11 @@ package object internal {
case null => true
case _ => false
}

def isReplayable(bodyType: RawBodyType[?]): Boolean = bodyType match {
case RawBodyType.StringBody(_) | RawBodyType.ByteArrayBody | RawBodyType.ByteBufferBody | RawBodyType.InputStreamBody |
RawBodyType.InputStreamRangeBody =>
true
case RawBodyType.FileBody | _: RawBodyType.MultipartBody => false
}
}
22 changes: 22 additions & 0 deletions core/src/test/scala/sttp/tapir/SecondaryBodyTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package sttp.tapir

import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers

class SecondaryBodyTest extends AnyFlatSpec with Matchers {
it should "not treat a key built from the same name as a secondary marker" in {
fileBody.attribute(new AttributeKey[Unit]("sttp.tapir.internal.SecondaryBody"), ()).isSecondary shouldBe false
}

it should "not compile for file bodies" in {
assertDoesNotCompile("fileBody.asSecondary")
}

it should "not compile for multipart bodies" in {
assertDoesNotCompile("multipartBody.asSecondary")
}

it should "render a secondary body distinctly in show" in {
stringBody.asSecondary.show shouldBe "{secondary body as text/plain (UTF-8)}"
}
}
Loading
Loading