Skip to content

Move Response extraction logic out of init of DavException #81

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 14 additions & 19 deletions src/main/kotlin/at/bitfire/dav4jvm/DavResource.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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 ->
Expand All @@ -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")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import java.net.HttpURLConnection

class ConflictException: HttpException {

constructor(response: Response): super(response)
companion object: DavExceptionCompanion<ConflictException> {
override fun constructor(message: String?): ConflictException = ConflictException(message)
}

constructor(message: String?): super(HttpURLConnection.HTTP_CONFLICT, message)

}
85 changes: 43 additions & 42 deletions src/main/kotlin/at/bitfire/dav4jvm/exception/DavException.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,29 +33,29 @@ 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) }
}

}

private val logger
get() = Logger.getLogger(javaClass.name)

var request: String? = null
private set

/**
* Body excerpt of [request] (up to [MAX_EXCERPT_SIZE] characters). Only available
Expand All @@ -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
Expand All @@ -79,8 +80,12 @@ open class DavException @JvmOverloads constructor(
var errors: List<Error> = 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()

Expand All @@ -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)
}
}
}
Expand All @@ -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
}
}

}
Original file line number Diff line number Diff line change
@@ -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<CL: DavException> {
fun constructor(message: String?): CL

fun fromHttpResponse(httpResponse: Response): CL {
return constructor(httpResponse.message).apply { populateHttpResponse(httpResponse) }
}

fun fromMessage(message: String?): CL = constructor(message)
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import java.net.HttpURLConnection

class ForbiddenException: HttpException {

constructor(response: Response): super(response)
companion object: DavExceptionCompanion<ForbiddenException> {
override fun constructor(message: String?): ForbiddenException = ForbiddenException(message)
}

constructor(message: String?): super(HttpURLConnection.HTTP_FORBIDDEN, message)

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import java.net.HttpURLConnection

class GoneException: HttpException {

constructor(response: Response): super(response)
companion object: DavExceptionCompanion<GoneException> {
override fun constructor(message: String?): GoneException = GoneException(message)
}

constructor(message: String?): super(HttpURLConnection.HTTP_GONE, message)

}
13 changes: 7 additions & 6 deletions src/main/kotlin/at/bitfire/dav4jvm/exception/HttpException.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,16 @@ import okhttp3.Response
*/
open class HttpException: DavException {

var code: Int
companion object: DavExceptionCompanion<HttpException> {
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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import java.net.HttpURLConnection

class NotFoundException: HttpException {

constructor(response: Response): super(response)
companion object: DavExceptionCompanion<NotFoundException> {
override fun constructor(message: String?): NotFoundException = NotFoundException(message)
}

constructor(message: String?): super(HttpURLConnection.HTTP_NOT_FOUND, message)

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import java.net.HttpURLConnection

class PreconditionFailedException: HttpException {

constructor(response: Response): super(response)
companion object: DavExceptionCompanion<PreconditionFailedException> {
override fun constructor(message: String?): PreconditionFailedException = PreconditionFailedException(message)
}

constructor(message: String?): super(HttpURLConnection.HTTP_PRECON_FAILED, message)

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}


Expand All @@ -75,13 +52,38 @@ class ServiceUnavailableException : HttpException {
}


companion object {
companion object: DavExceptionCompanion<ServiceUnavailableException> {

// 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
}
}
}
)

}

}
Loading
Loading