diff --git a/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpEntity.scala b/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpEntity.scala old mode 100644 new mode 100755 index a68484c92f..22f5bd5a41 --- a/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpEntity.scala +++ b/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpEntity.scala @@ -16,10 +16,13 @@ import akka.stream.stage._ import akka.stream._ import akka.{ japi, stream } import akka.http.javadsl.model.HttpEntityStrict +import akka.http.scaladsl.model.ContentType.{ NonBinary, Binary } import akka.http.scaladsl.util.FastFuture import akka.http.javadsl.{ model ⇒ jm } import akka.http.impl.util.JavaMapping.Implicits._ +import scala.util.control.NonFatal + /** * Models the entity (aka "body" or "content) of an HTTP message. */ @@ -239,6 +242,27 @@ object HttpEntity { else Default(contentType, data.length, limitableByteSource(Source.single(data))) withSizeLimit maxBytes override def productPrefix = "HttpEntity.Strict" + + override def toString = { + val dataAsString = contentType match { + case _: Binary ⇒ + data.toString() + case nb: NonBinary ⇒ + try { + val maxBytes = 4096 + if (data.length > maxBytes) { + val truncatedString = data.take(maxBytes).decodeString(nb.charset.value).dropRight(1) + s"$truncatedString ... (${data.length} bytes total)" + } else + data.decodeString(nb.charset.value) + } catch { + case NonFatal(e) ⇒ + data.toString() + } + } + + s"$productPrefix($contentType,$dataAsString)" + } } /** diff --git a/akka-http-core/src/test/scala/akka/http/scaladsl/model/HttpEntitySpec.scala b/akka-http-core/src/test/scala/akka/http/scaladsl/model/HttpEntitySpec.scala old mode 100644 new mode 100755 index c0a390d48c..e8fe77e9df --- a/akka-http-core/src/test/scala/akka/http/scaladsl/model/HttpEntitySpec.scala +++ b/akka-http-core/src/test/scala/akka/http/scaladsl/model/HttpEntitySpec.scala @@ -9,7 +9,7 @@ import com.typesafe.config.{ ConfigFactory, Config } import scala.concurrent.{ Promise, Await } import scala.concurrent.duration._ import org.scalatest.{ BeforeAndAfterAll, MustMatchers, FreeSpec } -import org.scalatest.matchers.Matcher +import org.scalatest.matchers.{ MatchResult, Matcher } import akka.util.ByteString import akka.actor.ActorSystem import akka.stream.scaladsl._ @@ -17,6 +17,8 @@ import akka.stream.ActorMaterializer import akka.http.scaladsl.model.HttpEntity._ import akka.http.impl.util.StreamUtils +import scala.util.Random + class HttpEntitySpec extends FreeSpec with MustMatchers with BeforeAndAfterAll { val tpe: ContentType = ContentTypes.`application/octet-stream` val abc = ByteString("abc") @@ -28,7 +30,6 @@ class HttpEntitySpec extends FreeSpec with MustMatchers with BeforeAndAfterAll { akka.event-handlers = ["akka.testkit.TestEventListener"] akka.loglevel = WARNING""") implicit val system = ActorSystem(getClass.getSimpleName, testConf) - import system.dispatcher implicit val materializer = ActorMaterializer() override def afterAll() = system.shutdown() @@ -117,6 +118,36 @@ class HttpEntitySpec extends FreeSpec with MustMatchers with BeforeAndAfterAll { transformTo(Strict(tpe, doubleChars("abcfghijk") ++ trailer)) } } + "support toString" - { + "Strict with binary MediaType" in { + val binaryType = ContentTypes.`application/octet-stream` + val entity = Strict(binaryType, abc) + entity must renderStrictDataAs(entity.data.toString()) + } + "Strict with non-binary MediaType and less than 4096 bytes" in { + val nonBinaryType = ContentTypes.`application/json` + val entity = Strict(nonBinaryType, abc) + entity must renderStrictDataAs(entity.data.decodeString(nonBinaryType.charset.value)) + } + "Strict with non-binary MediaType and over 4096 bytes" in { + val utf8Type = ContentTypes.`text/plain(UTF-8)` + val longString = Random.alphanumeric.take(10000).mkString + val entity = Strict(utf8Type, ByteString.apply(longString, utf8Type.charset.value)) + entity must renderStrictDataAs(s"${longString.take(4095)} ... (10000 bytes total)") + } + "Default" in { + val entity = Default(tpe, 11, source(abc, de, fgh, ijk)) + entity.toString must include(entity.productPrefix) + } + "CloseDelimited" in { + val entity = CloseDelimited(tpe, source(abc, de, fgh, ijk)) + entity.toString must include(entity.productPrefix) + } + "Chunked" in { + val entity = Chunked(tpe, source(Chunk(abc))) + entity.toString must include(entity.productPrefix) + } + } } def source[T](elems: T*) = Source(elems.toList) @@ -136,6 +167,15 @@ class HttpEntitySpec extends FreeSpec with MustMatchers with BeforeAndAfterAll { Await.result(transformed.toStrict(250.millis), 250.millis) } + def renderStrictDataAs(dataRendering: String): Matcher[Strict] = + Matcher { strict: Strict ⇒ + val expectedRendering = s"${strict.productPrefix}(${strict.contentType},$dataRendering)" + MatchResult( + strict.toString == expectedRendering, + strict.toString + " != " + expectedRendering, + strict.toString + " == " + expectedRendering) + } + def duplicateBytesTransformer(): Flow[ByteString, ByteString, Unit] = Flow[ByteString].transform(() ⇒ StreamUtils.byteStringTransformer(doubleChars, () ⇒ trailer))