Skip to content
Merged
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
122 changes: 23 additions & 99 deletions src/main/kotlin/at/bitfire/dav4jvm/exception/DavException.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,8 @@
package at.bitfire.dav4jvm.exception

import at.bitfire.dav4jvm.Error
import at.bitfire.dav4jvm.XmlUtils
import at.bitfire.dav4jvm.XmlUtils.propertyName
import okhttp3.MediaType
import okhttp3.Response
import okio.Buffer
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException
import java.io.ByteArrayOutputStream
import java.io.StringReader
import javax.annotation.WillNotClose
import kotlin.math.min

/**
* Signals that an error occurred during a WebDAV-related operation.
Expand All @@ -43,102 +34,42 @@ import kotlin.math.min
* @param responseExcerpt cached excerpt of associated HTTP response body
* @param errors precondition/postcondition XML elements which have been found in the XML response
*/
open class DavException @JvmOverloads constructor(
open class DavException(
message: String? = null,
cause: Throwable? = null,
statusCode: Int? = null,
requestExcerpt: String? = null,
responseExcerpt: String? = null,
errors: List<Error> = emptyList()
open val statusCode: Int? = null,
val requestExcerpt: String? = null,
val responseExcerpt: String? = null,
val errors: List<Error> = emptyList()
): Exception(message, cause) {

var statusCode: Int? = statusCode
private set

var requestExcerpt: String? = requestExcerpt
private set

var responseExcerpt: String? = responseExcerpt
private set

var errors: List<Error> = errors
private set
// constructor from Response

/**
* Takes the request, response and errors from a given HTTP response.
*
* @param response response to extract status code and request/response excerpt from (if possible)
* @param message optional exception message
* @param cause optional exception cause
* @param response response to extract status code and request/response excerpt from (if possible)
*/
constructor(
message: String?,
@WillNotClose response: Response,
cause: Throwable? = null
) : this(message, cause) {
// extract status code
statusCode = response.code

// extract request body if it's text
val request = response.request
val requestExcerptBuilder = StringBuilder(
"${request.method} ${request.url}"
)
request.body?.let { requestBody ->
if (requestBody.contentType()?.isText() == true) {
// Unfortunately Buffer doesn't have a size limit.
// However large bodies are usually streaming/one-shot away.
val buffer = Buffer()
requestBody.writeTo(buffer)

ByteArrayOutputStream().use { baos ->
buffer.writeTo(baos, min(buffer.size, MAX_EXCERPT_SIZE.toLong()))
requestExcerptBuilder
.append("\n\n")
.append(baos.toString())
}
} else
requestExcerptBuilder.append("\n\n<request body>")
}
requestExcerpt = requestExcerptBuilder.toString()

// extract response body if it's text
val mimeType = response.body.contentType()
val responseBody =
if (mimeType?.isText() == true)
try {
response.peekBody(MAX_EXCERPT_SIZE.toLong()).string()
} catch (_: Exception) {
// response body not available anymore, probably already consumed / closed
null
}
else
null
responseExcerpt = responseBody
message: String,
cause: Throwable? = null,
@WillNotClose response: Response
) : this(message, cause, HttpResponseInfo.fromResponse(response))

// get XML errors from request body excerpt
if (mimeType?.isXml() == true && responseBody != null)
errors = extractErrors(responseBody)
}

private fun extractErrors(xml: String): List<Error> {
try {
val parser = XmlUtils.newPullParser()
parser.setInput(StringReader(xml))

var eventType = parser.eventType
while (eventType != XmlPullParser.END_DOCUMENT) {
if (eventType == XmlPullParser.START_TAG && parser.depth == 1)
if (parser.propertyName() == Error.NAME)
return Error.parseError(parser)
eventType = parser.next()
}
} catch (_: XmlPullParserException) {
// Couldn't parse XML, either invalid or maybe it wasn't even XML
}

return emptyList()
}
private constructor(
message: String?,
cause: Throwable? = null,
httpResponseInfo: HttpResponseInfo
): this(
message = message,
cause = cause,
statusCode = httpResponseInfo.statusCode,
requestExcerpt = httpResponseInfo.requestExcerpt,
responseExcerpt = httpResponseInfo.responseExcerpt,
errors = httpResponseInfo.errors
)


companion object {
Expand All @@ -148,13 +79,6 @@ open class DavException @JvmOverloads constructor(
*/
const val MAX_EXCERPT_SIZE = 20*1024

private fun MediaType.isText() =
type == "text" ||
(type == "application" && subtype in arrayOf("html", "xml"))

private fun MediaType.isXml() =
type in arrayOf("application", "text") && subtype == "xml"

}

}
55 changes: 51 additions & 4 deletions src/main/kotlin/at/bitfire/dav4jvm/exception/HttpException.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,63 @@

package at.bitfire.dav4jvm.exception

import at.bitfire.dav4jvm.Error
import okhttp3.Response
import javax.annotation.WillNotClose

/**
* Signals that a HTTP error was sent by the server in the context of a WebDAV operation.
*/
open class HttpException: DavException {
open class HttpException(
message: String? = null,
cause: Throwable? = null,
override val statusCode: Int,
requestExcerpt: String?,
responseExcerpt: String?,
errors: List<Error> = emptyList()
): DavException(message, cause, statusCode, requestExcerpt, responseExcerpt, errors) {

constructor(response: Response) : super(
message = "HTTP ${response.code} ${response.message}",
response = response
// constructor from Response

/**
* Takes the request, response and errors from a given HTTP response.
*
* @param response response to extract status code and request/response excerpt from (if possible)
* @param message optional exception message
* @param cause optional exception cause
*/
constructor(
@WillNotClose response: Response,
message: String = "HTTP ${response.code} ${response.message}",
cause: Throwable? = null
) : this(HttpResponseInfo.fromResponse(response), message, cause)

private constructor(
httpResponseInfo: HttpResponseInfo,
message: String?,
cause: Throwable? = null
): this(
message = message,
cause = cause,
statusCode = httpResponseInfo.statusCode,
requestExcerpt = httpResponseInfo.requestExcerpt,
responseExcerpt = httpResponseInfo.responseExcerpt,
errors = httpResponseInfo.errors
)


// status code classes

/** Whether the [statusCode] is 3xx and thus indicates a redirection. */
val isRedirect
get() = statusCode / 100 == 3

/** Whether the [statusCode] is 4xx and thus indicates a client error. */
val isClientError
get() = statusCode / 100 == 4

/** Whether the [statusCode] is 5xx and thus indicates a server error. */
val isServerError
get() = statusCode / 100 == 5

}
117 changes: 117 additions & 0 deletions src/main/kotlin/at/bitfire/dav4jvm/exception/HttpResponseInfo.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* 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 at.bitfire.dav4jvm.Error
import at.bitfire.dav4jvm.XmlUtils
import at.bitfire.dav4jvm.XmlUtils.propertyName
import at.bitfire.dav4jvm.exception.DavException.Companion.MAX_EXCERPT_SIZE
import okhttp3.MediaType
import okhttp3.Response
import okio.Buffer
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException
import java.io.ByteArrayOutputStream
import java.io.StringReader
import javax.annotation.WillNotClose
import kotlin.math.min

internal class HttpResponseInfo private constructor(
val statusCode: Int,
val requestExcerpt: String?,
val responseExcerpt: String?,
val errors: List<Error>
) {

companion object {

fun fromResponse(@WillNotClose response: Response): HttpResponseInfo {
// extract request body if it's text
val request = response.request
val requestExcerptBuilder = StringBuilder(
"${request.method} ${request.url}"
)
request.body?.let { requestBody ->
if (requestBody.contentType()?.isText() == true) {
// Unfortunately Buffer doesn't have a size limit.
// However large bodies are usually streaming/one-shot away.
val buffer = Buffer()
requestBody.writeTo(buffer)

ByteArrayOutputStream().use { baos ->
buffer.writeTo(baos, min(buffer.size, MAX_EXCERPT_SIZE.toLong()))
requestExcerptBuilder
.append("\n\n")
.append(baos.toString())
}
} else
requestExcerptBuilder.append("\n\n<request body (${requestBody.contentLength()} bytes)>")
}

// extract response body if it's text
val mimeType = response.body.contentType()
val responseBody =
if (mimeType?.isText() == true)
try {
response.peekBody(MAX_EXCERPT_SIZE.toLong()).string()
} catch (_: Exception) {
// response body not available anymore, probably already consumed / closed
null
}
else
null

// get XML errors from request body excerpt
val errors: List<Error> = if (mimeType?.isXml() == true && responseBody != null)
extractErrors(responseBody)
else
emptyList()

return HttpResponseInfo(
statusCode = response.code,
requestExcerpt = requestExcerptBuilder.toString(),
responseExcerpt = responseBody,
errors = errors
)
}

private fun extractErrors(xml: String): List<Error> {
try {
val parser = XmlUtils.newPullParser()
parser.setInput(StringReader(xml))

var eventType = parser.eventType
while (eventType != XmlPullParser.END_DOCUMENT) {
if (eventType == XmlPullParser.START_TAG && parser.depth == 1)
if (parser.propertyName() == Error.NAME)
return Error.parseError(parser)
eventType = parser.next()
}
} catch (_: XmlPullParserException) {
// Couldn't parse XML, either invalid or maybe it wasn't even XML
}

return emptyList()
}


// extensions

private fun MediaType.isText() =
type == "text" ||
(type == "application" && subtype in arrayOf("html", "xml"))

private fun MediaType.isXml() =
type in arrayOf("application", "text") && subtype == "xml"

}

}
Loading