diff --git a/akka-http-core/src/main/java/akka/http/javadsl/model/HttpMethod.java b/akka-http-core/src/main/java/akka/http/javadsl/model/HttpMethod.java index 314a23c63a..c0ab260bda 100644 --- a/akka-http-core/src/main/java/akka/http/javadsl/model/HttpMethod.java +++ b/akka-http-core/src/main/java/akka/http/javadsl/model/HttpMethod.java @@ -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(); } diff --git a/akka-http-core/src/main/java/akka/http/javadsl/model/HttpMethods.java b/akka-http-core/src/main/java/akka/http/javadsl/model/HttpMethods.java index 859250876f..873bf6428c 100644 --- a/akka-http-core/src/main/java/akka/http/javadsl/model/HttpMethods.java +++ b/akka-http-core/src/main/java/akka/http/javadsl/model/HttpMethods.java @@ -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); } /** diff --git a/akka-http-core/src/main/scala/akka/http/impl/engine/rendering/HttpRequestRendererFactory.scala b/akka-http-core/src/main/scala/akka/http/impl/engine/rendering/HttpRequestRendererFactory.scala index 812a5e6383..e7a91775f2 100644 --- a/akka-http-core/src/main/scala/akka/http/impl/engine/rendering/HttpRequestRendererFactory.scala +++ b/akka-http-core/src/main/scala/akka/http/impl/engine/rendering/HttpRequestRendererFactory.scala @@ -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)) diff --git a/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpMethod.scala b/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpMethod.scala index 8145d011c8..786de0c1cd 100644 --- a/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpMethod.scala +++ b/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpMethod.scala @@ -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 } diff --git a/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/RequestParserSpec.scala b/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/RequestParserSpec.scala index c04c068c79..2640c995d0 100644 --- a/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/RequestParserSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/RequestParserSpec.scala @@ -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")) } } } diff --git a/akka-http-core/src/test/scala/akka/http/impl/engine/rendering/RequestRendererSpec.scala b/akka-http-core/src/test/scala/akka/http/impl/engine/rendering/RequestRendererSpec.scala index 81c5e4cfc2..70c2918287 100644 --- a/akka-http-core/src/test/scala/akka/http/impl/engine/rendering/RequestRendererSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/impl/engine/rendering/RequestRendererSpec.scala @@ -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" - {