Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 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
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,7 +100,9 @@ private[http4s] class EndpointToHttp4sClient(clientOptions: Http4sClientOptions)
currentUri.withQueryParam(key, values)
}
req.withUri(uri)
case EndpointIO.Empty(_, _) => req
case EndpointIO.Empty(_, _) => req
case b @ EndpointIO.Body(_, _, _) if b.isSecondary =>
req // decoded server-side only; not part of the request the client sends
case EndpointIO.Body(bodyType, codec, _) => setBody(value, bodyType, codec, req)
case ob: EndpointIO.OneOfBody[_, _] =>
ob.headVariantBodyWithAppliedMapping match {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +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.Empty(_, _) => req
case b @ EndpointIO.Body(_, _, _) if b.isSecondary =>
req // decoded server-side only; not part of the request the client sends
case EndpointIO.Body(bodyType, codec, _) =>
val req2 = setBody(value, bodyType, codec, req)
req2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +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.Empty(_, _) => req
case b @ EndpointIO.Body(_, _, _) if b.isSecondary =>
req // decoded server-side only; not part of the request the client sends
case EndpointIO.Body(bodyType, codec, _) =>
val req2 = setBody(value, bodyType, codec, req)
req2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,10 @@ 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.Empty(_, _) => (uri, req)
case b @ EndpointIO.Body(_, _, _) if b.isSecondary =>
// decoded server-side only; not part of the request the client sends
(uri, req)
case EndpointIO.Body(bodyType, codec, _) =>
val req2 = setBody(value, bodyType, codec, req)
(uri, req2)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,10 @@ 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.Empty(_, _) => (uri, req, streamBody)
case b @ EndpointIO.Body(_, _, _) if b.isSecondary =>
// decoded server-side only; not part of the request the client sends
(uri, req, streamBody)
case EndpointIO.Body(bodyType, codec, _) =>
val req2 = setBody(value, bodyType, codec, req)
(uri, req2, streamBody)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package sttp.tapir.client.sttp4

import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import sttp.client4.Request
import sttp.model.Uri._
import sttp.tapir._

class SecondaryBodyClientTest extends AnyFlatSpec with Matchers {
it should "send only the primary body, ignoring the secondary one" in {
// the secondary body is on `in`, processed after `securityIn` - so an unskipped one would overwrite the primary
val e = endpoint.post
.in("people")
.securityIn(stringBody)
.in(stringBody.asSecondary)
.out(stringBody)

val request: Request[_] =
SttpClientInterpreter().toSecureRequestThrowDecodeFailures(e, Some(uri"http://example.com"))("sent")("ignored")

request.body.show should include("sent")
request.body.show should not include ("ignored")
}
}
15 changes: 14 additions & 1 deletion core/src/main/scala/sttp/tapir/EndpointIO.scala
Original file line number Diff line number Diff line change
Expand Up @@ -486,13 +486,26 @@ 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: ReplayableRawBody[R]): Body[R, T] = {
val _ = ev
attribute(SecondaryBody.attributeKey, SecondaryBody())
}

def isSecondary: Boolean = info.attribute(SecondaryBody.attributeKey).isDefined

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}"
}
}

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

import java.io.InputStream
import java.nio.ByteBuffer
import scala.annotation.implicitNotFound

/** Attribute value marking a body input as a secondary definition: decoded from the request on the server, but not part of the API
* contract. Secondary bodies are excluded from documentation and ignored by client interpreters, which allows the request body to be
* decoded more than once - e.g. in `serverSecurityLogic` and again in the main logic.
*
* Set using [[EndpointIO.Body.asSecondary]].
*/
case class SecondaryBody()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

both SecondaryBody & ReplayableRawBody aren't really directly used by users. So maybe they don't have to be in the main namespace, which is auto-imported by import sttp.tapir.*. Instead, maybe we could put them e.g. inside EndpointIO.Body companion object? Or maybe there's a better place existing in the hierarchy?


object SecondaryBody {
val attributeKey: AttributeKey[SecondaryBody] = new AttributeKey[SecondaryBody]("sttp.tapir.SecondaryBody")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 Claude-generated review

SecondaryBody and attributeKey are public, and so is Body.attribute — so fileBody.attribute(SecondaryBody.attributeKey, SecondaryBody()) compiles and produces exactly the state ReplayableRawBody exists to prevent. asSecondary/isSecondary are the whole intended surface; both can be private[tapir]. The key is identified by its string, so lookup is unaffected.

}

/** 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. File, multipart and streaming bodies cannot be " +
"read twice."
)
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
}
5 changes: 5 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,9 @@ package object internal {
case null => true
case _ => false
}

def isSecondaryBodyInput(input: EndpointInput[?]): Boolean = input match {
case b: EndpointIO.Body[?, ?] => b.isSecondary
case _ => false
}
}
127 changes: 127 additions & 0 deletions core/src/main/scala/sttp/tapir/server/EndpointBodyVerifier.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package sttp.tapir.server

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 Claude-generated review

The module is right — testing depends on core only — but the package makes this permanently MiMa-tracked: enableMimaSettings runs on core and excludes only sttp.tapir.internal.*, generic.internal.* and typelevel.internal.* (build.sbt:114-118), and private[tapir] is public in bytecode. Any later change to EndpointBodyProblems, or a rename of verifyOne, will need a new exclusion.

sttp.tapir.internal gets the exclusion for free, and is where the PR already put isSecondaryBodyInput.

Two smaller things: throwOnErrors (line 24) is private[tapir] inside an already-private[tapir] object, and all six call sites spell out throwOnErrors(verify(...)) / throwOnErrors(verifyOne(...)) — one verifyOrThrow with an overload would read better.


import sttp.model.Method
import sttp.tapir.internal._
import sttp.tapir.{AnyEndpoint, EndpointIO, EndpointInput, RawBodyType}

/** Errors make an endpoint unserveable; warnings describe one that works, but whose published contract probably isn't what was intended. */
private[tapir] case class EndpointBodyProblems(errors: List[String], warnings: List[String]) {
def ++(other: EndpointBodyProblems): EndpointBodyProblems =
EndpointBodyProblems(errors ++ other.errors, warnings ++ other.warnings)
}

private[tapir] object EndpointBodyProblems {
val Empty: EndpointBodyProblems = EndpointBodyProblems(Nil, Nil)
}

/** Verifies that endpoint descriptions are structurally serveable. Run by server interpreters when routes are constructed; warnings are not
* logged anywhere, so call this directly to assert on them.
*/
private[tapir] object EndpointBodyVerifier {
def verify(endpoints: List[AnyEndpoint]): EndpointBodyProblems =
endpoints.map(verifyOne).foldLeft(EndpointBodyProblems.Empty)(_ ++ _)

private[tapir] def throwOnErrors(problems: EndpointBodyProblems): Unit =
if (problems.errors.nonEmpty) throw new IllegalArgumentException(problems.errors.mkString("\n"))

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

val secondary = inputs.collect { case b: EndpointIO.Body[?, ?] if b.isSecondary => b }
def primaryBodiesOf(basics: Vector[EndpointInput.Basic[?]]): Vector[EndpointInput.Basic[?]] = basics.collect {
case b: EndpointIO.Body[?, ?] if !b.isSecondary => b
case b: EndpointIO.OneOfBody[?, ?] => b
case b: EndpointIO.StreamBodyWrapper[?, ?] => b
}
val securityPrimaryBodies = primaryBodiesOf(securityInputs)
val inPrimaryBodies = primaryBodiesOf(ordinaryInputs)
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 nonReplayablePrimary = primaryBodyAtoms.exists {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 Claude-generated review

"Which raw types can be replayed" is now stated three times: ReplayableRawBody's instances, CachingRequestBody.toRaw's cases, and here. The first two list the positives, this lists the negatives — so a new replayable raw body type added to the other two is silently treated as replayable here as well, and the failure mode is a body read from a drained request rather than a compile error.

Matching positively, or a shared private[tapir] def isReplayable(bodyType: RawBodyType[?]) next to RawBodyType, closes it.

case b: EndpointIO.Body[?, ?] =>
b.bodyType match {
case RawBodyType.FileBody => true
case _: RawBodyType.MultipartBody => true
case _ => false
}
case _ => false
}
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[?, ?] => ob }
.collect {
case ob if ob.variants.map(_.bodyAsAtom).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 tooManyPrimaries: List[String] =

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 Claude-generated review

Needs a decision before merge. This also rejects the hidden-schema pattern from #3820:

endpoint.post
  .securityIn(byteArrayBody.schema(_.hidden(true)))
  .in("api" / "echo")
  .in(jsonBody[FruitAmount])

primaryBodiesOf doesn't look at schema.hidden, so both count as primary. The pattern is still covered by VerifyYamlSecurityTest.scala:210 ("should respect hidden annotation for security body") — that keeps passing because it's a docs test, not a route-building one — and it works today on vertx/play/armeria. Apps using it will now throw IllegalArgumentException at startup.

It's the existing workaround for exactly the problem this PR solves, so either treat a hidden-schema body as secondary, or call it out explicitly in the release notes. The current message only mentions asSecondary, which doesn't help someone hitting this.

if (secondaryInsideOneOfBody.nonEmpty) Nil
else 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 the securityIn one: " +
s"stringBody.asSecondary."
)
else if (securityPrimaryBodies.size > 1)
List(
s"Endpoint $shown declares more than one request body in securityIn. Only one request body may be part " +
s"of the API contract."
)
else if (inPrimaryBodies.size > 1)
List(
s"Endpoint $shown declares more than one request body in in. Only one request body may be part of the " +
s"API contract."
)
else Nil

val streamWithSecondary =

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 Claude-generated review

Smaller things in this file:

  • streamWithSecondary (line 90) and nonReplayableWithSecondary (line 98) are the same check — secondary.nonEmpty && the primary can't be re-read — over the same primaryBodyAtoms, written twice. One condition naming the offending kind saves ~12 lines.
  • line 61: .collect { case ob: OneOfBody => ob }.collect { case ob if ... => msg } is one collect with a guard.
  • line 71: if (secondaryInsideOneOfBody.nonEmpty) Nil else ... couples two otherwise independent checks, only to avoid printing a second message that is also true. Reporting both is simpler.
  • line 106: the bodyCarryingMethod gate silently drops the warning for DELETE and for endpoints with no method set, and nothing says why.
  • line 56: val shownlazy val; it's built for every endpoint even when there are no problems.

if (streamingPrimary && secondary.nonEmpty)
List(
s"Endpoint $shown combines a streaming body with a secondary body. The request body can either be " +
s"streamed lazily or buffered for repeated reads, not both."
)
else Nil

val nonReplayableWithSecondary =
if (nonReplayablePrimary && secondary.nonEmpty)
List(
s"Endpoint $shown combines a file or multipart body with a secondary body. Reading the secondary body " +
s"consumes the request; the file or multipart body would then be read from an already-drained request."
)
else Nil

val bodyCarryingMethod = endpoint.method.exists(m => m == Method.POST || m == Method.PUT || m == Method.PATCH)
val secondaryWithoutPrimary =
if (secondary.nonEmpty && primaryBodies.isEmpty && bodyCarryingMethod)
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 the body in `in` " +
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 ++ tooManyPrimaries ++ streamWithSecondary ++ nonReplayableWithSecondary,
warnings = (secondaryWithoutPrimary ++ uselessMetadata).toList
)
}
}
57 changes: 57 additions & 0 deletions core/src/test/scala/sttp/tapir/SecondaryBodyTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package sttp.tapir

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

class SecondaryBodyTest extends AnyFlatSpec with Matchers {
it should "mark a string body as secondary" in {
stringBody.asSecondary.attribute(SecondaryBody.attributeKey) shouldBe Some(SecondaryBody())
}

it should "mark a json-style string body as secondary" in {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 Claude-generated review

Some trimming:

  • line 11 duplicates line 7 — same attribute(...) call, different body constructor, no distinct code path.
  • line 25 "preserve the codec and body type" — asSecondary is a case-class copy; this tests Scala.
  • lines 7, 21 and 51 assert the same fact through three accessors, and line 16 already asserts two of them together.

Worth a comment on line 39: assertDoesNotCompile("oneOfBody(...).asSecondary") passes because OneOfBody has no asSecondary member at all, not because of the ReplayableRawBody constraint — a future reader could easily take it as evidence the implicit is doing the work.

val body = stringBodyUtf8AnyFormat(Codec.string)
body.asSecondary.attribute(SecondaryBody.attributeKey) shouldBe Some(SecondaryBody())
}

it should "leave a plain body unmarked" in {
stringBody.attribute(SecondaryBody.attributeKey) shouldBe None
stringBody.isSecondary shouldBe false
}

it should "report a marked body through isSecondary" in {
stringBody.asSecondary.isSecondary shouldBe true
}

it should "preserve the codec and body type" in {
val secondary = byteArrayBody.asSecondary
secondary.bodyType shouldBe RawBodyType.ByteArrayBody
secondary.codec shouldBe byteArrayBody.codec
}

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

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

it should "not compile for oneOfBody" in {
assertDoesNotCompile("""oneOfBody(stringBody, stringBody).asSecondary""")
}

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

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

it should "report secondary bodies through the internal predicate" in {
import sttp.tapir.internal._
isSecondaryBodyInput(stringBody.asSecondary) shouldBe true
isSecondaryBodyInput(stringBody) shouldBe false
isSecondaryBodyInput(query[String]("q")) shouldBe false
}
}
Loading
Loading