Skip to content

Commit

Permalink
add Idempotency-Key header to post status requests (#233)
Browse files Browse the repository at this point in the history
String that is added is a UUID derived from the parameter list of the request in question.

Closes #118
  • Loading branch information
bocops authored Jul 16, 2023
1 parent dacb8f5 commit f422c3c
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 13 deletions.
31 changes: 22 additions & 9 deletions bigbone/src/main/kotlin/social/bigbone/MastodonClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -226,19 +226,21 @@ private constructor(
* @param endpoint the Mastodon API endpoint to call
* @param method the HTTP method to use
* @param parameters parameters to use in the action; can be null
* @param addIdempotencyKey if true, adds idempotency key to header to avoid duplicate POST requests
*/
internal inline fun <reified T : Any> getMastodonRequest(
endpoint: String,
method: Method,
parameters: Parameters? = null
parameters: Parameters? = null,
addIdempotencyKey: Boolean = false
): MastodonRequest<T> {
return MastodonRequest(
{
when (method) {
Method.DELETE -> delete(endpoint, parameters)
Method.GET -> get(endpoint, parameters)
Method.PATCH -> patch(endpoint, parameters)
Method.POST -> post(endpoint, parameters)
Method.POST -> post(endpoint, parameters, addIdempotencyKey)
Method.PUT -> put(endpoint, parameters)
}
},
Expand Down Expand Up @@ -386,27 +388,38 @@ private constructor(
* Get a response from the Mastodon instance defined for this client using the POST method.
* @param path an absolute path to the API endpoint to call
* @param body the parameters to use in the request body for this request; may be null
* @param addIdempotencyKey if true, generate idempotency key for this request
*/
fun post(path: String, body: Parameters? = null): Response =
postRequestBody(path, parameterBody(body))
fun post(path: String, body: Parameters? = null, addIdempotencyKey: Boolean = false): Response {
val idempotencyKey = if (addIdempotencyKey) {
body?.uuid()
} else {
null
}
return postRequestBody(path, parameterBody(body), idempotencyKey)
}

/**
* Get a response from the Mastodon instance defined for this client using the POST method. Use this method if
* you need to build your own RequestBody; see post() otherwise.
* @param path an absolute path to the API endpoint to call
* @param body the RequestBody to use for this request
* @param idempotencyKey optional idempotency value to avoid duplicate calls
*
* @see post
*/
fun postRequestBody(path: String, body: RequestBody): Response {
fun postRequestBody(path: String, body: RequestBody, idempotencyKey: String? = null): Response {
try {
val url = fullUrl(scheme, instanceName, port, path)
debugPrintUrl(url)
val call = client.newCall(
Request.Builder()
.url(url)
.post(body)
.build()
Request.Builder().apply {
url(url)
post(body)
idempotencyKey?.let {
header("Idempotency-Key", it)
}
}.build()
)
return call.execute()
} catch (e: IllegalArgumentException) {
Expand Down
15 changes: 15 additions & 0 deletions bigbone/src/main/kotlin/social/bigbone/Parameters.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package social.bigbone

import java.net.URLEncoder
import java.util.ArrayList
import java.util.UUID

/**
* Parameters holds a list of String key/value pairs that can be used as query part of a URL, or in the body of a request.
Expand Down Expand Up @@ -80,4 +81,18 @@ class Parameters {
parameterList.joinToString(separator = "&") {
"${it.first}=${URLEncoder.encode(it.second, "utf-8")}"
}

/**
* Generates a UUID string for this parameter list. UUIDs returned for different Parameters instances should be
* the same if they contain the same list of key/value pairs, even if pairs were appended in different order,
* and should be different as soon as at least one parameter key or value differs.
* @return Type 3 UUID as a String.
*/
fun uuid(): String {
val parameterString = parameterList
.sortedWith(compareBy { it.first })
.joinToString { "${it.first}${it.second}" }
val uuid = UUID.nameUUIDFromBytes(parameterString.toByteArray())
return uuid.toString()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,8 @@ class StatusMethods(private val client: MastodonClient) {
append("sensitive", sensitive)
spoilerText?.let { append("spoiler_text", it) }
language?.let { append("language", it) }
}
},
addIdempotencyKey = true
)
}

Expand Down Expand Up @@ -171,7 +172,8 @@ class StatusMethods(private val client: MastodonClient) {
append("sensitive", sensitive)
spoilerText?.let { append("spoiler_text", it) }
language?.let { append("language", it) }
}
},
addIdempotencyKey = true
)
}

Expand Down Expand Up @@ -212,7 +214,8 @@ class StatusMethods(private val client: MastodonClient) {
append("sensitive", sensitive)
spoilerText?.let { append("spoiler_text", it) }
language?.let { append("language", it) }
}
},
addIdempotencyKey = true
)
}

Expand Down Expand Up @@ -255,7 +258,8 @@ class StatusMethods(private val client: MastodonClient) {
append("sensitive", sensitive)
spoilerText?.let { append("spoiler_text", it) }
language?.let { append("language", it) }
}
},
addIdempotencyKey = true
)
}

Expand Down
38 changes: 38 additions & 0 deletions bigbone/src/test/kotlin/social/bigbone/ParametersTest.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package social.bigbone

import org.amshove.kluent.shouldBeEqualTo
import org.amshove.kluent.shouldNotBeEqualTo
import org.junit.jupiter.api.Test

class ParametersTest {
Expand Down Expand Up @@ -32,4 +33,41 @@ class ParametersTest {
.append("media_ids", listOf(1, 3, 4))
.toQuery() shouldBeEqualTo "media_ids[]=1&media_ids[]=3&media_ids[]=4"
}

@Test
fun sameUuidWithDifferentParameterOrder() {
val params1 = Parameters()
.append("One", "1")
.append("Two", "2")
.append("Three", "3")

val params2 = Parameters()
.append("Three", "3")
.append("Two", "2")
.append("One", "1")

params1.uuid() shouldBeEqualTo params2.uuid()
}

@Test
fun differentUuidWithDifferentKey() {
val params1 = Parameters()
.append("Key", "foo")

val params2 = Parameters()
.append("OtherKey", "foo")

params1.uuid() shouldNotBeEqualTo params2.uuid()
}

@Test
fun differentUuidWithDifferentValue() {
val params1 = Parameters()
.append("Key", "foo")

val params2 = Parameters()
.append("Key", "bar")

params1.uuid() shouldNotBeEqualTo params2.uuid()
}
}
2 changes: 2 additions & 0 deletions bigbone/src/test/kotlin/social/bigbone/testtool/MockClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ object MockClient {
every { clientMock.get(ofType<String>(), any()) } returns response
every { clientMock.patch(ofType<String>(), any()) } returns response
every { clientMock.post(ofType<String>(), any()) } returns response
every { clientMock.post(ofType<String>(), any(), any()) } returns response
every { clientMock.postRequestBody(ofType<String>(), any()) } returns response
every { clientMock.put(ofType<String>(), any()) } returns response
every { clientMock.getSerializer() } returns Gson()
Expand All @@ -69,6 +70,7 @@ object MockClient {
every { clientMock.get(ofType<String>(), any()) } returns response
every { clientMock.patch(ofType<String>(), any()) } returns response
every { clientMock.post(ofType<String>(), any()) } returns response
every { clientMock.post(ofType<String>(), any(), any()) } returns response
every { clientMock.postRequestBody(ofType<String>(), any()) } returns response
every { clientMock.put(ofType<String>(), any()) } returns response
every { clientMock.performAction(ofType<String>(), any()) } throws BigBoneRequestException("mock")
Expand Down

0 comments on commit f422c3c

Please sign in to comment.