Skip to content
This repository has been archived by the owner on Apr 24, 2024. It is now read-only.

Commit

Permalink
! http, can: support for custom status codes, fixes #564
Browse files Browse the repository at this point in the history
Breaking behavior: HttpSuccess + HttpFailure are not public API
any more.
  • Loading branch information
jrudolph committed Oct 9, 2013
1 parent 5ac8b64 commit a9e0d2c
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 15 deletions.
1 change: 1 addition & 0 deletions spray-can-tests/src/test/scala/spray/can/TestSupport.scala
Expand Up @@ -24,6 +24,7 @@ import HttpHeaders._
import MediaTypes._

object TestSupport {
val ServerOnTheMove = StatusCodes.registerCustom(330, "Server on the move")

def defaultParserSettings = ParserSettings(ConfigFactory.load())

Expand Down
Expand Up @@ -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("""
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
}

Expand Down
Expand Up @@ -26,6 +26,7 @@ import HttpHeaders._
import HttpMethods._
import HttpProtocols._
import MediaTypes._
import spray.can.TestSupport

class ResponseRendererSpec extends mutable.Specification with DataTables {

Expand Down Expand Up @@ -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(
Expand Down
61 changes: 47 additions & 14 deletions spray-http/src/main/scala/spray/http/StatusCode.scala
Expand Up @@ -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 }
Expand All @@ -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}
Expand Down

0 comments on commit a9e0d2c

Please sign in to comment.