Permalink
Browse files

! http, can: support for custom status codes, fixes #564

Breaking behavior: HttpSuccess + HttpFailure are not public API
any more.
  • Loading branch information...
jrudolph committed Oct 9, 2013
1 parent 5ac8b64 commit a9e0d2c963ccf31a839636717453a83667321910
@@ -24,6 +24,7 @@ import HttpHeaders._
import MediaTypes._
object TestSupport {
+ val ServerOnTheMove = StatusCodes.registerCustom(330, "Server on the move")
def defaultParserSettings = ParserSettings(ConfigFactory.load())
@@ -27,6 +27,7 @@ import HttpHeaders._
import HttpMethods._
import StatusCodes._
import HttpProtocols._
+import spray.can.TestSupport
class ResponseParserSpec extends Specification {
val testConf: Config = ConfigFactory.parseString("""
@@ -57,6 +58,15 @@ class ResponseParserSpec extends Specification {
} === Seq(NoContent, "", Nil, `HTTP/1.1`, 'dontClose)
}
+ "a response with a custom status code" in {
+ parse {
+ """HTTP/1.1 330 Server on the move
+ |Content-Length: 0
+ |
+ |"""
+ } === Seq(TestSupport.ServerOnTheMove, "", List(`Content-Length`(0)), `HTTP/1.1`, 'dontClose)
+ }
+
"a response with one header, a body, but no Content-Length header" in {
parse("""HTTP/1.0 404 Not Found
|Host: api.example.com
@@ -201,7 +211,6 @@ class ResponseParserSpec extends Specification {
}
"an illegal status code" in {
- parse("HTTP/1.1 700 Something") === Seq("Illegal response status code")
parse("HTTP/1.1 2000 Something") === Seq("Illegal response status code")
}
@@ -26,6 +26,7 @@ import HttpHeaders._
import HttpMethods._
import HttpProtocols._
import MediaTypes._
+import spray.can.TestSupport
class ResponseRendererSpec extends mutable.Specification with DataTables {
@@ -95,6 +96,17 @@ class ResponseRendererSpec extends mutable.Specification with DataTables {
} -> false
}
+ "a response with a custom status code, no headers and no body" in new TestSetup() {
+ render(HttpResponse(TestSupport.ServerOnTheMove)) === result {
+ """HTTP/1.1 330 Server on the move
+ |Server: spray-can/1.0.0
+ |Date: Thu, 25 Aug 2011 09:10:29 GMT
+ |Content-Length: 0
+ |
+ |"""
+ } -> false
+ }
+
"a response to a HEAD request" in new TestSetup() {
render(requestMethod = HEAD,
response = HttpResponse(
@@ -28,21 +28,23 @@ sealed abstract class StatusCode extends LazyValueBytesRenderable {
object StatusCode {
import StatusCodes._
- implicit def int2StatusCode(code: Int): StatusCode = getForKey(code) getOrElse InternalServerError
-}
-
-sealed abstract class HttpSuccess extends StatusCode {
- def isSuccess = true
- def isFailure = false
-}
-sealed abstract class HttpFailure extends StatusCode {
- def isSuccess = false
- def isFailure = true
- def allowsEntity = true
+ implicit def int2StatusCode(code: Int): StatusCode =
+ getForKey(code).getOrElse(
+ throw new RuntimeException(
+ "Non-standard status codes cannot be created by implicit conversion. Use `StatusCodes.custom` instead."))
}
object StatusCodes extends ObjectRegistry[Int, StatusCode] {
-
+ sealed protected abstract class HttpSuccess extends StatusCode {
+ def isSuccess = true
+ def isFailure = false
+ }
+ sealed protected abstract class HttpFailure extends StatusCode {
+ def isSuccess = false
+ def isFailure = true
+ def allowsEntity = true
+ }
+
// format: OFF
case class Informational private[StatusCodes] (intValue: Int)(val reason: String,
val defaultMessage: String) extends HttpSuccess { def allowsEntity = false }
@@ -52,8 +54,39 @@ object StatusCodes extends ObjectRegistry[Int, StatusCode] {
val htmlTemplate: String, val allowsEntity: Boolean = true) extends HttpSuccess
case class ClientError private[StatusCodes] (intValue: Int)(val reason: String, val defaultMessage: String) extends HttpFailure
case class ServerError private[StatusCodes] (intValue: Int)(val reason: String, val defaultMessage: String) extends HttpFailure
-
- private def reg[T <: StatusCode](code: T): T = register(code.intValue, code)
+
+ case class CustomStatusCode private[StatusCodes] (intValue: Int)(
+ val reason: String,
+ val defaultMessage: String,
+ val isSuccess: Boolean,
+ val allowsEntity: Boolean) extends StatusCode {
+ def isFailure: Boolean = !isSuccess
+ }
+
+ private def reg[T <: StatusCode](code: T): T = {
+ require(getForKey(code.intValue).isEmpty, s"Status code for ${code.intValue} already registered as '${getForKey(code.intValue).get}'.")
+
+ register(code.intValue, code)
+ }
+
+ /**
+ * Create and register a custom status code and allow full customization of behavior. The value of `allowsEntity`
+ * changes the parser behavior: If it is set to true, a response with this status code is required to include a
+ * `Content-Length` header to be parsed correctly when keep-alive is enabled (which is the default in HTTP/1.1).
+ * If `allowsEntity` is false, an entity is never expected.
+ */
+ def registerCustom(intValue: Int, reason: String, defaultMessage: String, isSuccess: Boolean, allowsEntity: Boolean): StatusCode =
+ reg(CustomStatusCode(intValue)(reason, defaultMessage, isSuccess, allowsEntity))
+
+ /** Create and register a custom status code with default behavior for its value region. */
+ def registerCustom(intValue: Int, reason: String, defaultMessage: String = ""): StatusCode = reg (
+ if (100 to 199 contains intValue) Informational(intValue)(reason, defaultMessage)
+ else if (200 to 299 contains intValue) Success(intValue)(reason, defaultMessage)
+ else if (300 to 399 contains intValue) Redirection(intValue)(reason, defaultMessage, defaultMessage)
+ else if (400 to 499 contains intValue) ClientError(intValue)(reason, defaultMessage)
+ else if (500 to 599 contains intValue) ServerError(intValue)(reason, defaultMessage)
+ else sys.error("Can't register status code in non-standard region without additional information")
+ )
import Informational.{apply => i}
import Success .{apply => s}

0 comments on commit a9e0d2c

Please sign in to comment.