Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit 84f9d206 authored by Ricki Hirner's avatar Ricki Hirner Committed by GitHub
Browse files

[Ktor] Use scoped `execute()` for streaming responses (#122)

* Refactor HTTP response handling

- Update `HttpResponseInfo` to handle request and response bodies more efficiently
- Use `runBlocking` for suspending functions in constructors
- Remove unnecessary `WillNotClose` annotations
- Cleanup and streamline exception handling logic

* Refactor response info extraction

- Remove redundant parameters in MockEngine
- Simplify HttpClient initialization
- Update request excerpt formatting
- Add test for request excerpt with no body
- Rename test for large text content

* Add support for already consumed response bodies

* KDoc

* Make DavException and HttpException construction non-blocking

* [WIP] Remove derived exception classes

* Add specific HTTP response exceptions for common status codes

- Add new exception classes for common HTTP status codes:
  - `UnauthorizedException` (401)
  - `ForbiddenException` (403)
  - `NotFoundException` (404)
  - `ConflictException` (409)
  - `GoneException` (410)
  - `PreconditionFailedException` (412)
- Update `ServiceUnavailableException` to handle `Retry-After` header
- Update `HttpResponseInfo` to use `HttpStatusCode` instead of raw status code
- Update `DavException` and `HttpException` to use `HttpResponseInfo` for response details
- Update tests to reflect changes in exception handling

* Improve error handling and logging in `assertMultiStatus` and `processMultiStatus` methods

- Update `DavResourceTest` to use `bodyAsChannel` method for response body handling.
- Move `ContentType` extension methods from `HttpResponseInfo` to `KtorHttpUtils`.
- Refactor `assertMultiStatus` in `DavResource` to handle response bodies using `ByteReadChannel`.
- Update `processMultiStatus` to use `ByteReadChannel` directly instead of `Reader`.
- Improve error handling and logging in `assertMultiStatus` and `processMultiStatus` methods.

* Refactor and optimize HTTP response handling in Ktor

- Optimize condition check order for request content type and content in `HttpResponseInfo.kt`.
- Update `DavResource.kt` to use `encodeToByteString` for XML signature and improve the `assertMultiStatus` function.
- Enhance documentation for the `assertMultiStatus` method by clarifying parameter usage and exceptions thrown.

* Minor changes

* Update followRedirects

* Allow streaming of DavResource calls

* Follow redirects, implement streaming

* Fix tests

* Modify redirect status check to use specific HttpStatusCode values

- Refactor HttpClient initialization to remove followRedirects = false
- Update DavResource documentation to indicate that provideBody may be called multiple times on redirects
parent 5ccb1b40
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -43,6 +43,10 @@ fun interface ResponseCallback {
    /**
     * Called for a HTTP response. Typically this is only called for successful/redirect
     * responses because HTTP errors throw an exception before this callback is called.
     *
     * @param response      scoped response that can be used to access the body
     *                      in a streaming way (**body won't be accessible anymore when
     *                      callback returns!**)
     */
    suspend fun onResponse(response: HttpResponse)
}
 No newline at end of file
+60 −41
Original line number Diff line number Diff line
@@ -19,30 +19,25 @@ import at.bitfire.dav4jvm.property.common.HrefListProperty
import at.bitfire.dav4jvm.property.webdav.GetContentType
import at.bitfire.dav4jvm.property.webdav.GetETag
import at.bitfire.dav4jvm.property.webdav.NS_WEBDAV
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.util.logging.*
import io.ktor.client.HttpClient
import io.ktor.client.request.header
import io.ktor.client.request.prepareRequest
import io.ktor.client.request.setBody
import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpMethod
import io.ktor.http.Url
import io.ktor.http.contentType
import io.ktor.util.logging.Logger
import org.slf4j.LoggerFactory
import java.io.StringWriter

@Suppress("unused")
class DavAddressBook @JvmOverloads constructor(
class DavAddressBook(
    httpClient: HttpClient,
    location: Url,
    logger: Logger = LoggerFactory.getLogger(DavAddressBook::javaClass.name)
): DavCollection(httpClient, location, logger) {

    companion object {
        val MIME_JCARD = ContentType.Companion.parse("application/vcard+json")
        val MIME_VCARD3_UTF8 = ContentType.Companion.parse("text/vcard;charset=utf-8")
        val MIME_VCARD4 = ContentType.Companion.parse("text/vcard;version=4.0")

        val ADDRESSBOOK_QUERY = Property.Name(NS_CARDDAV, "addressbook-query")
        val ADDRESSBOOK_MULTIGET = Property.Name(NS_CARDDAV, "addressbook-multiget")
        val FILTER = Property.Name(NS_CARDDAV, "filter")
    }

    /**
     * Sends an addressbook-query REPORT request to the resource.
     *
@@ -69,23 +64,26 @@ class DavAddressBook @JvmOverloads constructor(
        serializer.setPrefix("CARD", NS_CARDDAV)
        serializer.insertTag(ADDRESSBOOK_QUERY) {
            insertTag(PROP) {
                insertTag(GetETag.Companion.NAME)
                insertTag(GetETag.NAME)
            }
            insertTag(FILTER)
        }
        serializer.endDocument()

        followRedirects {
            httpClient.prepareRequest {
                url(location)
                method = HttpMethod.Companion.parse("REPORT")
                headers.append(HttpHeaders.ContentType, MIME_XML_UTF8.toString())
        var result: List<Property>? = null
        followRedirects({
            httpClient.prepareRequest(location) {
                method = HttpMethod.parse("REPORT")

                header(HttpHeaders.Depth, "1")

                contentType(MIME_XML_UTF8)
                setBody(writer.toString())
                headers.append(HttpHeaders.Depth, "1")
            }.execute()
        }.let { response ->
            return processMultiStatus(response, callback)
            }
        }) { response ->
            result = processMultiStatus(response, callback)
        }
        return result ?: emptyList()
    }

    /**
@@ -105,7 +103,12 @@ class DavAddressBook @JvmOverloads constructor(
     * @throws at.bitfire.dav4jvm.ktor.exception.HttpException on HTTP error
     * @throws at.bitfire.dav4jvm.ktor.exception.DavException on WebDAV error
     */
    suspend fun multiget(urls: List<Url>, contentType: String? = null, version: String? = null, callback: MultiResponseCallback): List<Property> {
    suspend fun multiget(
        urls: List<Url>,
        contentType: String? = null,
        version: String? = null,
        callback: MultiResponseCallback
    ): List<Property> {
        /* <!ELEMENT addressbook-multiget ((DAV:allprop |
                                            DAV:propname |
                                            DAV:prop)?,
@@ -119,13 +122,13 @@ class DavAddressBook @JvmOverloads constructor(
        serializer.setPrefix("CARD", NS_CARDDAV)
        serializer.insertTag(ADDRESSBOOK_MULTIGET) {
            insertTag(PROP) {
                insertTag(GetContentType.Companion.NAME)
                insertTag(GetETag.Companion.NAME)
                insertTag(AddressData.Companion.NAME) {
                insertTag(GetContentType.NAME)
                insertTag(GetETag.NAME)
                insertTag(AddressData.NAME) {
                    if (contentType != null)
                        attribute(null, AddressData.Companion.CONTENT_TYPE, contentType)
                        attribute(null, AddressData.CONTENT_TYPE, contentType)
                    if (version != null)
                        attribute(null, AddressData.Companion.VERSION, version)
                        attribute(null, AddressData.VERSION, version)
                }
            }
            for (url in urls)
@@ -135,17 +138,33 @@ class DavAddressBook @JvmOverloads constructor(
        }
        serializer.endDocument()

        followRedirects {
            httpClient.prepareRequest {
                url(location)
                method = HttpMethod.Companion.parse("REPORT")
        var result: List<Property>? = null
        followRedirects({
            httpClient.prepareRequest(location) {
                method = HttpMethod.parse("REPORT")

                header(HttpHeaders.Depth, "0")

                contentType(MIME_XML_UTF8)
                setBody(writer.toString())
                headers.append(HttpHeaders.ContentType, MIME_XML_UTF8.toString())
                headers.append(HttpHeaders.Depth, "0")
            }.execute()
        }.let { response ->
            return processMultiStatus(response, callback)
            }
        }) { response ->
            result = processMultiStatus(response, callback)
        }
        return result ?: emptyList()
    }


    companion object {

        val MIME_JCARD = ContentType.parse("application/vcard+json")
        val MIME_VCARD3_UTF8 = ContentType.parse("text/vcard;charset=utf-8")
        val MIME_VCARD4 = ContentType.parse("text/vcard;version=4.0")

        val ADDRESSBOOK_QUERY = Property.Name(NS_CARDDAV, "addressbook-query")
        val ADDRESSBOOK_MULTIGET = Property.Name(NS_CARDDAV, "addressbook-multiget")
        val FILTER = Property.Name(NS_CARDDAV, "filter")

    }

}
 No newline at end of file
+73 −50
Original line number Diff line number Diff line
@@ -20,43 +20,31 @@ import at.bitfire.dav4jvm.property.common.HrefListProperty
import at.bitfire.dav4jvm.property.webdav.GetContentType
import at.bitfire.dav4jvm.property.webdav.GetETag
import at.bitfire.dav4jvm.property.webdav.NS_WEBDAV
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.util.logging.*
import io.ktor.client.HttpClient
import io.ktor.client.request.header
import io.ktor.client.request.prepareRequest
import io.ktor.client.request.setBody
import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpMethod
import io.ktor.http.Url
import io.ktor.http.contentType
import io.ktor.util.logging.Logger
import org.slf4j.LoggerFactory
import java.io.StringWriter
import java.time.Instant
import java.time.ZoneOffset
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.util.*
import java.util.Locale

@Suppress("unused")
class DavCalendar @JvmOverloads constructor(
class DavCalendar(
    httpClient: HttpClient,
    location: Url,
    logger: Logger = LoggerFactory.getLogger(DavCalendar::javaClass.name)
): DavCollection(httpClient, location, logger) {

    companion object {
        val MIME_ICALENDAR = ContentType.Companion.parse("text/calendar")
        val MIME_ICALENDAR_UTF8 = ContentType.Companion.parse("text/calendar;charset=utf-8")

        val CALENDAR_QUERY = Property.Name(NS_CALDAV, "calendar-query")
        val CALENDAR_MULTIGET = Property.Name(NS_CALDAV, "calendar-multiget")

        val FILTER = Property.Name(NS_CALDAV, "filter")
        val COMP_FILTER = Property.Name(NS_CALDAV, "comp-filter")
        const val COMP_FILTER_NAME = "name"
        val TIME_RANGE = Property.Name(NS_CALDAV, "time-range")
        const val TIME_RANGE_START = "start"
        const val TIME_RANGE_END = "end"

        private val timeFormatUTC = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmssVV", Locale.US)
    }


    /**
     * Sends a calendar-query REPORT to the resource.
     *
@@ -72,7 +60,12 @@ class DavCalendar @JvmOverloads constructor(
     * @throws at.bitfire.dav4jvm.ktor.exception.HttpException on HTTP error
     * @throws at.bitfire.dav4jvm.ktor.exception.DavException on WebDAV error
     */
    suspend fun calendarQuery(component: String, start: Instant?, end: Instant?, callback: MultiResponseCallback): List<Property> {
    suspend fun calendarQuery(
        component: String,
        start: Instant?,
        end: Instant?,
        callback: MultiResponseCallback
    ): List<Property> {
        /* <!ELEMENT calendar-query ((DAV:allprop |
                                      DAV:propname |
                                      DAV:prop)?, filter, timezone?)>
@@ -92,7 +85,7 @@ class DavCalendar @JvmOverloads constructor(
        serializer.setPrefix("CAL", NS_CALDAV)
        serializer.insertTag(CALENDAR_QUERY) {
            insertTag(PROP) {
                insertTag(GetETag.Companion.NAME)
                insertTag(GetETag.NAME)
            }
            insertTag(FILTER) {
                insertTag(COMP_FILTER) {
@@ -117,17 +110,20 @@ class DavCalendar @JvmOverloads constructor(
        }
        serializer.endDocument()

        followRedirects {
            httpClient.prepareRequest {
                url(location)
                method = HttpMethod.Companion.parse("REPORT")
        var result: List<Property>? = null
        followRedirects({
            httpClient.prepareRequest(location) {
                method = HttpMethod.parse("REPORT")

                header(HttpHeaders.Depth, "1")

                contentType(MIME_XML_UTF8)
                setBody(writer.toString())
                headers.append(HttpHeaders.ContentType, MIME_XML_UTF8.toString())
                headers.append(HttpHeaders.Depth, "1")
            }.execute()
        }.let { response ->
            return processMultiStatus(response, callback)
            }
        }) { response ->
            result = processMultiStatus(response, callback)
        }
        return result ?: emptyList()
    }

    /**
@@ -147,7 +143,12 @@ class DavCalendar @JvmOverloads constructor(
     * @throws at.bitfire.dav4jvm.ktor.exception.HttpException on HTTP error
     * @throws at.bitfire.dav4jvm.ktor.exception.DavException on WebDAV error
     */
    suspend fun multiget(urls: List<Url>, contentType: String? = null, version: String? = null, callback: MultiResponseCallback): List<Property> {
    suspend fun multiget(
        urls: List<Url>,
        contentType: String? = null,
        version: String? = null,
        callback: MultiResponseCallback
    ): List<Property> {
        /* <!ELEMENT calendar-multiget ((DAV:allprop |
                                        DAV:propname |
                                        DAV:prop)?, DAV:href+)>
@@ -160,14 +161,14 @@ class DavCalendar @JvmOverloads constructor(
        serializer.setPrefix("CAL", NS_CALDAV)
        serializer.insertTag(CALENDAR_MULTIGET) {
            insertTag(PROP) {
                insertTag(GetContentType.Companion.NAME)     // to determine the character set
                insertTag(GetETag.Companion.NAME)
                insertTag(ScheduleTag.Companion.NAME)
                insertTag(CalendarData.Companion.NAME) {
                insertTag(GetContentType.NAME)     // to determine the character set
                insertTag(GetETag.NAME)
                insertTag(ScheduleTag.NAME)
                insertTag(CalendarData.NAME) {
                    if (contentType != null)
                        attribute(null, CalendarData.Companion.CONTENT_TYPE, contentType)
                        attribute(null, CalendarData.CONTENT_TYPE, contentType)
                    if (version != null)
                        attribute(null, CalendarData.Companion.VERSION, version)
                        attribute(null, CalendarData.VERSION, version)
                }
            }
            for (url in urls)
@@ -177,16 +178,38 @@ class DavCalendar @JvmOverloads constructor(
        }
        serializer.endDocument()

        followRedirects {
            httpClient.prepareRequest {
                url(location)
                method = HttpMethod.Companion.parse("REPORT")
        var result: List<Property>? = null
        followRedirects({
            httpClient.prepareRequest(location) {
                method = HttpMethod.parse("REPORT")

                contentType(MIME_XML_UTF8)
                setBody(writer.toString())
                headers.append(HttpHeaders.ContentType, MIME_XML_UTF8.toString())
            }.execute()
        }.let { response ->
            return processMultiStatus(response, callback)
            }
        }) { response ->
            result = processMultiStatus(response, callback)
        }
        return result ?: emptyList()
    }


    companion object {

        val MIME_ICALENDAR = ContentType.parse("text/calendar")
        val MIME_ICALENDAR_UTF8 = ContentType.parse("text/calendar;charset=utf-8")

        val CALENDAR_QUERY = Property.Name(NS_CALDAV, "calendar-query")
        val CALENDAR_MULTIGET = Property.Name(NS_CALDAV, "calendar-multiget")

        val FILTER = Property.Name(NS_CALDAV, "filter")
        val COMP_FILTER = Property.Name(NS_CALDAV, "comp-filter")
        const val COMP_FILTER_NAME = "name"
        val TIME_RANGE = Property.Name(NS_CALDAV, "time-range")
        const val TIME_RANGE_START = "start"
        const val TIME_RANGE_END = "end"

        private val timeFormatUTC = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmssVV", Locale.US)

    }

}
 No newline at end of file
+31 −18
Original line number Diff line number Diff line
@@ -19,10 +19,10 @@ import io.ktor.client.HttpClient
import io.ktor.client.request.header
import io.ktor.client.request.prepareRequest
import io.ktor.client.request.setBody
import io.ktor.client.request.url
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpMethod
import io.ktor.http.Url
import io.ktor.http.contentType
import io.ktor.util.logging.Logger
import org.slf4j.LoggerFactory
import java.io.StringWriter
@@ -36,13 +36,6 @@ open class DavCollection @JvmOverloads constructor(
    logger: Logger = LoggerFactory.getLogger(DavCollection::class.java.name)
): DavResource(httpClient, location, logger) {

    companion object {
        val SYNC_COLLECTION = Property.Name(NS_WEBDAV, "sync-collection")
        val SYNC_LEVEL = Property.Name(NS_WEBDAV, "sync-level")
        val LIMIT = Property.Name(NS_WEBDAV, "limit")
        val NRESULTS = Property.Name(NS_WEBDAV, "nresults")
    }

    /**
     * Sends a REPORT sync-collection request.
     *
@@ -59,7 +52,13 @@ open class DavCollection @JvmOverloads constructor(
     * @throws at.bitfire.dav4jvm.ktor.exception.HttpException on HTTP error
     * @throws at.bitfire.dav4jvm.ktor.exception.DavException on WebDAV error
     */
    suspend fun reportChanges(syncToken: String?, infiniteDepth: Boolean, limit: Int?, vararg properties: Property.Name, callback: MultiResponseCallback): List<Property> {
    suspend fun reportChanges(
        syncToken: String?,
        infiniteDepth: Boolean,
        limit: Int?,
        vararg properties: Property.Name,
        callback: MultiResponseCallback
    ): List<Property> {
        /* <!ELEMENT sync-collection (sync-token, sync-level, limit?, prop)>

           <!ELEMENT sync-token CDATA>       <!-- Text MUST be a URI -->
@@ -96,16 +95,30 @@ open class DavCollection @JvmOverloads constructor(
        }
        serializer.endDocument()

        followRedirects {
            httpClient.prepareRequest {
                url(location)
                method = HttpMethod.Companion.parse("REPORT")
                setBody(writer.toString())
                header(HttpHeaders.ContentType, MIME_XML_UTF8)
        var result: List<Property>? = null
        followRedirects({
            httpClient.prepareRequest(location) {
                method = HttpMethod.parse("REPORT")

                header(HttpHeaders.Depth, "0")
            }.execute()
        }.let { response ->
            return processMultiStatus(response, callback)

                contentType(MIME_XML_UTF8)
                setBody(writer.toString())
            }
        }) { response ->
            result = processMultiStatus(response, callback)
        }
        return result ?: emptyList()
    }


    companion object {

        val SYNC_COLLECTION = Property.Name(NS_WEBDAV, "sync-collection")
        val SYNC_LEVEL = Property.Name(NS_WEBDAV, "sync-level")
        val LIMIT = Property.Name(NS_WEBDAV, "limit")
        val NRESULTS = Property.Name(NS_WEBDAV, "nresults")

    }

}
 No newline at end of file
+190 −206

File changed.

Preview size limit exceeded, changes collapsed.

Loading