diff --git a/src/main/kotlin/at/bitfire/dav4jvm/DavResource.kt b/src/main/kotlin/at/bitfire/dav4jvm/DavResource.kt index 25a94bb..ed8e8de 100644 --- a/src/main/kotlin/at/bitfire/dav4jvm/DavResource.kt +++ b/src/main/kotlin/at/bitfire/dav4jvm/DavResource.kt @@ -215,7 +215,7 @@ open class DavResource @JvmOverloads constructor( /* Multiple resources were to be affected by the MOVE, but errors on some of them prevented the operation from taking place. [_] (RFC 4918 9.9.4. Status Codes for MOVE Method) */ - throw HttpException(response) + throw HttpException.fromHttpResponse(response) // update location location.resolve(response.header("Location") ?: destination.toString())?.let { @@ -258,7 +258,7 @@ open class DavResource @JvmOverloads constructor( /* Multiple resources were to be affected by the COPY, but errors on some of them prevented the operation from taking place. [_] (RFC 4918 9.8.5. Status Codes for COPY Method) */ - throw HttpException(response) + throw HttpException.fromHttpResponse(response) callback.onResponse(response) } @@ -549,7 +549,7 @@ open class DavResource @JvmOverloads constructor( /* If an error occurs deleting a member resource (a resource other than the resource identified in the Request-URI), then the response can be a 207 (Multi-Status). […] (RFC 4918 9.6.1. DELETE for Collections) */ - throw HttpException(response) + throw HttpException.fromHttpResponse(response) callback.onResponse(response) } @@ -680,20 +680,15 @@ open class DavResource @JvmOverloads constructor( return throw when (code) { - HttpURLConnection.HTTP_UNAUTHORIZED -> - if (response != null) UnauthorizedException(response) else UnauthorizedException(message) - HttpURLConnection.HTTP_FORBIDDEN -> - if (response != null) ForbiddenException(response) else ForbiddenException(message) - HttpURLConnection.HTTP_NOT_FOUND -> - if (response != null) NotFoundException(response) else NotFoundException(message) - HttpURLConnection.HTTP_CONFLICT -> - if (response != null) ConflictException(response) else ConflictException(message) - HttpURLConnection.HTTP_PRECON_FAILED -> - if (response != null) PreconditionFailedException(response) else PreconditionFailedException(message) - HttpURLConnection.HTTP_UNAVAILABLE -> - if (response != null) ServiceUnavailableException(response) else ServiceUnavailableException(message) - else -> - if (response != null) HttpException(response) else HttpException(code, message) + HttpURLConnection.HTTP_UNAUTHORIZED -> UnauthorizedException + HttpURLConnection.HTTP_FORBIDDEN -> ForbiddenException + HttpURLConnection.HTTP_NOT_FOUND -> NotFoundException + HttpURLConnection.HTTP_CONFLICT -> ConflictException + HttpURLConnection.HTTP_PRECON_FAILED -> PreconditionFailedException + HttpURLConnection.HTTP_UNAVAILABLE -> ServiceUnavailableException + else -> HttpException + }.let { exceptionClass -> + if (response != null) exceptionClass.fromHttpResponse(response) else exceptionClass.fromMessage(message) } } @@ -739,7 +734,7 @@ open class DavResource @JvmOverloads constructor( */ fun assertMultiStatus(response: Response) { if (response.code != HTTP_MULTISTATUS) - throw DavException("Expected 207 Multi-Status, got ${response.code} ${response.message}", httpResponse = response) + throw DavException.fromHttpResponse("Expected 207 Multi-Status, got ${response.code} ${response.message}", httpResponse = response) response.peekBody(XML_SIGNATURE.size.toLong()).use { body -> body.contentType()?.let { mimeType -> @@ -760,7 +755,7 @@ open class DavResource @JvmOverloads constructor( logger.log(Level.WARNING, "Couldn't scan for XML signature", e) } - throw DavException("Received non-XML 207 Multi-Status", httpResponse = response) + throw DavException.fromHttpResponse("Received non-XML 207 Multi-Status", httpResponse = response) } } ?: logger.warning("Received 207 Multi-Status without Content-Type, assuming XML") } diff --git a/src/main/kotlin/at/bitfire/dav4jvm/exception/ConflictException.kt b/src/main/kotlin/at/bitfire/dav4jvm/exception/ConflictException.kt index 03e52c4..5b24ae8 100644 --- a/src/main/kotlin/at/bitfire/dav4jvm/exception/ConflictException.kt +++ b/src/main/kotlin/at/bitfire/dav4jvm/exception/ConflictException.kt @@ -15,7 +15,10 @@ import java.net.HttpURLConnection class ConflictException: HttpException { - constructor(response: Response): super(response) + companion object: DavExceptionCompanion { + override fun constructor(message: String?): ConflictException = ConflictException(message) + } + constructor(message: String?): super(HttpURLConnection.HTTP_CONFLICT, message) } diff --git a/src/main/kotlin/at/bitfire/dav4jvm/exception/DavException.kt b/src/main/kotlin/at/bitfire/dav4jvm/exception/DavException.kt index 19e8042..4a3abb0 100644 --- a/src/main/kotlin/at/bitfire/dav4jvm/exception/DavException.kt +++ b/src/main/kotlin/at/bitfire/dav4jvm/exception/DavException.kt @@ -33,22 +33,21 @@ import java.util.logging.Logger * received, but also an explicit HTTP error. */ open class DavException @JvmOverloads constructor( - message: String, - ex: Throwable? = null, - - /** - * An associated HTTP [Response]. Will be closed after evaluation. - */ - httpResponse: Response? = null -): Exception(message, ex), Serializable { + message: String, + ex: Throwable? = null, +) : Exception(message, ex), Serializable { companion object { - const val MAX_EXCERPT_SIZE = 10*1024 // don't dump more than 20 kB + const val MAX_EXCERPT_SIZE = 10 * 1024 // don't dump more than 20 kB fun isPlainText(type: MediaType) = - type.type == "text" || - (type.type == "application" && type.subtype in arrayOf("html", "xml")) + type.type == "text" || + (type.type == "application" && type.subtype in arrayOf("html", "xml")) + + fun fromHttpResponse(message: String, ex: Throwable? = null, httpResponse: Response?): DavException { + return DavException(message, ex).apply { populateHttpResponse(httpResponse) } + } } @@ -56,6 +55,7 @@ open class DavException @JvmOverloads constructor( get() = Logger.getLogger(javaClass.name) var request: String? = null + private set /** * Body excerpt of [request] (up to [MAX_EXCERPT_SIZE] characters). Only available @@ -64,7 +64,8 @@ open class DavException @JvmOverloads constructor( var requestBody: String? = null private set - val response: String? + var response: String? = null + private set /** * Body excerpt of [response] (up to [MAX_EXCERPT_SIZE] characters). Only available @@ -79,8 +80,12 @@ open class DavException @JvmOverloads constructor( var errors: List = listOf() private set - - init { + /** + * Fills [request], [requestBody], [response], [responseBody] and [errors] according to the given [httpResponse]. + * + * The whole response body may be loaded, so this function should be called in blocking-sensitive contexts. + */ + fun populateHttpResponse(httpResponse: Response?) { if (httpResponse != null) { response = httpResponse.toString() @@ -106,35 +111,30 @@ open class DavException @JvmOverloads constructor( } try { - // save response body excerpt - if (httpResponse.body?.source() != null) { - // response body has a source - - httpResponse.peekBody(MAX_EXCERPT_SIZE.toLong()).let { body -> - body.contentType()?.let { mimeType -> - if (isPlainText(mimeType)) - responseBody = body.string() - } + httpResponse.peekBody(MAX_EXCERPT_SIZE.toLong()).let { body -> + body.contentType()?.let { mimeType -> + if (isPlainText(mimeType)) + responseBody = body.string() } + } - httpResponse.body?.use { body -> - body.contentType()?.let { - if (it.type in arrayOf("application", "text") && it.subtype == "xml") { - // look for precondition/postcondition XML elements - try { - val parser = XmlUtils.newPullParser() - parser.setInput(body.charStream()) - - var eventType = parser.eventType - while (eventType != XmlPullParser.END_DOCUMENT) { - if (eventType == XmlPullParser.START_TAG && parser.depth == 1) - if (parser.propertyName() == Error.NAME) - errors = Error.parseError(parser) - eventType = parser.next() - } - } catch (e: XmlPullParserException) { - logger.log(Level.WARNING, "Couldn't parse XML response", e) + httpResponse.body.use { body -> + body.contentType()?.let { + if (it.type in arrayOf("application", "text") && it.subtype == "xml") { + // look for precondition/postcondition XML elements + try { + val parser = XmlUtils.newPullParser() + parser.setInput(body.charStream()) + + var eventType = parser.eventType + while (eventType != XmlPullParser.END_DOCUMENT) { + if (eventType == XmlPullParser.START_TAG && parser.depth == 1) + if (parser.propertyName() == Error.NAME) + errors = Error.parseError(parser) + eventType = parser.next() } + } catch (e: XmlPullParserException) { + logger.log(Level.WARNING, "Couldn't parse XML response", e) } } } @@ -143,10 +143,11 @@ open class DavException @JvmOverloads constructor( logger.log(Level.WARNING, "Couldn't read HTTP response", e) responseBody = "Couldn't read HTTP response: ${e.message}" } finally { - httpResponse.body?.close() + httpResponse.body.close() } - } else + } else { response = null + } } } \ No newline at end of file diff --git a/src/main/kotlin/at/bitfire/dav4jvm/exception/DavExceptionCompanion.kt b/src/main/kotlin/at/bitfire/dav4jvm/exception/DavExceptionCompanion.kt new file mode 100644 index 0000000..249d5e0 --- /dev/null +++ b/src/main/kotlin/at/bitfire/dav4jvm/exception/DavExceptionCompanion.kt @@ -0,0 +1,23 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +package at.bitfire.dav4jvm.exception + +import okhttp3.Response + +interface DavExceptionCompanion { + fun constructor(message: String?): CL + + fun fromHttpResponse(httpResponse: Response): CL { + return constructor(httpResponse.message).apply { populateHttpResponse(httpResponse) } + } + + fun fromMessage(message: String?): CL = constructor(message) +} diff --git a/src/main/kotlin/at/bitfire/dav4jvm/exception/ForbiddenException.kt b/src/main/kotlin/at/bitfire/dav4jvm/exception/ForbiddenException.kt index 884cc00..465c10d 100644 --- a/src/main/kotlin/at/bitfire/dav4jvm/exception/ForbiddenException.kt +++ b/src/main/kotlin/at/bitfire/dav4jvm/exception/ForbiddenException.kt @@ -15,7 +15,10 @@ import java.net.HttpURLConnection class ForbiddenException: HttpException { - constructor(response: Response): super(response) + companion object: DavExceptionCompanion { + override fun constructor(message: String?): ForbiddenException = ForbiddenException(message) + } + constructor(message: String?): super(HttpURLConnection.HTTP_FORBIDDEN, message) } diff --git a/src/main/kotlin/at/bitfire/dav4jvm/exception/GoneException.kt b/src/main/kotlin/at/bitfire/dav4jvm/exception/GoneException.kt index 0cbd9b3..ec27050 100644 --- a/src/main/kotlin/at/bitfire/dav4jvm/exception/GoneException.kt +++ b/src/main/kotlin/at/bitfire/dav4jvm/exception/GoneException.kt @@ -15,7 +15,10 @@ import java.net.HttpURLConnection class GoneException: HttpException { - constructor(response: Response): super(response) + companion object: DavExceptionCompanion { + override fun constructor(message: String?): GoneException = GoneException(message) + } + constructor(message: String?): super(HttpURLConnection.HTTP_GONE, message) } diff --git a/src/main/kotlin/at/bitfire/dav4jvm/exception/HttpException.kt b/src/main/kotlin/at/bitfire/dav4jvm/exception/HttpException.kt index 08534a4..5c699cc 100644 --- a/src/main/kotlin/at/bitfire/dav4jvm/exception/HttpException.kt +++ b/src/main/kotlin/at/bitfire/dav4jvm/exception/HttpException.kt @@ -17,15 +17,16 @@ import okhttp3.Response */ open class HttpException: DavException { - var code: Int + companion object: DavExceptionCompanion { + override fun constructor(message: String?): HttpException = HttpException(-1, message) - constructor(response: Response): super( - "HTTP ${response.code} ${response.message}", - httpResponse = response - ) { - code = response.code + override fun fromHttpResponse(httpResponse: Response): HttpException { + return HttpException(httpResponse.code, httpResponse.message).apply { populateHttpResponse(httpResponse) } + } } + var code: Int + constructor(code: Int, message: String?): super("HTTP $code $message") { this.code = code } diff --git a/src/main/kotlin/at/bitfire/dav4jvm/exception/NotFoundException.kt b/src/main/kotlin/at/bitfire/dav4jvm/exception/NotFoundException.kt index d04c0ac..c768c3b 100644 --- a/src/main/kotlin/at/bitfire/dav4jvm/exception/NotFoundException.kt +++ b/src/main/kotlin/at/bitfire/dav4jvm/exception/NotFoundException.kt @@ -15,7 +15,10 @@ import java.net.HttpURLConnection class NotFoundException: HttpException { - constructor(response: Response): super(response) + companion object: DavExceptionCompanion { + override fun constructor(message: String?): NotFoundException = NotFoundException(message) + } + constructor(message: String?): super(HttpURLConnection.HTTP_NOT_FOUND, message) } diff --git a/src/main/kotlin/at/bitfire/dav4jvm/exception/PreconditionFailedException.kt b/src/main/kotlin/at/bitfire/dav4jvm/exception/PreconditionFailedException.kt index 6760736..eb8bcff 100644 --- a/src/main/kotlin/at/bitfire/dav4jvm/exception/PreconditionFailedException.kt +++ b/src/main/kotlin/at/bitfire/dav4jvm/exception/PreconditionFailedException.kt @@ -15,7 +15,10 @@ import java.net.HttpURLConnection class PreconditionFailedException: HttpException { - constructor(response: Response): super(response) + companion object: DavExceptionCompanion { + override fun constructor(message: String?): PreconditionFailedException = PreconditionFailedException(message) + } + constructor(message: String?): super(HttpURLConnection.HTTP_PRECON_FAILED, message) } diff --git a/src/main/kotlin/at/bitfire/dav4jvm/exception/ServiceUnavailableException.kt b/src/main/kotlin/at/bitfire/dav4jvm/exception/ServiceUnavailableException.kt index 0665fc0..9a8e709 100644 --- a/src/main/kotlin/at/bitfire/dav4jvm/exception/ServiceUnavailableException.kt +++ b/src/main/kotlin/at/bitfire/dav4jvm/exception/ServiceUnavailableException.kt @@ -22,33 +22,10 @@ import java.util.logging.Logger class ServiceUnavailableException : HttpException { - private val logger - get() = Logger.getLogger(javaClass.name) - val retryAfter: Instant? - constructor(message: String?) : super(HttpURLConnection.HTTP_UNAVAILABLE, message) { - retryAfter = null - } - - constructor(response: Response) : super(response) { - // Retry-After = "Retry-After" ":" ( HTTP-date | delta-seconds ) - // HTTP-date = rfc1123-date | rfc850-date | asctime-date - - var retryAfterValue: Instant? = null - response.header("Retry-After")?.let { after -> - retryAfterValue = HttpUtils.parseDate(after) ?: - // not a HTTP-date, must be delta-seconds - try { - val seconds = after.toLong() - Instant.now().plusSeconds(seconds) - } catch (e: NumberFormatException) { - logger.log(Level.WARNING, "Received Retry-After which was not a HTTP-date nor delta-seconds: $after", e) - null - } - } - - retryAfter = retryAfterValue + constructor(message: String?, retryAfter: Instant? = null) : super(HttpURLConnection.HTTP_UNAVAILABLE, message) { + this.retryAfter = retryAfter } @@ -75,13 +52,38 @@ class ServiceUnavailableException : HttpException { } - companion object { + companion object: DavExceptionCompanion { // default values for getDelayUntil const val DELAY_UNTIL_DEFAULT = 15 * 60L // 15 min const val DELAY_UNTIL_MIN = 1 * 60L // 1 min const val DELAY_UNTIL_MAX = 2 * 60 * 60L // 2 hours + private val logger + get() = Logger.getLogger(this::javaClass.name) + + override fun constructor(message: String?): ServiceUnavailableException = ServiceUnavailableException(message) + + override fun fromHttpResponse(httpResponse: Response): ServiceUnavailableException = ServiceUnavailableException( + message = httpResponse.message, + retryAfter = httpResponse.let { response -> + // Retry-After = "Retry-After" ":" ( HTTP-date | delta-seconds ) + // HTTP-date = rfc1123-date | rfc850-date | asctime-date + + response.header("Retry-After")?.let { after -> + HttpUtils.parseDate(after) ?: + // not a HTTP-date, must be delta-seconds + try { + val seconds = after.toLong() + Instant.now().plusSeconds(seconds) + } catch (e: NumberFormatException) { + logger.log(Level.WARNING, "Received Retry-After which was not a HTTP-date nor delta-seconds: $after", e) + null + } + } + } + ) + } } \ No newline at end of file diff --git a/src/main/kotlin/at/bitfire/dav4jvm/exception/UnauthorizedException.kt b/src/main/kotlin/at/bitfire/dav4jvm/exception/UnauthorizedException.kt index 98a6183..8ea030c 100644 --- a/src/main/kotlin/at/bitfire/dav4jvm/exception/UnauthorizedException.kt +++ b/src/main/kotlin/at/bitfire/dav4jvm/exception/UnauthorizedException.kt @@ -15,7 +15,10 @@ import java.net.HttpURLConnection class UnauthorizedException: HttpException { - constructor(response: Response): super(response) + companion object: DavExceptionCompanion { + override fun constructor(message: String?): UnauthorizedException = UnauthorizedException(message) + } + constructor(message: String?): super(HttpURLConnection.HTTP_UNAUTHORIZED, message) } diff --git a/src/test/kotlin/at/bitfire/dav4jvm/exception/DavExceptionTest.kt b/src/test/kotlin/at/bitfire/dav4jvm/exception/DavExceptionTest.kt index d7c2f9c..7526e46 100644 --- a/src/test/kotlin/at/bitfire/dav4jvm/exception/DavExceptionTest.kt +++ b/src/test/kotlin/at/bitfire/dav4jvm/exception/DavExceptionTest.kt @@ -58,15 +58,19 @@ class DavExceptionTest { builder.append(CharArray(DavException.MAX_EXCERPT_SIZE+100) { '*' }) val body = builder.toString() - val e = DavException("Error with large request body", null, Response.Builder() - .request(Request.Builder() - .url("http://example.com") - .post(body.toRequestBody("text/plain".toMediaType())) - .build()) - .protocol(Protocol.HTTP_1_1) - .code(204) - .message("No Content") - .build()) + val e = DavException.fromHttpResponse( + "Error with large request body", + null, + Response.Builder() + .request(Request.Builder() + .url("http://example.com") + .post(body.toRequestBody("text/plain".toMediaType())) + .build()) + .protocol(Protocol.HTTP_1_1) + .code(204) + .message("No Content") + .build() + ) assertTrue(e.errors.isEmpty()) assertEquals( diff --git a/src/test/kotlin/at/bitfire/dav4jvm/exception/HttpExceptionTest.kt b/src/test/kotlin/at/bitfire/dav4jvm/exception/HttpExceptionTest.kt index b2298a7..390cd98 100644 --- a/src/test/kotlin/at/bitfire/dav4jvm/exception/HttpExceptionTest.kt +++ b/src/test/kotlin/at/bitfire/dav4jvm/exception/HttpExceptionTest.kt @@ -37,7 +37,7 @@ class HttpExceptionTest { .message(responseMessage) .body("SERVER\r\nRESPONSE".toResponseBody("text/something-other".toMediaType())) .build() - val e = HttpException(response) + val e = HttpException.fromHttpResponse(response) assertTrue(e.message!!.contains("500")) assertTrue(e.message!!.contains(responseMessage)) assertTrue(e.requestBody!!.contains("REQUEST\nBODY")) diff --git a/src/test/kotlin/at/bitfire/dav4jvm/exception/ServiceUnavailableExceptionTest.kt b/src/test/kotlin/at/bitfire/dav4jvm/exception/ServiceUnavailableExceptionTest.kt index c214d32..763ea2b 100644 --- a/src/test/kotlin/at/bitfire/dav4jvm/exception/ServiceUnavailableExceptionTest.kt +++ b/src/test/kotlin/at/bitfire/dav4jvm/exception/ServiceUnavailableExceptionTest.kt @@ -33,7 +33,7 @@ class ServiceUnavailableExceptionTest { @Test fun testRetryAfter_NoTime() { - val e = ServiceUnavailableException(response503) + val e = ServiceUnavailableException.fromHttpResponse(response503) assertNull(e.retryAfter) } @@ -42,7 +42,7 @@ class ServiceUnavailableExceptionTest { val response = response503.newBuilder() .header("Retry-After", "120") .build() - val e = ServiceUnavailableException(response) + val e = ServiceUnavailableException.fromHttpResponse(response) assertNotNull(e.retryAfter) assertTrue(withinTimeRange(e.retryAfter!!, 120)) } @@ -53,7 +53,7 @@ class ServiceUnavailableExceptionTest { val response = response503.newBuilder() .header("Retry-After", HttpUtils.formatDate(after30min)) .build() - val e = ServiceUnavailableException(response) + val e = ServiceUnavailableException.fromHttpResponse(response) assertNotNull(e.retryAfter) assertTrue(withinTimeRange(e.retryAfter!!, 30*60)) }