Permalink
Browse files

! http: add CONNECT method and support for custom HTTP methods, closes

…#428

We don't want to add even even more (and comparatively rarely used) predefined methods in order to avoid namespace pollution (an `import HttpMethods._` is probably quite frequent).

The breaking change comes from a naming cleanup:
`HttpMethod::entityAccepted` is now named `HttpMethod::isEntityAccepted` for consistency with the other boolean members.
  • Loading branch information...
sirthias committed Sep 25, 2013
1 parent 8f941d5 commit 5d78dae96f83c0be3509a212ac74e5d68f9a2455
@@ -37,6 +37,7 @@ class RequestParserSpec extends Specification {
spray.can.parsing.max-content-length = 4000000000
spray.can.parsing.incoming-auto-chunking-threshold-size = 20""")
val system = ActorSystem(Utils.actorSystemNameFrom(getClass), testConf)
+ val BOLT = HttpMethods.register(HttpMethod.custom("BOLT", safe = false, idempotent = true, entityAccepted = true))
"The request parsing logic" should {
"properly parse a request" in {
@@ -118,15 +119,24 @@ class RequestParserSpec extends Specification {
parse(parser)("DEFGH") === (PUT, Uri("/resource/yes"), `HTTP/1.1`, List(Host("x"), `Content-Length`(4)),
"ABCD", "EFGH", false)
}
- "reject requests with Content-Length > Int.MaxSize" in {
+
+ "with a custom HTTP method" in {
+ parse {
+ """BOLT / HTTP/1.0
+ |
+ |"""
+ } === (BOLT, Uri("/"), `HTTP/1.0`, Nil, "", "", true)
+ }
+
+ "with Content-Length > Int.MaxSize if autochunking is enabled" in {
val request =
"""PUT /resource/yes HTTP/1.1
|Content-length: 2147483649
|Host: x
|
|"""
- val parser = new HttpRequestPartParser(ParserSettings(system).copy(autoChunkingThreshold = Long.MaxValue))()
- parse(parser)(request) === (400: StatusCode, "Content-Length > Int.MaxSize not supported for non-(auto)-chunked requests")
+ val parser = newParser
+ parse(parser)(request) === (PUT, Uri("/resource/yes"), `HTTP/1.1`, List(Host("x"), `Content-Length`(2147483649L)), "", false)
}
}
@@ -196,6 +206,7 @@ class RequestParserSpec extends Specification {
(GET, Uri("/data"), `HTTP/1.1`, List(`Content-Length`(25), Host("ping"), `Content-Type`(`application/pdf`)),
"rest", false)
}
+
"request start if complete message is already available" in {
val parser = newParser
parse(parser)(start(25) + "rest1rest2rest3rest4rest5") ===
@@ -211,6 +222,7 @@ class RequestParserSpec extends Specification {
parse(parser)("rest") === ("rest", "", "", false)
()
}
+
"request end" in {
val parser = newParser
parse(parser)(start(25) + "rest")
@@ -223,27 +235,6 @@ class RequestParserSpec extends Specification {
parse(parser)("\0GET /data HTTP/1.1") === ("", Nil, "GET /data HTTP/1.1", false) // next parse run produced end
parse(parser)("GET /data HTTP/1.1") === Result.NeedMoreData // start of next request
}
- "don't reject requests with Content-Length > Int.MaxSize" in {
- val request =
- """PUT /resource/yes HTTP/1.1
- |Content-length: 2147483649
- |Host: x
- |
- |"""
- val parser = newParser
- parse(parser)(request) === (PUT, Uri("/resource/yes"), `HTTP/1.1`, List(Host("x"), `Content-Length`(2147483649L)), "", false)
- }
- "reject requests with Content-Length > Long.MaxSize" in {
- // content-length = (Long.MaxValue + 1) * 10, which is 0 when calculated overflow
- val request =
- """PUT /resource/yes HTTP/1.1
- |Content-length: 92233720368547758080
- |Host: x
- |
- |"""
- val parser = newParser
- parse(parser)(request) === (400: StatusCode, "Illegal `Content-Length` header value")
- }
}
"reject a message chunk with" in {
@@ -294,8 +285,8 @@ class RequestParserSpec extends Specification {
"reject a request with" in {
"an illegal HTTP method" in {
- parse("get") === (NotImplemented, "Unsupported HTTP method")
- parse("GETX") === (NotImplemented, "Unsupported HTTP method")
+ parse("get ") === (NotImplemented, "Unsupported HTTP method: get")
+ parse("GETX ") === (NotImplemented, "Unsupported HTTP method: GETX")
}
"two Content-Length headers" in {
@@ -346,6 +337,29 @@ class RequestParserSpec extends Specification {
|abc"""
} === (BadRequest, "Illegal `Content-Length` header value")
}
+
+ "with Content-Length > Int.MaxSize if autochunking is disabled" in {
+ val request =
+ """PUT /resource/yes HTTP/1.1
+ |Content-length: 2147483649
+ |Host: x
+ |
+ |"""
+ val parser = new HttpRequestPartParser(ParserSettings(system).copy(autoChunkingThreshold = Long.MaxValue))()
+ parse(parser)(request) === (400: StatusCode, "Content-Length > Int.MaxSize not supported for non-(auto)-chunked requests")
+ }
+
+ "with Content-Length > Long.MaxSize" in {
+ // content-length = (Long.MaxValue + 1) * 10, which is 0 when calculated overflow
+ val request =
+ """PUT /resource/yes HTTP/1.1
+ |Content-length: 92233720368547758080
+ |Host: x
+ |
+ |"""
+ val parser = newParser
+ parse(parser)(request) === (400: StatusCode, "Illegal `Content-Length` header value")
+ }
}
}
@@ -16,6 +16,7 @@
package spray.can.parsing
+import java.lang.{ StringBuilder JStringBuilder }
import scala.annotation.tailrec
import akka.util.CompactByteString
import spray.http._
@@ -44,29 +45,41 @@ class HttpRequestPartParser(_settings: ParserSettings)(_headerParser: HttpHeader
}
def parseMethod(input: CompactByteString): Int = {
- def badMethod = throw new ParsingException(NotImplemented, ErrorInfo("Unsupported HTTP method"))
+ @tailrec def parseCustomMethod(ix: Int = 0, sb: JStringBuilder = new JStringBuilder(16)): Int =
+ if (ix < 16) { // hard-coded maximum custom method length
+ byteChar(input, ix) match {
+ case ' '
+ HttpMethods.getForKey(sb.toString) match {
+ case Some(m) method = m; ix + 1
+ case None parseCustomMethod(Int.MaxValue, sb)
+ }
+ case c parseCustomMethod(ix + 1, sb.append(c))
+ }
+ } else throw new ParsingException(NotImplemented, ErrorInfo("Unsupported HTTP method", sb.toString))
+
@tailrec def parseMethod(meth: HttpMethod, ix: Int = 1): Int =
if (ix == meth.value.length)
if (byteChar(input, ix) == ' ') {
method = meth
ix + 1
- } else badMethod
+ } else parseCustomMethod()
else if (byteChar(input, ix) == meth.value.charAt(ix)) parseMethod(meth, ix + 1)
- else badMethod
+ else parseCustomMethod()
byteChar(input, 0) match {
case 'G' parseMethod(GET)
case 'P' byteChar(input, 1) match {
case 'O' parseMethod(POST, 2)
case 'U' parseMethod(PUT, 2)
case 'A' parseMethod(PATCH, 2)
- case _ badMethod
+ case _ parseCustomMethod()
}
case 'D' parseMethod(DELETE)
case 'H' parseMethod(HEAD)
case 'O' parseMethod(OPTIONS)
case 'T' parseMethod(TRACE)
- case _ badMethod
+ case 'C' parseMethod(CONNECT)
+ case _ parseCustomMethod()
}
}
@@ -63,7 +63,7 @@ trait RequestRenderingComponent {
def renderRequest(request: HttpRequest): Unit = {
renderRequestStart(request)
val bodyLength = request.entity.data.length
- if (bodyLength > 0 || request.method.entityAccepted) r ~~ `Content-Length` ~~ bodyLength ~~ CrLf
+ if (bodyLength > 0 || request.method.isEntityAccepted) r ~~ `Content-Length` ~~ bodyLength ~~ CrLf
r ~~ CrLf ~~ request.entity.data
}
@@ -25,7 +25,7 @@ case class HttpEncoding private[http] (value: String) extends HttpEncodingRange
}
object HttpEncoding {
- def custom(value: String) = apply(value)
+ def custom(value: String): HttpEncoding = apply(value)
}
// see http://www.iana.org/assignments/http-parameters/http-parameters.xml
@@ -19,12 +19,12 @@ package spray.http
/**
* @param isSafe true if the resource should not be altered on the server
* @param isIdempotent true if requests can be safely (& automatically) repeated
- * @param entityAccepted true if meaning of request entities is properly defined
+ * @param isEntityAccepted true if meaning of request entities is properly defined
*/
case class HttpMethod private[http] (value: String,
isSafe: Boolean,
isIdempotent: Boolean,
- entityAccepted: Boolean) extends LazyValueBytesRenderable {
+ isEntityAccepted: Boolean) extends LazyValueBytesRenderable {
// for faster equality checks we use the hashcode of the method name (and make sure it's distinct during registration)
private[http] val fingerprint = value.##
@@ -38,20 +38,29 @@ case class HttpMethod private[http] (value: String,
}
}
+object HttpMethod {
+ def custom(value: String, safe: Boolean, idempotent: Boolean, entityAccepted: Boolean): HttpMethod = {
+ require(value.nonEmpty, "value must be non-empty")
+ require(!safe || idempotent, "An HTTP method cannot be safe without being idempotent")
+ apply(value, safe, idempotent, entityAccepted)
+ }
+}
+
object HttpMethods extends ObjectRegistry[String, HttpMethod] {
- private def register(method: HttpMethod): HttpMethod = {
+ def register(method: HttpMethod): HttpMethod = {
registry.values foreach { m if (m.fingerprint == method.fingerprint) sys.error("Method fingerprint collision") }
register(method.value, method)
}
// format: OFF
- val DELETE = register(HttpMethod("DELETE" , isSafe = false, isIdempotent = true , entityAccepted = false))
- val GET = register(HttpMethod("GET" , isSafe = true , isIdempotent = true , entityAccepted = false))
- val HEAD = register(HttpMethod("HEAD" , isSafe = true , isIdempotent = true , entityAccepted = false))
- val OPTIONS = register(HttpMethod("OPTIONS", isSafe = true , isIdempotent = true , entityAccepted = true))
- val PATCH = register(HttpMethod("PATCH" , isSafe = false, isIdempotent = false, entityAccepted = true))
- val POST = register(HttpMethod("POST" , isSafe = false, isIdempotent = false, entityAccepted = true))
- val PUT = register(HttpMethod("PUT" , isSafe = false, isIdempotent = true , entityAccepted = true))
- val TRACE = register(HttpMethod("TRACE" , isSafe = true , isIdempotent = true , entityAccepted = false))
+ 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))
// format: ON
}

0 comments on commit 5d78dae

Please sign in to comment.