mirror of
https://github.com/jlengrand/ktor.git
synced 2026-03-10 08:31:20 +00:00
Fix conditional headers processing
This commit is contained in:
@@ -1091,18 +1091,31 @@ public final class io/ktor/http/content/CachingOptionsKt {
|
||||
}
|
||||
|
||||
public final class io/ktor/http/content/EntityTagVersion : io/ktor/http/content/Version {
|
||||
public fun <init> (Ljava/lang/String;)V
|
||||
public static final field Companion Lio/ktor/http/content/EntityTagVersion$Companion;
|
||||
public synthetic fun <init> (Ljava/lang/String;)V
|
||||
public fun <init> (Ljava/lang/String;Z)V
|
||||
public fun appendHeadersTo (Lio/ktor/http/HeadersBuilder;)V
|
||||
public fun check (Lio/ktor/http/Headers;)Lio/ktor/http/content/VersionCheckResult;
|
||||
public final fun component1 ()Ljava/lang/String;
|
||||
public final fun copy (Ljava/lang/String;)Lio/ktor/http/content/EntityTagVersion;
|
||||
public static synthetic fun copy$default (Lio/ktor/http/content/EntityTagVersion;Ljava/lang/String;ILjava/lang/Object;)Lio/ktor/http/content/EntityTagVersion;
|
||||
public final fun component2 ()Z
|
||||
public final fun copy (Ljava/lang/String;Z)Lio/ktor/http/content/EntityTagVersion;
|
||||
public static synthetic fun copy$default (Lio/ktor/http/content/EntityTagVersion;Ljava/lang/String;ZILjava/lang/Object;)Lio/ktor/http/content/EntityTagVersion;
|
||||
public fun equals (Ljava/lang/Object;)Z
|
||||
public final fun getEtag ()Ljava/lang/String;
|
||||
public final fun getWeak ()Z
|
||||
public fun hashCode ()I
|
||||
public final fun match (Lio/ktor/http/content/EntityTagVersion;)Z
|
||||
public final fun match (Ljava/util/List;)Lio/ktor/http/content/VersionCheckResult;
|
||||
public final fun noneMatch (Ljava/util/List;)Lio/ktor/http/content/VersionCheckResult;
|
||||
public fun toString ()Ljava/lang/String;
|
||||
}
|
||||
|
||||
public final class io/ktor/http/content/EntityTagVersion$Companion {
|
||||
public final fun getSTAR ()Lio/ktor/http/content/EntityTagVersion;
|
||||
public final fun parse (Ljava/lang/String;)Ljava/util/List;
|
||||
public final fun parseSingle (Ljava/lang/String;)Lio/ktor/http/content/EntityTagVersion;
|
||||
}
|
||||
|
||||
public final class io/ktor/http/content/LastModifiedVersion : io/ktor/http/content/Version {
|
||||
public fun <init> (Lio/ktor/util/date/GMTDate;)V
|
||||
public fun <init> (Ljava/util/Date;)V
|
||||
@@ -1114,6 +1127,8 @@ public final class io/ktor/http/content/LastModifiedVersion : io/ktor/http/conte
|
||||
public fun equals (Ljava/lang/Object;)Z
|
||||
public final fun getLastModified ()Lio/ktor/util/date/GMTDate;
|
||||
public fun hashCode ()I
|
||||
public final fun ifModifiedSince (Ljava/util/List;)Z
|
||||
public final fun ifUnmodifiedSince (Ljava/util/List;)Z
|
||||
public fun toString ()Ljava/lang/String;
|
||||
}
|
||||
|
||||
@@ -1241,6 +1256,7 @@ public final class io/ktor/http/content/VersionCheckResult : java/lang/Enum {
|
||||
}
|
||||
|
||||
public final class io/ktor/http/content/VersionsKt {
|
||||
public static final fun EntityTagVersion (Ljava/lang/String;)Lio/ktor/http/content/EntityTagVersion;
|
||||
public static final fun getVersionListProperty ()Lio/ktor/util/AttributeKey;
|
||||
public static final fun getVersions (Lio/ktor/http/content/OutgoingContent;)Ljava/util/List;
|
||||
public static final fun setVersions (Lio/ktor/http/content/OutgoingContent;Ljava/util/List;)V
|
||||
|
||||
@@ -74,23 +74,24 @@ enum class VersionCheckResult(val statusCode: HttpStatusCode) {
|
||||
* @param lastModified of the current content, for example file's last modified date
|
||||
*/
|
||||
data class LastModifiedVersion(val lastModified: GMTDate) : Version {
|
||||
constructor(lastModified: Date) : this(GMTDate(lastModified.time))
|
||||
|
||||
private val truncatedModificationDate: GMTDate = lastModified.truncateToSeconds()
|
||||
|
||||
/**
|
||||
* @return [VersionCheckResult.OK] if all header pass or there was no headers in the request,
|
||||
* [VersionCheckResult.NOT_MODIFIED] for If-Modified-Since,
|
||||
* [VersionCheckResult.PRECONDITION_FAILED] for If-Unmodified*Since
|
||||
*/
|
||||
override fun check(requestHeaders: Headers): VersionCheckResult {
|
||||
val normalized = lastModified.truncateToSeconds()
|
||||
val ifModifiedSince = requestHeaders[HttpHeaders.IfModifiedSince]?.fromHttpToGmtDate()
|
||||
val ifUnmodifiedSince = requestHeaders[HttpHeaders.IfUnmodifiedSince]?.fromHttpToGmtDate()
|
||||
|
||||
if (ifModifiedSince != null) {
|
||||
if (normalized <= ifModifiedSince) {
|
||||
requestHeaders.getAll(HttpHeaders.IfModifiedSince)?.parseDates()?.let { dates ->
|
||||
if (!ifModifiedSince(dates)) {
|
||||
return VersionCheckResult.NOT_MODIFIED
|
||||
}
|
||||
}
|
||||
if (ifUnmodifiedSince != null) {
|
||||
if (normalized > ifUnmodifiedSince) {
|
||||
|
||||
requestHeaders.getAll(HttpHeaders.IfUnmodifiedSince)?.parseDates()?.let { dates ->
|
||||
if (!ifUnmodifiedSince(dates)) {
|
||||
return VersionCheckResult.PRECONDITION_FAILED
|
||||
}
|
||||
}
|
||||
@@ -98,11 +99,42 @@ data class LastModifiedVersion(val lastModified: GMTDate) : Version {
|
||||
return VersionCheckResult.OK
|
||||
}
|
||||
|
||||
constructor(lastModified: Date) : this(GMTDate(lastModified.time))
|
||||
/**
|
||||
* If-Modified-Since logic: all [dates] should be _before_ this date (truncated to seconds).
|
||||
*/
|
||||
@KtorExperimentalAPI
|
||||
fun ifModifiedSince(dates: List<GMTDate>): Boolean {
|
||||
return dates.any { truncatedModificationDate > it }
|
||||
}
|
||||
|
||||
/**
|
||||
* If-Unmodified-Since logic: all [dates] should not be before this date (truncated to seconds).
|
||||
*/
|
||||
@KtorExperimentalAPI
|
||||
fun ifUnmodifiedSince(dates: List<GMTDate>): Boolean {
|
||||
return dates.all { truncatedModificationDate <= it }
|
||||
}
|
||||
|
||||
override fun appendHeadersTo(builder: HeadersBuilder) {
|
||||
builder[HttpHeaders.LastModified] = lastModified.toHttpDate()
|
||||
}
|
||||
|
||||
private fun List<String>.parseDates(): List<GMTDate>? = filter { it.isNotBlank() }.
|
||||
mapNotNull { try {
|
||||
it.fromHttpToGmtDate()
|
||||
} catch (_: Throwable) {
|
||||
// according to RFC7232 sec 3.3 illegal dates should be ignored
|
||||
null
|
||||
}
|
||||
}.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance of [EntityTagVersion] parsing the [spec] via [EntityTagVersion.parseSingle].
|
||||
*/
|
||||
@Suppress("FunctionName", "CONFLICTING_OVERLOADS")
|
||||
fun EntityTagVersion(spec: String): EntityTagVersion {
|
||||
return EntityTagVersion.parseSingle(spec)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,30 +145,136 @@ data class LastModifiedVersion(val lastModified: GMTDate) : Version {
|
||||
* See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26 for more details
|
||||
*
|
||||
* @param etag - entity tag, for example file's content hash
|
||||
* @param weak - whether strong or weak validation should be applied
|
||||
* @return [VersionCheckResult.OK] if all headers pass or there was no related headers,
|
||||
* [VersionCheckResult.NOT_MODIFIED] for successful If-None-Match,
|
||||
* [VersionCheckResult.PRECONDITION_FAILED] for failed If-Match
|
||||
*/
|
||||
data class EntityTagVersion(val etag: String) : Version {
|
||||
data class EntityTagVersion(val etag: String, val weak: Boolean) : Version {
|
||||
@Suppress("unused", "CONFLICTING_OVERLOADS")
|
||||
@Deprecated("Binary compatibility.", level = DeprecationLevel.HIDDEN)
|
||||
constructor(etag: String) : this(etag.removePrefix("W/"), etag.startsWith("W/"))
|
||||
|
||||
private val normalized: String = when {
|
||||
etag == "*" -> etag
|
||||
etag.startsWith("\"") -> etag
|
||||
else -> etag.quote()
|
||||
}
|
||||
|
||||
init {
|
||||
for (index in etag.indices) {
|
||||
val ch = etag[index]
|
||||
if (ch <= ' ' || ch == '\"') {
|
||||
require(index == 0 || index == etag.lastIndex) { "Character '$ch' is not allowed in entity-tag." }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun check(requestHeaders: Headers): VersionCheckResult {
|
||||
val givenNoneMatchEtags = requestHeaders[HttpHeaders.IfNoneMatch]?.parseMatchTag()
|
||||
val givenMatchEtags = requestHeaders[HttpHeaders.IfMatch]?.parseMatchTag()
|
||||
|
||||
if (givenNoneMatchEtags != null && etag in givenNoneMatchEtags && "*" !in givenNoneMatchEtags) {
|
||||
return VersionCheckResult.NOT_MODIFIED
|
||||
requestHeaders[HttpHeaders.IfNoneMatch]?.let { parse(it) }?.let { givenNoneMatchEtags ->
|
||||
noneMatch(givenNoneMatchEtags).let { result ->
|
||||
if (result != VersionCheckResult.OK) return result
|
||||
}
|
||||
}
|
||||
|
||||
if (givenMatchEtags != null && givenMatchEtags.isNotEmpty() && etag !in givenMatchEtags && "*" !in givenMatchEtags) {
|
||||
return VersionCheckResult.PRECONDITION_FAILED
|
||||
requestHeaders[HttpHeaders.IfMatch]?.let { parse(it) }?.let { givenMatchEtags ->
|
||||
match(givenMatchEtags).let { result ->
|
||||
if (result != VersionCheckResult.OK) return result
|
||||
}
|
||||
}
|
||||
|
||||
return VersionCheckResult.OK
|
||||
}
|
||||
|
||||
private fun String.parseMatchTag() = split("\\s*,\\s*".toRegex()).map { it.removePrefix("W/") }.filter { it.isNotEmpty() }.toSet()
|
||||
/**
|
||||
* Examine two entity-tags for match (strong).
|
||||
*/
|
||||
@KtorExperimentalAPI
|
||||
fun match(other: EntityTagVersion): Boolean {
|
||||
if (this == STAR || other == STAR) return true
|
||||
return normalized == other.normalized
|
||||
}
|
||||
|
||||
/**
|
||||
* `If-None-Match` logic using [match] function.
|
||||
*/
|
||||
@KtorExperimentalAPI
|
||||
fun noneMatch(givenNoneMatchEtags: List<EntityTagVersion>): VersionCheckResult {
|
||||
if (STAR in givenNoneMatchEtags) return VersionCheckResult.OK
|
||||
|
||||
if (givenNoneMatchEtags.any { match(it) }) {
|
||||
return VersionCheckResult.NOT_MODIFIED
|
||||
}
|
||||
|
||||
return VersionCheckResult.OK
|
||||
}
|
||||
|
||||
/**
|
||||
* `If-Match` logic using [match] function.
|
||||
*/
|
||||
@KtorExperimentalAPI
|
||||
fun match(givenMatchEtags: List<EntityTagVersion>): VersionCheckResult {
|
||||
if (givenMatchEtags.isEmpty()) return VersionCheckResult.OK
|
||||
if (STAR in givenMatchEtags) return VersionCheckResult.OK
|
||||
|
||||
for (given in givenMatchEtags) {
|
||||
if (match(given)) {
|
||||
return VersionCheckResult.OK
|
||||
}
|
||||
}
|
||||
|
||||
return VersionCheckResult.PRECONDITION_FAILED
|
||||
}
|
||||
|
||||
override fun appendHeadersTo(builder: HeadersBuilder) {
|
||||
builder.etag(etag)
|
||||
builder.etag(normalized)
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Instance for `*` entity-tag pattern.
|
||||
*/
|
||||
@KtorExperimentalAPI
|
||||
val STAR: EntityTagVersion = EntityTagVersion("*", false)
|
||||
|
||||
/**
|
||||
* Parse headers with a list of entity-tags. Useful for headers such as `If-Match`/`If-None-Match`.
|
||||
*/
|
||||
@KtorExperimentalAPI
|
||||
fun parse(headerValue: String): List<EntityTagVersion> {
|
||||
val rawEntries = parseHeaderValue(headerValue)
|
||||
return rawEntries.map { entry ->
|
||||
check (entry.quality == 1.0) { "entity-tag quality parameter is not allowed: ${entry.quality}."}
|
||||
check (entry.params.isEmpty()) { "entity-tag parameters are not allowed: ${entry.params}." }
|
||||
|
||||
parseSingle(entry.value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse single entity-tag or pattern specification.
|
||||
*/
|
||||
@KtorExperimentalAPI
|
||||
fun parseSingle(value: String): EntityTagVersion {
|
||||
if (value == "*") return STAR
|
||||
|
||||
val weak: Boolean
|
||||
val rawEtag: String
|
||||
|
||||
if (value.startsWith("W/")) {
|
||||
weak = true
|
||||
rawEtag = value.drop(2)
|
||||
} else {
|
||||
weak = false
|
||||
rawEtag = value
|
||||
}
|
||||
|
||||
val etag = when {
|
||||
rawEtag.startsWith("\"") -> rawEtag
|
||||
else -> rawEtag.quote()
|
||||
}
|
||||
|
||||
return EntityTagVersion(etag, weak)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import io.ktor.util.pipeline.*
|
||||
import io.ktor.request.*
|
||||
import io.ktor.response.*
|
||||
import io.ktor.util.*
|
||||
import io.ktor.util.date.*
|
||||
import kotlinx.coroutines.*
|
||||
import io.ktor.utils.io.*
|
||||
import kotlin.coroutines.*
|
||||
@@ -107,43 +108,43 @@ class PartialContent(private val maxRangeCount: Int) {
|
||||
call: ApplicationCall
|
||||
): Boolean {
|
||||
val conditionalHeadersFeature = call.application.featureOrNull(ConditionalHeaders)
|
||||
val versions = conditionalHeadersFeature?.versionsFor(content) ?: content.defaultVersions
|
||||
val ifRange = call.request.header(HttpHeaders.IfRange)?.trim() ?: return true
|
||||
|
||||
if (ifRange.endsWith(" GMT")) { // E-Tag If-Range spec can't have GMT suffix
|
||||
return checkIfRangeDateHeader(ifRange, versions)
|
||||
val ifRange = try {
|
||||
call.request.headers.getAll(HttpHeaders.IfRange)
|
||||
?.map { parseIfRangeHeader(it) }
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
?.reduce { acc, list -> acc + list }
|
||||
?.parseVersions()
|
||||
?: return true
|
||||
} catch (_: Throwable) {
|
||||
return false
|
||||
}
|
||||
|
||||
return checkIfRangeETagHeader(versions, ifRange)
|
||||
}
|
||||
|
||||
private fun checkIfRangeETagHeader(
|
||||
versions: List<Version>,
|
||||
ifRange: String
|
||||
): Boolean {
|
||||
val parsed = ifRange.parseMatchTag()
|
||||
val versions = conditionalHeadersFeature?.versionsFor(content) ?: content.defaultVersions
|
||||
|
||||
return versions.all { version ->
|
||||
when (version) {
|
||||
is EntityTagVersion -> version.etag in parsed
|
||||
is LastModifiedVersion -> checkLastModified(version, ifRange)
|
||||
is EntityTagVersion -> checkEntityTags(version, ifRange)
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkIfRangeDateHeader(
|
||||
ifRange: String,
|
||||
versions: List<Version>
|
||||
): Boolean {
|
||||
val ifRangeDate = try {
|
||||
ifRange.fromHttpToGmtDate()
|
||||
} catch (_: Throwable) {
|
||||
return false
|
||||
}
|
||||
private fun checkLastModified(actual: LastModifiedVersion, ifRange: List<Version>): Boolean {
|
||||
val actualDate = actual.lastModified.truncateToSeconds()
|
||||
|
||||
return versions.all { version ->
|
||||
when (version) {
|
||||
is LastModifiedVersion -> version.lastModified <= ifRangeDate
|
||||
return ifRange.all { condition ->
|
||||
when (condition) {
|
||||
is LastModifiedVersion -> actualDate <= condition.lastModified
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkEntityTags(actual: EntityTagVersion, ifRange: List<Version>): Boolean {
|
||||
return ifRange.all { condition ->
|
||||
when (condition) {
|
||||
is EntityTagVersion -> actual.etag == condition.etag
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
@@ -221,7 +222,7 @@ class PartialContent(private val maxRangeCount: Int) {
|
||||
|
||||
class Single(
|
||||
val get: Boolean,
|
||||
original: OutgoingContent.ReadChannelContent,
|
||||
original: ReadChannelContent,
|
||||
val range: LongRange,
|
||||
val fullLength: Long
|
||||
) : PartialOutgoingContent(original) {
|
||||
@@ -243,7 +244,7 @@ class PartialContent(private val maxRangeCount: Int) {
|
||||
class Multiple(
|
||||
override val coroutineContext: CoroutineContext,
|
||||
val get: Boolean,
|
||||
original: OutgoingContent.ReadChannelContent,
|
||||
original: ReadChannelContent,
|
||||
val ranges: List<LongRange>,
|
||||
val length: Long,
|
||||
val boundary: String
|
||||
@@ -280,9 +281,33 @@ class PartialContent(private val maxRangeCount: Int) {
|
||||
|
||||
private fun ApplicationCall.isGet() = request.local.method == HttpMethod.Get
|
||||
private fun ApplicationCall.isGetOrHead() = isGet() || request.local.method == HttpMethod.Head
|
||||
private fun String.parseMatchTag() =
|
||||
split("\\s*,\\s*".toRegex()).map { it.removePrefix("W/") }.filter { it.isNotEmpty() }.toSet()
|
||||
}
|
||||
|
||||
private fun List<LongRange>.isAscending(): Boolean =
|
||||
fold(true to 0L) { acc, e -> (acc.first && acc.second <= e.start) to e.start }.first
|
||||
|
||||
private fun parseIfRangeHeader(header: String): List<HeaderValue> {
|
||||
if (header.endsWith(" GMT")) {
|
||||
return listOf(HeaderValue(header))
|
||||
}
|
||||
|
||||
return parseHeaderValue(header)
|
||||
}
|
||||
|
||||
private fun List<HeaderValue>.parseVersions(): List<Version> = mapNotNull { field ->
|
||||
check(field.quality == 1.0) { "If-Range doesn't support quality" }
|
||||
check(field.params.isEmpty()) { "If-Range doesn't support parameters" }
|
||||
|
||||
parseVersion(field.value)
|
||||
}
|
||||
|
||||
private fun parseVersion(value: String): Version? {
|
||||
if (value.isBlank()) return null
|
||||
check(!value.startsWith("W/"))
|
||||
|
||||
if (value.startsWith("\"")) {
|
||||
return EntityTagVersion.parseSingle(value)
|
||||
}
|
||||
|
||||
return LastModifiedVersion(value.fromHttpToGmtDate())
|
||||
}
|
||||
|
||||
@@ -38,28 +38,28 @@ class ETagsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNoConditions() = withConditionalApplication {
|
||||
fun testNoConditions(): Unit = withConditionalApplication {
|
||||
val result = handleRequest {}
|
||||
assertTrue(result.requestHandled)
|
||||
assertEquals(HttpStatusCode.OK, result.response.status())
|
||||
assertEquals("response", result.response.content)
|
||||
assertEquals("tag1", result.response.headers[HttpHeaders.ETag])
|
||||
assertEquals("\"tag1\"", result.response.headers[HttpHeaders.ETag])
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testIfMatchConditionAccepted() = withConditionalApplication {
|
||||
fun testIfMatchConditionAccepted(): Unit = withConditionalApplication {
|
||||
val result = handleRequest {
|
||||
addHeader(HttpHeaders.IfMatch, "tag1")
|
||||
}
|
||||
assertTrue(result.requestHandled)
|
||||
assertEquals(HttpStatusCode.OK, result.response.status())
|
||||
assertEquals("response", result.response.content)
|
||||
assertEquals("tag1", result.response.headers[HttpHeaders.ETag])
|
||||
assertEquals("\"tag1\"", result.response.headers[HttpHeaders.ETag])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIfMatchConditionFailed() = withConditionalApplication {
|
||||
fun testIfMatchConditionFailed(): Unit = withConditionalApplication {
|
||||
val result = handleRequest {
|
||||
addHeader(HttpHeaders.IfMatch, "tag2")
|
||||
}
|
||||
@@ -68,7 +68,7 @@ class ETagsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIfNoneMatchConditionAccepted() = withConditionalApplication {
|
||||
fun testIfNoneMatchConditionAccepted(): Unit = withConditionalApplication {
|
||||
val result = handleRequest {
|
||||
addHeader(HttpHeaders.IfNoneMatch, "tag1")
|
||||
}
|
||||
@@ -77,7 +77,7 @@ class ETagsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIfNoneMatchWeakConditionAccepted() = withConditionalApplication {
|
||||
fun testIfNoneMatchWeakConditionAccepted(): Unit = withConditionalApplication {
|
||||
val result = handleRequest {
|
||||
addHeader(HttpHeaders.IfNoneMatch, "W/tag1")
|
||||
}
|
||||
@@ -86,29 +86,29 @@ class ETagsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIfNoneMatchConditionFailed() = withConditionalApplication {
|
||||
fun testIfNoneMatchConditionFailed(): Unit = withConditionalApplication {
|
||||
val result = handleRequest {
|
||||
addHeader(HttpHeaders.IfNoneMatch, "tag2")
|
||||
}
|
||||
assertTrue(result.requestHandled)
|
||||
assertEquals(HttpStatusCode.OK, result.response.status())
|
||||
assertEquals("response", result.response.content)
|
||||
assertEquals("tag1", result.response.headers[HttpHeaders.ETag])
|
||||
assertEquals("\"tag1\"", result.response.headers[HttpHeaders.ETag])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIfMatchStar() = withConditionalApplication {
|
||||
fun testIfMatchStar(): Unit = withConditionalApplication {
|
||||
val result = handleRequest {
|
||||
addHeader(HttpHeaders.IfMatch, "*")
|
||||
}
|
||||
assertTrue(result.requestHandled)
|
||||
assertEquals(HttpStatusCode.OK, result.response.status())
|
||||
assertEquals("response", result.response.content)
|
||||
assertEquals("tag1", result.response.headers[HttpHeaders.ETag])
|
||||
assertEquals("\"tag1\"", result.response.headers[HttpHeaders.ETag])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIfNoneMatchStar() = withConditionalApplication {
|
||||
fun testIfNoneMatchStar(): Unit = withConditionalApplication {
|
||||
val result = handleRequest {
|
||||
addHeader(HttpHeaders.IfNoneMatch, "*")
|
||||
}
|
||||
@@ -120,7 +120,7 @@ class ETagsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIfNoneMatchListConditionFailed() = withConditionalApplication {
|
||||
fun testIfNoneMatchListConditionFailed(): Unit = withConditionalApplication {
|
||||
val result = handleRequest {
|
||||
addHeader(HttpHeaders.IfNoneMatch, "tag0,tag1,tag3")
|
||||
}
|
||||
@@ -129,29 +129,29 @@ class ETagsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIfNoneMatchListConditionSuccess() = withConditionalApplication {
|
||||
fun testIfNoneMatchListConditionSuccess(): Unit = withConditionalApplication {
|
||||
val result = handleRequest {
|
||||
addHeader(HttpHeaders.IfNoneMatch, "tag2")
|
||||
}
|
||||
assertTrue(result.requestHandled)
|
||||
assertEquals(HttpStatusCode.OK, result.response.status())
|
||||
assertEquals("response", result.response.content)
|
||||
assertEquals("tag1", result.response.headers[HttpHeaders.ETag])
|
||||
assertEquals("\"tag1\"", result.response.headers[HttpHeaders.ETag])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIfMatchListConditionAccepted() = withConditionalApplication {
|
||||
fun testIfMatchListConditionAccepted(): Unit = withConditionalApplication {
|
||||
val result = handleRequest {
|
||||
addHeader(HttpHeaders.IfMatch, "tag0,tag1,tag3")
|
||||
}
|
||||
assertTrue(result.requestHandled)
|
||||
assertEquals(HttpStatusCode.OK, result.response.status())
|
||||
assertEquals("response", result.response.content)
|
||||
assertEquals("tag1", result.response.headers[HttpHeaders.ETag])
|
||||
assertEquals("\"tag1\"", result.response.headers[HttpHeaders.ETag])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIfMatchListConditionFailed() = withConditionalApplication {
|
||||
fun testIfMatchListConditionFailed(): Unit = withConditionalApplication {
|
||||
val result = handleRequest {
|
||||
addHeader(HttpHeaders.IfMatch, "tag0,tag2,tag3")
|
||||
}
|
||||
@@ -166,7 +166,8 @@ class LastModifiedTest(@Suppress("UNUSED_PARAMETER") name: String, zone: ZoneId)
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@Parameterized.Parameters(name = "{0}")
|
||||
fun zones(): List<Array<Any>> = listOf(arrayOf<Any>("GMT", ZoneId.of("GMT")), arrayOf<Any>("SomeLocal", ZoneId.of("GMT+1")))
|
||||
fun zones(): List<Array<Any>> =
|
||||
listOf(arrayOf<Any>("GMT", ZoneId.of("GMT")), arrayOf<Any>("SomeLocal", ZoneId.of("GMT+1")))
|
||||
}
|
||||
|
||||
private val date = ZonedDateTime.now(zone)!!
|
||||
@@ -185,7 +186,7 @@ class LastModifiedTest(@Suppress("UNUSED_PARAMETER") name: String, zone: ZoneId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNoHeaders() = withConditionalApplication {
|
||||
fun testNoHeaders(): Unit = withConditionalApplication {
|
||||
handleRequest(HttpMethod.Get, "/").let { result ->
|
||||
assertEquals(HttpStatusCode.OK, result.response.status())
|
||||
assertEquals("response", result.response.content)
|
||||
@@ -193,8 +194,13 @@ class LastModifiedTest(@Suppress("UNUSED_PARAMETER") name: String, zone: ZoneId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIfModifiedSinceEq() = withConditionalApplication {
|
||||
handleRequest(HttpMethod.Get, "/", { addHeader(HttpHeaders.IfModifiedSince, date.toHttpDateString()) }).let { result ->
|
||||
fun testIfModifiedSinceEq(): Unit = withConditionalApplication {
|
||||
handleRequest(HttpMethod.Get, "/") {
|
||||
addHeader(
|
||||
HttpHeaders.IfModifiedSince,
|
||||
date.toHttpDateString()
|
||||
)
|
||||
}.let { result ->
|
||||
assertEquals(HttpStatusCode.NotModified, result.response.status())
|
||||
assertNull(result.response.content)
|
||||
}
|
||||
@@ -212,7 +218,12 @@ class LastModifiedTest(@Suppress("UNUSED_PARAMETER") name: String, zone: ZoneId)
|
||||
}
|
||||
}
|
||||
|
||||
handleRequest(HttpMethod.Get, "/", { addHeader(HttpHeaders.IfModifiedSince, date.toHttpDateString()) }).let { result ->
|
||||
handleRequest(HttpMethod.Get, "/") {
|
||||
addHeader(
|
||||
HttpHeaders.IfModifiedSince,
|
||||
date.toHttpDateString()
|
||||
)
|
||||
}.let { result ->
|
||||
assertEquals(HttpStatusCode.NotModified, result.response.status())
|
||||
assertNull(result.response.content)
|
||||
}
|
||||
@@ -220,72 +231,287 @@ class LastModifiedTest(@Suppress("UNUSED_PARAMETER") name: String, zone: ZoneId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIfModifiedSinceLess() = withConditionalApplication {
|
||||
handleRequest(HttpMethod.Get, "/", { addHeader(HttpHeaders.IfModifiedSince, date.minusDays(1).toHttpDateString()) }).let { result ->
|
||||
fun testIfModifiedSinceLess(): Unit = withConditionalApplication {
|
||||
handleRequest(HttpMethod.Get, "/") {
|
||||
addHeader(
|
||||
HttpHeaders.IfModifiedSince,
|
||||
date.minusDays(1).toHttpDateString()
|
||||
)
|
||||
}.let { result ->
|
||||
assertEquals(HttpStatusCode.OK, result.response.status())
|
||||
assertEquals("response", result.response.content)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIfModifiedSinceGt() = withConditionalApplication {
|
||||
handleRequest(HttpMethod.Get, "/", { addHeader(HttpHeaders.IfModifiedSince, date.plusDays(1).toHttpDateString()) }).let { result ->
|
||||
fun testIfModifiedSinceGt(): Unit = withConditionalApplication {
|
||||
handleRequest(HttpMethod.Get, "/") {
|
||||
addHeader(
|
||||
HttpHeaders.IfModifiedSince,
|
||||
date.plusDays(1).toHttpDateString()
|
||||
)
|
||||
}.let { result ->
|
||||
assertEquals(HttpStatusCode.NotModified, result.response.status())
|
||||
assertNull(result.response.content)
|
||||
}
|
||||
}
|
||||
|
||||
// this test is disabled since non-GMT timezones are strictly prohibited by the specification
|
||||
fun testIfModifiedSinceTimeZoned() = withConditionalApplication {
|
||||
val expectedDate = date.toHttpDateString()
|
||||
val customFormat = httpDateFormat.withZone(ZoneId.of("Europe/Moscow"))!!
|
||||
|
||||
handleRequest(HttpMethod.Get, "/", { addHeader(HttpHeaders.IfModifiedSince, customFormat.format(date).replace("MT", "MSK")) }).let { result ->
|
||||
assertEquals(HttpStatusCode.NotModified, result.response.status())
|
||||
assertNull(result.response.content)
|
||||
}
|
||||
|
||||
handleRequest(HttpMethod.Get, "/", { addHeader(HttpHeaders.IfModifiedSince, customFormat.format(date.plusDays(1)).replace("MT", "MSK")) }).let { result ->
|
||||
assertEquals(HttpStatusCode.NotModified, result.response.status())
|
||||
assertNull(result.response.content)
|
||||
}
|
||||
|
||||
handleRequest(HttpMethod.Get, "/", { addHeader(HttpHeaders.IfModifiedSince, customFormat.format(date.minusDays(1)).replace("MT", "MSK")) }).let { result ->
|
||||
assertEquals(HttpStatusCode.OK, result.response.status())
|
||||
assertEquals("response", result.response.content)
|
||||
assertEquals(expectedDate, result.response.headers[HttpHeaders.LastModified])
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIfUnModifiedSinceEq() = withConditionalApplication {
|
||||
handleRequest(HttpMethod.Get, "/", { addHeader(HttpHeaders.IfUnmodifiedSince, date.toHttpDateString()) }).let { result ->
|
||||
fun testIfUnModifiedSinceEq(): Unit = withConditionalApplication {
|
||||
handleRequest(HttpMethod.Get, "/") {
|
||||
addHeader(
|
||||
HttpHeaders.IfUnmodifiedSince,
|
||||
date.toHttpDateString()
|
||||
)
|
||||
}.let { result ->
|
||||
assertEquals(HttpStatusCode.OK, result.response.status())
|
||||
assertEquals("response", result.response.content)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIfUnModifiedSinceLess() = withConditionalApplication {
|
||||
handleRequest(HttpMethod.Get, "/", { addHeader(HttpHeaders.IfUnmodifiedSince, date.minusDays(1).toHttpDateString()) }).let { result ->
|
||||
fun testIfUnModifiedSinceLess(): Unit = withConditionalApplication {
|
||||
handleRequest(HttpMethod.Get, "/") {
|
||||
addHeader(
|
||||
HttpHeaders.IfUnmodifiedSince,
|
||||
date.minusDays(1).toHttpDateString()
|
||||
)
|
||||
}.let { result ->
|
||||
assertEquals(HttpStatusCode.PreconditionFailed, result.response.status())
|
||||
assertNull(result.response.content)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIfUnModifiedSinceGt() = withConditionalApplication {
|
||||
handleRequest(HttpMethod.Get, "/", { addHeader(HttpHeaders.IfUnmodifiedSince, date.plusDays(1).toHttpDateString()) }).let { result ->
|
||||
fun testIfUnModifiedSinceGt(): Unit = withConditionalApplication {
|
||||
handleRequest(HttpMethod.Get, "/") {
|
||||
addHeader(
|
||||
HttpHeaders.IfUnmodifiedSince,
|
||||
date.plusDays(1).toHttpDateString()
|
||||
)
|
||||
}.let { result ->
|
||||
assertEquals(HttpStatusCode.OK, result.response.status())
|
||||
assertEquals("response", result.response.content)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIfUnmodifiedSinceIllegal(): Unit = withConditionalApplication {
|
||||
handleRequest(HttpMethod.Get, "/") {
|
||||
addHeader(
|
||||
HttpHeaders.IfUnmodifiedSince,
|
||||
"zzz"
|
||||
)
|
||||
}.let { result ->
|
||||
assertEquals(HttpStatusCode.OK, result.response.status())
|
||||
assertEquals("response", result.response.content)
|
||||
}
|
||||
|
||||
handleRequest(HttpMethod.Get, "/") {
|
||||
addHeader(
|
||||
HttpHeaders.IfUnmodifiedSince,
|
||||
"zzz"
|
||||
)
|
||||
addHeader(
|
||||
HttpHeaders.IfUnmodifiedSince,
|
||||
date.toHttpDateString()
|
||||
)
|
||||
}.let { result ->
|
||||
assertEquals(HttpStatusCode.OK, result.response.status())
|
||||
assertEquals("response", result.response.content)
|
||||
}
|
||||
|
||||
handleRequest(HttpMethod.Get, "/") {
|
||||
addHeader(
|
||||
HttpHeaders.IfUnmodifiedSince,
|
||||
"zzz"
|
||||
)
|
||||
addHeader(
|
||||
HttpHeaders.IfUnmodifiedSince,
|
||||
date.plusDays(1).toHttpDateString()
|
||||
)
|
||||
}.let { result ->
|
||||
assertEquals(HttpStatusCode.OK, result.response.status())
|
||||
assertEquals("response", result.response.content)
|
||||
}
|
||||
|
||||
handleRequest(HttpMethod.Get, "/") {
|
||||
addHeader(
|
||||
HttpHeaders.IfUnmodifiedSince,
|
||||
"zzz"
|
||||
)
|
||||
addHeader(
|
||||
HttpHeaders.IfUnmodifiedSince,
|
||||
date.plusDays(-1).toHttpDateString()
|
||||
)
|
||||
}.let { result ->
|
||||
assertEquals(HttpStatusCode.PreconditionFailed, result.response.status())
|
||||
assertEquals(null, result.response.content)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIfUnmodifiedSinceMultiple(): Unit = withConditionalApplication {
|
||||
handleRequest(HttpMethod.Get, "/") {
|
||||
addHeader(
|
||||
HttpHeaders.IfUnmodifiedSince,
|
||||
""
|
||||
)
|
||||
}.let { result ->
|
||||
assertEquals(HttpStatusCode.OK, result.response.status())
|
||||
assertEquals("response", result.response.content)
|
||||
}
|
||||
|
||||
handleRequest(HttpMethod.Get, "/") {
|
||||
addHeader(
|
||||
HttpHeaders.IfUnmodifiedSince,
|
||||
date.plusDays(1).toHttpDateString()
|
||||
)
|
||||
addHeader(
|
||||
HttpHeaders.IfUnmodifiedSince,
|
||||
date.plusDays(2).toHttpDateString()
|
||||
)
|
||||
}.let { result ->
|
||||
assertEquals(HttpStatusCode.OK, result.response.status())
|
||||
assertEquals("response", result.response.content)
|
||||
}
|
||||
|
||||
handleRequest(HttpMethod.Get, "/") {
|
||||
addHeader(
|
||||
HttpHeaders.IfUnmodifiedSince,
|
||||
date.plusDays(-1).toHttpDateString()
|
||||
)
|
||||
addHeader(
|
||||
HttpHeaders.IfUnmodifiedSince,
|
||||
date.plusDays(2).toHttpDateString()
|
||||
)
|
||||
}.let { result ->
|
||||
assertEquals(HttpStatusCode.PreconditionFailed, result.response.status())
|
||||
assertNull(result.response.content)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIfModifiedSinceMultiple(): Unit = withConditionalApplication {
|
||||
handleRequest(HttpMethod.Get, "/") {
|
||||
addHeader(
|
||||
HttpHeaders.IfModifiedSince,
|
||||
""
|
||||
)
|
||||
}.let { result ->
|
||||
assertEquals(HttpStatusCode.OK, result.response.status())
|
||||
assertEquals("response", result.response.content)
|
||||
}
|
||||
|
||||
handleRequest(HttpMethod.Get, "/") {
|
||||
addHeader(
|
||||
HttpHeaders.IfModifiedSince,
|
||||
date.plusDays(-1).toHttpDateString()
|
||||
)
|
||||
addHeader(
|
||||
HttpHeaders.IfModifiedSince,
|
||||
date.plusDays(2).toHttpDateString()
|
||||
)
|
||||
}.let { result ->
|
||||
assertEquals(HttpStatusCode.OK, result.response.status())
|
||||
assertEquals("response", result.response.content)
|
||||
}
|
||||
|
||||
handleRequest(HttpMethod.Get, "/") {
|
||||
addHeader(
|
||||
HttpHeaders.IfModifiedSince,
|
||||
date.plusDays(1).toHttpDateString()
|
||||
)
|
||||
addHeader(
|
||||
HttpHeaders.IfModifiedSince,
|
||||
date.plusDays(2).toHttpDateString()
|
||||
)
|
||||
}.let { result ->
|
||||
assertEquals(HttpStatusCode.NotModified, result.response.status())
|
||||
assertNull(result.response.content)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBoth(): Unit = withConditionalApplication {
|
||||
handleRequest(HttpMethod.Get, "/") {
|
||||
addHeader(
|
||||
HttpHeaders.IfModifiedSince,
|
||||
""
|
||||
)
|
||||
addHeader(
|
||||
HttpHeaders.IfUnmodifiedSince,
|
||||
""
|
||||
)
|
||||
}.let { result ->
|
||||
assertEquals(HttpStatusCode.OK, result.response.status())
|
||||
assertEquals("response", result.response.content)
|
||||
}
|
||||
|
||||
handleRequest(HttpMethod.Get, "/") {
|
||||
addHeader(
|
||||
HttpHeaders.IfModifiedSince,
|
||||
date.plusDays(-1).toHttpDateString()
|
||||
)
|
||||
addHeader(
|
||||
HttpHeaders.IfUnmodifiedSince,
|
||||
date.plusDays(1).toHttpDateString()
|
||||
)
|
||||
}.let { result ->
|
||||
assertEquals(HttpStatusCode.OK, result.response.status())
|
||||
assertEquals("response", result.response.content)
|
||||
}
|
||||
|
||||
handleRequest(HttpMethod.Get, "/") {
|
||||
addHeader(
|
||||
HttpHeaders.IfModifiedSince,
|
||||
date.plusDays(-1).toHttpDateString()
|
||||
)
|
||||
addHeader(
|
||||
HttpHeaders.IfUnmodifiedSince,
|
||||
date.plusDays(-1).toHttpDateString()
|
||||
)
|
||||
}.let { result ->
|
||||
assertEquals(HttpStatusCode.PreconditionFailed, result.response.status())
|
||||
assertEquals(null, result.response.content)
|
||||
}
|
||||
|
||||
handleRequest(HttpMethod.Get, "/") {
|
||||
addHeader(
|
||||
HttpHeaders.IfModifiedSince,
|
||||
date.plusDays(1).toHttpDateString()
|
||||
)
|
||||
addHeader(
|
||||
HttpHeaders.IfUnmodifiedSince,
|
||||
date.plusDays(1).toHttpDateString()
|
||||
)
|
||||
}.let { result ->
|
||||
assertEquals(HttpStatusCode.NotModified, result.response.status())
|
||||
assertEquals(null, result.response.content)
|
||||
}
|
||||
|
||||
handleRequest(HttpMethod.Get, "/") {
|
||||
addHeader(
|
||||
HttpHeaders.IfModifiedSince,
|
||||
date.plusDays(1).toHttpDateString()
|
||||
)
|
||||
addHeader(
|
||||
HttpHeaders.IfUnmodifiedSince,
|
||||
date.plusDays(-1).toHttpDateString()
|
||||
)
|
||||
}.let { result ->
|
||||
// both conditions are not met but actually it is not clear which one should win
|
||||
// so we declare the order
|
||||
assertEquals(HttpStatusCode.NotModified, result.response.status())
|
||||
assertEquals(null, result.response.content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class LastModifiedVersionTest {
|
||||
private fun temporaryDefaultTimezone(timeZone : TimeZone, block : () -> Unit) {
|
||||
val originalTimeZone : TimeZone = TimeZone.getDefault()
|
||||
private fun temporaryDefaultTimezone(timeZone: TimeZone, block: () -> Unit) {
|
||||
val originalTimeZone: TimeZone = TimeZone.getDefault()
|
||||
TimeZone.setDefault(timeZone)
|
||||
try {
|
||||
block()
|
||||
@@ -294,15 +520,18 @@ class LastModifiedVersionTest {
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkLastModifiedHeaderIsIndependentOfLocalTimezone(constructLastModifiedVersion : (Date) -> LastModifiedVersion) {
|
||||
private fun checkLastModifiedHeaderIsIndependentOfLocalTimezone(constructLastModifiedVersion: (Date) -> LastModifiedVersion) {
|
||||
// setup: any non-zero-offset-Timezone will do
|
||||
temporaryDefaultTimezone(TimeZone.getTimeZone("GMT+08:00")) {
|
||||
|
||||
// guard: local default timezone needs to be different from GMT for the problem to manifest
|
||||
assertTrue(TimeZone.getDefault().rawOffset != 0, "invalid test setup - local timezone is GMT: ${TimeZone.getDefault()}")
|
||||
assertTrue(
|
||||
TimeZone.getDefault().rawOffset != 0,
|
||||
"invalid test setup - local timezone is GMT: ${TimeZone.getDefault()}"
|
||||
)
|
||||
|
||||
// setup: last modified for file
|
||||
val expectedLastModified : Date = SimpleDateFormat("yyyy-MM-dd HH:mm:ss z").parse("2018-03-04 15:12:23 GMT")
|
||||
val expectedLastModified: Date = SimpleDateFormat("yyyy-MM-dd HH:mm:ss z").parse("2018-03-04 15:12:23 GMT")
|
||||
|
||||
// setup: object to test
|
||||
val lastModifiedVersion = constructLastModifiedVersion(expectedLastModified)
|
||||
@@ -320,26 +549,26 @@ class LastModifiedVersionTest {
|
||||
|
||||
@Test
|
||||
fun lastModifiedHeaderFromDateIsIndependentOfLocalTimezone() {
|
||||
checkLastModifiedHeaderIsIndependentOfLocalTimezone { input : Date ->
|
||||
checkLastModifiedHeaderIsIndependentOfLocalTimezone { input: Date ->
|
||||
LastModifiedVersion(input)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun lastModifiedHeaderFromLocalDateTimeIsIndependentOfLocalTimezone() {
|
||||
checkLastModifiedHeaderIsIndependentOfLocalTimezone { input : Date ->
|
||||
checkLastModifiedHeaderIsIndependentOfLocalTimezone { input: Date ->
|
||||
LastModifiedVersion(ZonedDateTime.ofInstant(input.toInstant(), ZoneId.systemDefault()))
|
||||
}
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
val temporaryFolder = TemporaryFolder()
|
||||
val temporaryFolder: TemporaryFolder = TemporaryFolder()
|
||||
|
||||
@Test
|
||||
fun lastModifiedHeaderFromFileTimeIsIndependentOfLocalTimezone() {
|
||||
checkLastModifiedHeaderIsIndependentOfLocalTimezone { input : Date ->
|
||||
checkLastModifiedHeaderIsIndependentOfLocalTimezone { input: Date ->
|
||||
// setup: create file
|
||||
val file : File = temporaryFolder.newFile("foo.txt").apply {
|
||||
val file: File = temporaryFolder.newFile("foo.txt").apply {
|
||||
setLastModified(input.time)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import io.ktor.http.*
|
||||
import io.ktor.response.*
|
||||
import io.ktor.routing.*
|
||||
import io.ktor.server.testing.*
|
||||
import io.ktor.util.date.*
|
||||
import org.junit.Test
|
||||
import java.io.*
|
||||
import java.util.*
|
||||
@@ -22,8 +23,9 @@ class PartialContentTest {
|
||||
.first(File::exists)
|
||||
|
||||
private val localPath = "features/StaticContentTest.kt"
|
||||
private val fileEtag = "etag-99"
|
||||
|
||||
private fun withRangeApplication(maxRangeCount: Int? = null, test: TestApplicationEngine.(File) -> Unit) =
|
||||
private fun withRangeApplication(maxRangeCount: Int? = null, test: TestApplicationEngine.(File) -> Unit): Unit =
|
||||
withTestApplication {
|
||||
application.install(ConditionalHeaders)
|
||||
application.install(CachingHeaders)
|
||||
@@ -36,7 +38,9 @@ class PartialContentTest {
|
||||
handle {
|
||||
val file = basedir.resolve(localPath)
|
||||
if (file.isFile) {
|
||||
call.respond(LocalFileContent(file))
|
||||
call.respond(LocalFileContent(file).apply {
|
||||
versions += EntityTagVersion(fileEtag)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -240,10 +244,72 @@ class PartialContentTest {
|
||||
@Test
|
||||
fun testDontCrashWithEmptyIfRange(): Unit = withRangeApplication { file ->
|
||||
handleRequest(HttpMethod.Get, localPath) {
|
||||
addHeader("Range", "bytes=573-")
|
||||
addHeader("If-Range", "")
|
||||
addHeader(HttpHeaders.Range, "bytes=1-2")
|
||||
addHeader(HttpHeaders.IfRange, "")
|
||||
}.let { result ->
|
||||
assertNull(result.response.headers[HttpHeaders.ContentLength])
|
||||
assertEquals(HttpStatusCode.PartialContent, result.response.status())
|
||||
assertEquals("bytes 1-2/${file.length()}", result.response.headers[HttpHeaders.ContentRange])
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIfRangeETag(): Unit = withRangeApplication { file ->
|
||||
handleRequest(HttpMethod.Get, localPath) {
|
||||
addHeader(HttpHeaders.Range, "bytes=1-2")
|
||||
addHeader(HttpHeaders.IfRange, "\"$fileEtag\"")
|
||||
}.let { result ->
|
||||
assertEquals(HttpStatusCode.PartialContent, result.response.status())
|
||||
assertEquals("bytes 1-2/${file.length()}", result.response.headers[HttpHeaders.ContentRange])
|
||||
}
|
||||
|
||||
handleRequest(HttpMethod.Get, localPath) {
|
||||
addHeader(HttpHeaders.Range, "bytes=1-2")
|
||||
addHeader(HttpHeaders.IfRange, "\"wrong-$fileEtag\"")
|
||||
}.let { result ->
|
||||
assertEquals(HttpStatusCode.OK, result.response.status())
|
||||
assertEquals(null, result.response.headers[HttpHeaders.ContentRange])
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIfRangeDate(): Unit = withRangeApplication { file ->
|
||||
val fileDate = GMTDate(file.lastModified())
|
||||
|
||||
handleRequest(HttpMethod.Get, localPath) {
|
||||
addHeader(HttpHeaders.Range, "bytes=1-2")
|
||||
addHeader(HttpHeaders.IfRange, fileDate.toHttpDate())
|
||||
}.let { result ->
|
||||
assertEquals(HttpStatusCode.PartialContent, result.response.status())
|
||||
assertEquals("bytes 1-2/${file.length()}", result.response.headers[HttpHeaders.ContentRange])
|
||||
}
|
||||
|
||||
handleRequest(HttpMethod.Get, localPath) {
|
||||
addHeader(HttpHeaders.Range, "bytes=1-2")
|
||||
addHeader(HttpHeaders.IfRange, fileDate.plus(10000).toHttpDate())
|
||||
}.let { result ->
|
||||
assertEquals(HttpStatusCode.PartialContent, result.response.status())
|
||||
assertEquals("bytes 1-2/${file.length()}", result.response.headers[HttpHeaders.ContentRange])
|
||||
}
|
||||
|
||||
handleRequest(HttpMethod.Get, localPath) {
|
||||
addHeader(HttpHeaders.Range, "bytes=1-2")
|
||||
addHeader(HttpHeaders.IfRange, fileDate.minus(100000).toHttpDate())
|
||||
}.let { result ->
|
||||
assertEquals(HttpStatusCode.OK, result.response.status())
|
||||
assertEquals(null, result.response.headers[HttpHeaders.ContentRange])
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIfRangeWrongDate(): Unit = withRangeApplication { file ->
|
||||
val fileDate = GMTDate(file.lastModified())
|
||||
|
||||
handleRequest(HttpMethod.Get, localPath) {
|
||||
addHeader(HttpHeaders.Range, "bytes=1-2")
|
||||
addHeader(HttpHeaders.IfRange, fileDate.toHttpDate().drop(15))
|
||||
}.let { result ->
|
||||
assertEquals(HttpStatusCode.OK, result.response.status())
|
||||
assertEquals(null, result.response.headers[HttpHeaders.ContentRange])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user