You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@openwhisk.apache.org by cs...@apache.org on 2017/08/16 16:50:55 UTC

[incubator-openwhisk] branch master updated: Support json response in HTTP web actions without base64 encoding. (#2609)

This is an automated email from the ASF dual-hosted git repository.

csantanapr pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-openwhisk.git


The following commit(s) were added to refs/heads/master by this push:
     new de8340a  Support json response in HTTP web actions without base64 encoding. (#2609)
de8340a is described below

commit de8340a8c5351d0f1935f0734684334c79e37073
Author: rodric rabbah <ro...@gmail.com>
AuthorDate: Wed Aug 16 12:50:52 2017 -0400

    Support json response in HTTP web actions without base64 encoding. (#2609)
---
 .../scala/whisk/core/controller/WebActions.scala   | 33 +++++++++++---
 docs/webactions.md                                 |  6 ++-
 .../core/controller/test/WebActionsApiTests.scala  | 52 ++++++++++++++++++++--
 3 files changed, 79 insertions(+), 12 deletions(-)

diff --git a/core/controller/src/main/scala/whisk/core/controller/WebActions.scala b/core/controller/src/main/scala/whisk/core/controller/WebActions.scala
index 1c9ec8e..c26d32b 100644
--- a/core/controller/src/main/scala/whisk/core/controller/WebActions.scala
+++ b/core/controller/src/main/scala/whisk/core/controller/WebActions.scala
@@ -42,7 +42,7 @@ import akka.http.scaladsl.model.headers.`Content-Type`
 import akka.http.scaladsl.model.ContentType
 import akka.http.scaladsl.model.ContentTypes
 import akka.http.scaladsl.model.FormData
-import akka.http.scaladsl.model.HttpMethods.{ OPTIONS, GET, DELETE, POST, PUT, HEAD, PATCH}
+import akka.http.scaladsl.model.HttpMethods.{ OPTIONS, GET, DELETE, POST, PUT, HEAD, PATCH }
 import akka.http.scaladsl.model.HttpCharsets
 
 import spray.json._
@@ -241,7 +241,7 @@ protected[core] object WhiskWebActionsApi extends Directives {
 
             fields.get("body") map {
                 case JsString(str) => interpretHttpResponse(code, headers, str, transid)
-                case _             => terminate(BadRequest, Messages.httpContentTypeError)(transid)
+                case js            => interpretHttpResponseAsJson(code, headers, js, transid)
             } getOrElse {
                 respondWithHeaders(headers) {
                     // note that if header defined a content-type, it will be ignored
@@ -256,7 +256,7 @@ protected[core] object WhiskWebActionsApi extends Directives {
         }
     }
 
-    private def headersFromJson(k: String, v: JsValue) : Seq[RawHeader] = v match {
+    private def headersFromJson(k: String, v: JsValue): Seq[RawHeader] = v match {
         case JsString(v)  => Seq(RawHeader(k, v))
         case JsBoolean(v) => Seq(RawHeader(k, v.toString))
         case JsNumber(v)  => Seq(RawHeader(k, v.toString))
@@ -264,8 +264,12 @@ protected[core] object WhiskWebActionsApi extends Directives {
         case _            => throw new Throwable("Invalid header")
     }
 
-    private def interpretHttpResponse(code: StatusCode, headers: List[RawHeader], str: String, transid: TransactionId) = {
-        val parsedHeader: Try[MediaType] = headers.find(_.lowercaseName == `Content-Type`.lowercaseName) match {
+    /**
+     * Finds the content-type in the header list and maps it to a known media type. If it is not
+     * recognized, construct a failure with appropriate message.
+     */
+    private def findContentTypeInHeader(headers: List[RawHeader], transid: TransactionId, defaultType: MediaType): Try[MediaType] = {
+        headers.find(_.lowercaseName == `Content-Type`.lowercaseName) match {
             case Some(header) =>
                 MediaType.parse(header.value) match {
                     case Right(mediaType: MediaType) =>
@@ -276,10 +280,25 @@ protected[core] object WhiskWebActionsApi extends Directives {
                         }
                     case _ => Failure(RejectRequest(BadRequest, Messages.httpUnknownContentType)(transid))
                 }
-            case None => Success(`text/html`)
+            case None => Success(defaultType)
         }
+    }
+
+    private def interpretHttpResponseAsJson(code: StatusCode, headers: List[RawHeader], js: JsValue, transid: TransactionId) = {
+        findContentTypeInHeader(headers, transid, `application/json`) match {
+            case Success(mediaType) if (mediaType == `application/json`) =>
+                respondWithHeaders(headers) {
+                    complete(code, js)
+                }
 
-        parsedHeader.flatMap { mediaType =>
+            case _ =>
+                terminate(BadRequest, Messages.httpContentTypeError)(transid)
+        }
+    }
+
+    private def interpretHttpResponse(code: StatusCode, headers: List[RawHeader], str: String, transid: TransactionId) = {
+        findContentTypeInHeader(headers, transid, `text/html`).flatMap { mediaType =>
+            // base64 encoded json response supported for legacy reasons
             if (mediaType.binary || mediaType == `application/json`) {
                 Try(new String(Base64.getDecoder().decode(str), StandardCharsets.UTF_8)).map((mediaType, _))
             } else {
diff --git a/docs/webactions.md b/docs/webactions.md
index c597a98..4be9ef5 100644
--- a/docs/webactions.md
+++ b/docs/webactions.md
@@ -102,11 +102,13 @@ function main(params) {
     return {
         statusCode: 200,
         headers: { 'Content-Type': 'application/json' },
-        body: new Buffer(JSON.stringify(params)).toString('base64'),
+        body: params
     };
 }
 ```
 
+The default content-type for an HTTP response is `application/json` and the body may be any allowed JSON value. The default content-type may be omitted from the headers.
+
 It is important to be aware of the [response size limit](reference.md) for actions since a response that exceeds the predefined system limits will fail. Large objects should not be sent inline through OpenWhisk, but instead deferred to an object store, for example.
 
 ## Handling HTTP requests with actions
@@ -309,7 +311,7 @@ $ curl https://${APIHOST}/api/v1/web/guest/demo/hello.json?name=Jane -X POST -H
 }
 ```
 
-Notice in this case the JSON content is base64 encoded because it is treated as a binary value. The action must base64 decode and JSON parse this value to recover the JSON object. OpenWhisk uses the [Spray](https://github.com/spray/spray) framework to [determine](https://github.com/spray/spray/blob/master/spray-http/src/main/scala/spray/http/MediaType.scala#L282) which content types are binary and which are plain text.
+OpenWhisk uses the [Akka Http](http://doc.akka.io/docs/akka-http/current/scala/http/) framework to [determine](http://doc.akka.io/api/akka-http/10.0.4/akka/http/scaladsl/model/MediaTypes$.html) which content types are binary and which are plain text.
 
 
 ### Enabling raw HTTP handling
diff --git a/tests/src/test/scala/whisk/core/controller/test/WebActionsApiTests.scala b/tests/src/test/scala/whisk/core/controller/test/WebActionsApiTests.scala
index d0b4f0a..6cd92e4 100644
--- a/tests/src/test/scala/whisk/core/controller/test/WebActionsApiTests.scala
+++ b/tests/src/test/scala/whisk/core/controller/test/WebActionsApiTests.scala
@@ -771,7 +771,7 @@ trait WebActionsApiTests extends ControllerTestCommon with BeforeAndAfterEach wi
                 }
         }
 
-        it should s"handle http web action with base64 encoded response (auth? ${creds.isDefined})" in {
+        it should s"handle http web action with base64 encoded JSON response (auth? ${creds.isDefined})" in {
             implicit val tid = transid()
 
             Seq(s"$systemId/proxy/export_c.http").
@@ -795,6 +795,53 @@ trait WebActionsApiTests extends ControllerTestCommon with BeforeAndAfterEach wi
                 }
         }
 
+        it should s"handle http web action without base64 encoded JSON response (auth? ${creds.isDefined})" in {
+            implicit val tid = transid()
+
+            Seq((JsObject("content-type" -> "application/json".toJson), OK),
+                (JsObject(), OK),
+                (JsObject("content-type" -> "text/html".toJson), BadRequest)).foreach {
+                    case (headers, expectedCode) =>
+                        Seq(s"$systemId/proxy/export_c.http").
+                            foreach { path =>
+                                allowedMethods.foreach { m =>
+                                    invocationsAllowed += 1
+                                    actionResult = Some(JsObject(
+                                        "headers" -> headers,
+                                        webApiDirectives.statusCode -> OK.intValue.toJson,
+                                        "body" -> JsObject("field" -> "value".toJson)))
+
+                                    m(s"$testRoutePath/$path") ~> Route.seal(routes(creds)) ~> check {
+                                        status should be(expectedCode)
+
+                                        if (expectedCode == OK) {
+                                            header("content-type").map(_.toString shouldBe "content-type: application/json")
+                                            responseAs[JsObject] shouldBe JsObject("field" -> "value".toJson)
+                                        } else {
+                                            confirmErrorWithTid(responseAs[JsObject], Some(Messages.httpContentTypeError))
+                                        }
+                                    }
+                                }
+                            }
+                }
+
+            Seq(s"$systemId/proxy/export_c.http").
+                foreach { path =>
+                    allowedMethods.foreach { m =>
+                        invocationsAllowed += 1
+                        actionResult = Some(JsObject(
+                            webApiDirectives.statusCode -> OK.intValue.toJson,
+                            "body" -> JsNumber(3)))
+
+                        m(s"$testRoutePath/$path") ~> Route.seal(routes(creds)) ~> check {
+                            status should be(OK)
+                            header("content-type").map(_.toString shouldBe "content-type: application/json")
+                            responseAs[String].toInt shouldBe 3
+                        }
+                    }
+                }
+        }
+
         it should s"handle http web action with html/text response (auth? ${creds.isDefined})" in {
             implicit val tid = transid()
 
@@ -1149,8 +1196,7 @@ trait WebActionsApiTests extends ControllerTestCommon with BeforeAndAfterEach wi
                     Options(s"$testRoutePath/$path") ~> Route.seal(routes(creds)) ~> check {
                         headers should contain allOf (
                             RawHeader("Set-Cookie", "a=b"),
-                            RawHeader("Set-Cookie", "c=d; Path = /")
-                        )
+                            RawHeader("Set-Cookie", "c=d; Path = /"))
                     }
                 }
         }

-- 
To stop receiving notification emails like this one, please contact
['"commits@openwhisk.apache.org" <co...@openwhisk.apache.org>'].