All Downloads are FREE. Search and download functionalities are using the official Maven repository.
Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
commonMain.com.adamratzman.spotify.http.HttpRequest.kt Maven / Gradle / Ivy
/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
package com.adamratzman.spotify.http
import com.adamratzman.spotify.GenericSpotifyApi
import com.adamratzman.spotify.SpotifyApiOptions
import com.adamratzman.spotify.SpotifyException.AuthenticationException
import com.adamratzman.spotify.SpotifyException.BadRequestException
import com.adamratzman.spotify.SpotifyException.ParseException
import com.adamratzman.spotify.models.AuthenticationError
import com.adamratzman.spotify.models.ErrorResponse
import com.adamratzman.spotify.models.SpotifyRatelimitedException
import com.adamratzman.spotify.models.serialization.nonstrictJson
import com.adamratzman.spotify.models.serialization.toObject
import com.soywiz.klogger.Console
import com.soywiz.korio.async.launch
import io.ktor.client.HttpClient
import io.ktor.client.plugins.ResponseException
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.header
import io.ktor.client.request.request
import io.ktor.client.request.setBody
import io.ktor.client.request.url
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.HttpMethod
import io.ktor.http.content.ByteArrayContent
import io.ktor.utils.io.core.toByteArray
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.delay
import kotlinx.serialization.Serializable
public enum class HttpRequestMethod(internal val externalMethod: HttpMethod) {
GET(HttpMethod.Get),
POST(HttpMethod.Post),
PUT(HttpMethod.Put),
DELETE(HttpMethod.Delete);
}
@Serializable
public data class HttpHeader(val key: String, val value: String)
@Serializable
public data class HttpResponse(val responseCode: Int, val body: String, val headers: List)
public typealias HttpConnection = HttpRequest
/**
* Provides a fast, easy, and slim way to execute and retrieve HTTP GET, POST, PUT, and DELETE requests
*/
public class HttpRequest constructor(
public val url: String,
public val method: HttpRequestMethod,
public val bodyMap: Map<*, *>?,
public val bodyString: String?,
contentType: String?,
public val headers: List = listOf(),
public val api: GenericSpotifyApi? = null
) {
public val contentType: ContentType = contentType?.let { ContentType.parse(it) } ?: ContentType.Application.Json
public fun String?.toByteArrayContent(): ByteArrayContent? {
return if (this == null) null else ByteArrayContent(this.toByteArray(), contentType)
}
public fun buildRequest(additionalHeaders: List?): HttpRequestBuilder = HttpRequestBuilder().apply {
url([email protected] )
method = [email protected]
setBody(
when ([email protected] ) {
HttpRequestMethod.DELETE -> {
bodyString.toByteArrayContent() ?: body
}
HttpRequestMethod.PUT, HttpRequestMethod.POST -> {
val contentString = if (contentType == ContentType.Application.FormUrlEncoded) {
bodyMap?.map { "${it.key}=${it.value}" }?.joinToString("&") ?: bodyString
} else {
bodyString
}
contentString.toByteArrayContent() ?: ByteArrayContent("".toByteArray(), contentType)
}
else -> body
}
)
// let additionalHeaders overwrite headers
val allHeaders = if (additionalHeaders == null) {
[email protected]
} else {
[email protected] { oldHeaders -> oldHeaders.key !in additionalHeaders.map { it.key } } + additionalHeaders
}
allHeaders.forEach { (key, value) ->
header(key, value)
}
}
public suspend fun execute(
additionalHeaders: List? = null,
retryIfInternalServerErrorLeft: Int? = SpotifyApiOptions().retryOnInternalServerErrorTimes // default
): HttpResponse {
val httpRequest = buildRequest(additionalHeaders)
if (api?.spotifyApiOptions?.enableDebugMode == true) Console.debug("Request: $this")
try {
return httpClient.request(httpRequest).let { response ->
val respCode = response.status.value
if (respCode in 500..599 && (retryIfInternalServerErrorLeft == null || retryIfInternalServerErrorLeft == -1 || retryIfInternalServerErrorLeft > 0)) {
if (api?.spotifyApiOptions?.enableDebugMode == true) Console.debug("Received internal server error $respCode, attempting to retry ($retryIfInternalServerErrorLeft tries left)")
return@let execute(
additionalHeaders,
retryIfInternalServerErrorLeft =
if (retryIfInternalServerErrorLeft != null && retryIfInternalServerErrorLeft != -1) {
retryIfInternalServerErrorLeft - 1
} else {
retryIfInternalServerErrorLeft
}
)
}
// otherwise, if it's 5xx and retryIfInternalServerErrorLeft == 0 we just continue and fail
if (respCode == 429) {
if (api?.spotifyApiOptions?.enableDebugMode == true) Console.debug("Received 429, attempting to retry")
val ratelimit = response.headers["Retry-After"]!!.toLong() + 1L
if (api?.spotifyApiOptions?.retryWhenRateLimited == true) {
delay(ratelimit * 1000)
return@let execute(
additionalHeaders,
retryIfInternalServerErrorLeft = retryIfInternalServerErrorLeft
)
} else {
throw SpotifyRatelimitedException(ratelimit)
}
}
val body: String = response.bodyAsText()
if (api?.spotifyApiOptions?.enableDebugMode == true) {
Console.debug("Request status: $respCode - body: $body")
}
if (respCode == 401 && body.contains("access token") && api?.spotifyApiOptions?.automaticRefresh == true) {
api.refreshToken()
val newAdditionalHeaders =
additionalHeaders?.toMutableList()?.filter { it.key != "Authorization" }?.toMutableList()
?: mutableListOf()
newAdditionalHeaders.add(HttpHeader("Authorization", "Bearer ${api.token.accessToken}"))
return execute(
newAdditionalHeaders,
retryIfInternalServerErrorLeft = retryIfInternalServerErrorLeft
)
}
val httpResponseToReturn = HttpResponse(
responseCode = respCode,
body = body,
headers = response.headers.entries().map { (key, value) ->
HttpHeader(
key,
value.getOrNull(0) ?: "null"
)
}
)
api?.spotifyApiOptions?.httpResponseSubscriber?.let { subscriber ->
launch(currentCoroutineContext()) {
subscriber(this, httpResponseToReturn)
}
}
return httpResponseToReturn
}
} catch (e: CancellationException) {
throw e
} catch (e: ResponseException) {
val errorBody = e.response.bodyAsText()
if (api?.spotifyApiOptions?.enableDebugMode == true) Console.debug("Error body: $errorBody")
try {
val respCode = e.response.status.value
if (respCode in 500..599 && (retryIfInternalServerErrorLeft == null || retryIfInternalServerErrorLeft == -1 || retryIfInternalServerErrorLeft > 0)) {
return execute(
additionalHeaders,
retryIfInternalServerErrorLeft =
if (retryIfInternalServerErrorLeft != null && retryIfInternalServerErrorLeft != -1) {
retryIfInternalServerErrorLeft - 1
} else {
retryIfInternalServerErrorLeft
}
)
}
if (respCode == 429) {
val ratelimit = e.response.headers["Retry-After"]!!.toLong() + 1L
if (api?.spotifyApiOptions?.retryWhenRateLimited == true) {
// println("The request ($url) was ratelimited for $ratelimit seconds at ${getCurrentTimeMs()}")
delay(ratelimit * 1000)
return execute(
additionalHeaders,
retryIfInternalServerErrorLeft = retryIfInternalServerErrorLeft
)
} else {
throw SpotifyRatelimitedException(ratelimit)
}
}
if (e.response.status.value == 401 && errorBody.contains("access token") &&
api != null && api.spotifyApiOptions.automaticRefresh
) {
api.refreshToken()
val newAdditionalHeaders =
additionalHeaders?.toMutableList()?.filter { it.key != "Authorization" }?.toMutableList()
?: mutableListOf()
newAdditionalHeaders.add(HttpHeader("Authorization", "Bearer ${api.token.accessToken}"))
return execute(
newAdditionalHeaders,
retryIfInternalServerErrorLeft = retryIfInternalServerErrorLeft
)
}
val error = errorBody.toObject(
ErrorResponse.serializer(),
api,
api?.spotifyApiOptions?.json ?: nonstrictJson
).error
throw BadRequestException(error.copy(reason = (error.reason ?: "") + " URL: $url"))
} catch (ignored: ParseException) {
try {
val error = errorBody.toObject(
AuthenticationError.serializer(),
api,
api?.spotifyApiOptions?.json ?: nonstrictJson
)
throw AuthenticationException(error)
} catch (ignored: ParseException) {
throw BadRequestException(e)
}
}
}
}
override fun toString(): String {
// we don't want to print this sensitive information
val headersWithoutAuthorization = headers.filter { it.key != "Authorization" }
val hasAuthorizationHeader = headersWithoutAuthorization.size != headers.size
return """HttpConnection(
|url=$url,
|method=$method,
|body=${bodyString ?: bodyMap},
|contentType=$contentType,
|headers=${headersWithoutAuthorization.toList()}
|${if (hasAuthorizationHeader) "The authorization header was hidden." else "There was no authorization header."})
""".trimMargin()
}
internal companion object {
internal val httpClient = HttpClient {
expectSuccess = false
}
}
}