@@ -32,18 +32,28 @@ class HttpClient(
32 private val jsonMedia = "application/json".toMediaType()
32 private val jsonMedia = "application/json".toMediaType()
34 suspend fun get(url: String, auth: String?): Result<HttpResp> =
34 suspend fun get(url: String, auth: String?): Result<HttpResp> =
35− exec(Request.Builder().url(url).get(), auth)
35+ exec(url, auth) { get() }
37 suspend fun postJson(url: String, body: String, auth: String?): Result<HttpResp> =
37 suspend fun postJson(url: String, body: String, auth: String?): Result<HttpResp> =
38− exec(Request.Builder().url(url).post(body.toRequestBody(jsonMedia)), auth)
38+ exec(url, auth) { post(body.toRequestBody(jsonMedia)) }
40 suspend fun delete(url: String, auth: String?): Result<HttpResp> =
40 suspend fun delete(url: String, auth: String?): Result<HttpResp> =
41− exec(Request.Builder().url(url).delete(), auth)
41+ exec(url, auth) { delete() }
43+ // Build and run the request off the main thread. Building it — parsing [url]
44+ // in particular — can throw (e.g. a URL with no scheme), so that happens
45+ // inside [runCatching] too: a bad URL then surfaces as [Result.failure]
46+ // instead of crashing the caller's coroutine.
47+ private suspend fun exec(
50+ method: Request.Builder.() -> Request.Builder,
43− private suspend fun exec(builder: Request.Builder, auth: String?): Result<HttpResp> = withContext(Dispatchers.IO) {
51+ ): Result<HttpResp> = withContext(Dispatchers.IO) {
53+ val builder = Request.Builder().url(url).method()
44− if (auth != null) builder.header("Authorization", auth)
54+ if (auth != null) builder.header("Authorization", auth)
45− val request = builder.build()
46− runCatching { await(okhttp.newCall(request)) }
55+ await(okhttp.newCall(builder.build()))
49 private suspend fun await(call: Call): HttpResp = suspendCancellableCoroutine { cont ->
59 private suspend fun await(call: Call): HttpResp = suspendCancellableCoroutine { cont ->