+htc #16030 Age and Expires headers

* Added models/parsers for Age and Expires headers
* Updated tests using RawHeader to use typed headers
This commit is contained in:
Greg Beech 2014-12-29 21:42:34 +00:00
parent 0f1feac42e
commit fb968eb4be
11 changed files with 97 additions and 28 deletions

View file

@ -0,0 +1,17 @@
/**
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.model.japi.headers;
/**
* Model for the `Age` header.
* Specification: http://tools.ietf.org/html/rfc7234#section-5.1
*/
public abstract class Age extends akka.http.model.HttpHeader {
public abstract long deltaSeconds();
public static Age create(long deltaSeconds) {
return new akka.http.model.headers.Age(deltaSeconds);
}
}

View file

@ -0,0 +1,19 @@
/**
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.model.japi.headers;
import akka.http.model.japi.DateTime;
/**
* Model for the `Expires` header.
* Specification: http://tools.ietf.org/html/rfc7234#section-5.3
*/
public abstract class Expires extends akka.http.model.HttpHeader {
public abstract DateTime date();
public static Expires create(DateTime date) {
return new akka.http.model.headers.Expires(((akka.http.util.DateTime) date));
}
}

View file

@ -277,6 +277,13 @@ final case class `Access-Control-Request-Method`(method: HttpMethod) extends jap
protected def companion = `Access-Control-Request-Method`
}
// http://tools.ietf.org/html/rfc7234#section-5.1
object Age extends ModeledCompanion
final case class Age(deltaSeconds: Long) extends japi.headers.Age with ModeledHeader {
def renderValue[R <: Rendering](r: R): r.type = r ~~ deltaSeconds
protected def companion = Age
}
// http://tools.ietf.org/html/rfc7231#section-7.4.1
object Allow extends ModeledCompanion {
def apply(methods: HttpMethod*): Allow = apply(immutable.Seq(methods: _*))
@ -393,6 +400,13 @@ final case class ETag(etag: EntityTag) extends japi.headers.ETag with ModeledHea
protected def companion = ETag
}
// http://tools.ietf.org/html/rfc7234#section-5.3
object Expires extends ModeledCompanion
final case class Expires(date: DateTime) extends japi.headers.Expires with ModeledHeader {
def renderValue[R <: Rendering](r: R): r.type = date.renderRfc1123DateTimeString(r)
protected def companion = Expires
}
// http://tools.ietf.org/html/rfc7232#section-3.1
object `If-Match` extends ModeledCompanion {
val `*` = `If-Match`(EntityTagRange.`*`)

View file

@ -71,6 +71,7 @@ object HeaderParser {
"access-control-request-headers",
"access-control-request-method",
"accept",
"age",
"allow",
"authorization",
"cache-control",
@ -84,6 +85,7 @@ object HeaderParser {
"date",
"etag",
"expect",
"expires",
"host",
"if-match",
"if-modified-since",

View file

@ -61,6 +61,9 @@ private[parser] trait SimpleHeaders { this: Parser with CommonRules with CommonA
httpMethodDef ~ EOI ~> (`Access-Control-Request-Method`(_))
}
// http://tools.ietf.org/html/rfc7234#section-5.1
def age = rule { `delta-seconds` ~ EOI ~> (Age(_)) }
// http://tools.ietf.org/html/rfc7231#section-7.4.1
def allow = rule {
zeroOrMore(httpMethodDef).separatedBy(listSep) ~ EOI ~> (Allow(_))
@ -109,6 +112,9 @@ private[parser] trait SimpleHeaders { this: Parser with CommonRules with CommonA
ignoreCase("100-continue") ~ OWS ~ push(Expect.`100-continue`)
}
// http://tools.ietf.org/html/rfc7234#section-5.3
def `expires` = rule { `HTTP-date` ~ EOI ~> (Expires(_)) }
// http://tools.ietf.org/html/rfc7230#section-5.4
// We don't accept scoped IPv6 addresses as they should not appear in the Host header,
// see also https://issues.apache.org/bugzilla/show_bug.cgi?id=35122 (WONTFIX in Apache 2 issue) and

View file

@ -114,11 +114,11 @@ class ClientServerSpec extends WordSpec with Matchers with BeforeAndAfterAll {
val serverOutSub = serverOut.expectSubscription()
serverOutSub.expectRequest()
serverOutSub.sendNext(HttpResponse(206, List(RawHeader("Age", "42")), chunkedEntity))
serverOutSub.sendNext(HttpResponse(206, List(Age(42)), chunkedEntity))
val clientInSub = clientIn.expectSubscription()
clientInSub.request(1)
val HttpResponse(StatusCodes.PartialContent, List(RawHeader("Age", "42"), Server(_), Date(_)),
val HttpResponse(StatusCodes.PartialContent, List(Age(42), Server(_), Date(_)),
Chunked(`chunkedContentType`, chunkStream2), HttpProtocols.`HTTP/1.1`) = clientIn.expectNext()
Await.result(chunkStream2.grouped(1000).runWith(Sink.head), 100.millis) shouldEqual chunks

View file

@ -110,19 +110,20 @@ class HttpHeaderParserSpec extends WordSpec with Matchers with BeforeAndAfterAll
check {
""" ┌─\r-\n- EmptyHeader
| | ┌─c-h-a-r-s-e-t-:- (accept-charset)
| | | | ┌─e-n-c-o-d-i-n-g-:- (accept-encoding)
| | | l-a-n-g-u-a-g-e-:- (accept-language)
| | ┌─p-t---r-a-n-g-e-s-:- (accept-ranges)
| | ┌─p-t---e-n-c-o-d-i-n-g-:- (accept-encoding)
| | | | | l-a-n-g-u-a-g-e-:- (accept-language)
| | | | └─r-a-n-g-e-s-:- (accept-ranges)
| | | | ┌─\r-\n- Accept: */*
| | | └─:-(accept)- -*-/-*-\r-\n- Accept: */*
| | | ┌─c-r-e-d-e-n-t-i-a-l-s-:- (access-control-allow-credentials)
| | | ┌─h-e-a-d-e-r-s-:- (access-control-allow-headers)
| | | ┌─a-l-l-o-w---m-e-t-h-o-d-s-:- (access-control-allow-methods)
| | | ┌─a-l-l-o-w---c-r-e-d-e-n-t-i-a-l-s-:- (access-control-allow-credentials)
| | | | | | ┌─h-e-a-d-e-r-s-:- (access-control-allow-headers)
| | | | | | ┌─m-e-t-h-o-d-s-:- (access-control-allow-methods)
| | | | | └─o-r-i-g-i-n-:- (access-control-allow-origin)
| | | | └─e-x-p-o-s-e---h-e-a-d-e-r-s-:- (access-control-expose-headers)
| ┌─a-c-c-e-s-s---c-o-n-t-r-o-l---m-a-x---a-g-e-:- (access-control-max-age)
| | | | ┌─h-e-a-d-e-r-s-:- (access-control-request-headers)
| | | └─r-e-q-u-e-s-t---m-e-t-h-o-d-:- (access-control-request-method)
| | | | | ┌─e-x-p-o-s-e---h-e-a-d-e-r-s-:- (access-control-expose-headers)
| | | | └─m-a-x---a-g-e-:- (access-control-max-age)
| ┌─a-c-c-e-s-s---c-o-n-t-r-o-l---r-e-q-u-e-s-t---h-e-a-d-e-r-s-:- (access-control-request-headers)
| | | └─m-e-t-h-o-d-:- (access-control-request-method)
| | | ┌─g-e-:- (age)
| | └─l-l-o-w-:- (allow)
| | └─u-t-h-o-r-i-z-a-t-i-o-n-:- (authorization)
| | ┌─a-c-h-e---c-o-n-t-r-o-l-:-(cache-control)- -m-a-x---a-g-e-=-0-\r-\n- Cache-Control: max-age=0
@ -138,7 +139,8 @@ class HttpHeaderParserSpec extends WordSpec with Matchers with BeforeAndAfterAll
|-c-o-o-k-i-e-:- (cookie)
| | ┌─d-a-t-e-:- (date)
| | | ┌─t-a-g-:- (etag)
| | ┌─e-x-p-e-c-t-:-(expect)- -1-0-0---c-o-n-t-i-n-u-e-\r-\n- Expect: 100-continue
| | | | ┌─e-c-t-:-(expect)- -1-0-0---c-o-n-t-i-n-u-e-\r-\n- Expect: 100-continue
| | ┌─e-x-p-i-r-e-s-:- (expires)
| | | └─h-o-s-t-:- (host)
| | | ┌─a-t-c-h-:- (if-match)
| | ┌─i-f---m-o-d-i-f-i-e-d---s-i-n-c-e-:- (if-modified-since)
@ -160,7 +162,7 @@ class HttpHeaderParserSpec extends WordSpec with Matchers with BeforeAndAfterAll
| └─x---f-o-r-w-a-r-d-e-d---f-o-r-:- (x-forwarded-for)
|""" -> parser.formatTrie
}
parser.formatSizes shouldEqual "592 nodes, 40 branchData rows, 55 values"
parser.formatSizes shouldEqual "602 nodes, 42 branchData rows, 57 values"
parser.contentHistogram shouldEqual
Map("connection" -> 3, "Content-Length" -> 1, "accept" -> 2, "cache-control" -> 2, "expect" -> 1)
}
@ -232,8 +234,8 @@ class HttpHeaderParserSpec extends WordSpec with Matchers with BeforeAndAfterAll
}
randomHeaders.take(300).foldLeft(0) {
case (acc, rawHeader) acc + parseAndCache(rawHeader.toString + "\r\nx", rawHeader)
} shouldEqual 100 // number of cache hits
parser.formatSizes shouldEqual "3050 nodes, 114 branchData rows, 255 values"
} shouldEqual 99 // number of cache hits
parser.formatSizes shouldEqual "3040 nodes, 115 branchData rows, 255 values"
}
"continue parsing modelled headers even if the overall cache capacity is reached" in new TestSetup() {
@ -245,7 +247,7 @@ class HttpHeaderParserSpec extends WordSpec with Matchers with BeforeAndAfterAll
randomHostHeaders.take(300).foldLeft(0) {
case (acc, header) acc + parseAndCache(header.toString + "\r\nx", header)
} shouldEqual 12 // number of cache hits
parser.formatSizes shouldEqual "756 nodes, 49 branchData rows, 67 values"
parser.formatSizes shouldEqual "766 nodes, 51 branchData rows, 69 values"
}
"continue parsing raw headers even if the header-specific cache capacity is reached" in new TestSetup() {

View file

@ -57,7 +57,7 @@ class RequestRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll
"POST request, a few headers (incl. a custom Host header) and no body" in new TestSetup() {
HttpRequest(POST, "/abc/xyz", List(
RawHeader("X-Fancy", "naa"),
RawHeader("Age", "0"),
Age(0),
Host("spray.io", 9999))) should renderTo {
"""POST /abc/xyz HTTP/1.1
|X-Fancy: naa

View file

@ -55,7 +55,7 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll
}
"status 304 and a few headers" in new TestSetup() {
HttpResponse(304, List(RawHeader("X-Fancy", "of course"), RawHeader("Age", "0"))) should renderTo {
HttpResponse(304, List(RawHeader("X-Fancy", "of course"), Age(0))) should renderTo {
"""HTTP/1.1 304 Not Modified
|X-Fancy: of course
|Age: 0
@ -80,7 +80,7 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll
ResponseRenderingContext(
requestMethod = HttpMethods.HEAD,
response = HttpResponse(
headers = List(RawHeader("Age", "30"), Connection("Keep-Alive")),
headers = List(Age(30), Connection("Keep-Alive")),
entity = "Small f*ck up overhere!")) should renderTo(
"""HTTP/1.1 200 OK
|Age: 30
@ -95,7 +95,7 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll
"a response with a Strict body," - {
"status 400 and a few headers" in new TestSetup() {
HttpResponse(400, List(RawHeader("Age", "30"), Connection("Keep-Alive")), "Small f*ck up overhere!") should renderTo {
HttpResponse(400, List(Age(30), Connection("Keep-Alive")), "Small f*ck up overhere!") should renderTo {
"""HTTP/1.1 400 Bad Request
|Age: 30
|Server: akka-http/1.0.0
@ -108,7 +108,7 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll
}
"status 400, a few headers and a body with an explicitly suppressed Content Type header" in new TestSetup() {
HttpResponse(400, List(RawHeader("Age", "30"), Connection("Keep-Alive")),
HttpResponse(400, List(Age(30), Connection("Keep-Alive")),
HttpEntity(contentType = ContentTypes.NoContentType, "Small f*ck up overhere!")) should renderTo {
"""HTTP/1.1 400 Bad Request
|Age: 30
@ -136,7 +136,7 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll
}
"a response with a Default (streamed with explicit content-length body," - {
"status 400 and a few headers" in new TestSetup() {
HttpResponse(400, List(RawHeader("Age", "30"), Connection("Keep-Alive")),
HttpResponse(400, List(Age(30), Connection("Keep-Alive")),
entity = Default(contentType = ContentTypes.`text/plain(UTF-8)`, 23, source(ByteString("Small f*ck up overhere!")))) should renderTo {
"""HTTP/1.1 400 Bad Request
|Age: 30
@ -193,7 +193,7 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll
"a chunked response" - {
"with empty entity" in new TestSetup() {
pending // Disabled until #15981 is fixed
HttpResponse(200, List(RawHeader("Age", "30")),
HttpResponse(200, List(Age(30)),
Chunked(ContentTypes.NoContentType, source())) should renderTo {
"""HTTP/1.1 200 OK
|Age: 30
@ -206,7 +206,7 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll
"with empty entity but non-default Content-Type" in new TestSetup() {
pending // Disabled until #15981 is fixed
HttpResponse(200, List(RawHeader("Age", "30")),
HttpResponse(200, List(Age(30)),
Chunked(ContentTypes.`application/json`, source())) should renderTo {
"""HTTP/1.1 200 OK
|Age: 30
@ -238,7 +238,7 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll
"with one chunk and an explicit LastChunk" in new TestSetup() {
HttpResponse(entity = Chunked(ContentTypes.`text/plain(UTF-8)`,
source(Chunk(ByteString("body123"), """key=value;another="tl;dr""""),
LastChunk("foo=bar", List(RawHeader("Age", "30"), RawHeader("Cache-Control", "public")))))) should renderTo {
LastChunk("foo=bar", List(Age(30), RawHeader("Cache-Control", "public")))))) should renderTo {
"""HTTP/1.1 200 OK
|Server: akka-http/1.0.0
|Date: Thu, 25 Aug 2011 09:10:29 GMT
@ -292,7 +292,7 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll
requestProtocol = HttpProtocols.`HTTP/1.0`,
response = HttpResponse(entity = Chunked(ContentTypes.`text/plain(UTF-8)`,
source(Chunk(ByteString("body123"), """key=value;another="tl;dr""""),
LastChunk("foo=bar", List(RawHeader("Age", "30"), RawHeader("Cache-Control", "public"))))))) should renderTo(
LastChunk("foo=bar", List(Age(30), RawHeader("Cache-Control", "public"))))))) should renderTo(
"""HTTP/1.1 200 OK
|Server: akka-http/1.0.0
|Date: Thu, 25 Aug 2011 09:10:29 GMT

View file

@ -114,6 +114,10 @@ class HttpHeaderSpec extends FreeSpec with Matchers {
"Accept-Language: es-419, es" =!= `Accept-Language`(Language("es", "419"), Language("es"))
}
"Age" in {
"Age: 3600" =!= Age(3600)
}
"Allow" in {
"Allow: " =!= Allow()
"Allow: GET, PUT" =!= Allow(GET, PUT)
@ -237,6 +241,11 @@ class HttpHeaderSpec extends FreeSpec with Matchers {
"Expect: 100-continue" =!= Expect.`100-continue`
}
"Expires" in {
"Expires: Wed, 13 Jul 2011 08:12:31 GMT" =!= Expires(DateTime(2011, 7, 13, 8, 12, 31))
"Expires: 0" =!= Expires(DateTime.MinValue).renderedTo("Wed, 01 Jan 1800 00:00:00 GMT")
}
"Host" in {
"Host: www.spray.io:8080" =!= Host("www.spray.io", 8080)
"Host: spray.io" =!= Host("spray.io")

View file

@ -65,7 +65,7 @@ class UnmarshallingSpec extends FreeSpec with Matchers with BeforeAndAfterAll wi
|Content-type: text/xml
|Age: 12
|--XYZABC--""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts(
Multipart.General.BodyPart.Strict(HttpEntity.empty(MediaTypes.`text/xml`), List(RawHeader("Age", "12"))))
Multipart.General.BodyPart.Strict(HttpEntity.empty(MediaTypes.`text/xml`), List(Age(12))))
}
"one non-empty part" in {
Unmarshal(HttpEntity(`multipart/form-data` withBoundary "-",