@@ -13,140 +13,148 @@ package at.bitfire.dav4jvm.exception
13
13
import at.bitfire.dav4jvm.Error
14
14
import at.bitfire.dav4jvm.XmlUtils
15
15
import at.bitfire.dav4jvm.XmlUtils.propertyName
16
- import at.bitfire.dav4jvm.exception.DavException.Companion.MAX_EXCERPT_SIZE
17
16
import okhttp3.MediaType
18
17
import okhttp3.Response
19
18
import okio.Buffer
20
19
import org.xmlpull.v1.XmlPullParser
21
20
import org.xmlpull.v1.XmlPullParserException
22
21
import java.io.ByteArrayOutputStream
23
- import java.io.IOException
24
- import java.io.Serializable
25
- import java.lang.Long.min
26
- import java.util.logging.Level
27
- import java.util.logging.Logger
22
+ import java.io.StringReader
23
+ import javax.annotation.WillNotClose
24
+ import kotlin.math.min
28
25
29
26
/* *
30
27
* Signals that an error occurred during a WebDAV-related operation.
31
28
*
32
- * This could be a logical error like when a required ETag has not been
33
- * received, but also an explicit HTTP error.
29
+ * This could be a logical error like when a required ETag has not been received, but also an explicit HTTP error
30
+ * (usually with a subclass of [HttpException], which in turn extends this class).
31
+ *
32
+ * Often, HTTP response bodies contain valuable information about the error in text format (for instance, a HTML page
33
+ * that contains details about the error) and/or as `<DAV:error>` XML elements. However, such response bodies
34
+ * are sometimes very large.
35
+ *
36
+ * So, if possible and useful, a size-limited excerpt of the associated HTTP request and response can be
37
+ * attached and subsequently included in application-level debug info or shown to the user.
38
+ *
39
+ * Note: [Exception] is serializable, so objects of this class must contain only serializable objects.
40
+ *
41
+ * @param statusCode status code of associated HTTP response
42
+ * @param requestExcerpt cached excerpt of associated HTTP request body
43
+ * @param responseExcerpt cached excerpt of associated HTTP response body
44
+ * @param errors precondition/postcondition XML elements which have been found in the XML response
34
45
*/
35
46
open class DavException @JvmOverloads constructor(
36
- message : String ,
37
- ex : Throwable ? = null ,
38
-
39
- /* *
40
- * An associated HTTP [Response]. Will be closed after evaluation.
41
- */
42
- httpResponse : Response ? = null
43
- ): Exception(message, ex), Serializable {
44
-
45
- companion object {
46
-
47
- const val MAX_EXCERPT_SIZE = 10 * 1024 // don't dump more than 20 kB
48
-
49
- fun isPlainText (type : MediaType ) =
50
- type.type == " text" ||
51
- (type.type == " application" && type.subtype in arrayOf(" html" , " xml" ))
52
-
53
- }
54
-
55
- private val logger
56
- get() = Logger .getLogger(javaClass.name)
57
-
58
- var request: String? = null
47
+ message : String? = null ,
48
+ cause : Throwable ? = null ,
49
+ statusCode : Int? = null ,
50
+ requestExcerpt : String? = null ,
51
+ responseExcerpt : String? = null ,
52
+ errors : List <Error > = emptyList()
53
+ ): Exception(message, cause) {
54
+
55
+ var statusCode: Int? = statusCode
56
+ private set
59
57
60
- /* *
61
- * Body excerpt of [request] (up to [MAX_EXCERPT_SIZE] characters). Only available
62
- * if the HTTP request body was textual content and could be read again.
63
- */
64
- var requestBody: String? = null
58
+ var requestExcerpt: String? = requestExcerpt
65
59
private set
66
60
67
- val response: String?
61
+ var responseExcerpt: String? = responseExcerpt
62
+ private set
68
63
69
- /* *
70
- * Body excerpt of [response] (up to [MAX_EXCERPT_SIZE] characters). Only available
71
- * if the HTTP response body was textual content.
72
- */
73
- var responseBody: String? = null
64
+ var errors: List <Error > = errors
74
65
private set
75
66
76
67
/* *
77
- * Precondition/postcondition XML elements which have been found in the XML response.
68
+ * Takes the request, response and errors from a given HTTP response.
69
+ *
70
+ * @param response response to extract status code and request/response excerpt from (if possible)
71
+ * @param message optional exception message
72
+ * @param cause optional exception cause
78
73
*/
79
- var errors: List <Error > = listOf ()
80
- private set
74
+ constructor (
75
+ message: String? ,
76
+ @WillNotClose response: Response ,
77
+ cause: Throwable ? = null
78
+ ) : this (message, cause) {
79
+ // extract status code
80
+ statusCode = response.code
81
+
82
+ // extract request body if it's text
83
+ val request = response.request
84
+ val requestExcerptBuilder = StringBuilder (
85
+ " ${request.method} ${request.url} "
86
+ )
87
+ request.body?.let { requestBody ->
88
+ if (requestBody.contentType()?.isText() == true ) {
89
+ // Unfortunately Buffer doesn't have a size limit.
90
+ // However large bodies are usually streaming/one-shot away.
91
+ val buffer = Buffer ()
92
+ requestBody.writeTo(buffer)
93
+
94
+ ByteArrayOutputStream ().use { baos ->
95
+ buffer.writeTo(baos, min(buffer.size, MAX_EXCERPT_SIZE .toLong()))
96
+ requestExcerptBuilder
97
+ .append(" \n\n " )
98
+ .append(baos.toString())
99
+ }
100
+ } else
101
+ requestExcerptBuilder.append(" \n\n <request body>" )
102
+ }
103
+ requestExcerpt = requestExcerptBuilder.toString()
104
+
105
+ // extract response body if it's text
106
+ val mimeType = response.body.contentType()
107
+ val responseBody =
108
+ if (mimeType?.isText() == true )
109
+ try {
110
+ response.peekBody(MAX_EXCERPT_SIZE .toLong()).string()
111
+ } catch (_: Exception ) {
112
+ // response body not available anymore, probably already consumed / closed
113
+ null
114
+ }
115
+ else
116
+ null
117
+ responseExcerpt = responseBody
118
+
119
+ // get XML errors from request body excerpt
120
+ if (mimeType?.isXml() == true && responseBody != null )
121
+ errors = extractErrors(responseBody)
122
+ }
123
+
124
+ private fun extractErrors (xml : String ): List <Error > {
125
+ try {
126
+ val parser = XmlUtils .newPullParser()
127
+ parser.setInput(StringReader (xml))
128
+
129
+ var eventType = parser.eventType
130
+ while (eventType != XmlPullParser .END_DOCUMENT ) {
131
+ if (eventType == XmlPullParser .START_TAG && parser.depth == 1 )
132
+ if (parser.propertyName() == Error .NAME )
133
+ return Error .parseError(parser)
134
+ eventType = parser.next()
135
+ }
136
+ } catch (_: XmlPullParserException ) {
137
+ // Couldn't parse XML, either invalid or maybe it wasn't even XML
138
+ }
81
139
140
+ return emptyList()
141
+ }
82
142
83
- init {
84
- if (httpResponse != null ) {
85
- response = httpResponse.toString()
86
143
87
- try {
88
- request = httpResponse.request.toString()
144
+ companion object {
89
145
90
- httpResponse.request.body?.let { body ->
91
- body.contentType()?.let { type ->
92
- if (isPlainText(type)) {
93
- val buffer = Buffer ()
94
- body.writeTo(buffer)
146
+ /* *
147
+ * maximum size of extracted response body
148
+ */
149
+ const val MAX_EXCERPT_SIZE = 20 * 1024
95
150
96
- val baos = ByteArrayOutputStream ()
97
- buffer.writeTo(baos, min(buffer.size, MAX_EXCERPT_SIZE .toLong()))
151
+ private fun MediaType.isText () =
152
+ type == " text" ||
153
+ (type == " application" && subtype in arrayOf(" html" , " xml" ))
98
154
99
- requestBody = baos.toString(type.charset(Charsets .UTF_8 )!! .name())
100
- }
101
- }
102
- }
103
- } catch (e: Exception ) {
104
- logger.log(Level .WARNING , " Couldn't read HTTP request" , e)
105
- requestBody = " Couldn't read HTTP request: ${e.message} "
106
- }
155
+ private fun MediaType.isXml () =
156
+ type in arrayOf(" application" , " text" ) && subtype == " xml"
107
157
108
- try {
109
- // save response body excerpt
110
- if (httpResponse.body?.source() != null ) {
111
- // response body has a source
112
-
113
- httpResponse.peekBody(MAX_EXCERPT_SIZE .toLong()).let { body ->
114
- body.contentType()?.let { mimeType ->
115
- if (isPlainText(mimeType))
116
- responseBody = body.string()
117
- }
118
- }
119
-
120
- httpResponse.body?.use { body ->
121
- body.contentType()?.let {
122
- if (it.type in arrayOf(" application" , " text" ) && it.subtype == " xml" ) {
123
- // look for precondition/postcondition XML elements
124
- try {
125
- val parser = XmlUtils .newPullParser()
126
- parser.setInput(body.charStream())
127
-
128
- var eventType = parser.eventType
129
- while (eventType != XmlPullParser .END_DOCUMENT ) {
130
- if (eventType == XmlPullParser .START_TAG && parser.depth == 1 )
131
- if (parser.propertyName() == Error .NAME )
132
- errors = Error .parseError(parser)
133
- eventType = parser.next()
134
- }
135
- } catch (e: XmlPullParserException ) {
136
- logger.log(Level .WARNING , " Couldn't parse XML response" , e)
137
- }
138
- }
139
- }
140
- }
141
- }
142
- } catch (e: IOException ) {
143
- logger.log(Level .WARNING , " Couldn't read HTTP response" , e)
144
- responseBody = " Couldn't read HTTP response: ${e.message} "
145
- } finally {
146
- httpResponse.body?.close()
147
- }
148
- } else
149
- response = null
150
158
}
151
159
152
160
}
0 commit comments