Merge pull request #18667 from analytically/release-2.3-dev

=htc accept entity for DELETE and GET methods
This commit is contained in:
Roland Kuhn 2015-10-21 17:44:27 +02:00
commit 142ffea52b
6 changed files with 66 additions and 22 deletions

View file

@ -30,4 +30,9 @@ public abstract class HttpMethod {
* Returns if requests with this method may contain an entity.
*/
public abstract boolean isEntityAccepted();
/**
* Returns the entity acceptance level for this method.
*/
public abstract akka.http.scaladsl.model.RequestEntityAcceptance requestEntityAcceptance();
}

View file

@ -27,8 +27,8 @@ public final class HttpMethods {
/**
* Create a custom method type.
*/
public static HttpMethod custom(String value, boolean safe, boolean idempotent, boolean entityAccepted) {
return akka.http.scaladsl.model.HttpMethod.custom(value, safe, idempotent, entityAccepted);
public static HttpMethod custom(String value, boolean safe, boolean idempotent, akka.http.scaladsl.model.RequestEntityAcceptance requestEntityAcceptance) {
return akka.http.scaladsl.model.HttpMethod.custom(value, safe, idempotent, requestEntityAcceptance);
}
/**

View file

@ -5,6 +5,7 @@
package akka.http.impl.engine.rendering
import akka.http.ClientConnectionSettings
import akka.http.scaladsl.model.RequestEntityAcceptance._
import scala.annotation.tailrec
import akka.event.LoggingAdapter
@ -99,7 +100,7 @@ private[http] class HttpRequestRendererFactory(userAgentHeader: Option[headers.`
}
def renderContentLength(contentLength: Long) =
if (method.isEntityAccepted) r ~~ `Content-Length` ~~ contentLength ~~ CrLf else r
if (method.isEntityAccepted && (contentLength > 0 || method.requestEntityAcceptance == Expected)) r ~~ `Content-Length` ~~ contentLength ~~ CrLf else r
def renderStreamed(body: Source[ByteString, Any]): RequestRenderingOutput =
RequestRenderingOutput.Streamed(renderByteStrings(r, body))

View file

@ -6,47 +6,65 @@ package akka.http.scaladsl.model
import akka.http.impl.util._
import akka.http.javadsl.{ model jm }
import akka.http.scaladsl.model.RequestEntityAcceptance._
sealed trait RequestEntityAcceptance {
def isEntityAccepted: Boolean
}
object RequestEntityAcceptance {
case object Expected extends RequestEntityAcceptance {
override def isEntityAccepted: Boolean = true
}
case object Tolerated extends RequestEntityAcceptance {
override def isEntityAccepted: Boolean = true
}
case object Disallowed extends RequestEntityAcceptance {
override def isEntityAccepted: Boolean = false
}
}
/**
* The method of an HTTP request.
* @param isSafe true if the resource should not be altered on the server
* @param isIdempotent true if requests can be safely (& automatically) repeated
* @param isEntityAccepted true if meaning of request entities is properly defined
* @param requestEntityAcceptance Expected if meaning of request entities is properly defined
*/
final case class HttpMethod private[http] (override val value: String,
isSafe: Boolean,
isIdempotent: Boolean,
isEntityAccepted: Boolean) extends jm.HttpMethod with SingletonValueRenderable {
requestEntityAcceptance: RequestEntityAcceptance) extends jm.HttpMethod with SingletonValueRenderable {
def name = value
override def isEntityAccepted: Boolean = requestEntityAcceptance.isEntityAccepted
override def toString: String = s"HttpMethod($value)"
}
object HttpMethod {
def custom(name: String, safe: Boolean, idempotent: Boolean, entityAccepted: Boolean): HttpMethod = {
def custom(name: String, safe: Boolean, idempotent: Boolean, requestEntityAcceptance: RequestEntityAcceptance): HttpMethod = {
require(name.nonEmpty, "value must be non-empty")
require(!safe || idempotent, "An HTTP method cannot be safe without being idempotent")
apply(name, safe, idempotent, entityAccepted)
apply(name, safe, idempotent, requestEntityAcceptance)
}
/**
* Creates a custom method by name and assumes properties conservatively to be
* safe = idempotent = false and entityAccepted = true.
* safe = false, idempotent = false and requestEntityAcceptance = Expected.
*/
def custom(name: String): HttpMethod = custom(name, safe = false, idempotent = false, entityAccepted = true)
def custom(name: String): HttpMethod = custom(name, safe = false, idempotent = false, requestEntityAcceptance = Expected)
}
object HttpMethods extends ObjectRegistry[String, HttpMethod] {
private def register(method: HttpMethod): HttpMethod = register(method.value, method)
// format: OFF
val CONNECT = register(HttpMethod("CONNECT", isSafe = false, isIdempotent = false, isEntityAccepted = false))
val DELETE = register(HttpMethod("DELETE" , isSafe = false, isIdempotent = true , isEntityAccepted = false))
val GET = register(HttpMethod("GET" , isSafe = true , isIdempotent = true , isEntityAccepted = false))
val HEAD = register(HttpMethod("HEAD" , isSafe = true , isIdempotent = true , isEntityAccepted = false))
val OPTIONS = register(HttpMethod("OPTIONS", isSafe = true , isIdempotent = true , isEntityAccepted = true))
val PATCH = register(HttpMethod("PATCH" , isSafe = false, isIdempotent = false, isEntityAccepted = true))
val POST = register(HttpMethod("POST" , isSafe = false, isIdempotent = false, isEntityAccepted = true))
val PUT = register(HttpMethod("PUT" , isSafe = false, isIdempotent = true , isEntityAccepted = true))
val TRACE = register(HttpMethod("TRACE" , isSafe = true , isIdempotent = true , isEntityAccepted = false))
val CONNECT = register(HttpMethod("CONNECT", isSafe = false, isIdempotent = false, requestEntityAcceptance = Disallowed))
val DELETE = register(HttpMethod("DELETE" , isSafe = false, isIdempotent = true , requestEntityAcceptance = Tolerated))
val GET = register(HttpMethod("GET" , isSafe = true , isIdempotent = true , requestEntityAcceptance = Tolerated))
val HEAD = register(HttpMethod("HEAD" , isSafe = true , isIdempotent = true , requestEntityAcceptance = Disallowed))
val OPTIONS = register(HttpMethod("OPTIONS", isSafe = true , isIdempotent = true , requestEntityAcceptance = Expected))
val PATCH = register(HttpMethod("PATCH" , isSafe = false, isIdempotent = false, requestEntityAcceptance = Expected))
val POST = register(HttpMethod("POST" , isSafe = false, isIdempotent = false, requestEntityAcceptance = Expected))
val PUT = register(HttpMethod("PUT" , isSafe = false, isIdempotent = true , requestEntityAcceptance = Expected))
val TRACE = register(HttpMethod("TRACE" , isSafe = true , isIdempotent = true , requestEntityAcceptance = Disallowed))
// format: ON
}

View file

@ -12,6 +12,7 @@ import akka.http.scaladsl.model.HttpEntity._
import akka.http.scaladsl.model.HttpMethods._
import akka.http.scaladsl.model.HttpProtocols._
import akka.http.scaladsl.model.MediaTypes._
import akka.http.scaladsl.model.RequestEntityAcceptance.Expected
import akka.http.scaladsl.model.StatusCodes._
import akka.http.scaladsl.model._
import akka.http.scaladsl.model.headers._
@ -37,7 +38,7 @@ class RequestParserSpec extends FreeSpec with Matchers with BeforeAndAfterAll {
implicit val system = ActorSystem(getClass.getSimpleName, testConf)
import system.dispatcher
val BOLT = HttpMethod.custom("BOLT", safe = false, idempotent = true, entityAccepted = true)
val BOLT = HttpMethod.custom("BOLT", safe = false, idempotent = true, requestEntityAcceptance = Expected)
implicit val materializer = ActorMaterializer()
"The request parsing logic should" - {
@ -448,17 +449,26 @@ class RequestParserSpec extends FreeSpec with Matchers with BeforeAndAfterAll {
}
}
"with an illegal entity" in new Test {
"with an illegal entity using CONNECT" in new Test {
"""CONNECT /resource/yes HTTP/1.1
|Transfer-Encoding: chunked
|Host: x
|
|""" should parseToError(422: StatusCode, ErrorInfo("CONNECT requests must not have an entity"))
}
"with an illegal entity using HEAD" in new Test {
"""HEAD /resource/yes HTTP/1.1
|Content-length: 3
|Host: x
|
|foo""" should parseToError(422: StatusCode, ErrorInfo("HEAD requests must not have an entity"))
"""DELETE /resource/yes HTTP/1.1
}
"with an illegal entity using TRACE" in new Test {
"""TRACE /resource/yes HTTP/1.1
|Transfer-Encoding: chunked
|Host: x
|
|""" should parseToError(422: StatusCode, ErrorInfo("DELETE requests must not have an entity"))
|""" should parseToError(422: StatusCode, ErrorInfo("TRACE requests must not have an entity"))
}
}
}

View file

@ -124,6 +124,16 @@ class RequestRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll
|The content please!"""
}
}
"DELETE request without headers and without body" in new TestSetup() {
HttpRequest(DELETE, "/abc") should renderTo {
"""DELETE /abc HTTP/1.1
|Host: test.com:8080
|User-Agent: akka-http/1.0.0
|
|"""
}
}
}
"proper render a chunked" - {