Change JsonFraming to fail stage if completing within an object #29228
This commit is contained in:
parent
f6ceb4d49a
commit
2f2ee9e67c
3 changed files with 47 additions and 17 deletions
|
|
@ -4,18 +4,18 @@
|
||||||
|
|
||||||
package akka.stream.scaladsl
|
package akka.stream.scaladsl
|
||||||
|
|
||||||
|
import akka.stream.impl.JsonObjectParser
|
||||||
|
import akka.stream.scaladsl.Framing.FramingException
|
||||||
|
import akka.stream.scaladsl.JsonFraming.PartialObjectException
|
||||||
|
import akka.stream.testkit.scaladsl.TestSink
|
||||||
|
import akka.stream.testkit.{ TestPublisher, TestSubscriber }
|
||||||
|
import akka.testkit.AkkaSpec
|
||||||
|
import akka.util.ByteString
|
||||||
|
|
||||||
import scala.collection.immutable.Seq
|
import scala.collection.immutable.Seq
|
||||||
import scala.concurrent.Await
|
import scala.concurrent.Await
|
||||||
import scala.concurrent.duration._
|
import scala.concurrent.duration._
|
||||||
|
|
||||||
import akka.stream.impl.JsonObjectParser
|
|
||||||
import akka.stream.scaladsl.Framing.FramingException
|
|
||||||
import akka.stream.testkit.TestPublisher
|
|
||||||
import akka.stream.testkit.TestSubscriber
|
|
||||||
import akka.stream.testkit.scaladsl.TestSink
|
|
||||||
import akka.testkit.AkkaSpec
|
|
||||||
import akka.util.ByteString
|
|
||||||
|
|
||||||
class JsonFramingSpec extends AkkaSpec {
|
class JsonFramingSpec extends AkkaSpec {
|
||||||
|
|
||||||
"collecting multiple json" should {
|
"collecting multiple json" should {
|
||||||
|
|
@ -515,13 +515,29 @@ class JsonFramingSpec extends AkkaSpec {
|
||||||
|
|
||||||
probe.ensureSubscription()
|
probe.ensureSubscription()
|
||||||
probe
|
probe
|
||||||
.request(1)
|
.requestNext(ByteString("""{ "name": "john" }"""))
|
||||||
.expectNext(ByteString("""{ "name": "john" }"""))
|
.requestNext(ByteString("""{ "name": "jack" }"""))
|
||||||
.request(1)
|
|
||||||
.expectNext(ByteString("""{ "name": "jack" }"""))
|
|
||||||
.request(1)
|
.request(1)
|
||||||
.expectError()
|
.expectError()
|
||||||
.getMessage should include("exceeded")
|
.getMessage should include("exceeded")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"fail when completing inside an object" in {
|
||||||
|
val input = ByteString("{")
|
||||||
|
val probe = Source.single(input).via(JsonFraming.objectScanner(48)).runWith(TestSink.probe)
|
||||||
|
|
||||||
|
probe.ensureSubscription()
|
||||||
|
probe.request(1).expectError() shouldBe a[PartialObjectException]
|
||||||
|
}
|
||||||
|
|
||||||
|
"fail when pushing and inside an object" in {
|
||||||
|
val input = """ { "name": "john" }, { """
|
||||||
|
Source
|
||||||
|
.single(ByteString(input))
|
||||||
|
.via(JsonFraming.objectScanner(Int.MaxValue))
|
||||||
|
.runWith(Sink.last)
|
||||||
|
.failed
|
||||||
|
.futureValue shouldBe a[PartialObjectException]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,9 @@ import akka.util.ByteString
|
||||||
|
|
||||||
def isEmpty: Boolean = buffer.isEmpty
|
def isEmpty: Boolean = buffer.isEmpty
|
||||||
|
|
||||||
|
/** `true` if the buffer is in a valid state to end framing. */
|
||||||
|
def canComplete: Boolean = !insideObject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempt to locate next complete JSON object in buffered ByteString and returns `Some(it)` if found.
|
* Attempt to locate next complete JSON object in buffered ByteString and returns `Some(it)` if found.
|
||||||
* May throw a [[akka.stream.scaladsl.Framing.FramingException]] if the contained JSON is invalid or max object size is exceeded.
|
* May throw a [[akka.stream.scaladsl.Framing.FramingException]] if the contained JSON is invalid or max object size is exceeded.
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,23 @@
|
||||||
|
|
||||||
package akka.stream.scaladsl
|
package akka.stream.scaladsl
|
||||||
|
|
||||||
import scala.util.control.NonFatal
|
|
||||||
|
|
||||||
import akka.NotUsed
|
import akka.NotUsed
|
||||||
import akka.stream.Attributes
|
import akka.stream.Attributes
|
||||||
import akka.stream.impl.JsonObjectParser
|
import akka.stream.impl.JsonObjectParser
|
||||||
import akka.stream.impl.fusing.GraphStages.SimpleLinearGraphStage
|
import akka.stream.impl.fusing.GraphStages.SimpleLinearGraphStage
|
||||||
|
import akka.stream.scaladsl.Framing.FramingException
|
||||||
import akka.stream.stage.{ GraphStageLogic, InHandler, OutHandler }
|
import akka.stream.stage.{ GraphStageLogic, InHandler, OutHandler }
|
||||||
import akka.util.ByteString
|
import akka.util.ByteString
|
||||||
|
|
||||||
|
import scala.util.control.NonFatal
|
||||||
|
|
||||||
/** Provides JSON framing operators that can separate valid JSON objects from incoming [[ByteString]] objects. */
|
/** Provides JSON framing operators that can separate valid JSON objects from incoming [[ByteString]] objects. */
|
||||||
object JsonFraming {
|
object JsonFraming {
|
||||||
|
|
||||||
|
/** Thrown if upstream completes with a partial object in the buffer. */
|
||||||
|
class PartialObjectException(msg: String = "JSON stream completed with partial content in the buffer!")
|
||||||
|
extends FramingException(msg)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a Flow that implements a "brace counting" based framing operator for emitting valid JSON chunks.
|
* Returns a Flow that implements a "brace counting" based framing operator for emitting valid JSON chunks.
|
||||||
* It scans the incoming data stream for valid JSON objects and returns chunks of ByteStrings containing only those valid chunks.
|
* It scans the incoming data stream for valid JSON objects and returns chunks of ByteStrings containing only those valid chunks.
|
||||||
|
|
@ -37,6 +42,8 @@ object JsonFraming {
|
||||||
* elements are separated by multiple newlines or other whitespace characters. And of course is insensitive
|
* elements are separated by multiple newlines or other whitespace characters. And of course is insensitive
|
||||||
* (and does not impact the emitting frame) to the JSON object's internal formatting.
|
* (and does not impact the emitting frame) to the JSON object's internal formatting.
|
||||||
*
|
*
|
||||||
|
* If the stream completes while mid-object, the stage will fail with a [[PartialObjectException]].
|
||||||
|
*
|
||||||
* @param maximumObjectLength The maximum length of allowed frames while decoding. If the maximum length is exceeded
|
* @param maximumObjectLength The maximum length of allowed frames while decoding. If the maximum length is exceeded
|
||||||
* this Flow will fail the stream.
|
* this Flow will fail the stream.
|
||||||
*/
|
*/
|
||||||
|
|
@ -62,18 +69,22 @@ object JsonFraming {
|
||||||
override def onUpstreamFinish(): Unit = {
|
override def onUpstreamFinish(): Unit = {
|
||||||
buffer.poll() match {
|
buffer.poll() match {
|
||||||
case Some(json) => emit(out, json)
|
case Some(json) => emit(out, json)
|
||||||
case _ => completeStage()
|
case _ => complete()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def tryPopBuffer() = {
|
def tryPopBuffer(): Unit = {
|
||||||
try buffer.poll() match {
|
try buffer.poll() match {
|
||||||
case Some(json) => push(out, json)
|
case Some(json) => push(out, json)
|
||||||
case _ => if (isClosed(in)) completeStage() else pull(in)
|
case _ => if (isClosed(in)) complete() else pull(in)
|
||||||
} catch {
|
} catch {
|
||||||
case NonFatal(ex) => failStage(ex)
|
case NonFatal(ex) => failStage(ex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def complete(): Unit =
|
||||||
|
if (buffer.canComplete) completeStage()
|
||||||
|
else failStage(new PartialObjectException)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue