From ae9d0d3a3547954d7525c5940b2102da74269f94 Mon Sep 17 00:00:00 2001 From: Sebastian Schuberth Date: Tue, 18 Jun 2024 15:44:48 +0200 Subject: [PATCH] refactor(clients)!: Rename OSV classes according to ORT conventions OSV currently is the only client library project that provides another "wrapper" around the low-level service API. Use "ServiceWrapper" as a conventional suffix for that class. Reword some docs / rename some variables accordingly along the way. Signed-off-by: Sebastian Schuberth --- ...FunTest.kt => OsvServiceWrapperFunTest.kt} | 18 +- clients/osv/src/main/kotlin/OsvApiClient.kt | 154 -------------- clients/osv/src/main/kotlin/OsvService.kt | 197 ++++++++++-------- .../osv/src/main/kotlin/OsvServiceWrapper.kt | 129 ++++++++++++ clients/osv/src/test/kotlin/ModelTest.kt | 4 +- plugins/advisors/osv/src/main/kotlin/Osv.kt | 8 +- 6 files changed, 255 insertions(+), 255 deletions(-) rename clients/osv/src/funTest/kotlin/{OsvServiceFunTest.kt => OsvServiceWrapperFunTest.kt} (85%) delete mode 100644 clients/osv/src/main/kotlin/OsvApiClient.kt create mode 100644 clients/osv/src/main/kotlin/OsvServiceWrapper.kt diff --git a/clients/osv/src/funTest/kotlin/OsvServiceFunTest.kt b/clients/osv/src/funTest/kotlin/OsvServiceWrapperFunTest.kt similarity index 85% rename from clients/osv/src/funTest/kotlin/OsvServiceFunTest.kt rename to clients/osv/src/funTest/kotlin/OsvServiceWrapperFunTest.kt index e8fac872b42f9..0edcaec654602 100644 --- a/clients/osv/src/funTest/kotlin/OsvServiceFunTest.kt +++ b/clients/osv/src/funTest/kotlin/OsvServiceWrapperFunTest.kt @@ -62,14 +62,14 @@ private val emptyJsonObject = JsonObject(emptyMap()) private fun List.patchFields() = map { it.patchIgnorableFields().normalizeUrls() } -class OsvServiceFunTest : StringSpec({ +class OsvServiceWrapperFunTest : StringSpec({ "getVulnerabilitiesForPackage() returns the expected vulnerability when queried by commit" { val expectedResult = getAssetAsString("vulnerabilities-by-commit-expected-result.json") - val result = OsvService().getVulnerabilitiesForPackage(VULNERABILITY_FOR_PACKAGE_BY_COMMIT_REQUEST) + val result = OsvServiceWrapper().getVulnerabilitiesForPackage(VULNERABILITY_FOR_PACKAGE_BY_COMMIT_REQUEST) result.shouldBeSuccess { actualData -> - val expectedData = OsvApiClient.JSON.decodeFromString>(expectedResult) + val expectedData = OsvService.JSON.decodeFromString>(expectedResult) actualData.patchFields() shouldContainExactlyInAnyOrder expectedData.patchFields() } } @@ -77,10 +77,10 @@ class OsvServiceFunTest : StringSpec({ "getVulnerabilitiesForPackage() returns the expected vulnerability when queried by name and version" { val expectedResult = getAssetAsString("vulnerabilities-by-name-and-version-expected-result.json") - val result = OsvService().getVulnerabilitiesForPackage(VULNERABILITY_FOR_PACKAGE_BY_NAME_AND_VERSION) + val result = OsvServiceWrapper().getVulnerabilitiesForPackage(VULNERABILITY_FOR_PACKAGE_BY_NAME_AND_VERSION) result.shouldBeSuccess { actualData -> - val expectedData = OsvApiClient.JSON.decodeFromString>(expectedResult) + val expectedData = OsvService.JSON.decodeFromString>(expectedResult) actualData.patchFields() shouldContainExactlyInAnyOrder expectedData.patchFields() } } @@ -92,7 +92,7 @@ class OsvServiceFunTest : StringSpec({ VULNERABILITY_FOR_PACKAGE_BY_NAME_AND_VERSION ) - val result = OsvService().getVulnerabilityIdsForPackages(requests) + val result = OsvServiceWrapper().getVulnerabilityIdsForPackages(requests) result.shouldBeSuccess { it shouldBe listOf( @@ -124,10 +124,10 @@ class OsvServiceFunTest : StringSpec({ "getVulnerabilityForId() returns the expected vulnerability for the given ID" { val expectedResult = getAssetAsString("vulnerability-by-id-expected-result.json") - val result = OsvService().getVulnerabilityForId("GHSA-xvch-5gv4-984h") + val result = OsvServiceWrapper().getVulnerabilityForId("GHSA-xvch-5gv4-984h") result.shouldBeSuccess { actualData -> - val expectedData = OsvApiClient.JSON.decodeFromString(expectedResult) + val expectedData = OsvService.JSON.decodeFromString(expectedResult) actualData.patchIgnorableFields() shouldBe expectedData.patchIgnorableFields() } } @@ -135,7 +135,7 @@ class OsvServiceFunTest : StringSpec({ "getVulnerabilitiesForIds() return the vulnerabilities for the given IDs" { val ids = setOf("GHSA-xvch-5gv4-984h", "PYSEC-2014-82") - val result = OsvService().getVulnerabilitiesForIds(ids) + val result = OsvServiceWrapper().getVulnerabilitiesForIds(ids) result.shouldBeSuccess { it.map { vulnerability -> vulnerability.id } shouldContainExactlyInAnyOrder ids diff --git a/clients/osv/src/main/kotlin/OsvApiClient.kt b/clients/osv/src/main/kotlin/OsvApiClient.kt deleted file mode 100644 index b9a7d585a0023..0000000000000 --- a/clients/osv/src/main/kotlin/OsvApiClient.kt +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright (C) 2022 The ORT Project Authors (see ) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * License-Filename: LICENSE - */ - -package org.ossreviewtoolkit.clients.osv - -import io.ks3.java.typealiases.InstantAsString - -import java.util.concurrent.Executors - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonNamingStrategy - -import okhttp3.Dispatcher -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.OkHttpClient - -import retrofit2.Call -import retrofit2.Retrofit -import retrofit2.converter.kotlinx.serialization.asConverterFactory -import retrofit2.http.Body -import retrofit2.http.GET -import retrofit2.http.POST -import retrofit2.http.Path - -/** - * A rest API client for the Google Open Source Vulnerabilities API (OSV.dev), see https://osv.dev/. - */ -interface OsvApiClient { - companion object { - const val BATCH_REQUEST_MAX_SIZE = 1000 - - val JSON = Json { namingStrategy = JsonNamingStrategy.SnakeCase } - - /** - * Create an OsvApiClient instance for communicating with the given [server], optionally using a pre-built - * OkHttp [client]. - */ - fun create(server: Server, client: OkHttpClient? = null): OsvApiClient = create(server.url, client) - - fun create(serverUrl: String? = null, client: OkHttpClient? = null): OsvApiClient { - val converterFactory = JSON.asConverterFactory(contentType = "application/json".toMediaType()) - - return Retrofit.Builder() - .apply { client(client ?: defaultHttpClient()) } - .baseUrl(serverUrl ?: Server.PRODUCTION.url) - .addConverterFactory(converterFactory) - .build() - .create(OsvApiClient::class.java) - } - } - - enum class Server(val url: String) { - /** - * The production API server. - */ - PRODUCTION("https://api.osv.dev"), - - /** - * The staging API server. - */ - STAGING("https://api-staging.osv.dev") - } - - /** - * Get the vulnerabilities for the package matched by the given [request]. - */ - @POST("v1/query") - fun getVulnerabilitiesForPackage( - @Body request: VulnerabilitiesForPackageRequest - ): Call - - /** - * Get the identifiers of the vulnerabilities for the packages matched by the respective given [request]. - * The amount of requests contained in the give [batch request][request] must not exceed [BATCH_REQUEST_MAX_SIZE]. - */ - @POST("v1/querybatch") - fun getVulnerabilityIdsForPackages( - @Body request: VulnerabilitiesForPackageBatchRequest - ): Call - - /** - * Return the vulnerability denoted by the given [id]. - */ - @GET("v1/vulns/{id}") - fun getVulnerabilityForId(@Path("id") id: String): Call -} - -@Serializable -class VulnerabilitiesForPackageRequest private constructor( - val commit: String? = null, - @SerialName("package") - val pkg: Package? = null, - val version: String? = null -) { - constructor(commit: String, pkg: Package? = null) : this(commit = commit, pkg = pkg, version = null) - constructor(pkg: Package, version: String) : this(commit = null, pkg = pkg, version = version) -} - -@Serializable -data class VulnerabilitiesForPackageResponse( - @SerialName("vulns") - val vulnerabilities: List -) - -@Serializable -data class VulnerabilitiesForPackageBatchRequest( - val queries: List -) - -@Serializable -data class VulnerabilitiesForPackageBatchResponse( - val results: List -) { - @Serializable - data class IdList( - @SerialName("vulns") - val vulnerabilities: List = emptyList() - ) - - @Serializable - data class Id( - val id: String, - val modified: InstantAsString - ) -} - -private fun defaultHttpClient(): OkHttpClient { - // Experimentally determined value to speed-up execution time of 1000 single vulnerability-by-id requests. - val n = 100 - val dispatcher = Dispatcher(Executors.newFixedThreadPool(n)).apply { - maxRequests = n - maxRequestsPerHost = n - } - - return OkHttpClient.Builder().dispatcher(dispatcher).build() -} diff --git a/clients/osv/src/main/kotlin/OsvService.kt b/clients/osv/src/main/kotlin/OsvService.kt index 813937fe5a519..ed2815d8653dd 100644 --- a/clients/osv/src/main/kotlin/OsvService.kt +++ b/clients/osv/src/main/kotlin/OsvService.kt @@ -19,111 +19,136 @@ package org.ossreviewtoolkit.clients.osv -import java.io.IOException -import java.util.concurrent.ConcurrentLinkedQueue -import java.util.concurrent.CountDownLatch -import java.util.concurrent.atomic.AtomicReference +import io.ks3.java.typealiases.InstantAsString +import java.util.concurrent.Executors + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonNamingStrategy + +import okhttp3.Dispatcher +import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response +import retrofit2.Retrofit +import retrofit2.converter.kotlinx.serialization.asConverterFactory +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Path /** - * This class wraps the OSV Rest API client in order to make its use simpler and less error-prone. + * An interface for the REST API of the Google Open Source Vulnerabilities (OSV) service, see https://osv.dev/. */ -class OsvService(serverUrl: String? = null, httpClient: OkHttpClient? = null) { - private val client = OsvApiClient.create(serverUrl, httpClient) - - /** - * Get the vulnerabilities for the package matching the given [request]. - */ - fun getVulnerabilitiesForPackage(request: VulnerabilitiesForPackageRequest): Result> { - val response = client.getVulnerabilitiesForPackage(request).execute() - val body = response.body() - - return if (response.isSuccessful && body != null) { - Result.success(body.vulnerabilities) - } else { - Result.failure(IOException(response.message())) +interface OsvService { + companion object { + const val BATCH_REQUEST_MAX_SIZE = 1000 + + val JSON = Json { namingStrategy = JsonNamingStrategy.SnakeCase } + + /** + * Create a service instance for communicating with the given [server], optionally using a pre-built OkHttp + * [client]. + */ + fun create(server: Server, client: OkHttpClient? = null): OsvService = create(server.url, client) + + fun create(serverUrl: String? = null, client: OkHttpClient? = null): OsvService { + val converterFactory = JSON.asConverterFactory(contentType = "application/json".toMediaType()) + + return Retrofit.Builder() + .apply { client(client ?: defaultHttpClient()) } + .baseUrl(serverUrl ?: Server.PRODUCTION.url) + .addConverterFactory(converterFactory) + .build() + .create(OsvService::class.java) } } - /** - * Return the vulnerability IDs for the respective package matched by the given [requests]. - */ - fun getVulnerabilityIdsForPackages(requests: List): Result>> { - if (requests.isEmpty()) return Result.success(emptyList()) - - val result = mutableListOf>() - - requests.chunked(OsvApiClient.BATCH_REQUEST_MAX_SIZE).forEach { requestsChunk -> - val batchRequest = VulnerabilitiesForPackageBatchRequest(requestsChunk) - val response = client.getVulnerabilityIdsForPackages(batchRequest).execute() - val body = response.body() - - if (!response.isSuccessful || body == null) { - val errorMessage = response.errorBody()?.string()?.let { - val errorResponse = OsvApiClient.JSON.decodeFromString(it) - "Error code ${errorResponse.code}: ${errorResponse.message}" - } ?: with(response) { "HTTP code ${code()}: ${message()}" } - - return Result.failure(IOException(errorMessage)) - } - - result += body.results.map { batchResponse -> - batchResponse.vulnerabilities.mapTo(mutableListOf()) { it.id } - } - } + enum class Server(val url: String) { + /** + * The production API server. + */ + PRODUCTION("https://api.osv.dev"), - return Result.success(result) + /** + * The staging API server. + */ + STAGING("https://api-staging.osv.dev") } /** - * Return the vulnerability denoted by the given [id]. + * Get the vulnerabilities for the package matched by the given [request]. */ - fun getVulnerabilityForId(id: String): Result { - val response = client.getVulnerabilityForId(id).execute() - val body = response.body() - - return if (response.isSuccessful && body != null) { - Result.success(body) - } else { - Result.failure(IOException(response.message())) - } - } + @POST("v1/query") + fun getVulnerabilitiesForPackage( + @Body request: VulnerabilitiesForPackageRequest + ): Call /** - * Return the vulnerabilities denoted by the given [ids]. - * - * This executes a separate request for each given identifier since a batch request is not available. - * It's been considered to add a batch API in the future, see - * https://github.com/google/osv.dev/issues/466#issuecomment-1163337495. + * Get the identifiers of the vulnerabilities for the packages matched by the respective given [request]. + * The amount of requests contained in the give [batch request][request] must not exceed [BATCH_REQUEST_MAX_SIZE]. */ - fun getVulnerabilitiesForIds(ids: Set): Result> { - val result = ConcurrentLinkedQueue() - val failureThrowable = AtomicReference(null) - val latch = CountDownLatch(ids.size) - - ids.forEach { id -> - client.getVulnerabilityForId(id).enqueue(object : Callback { - override fun onResponse(call: Call, response: Response) { - response.body()?.let { result += it } - latch.countDown() - } - - override fun onFailure(call: Call, t: Throwable) { - val exception = IOException("Could not get vulnerability information for '$id'.", t) - failureThrowable.set(exception) - latch.countDown() - } - }) - } + @POST("v1/querybatch") + fun getVulnerabilityIdsForPackages( + @Body request: VulnerabilitiesForPackageBatchRequest + ): Call - latch.await() + /** + * Return the vulnerability denoted by the given [id]. + */ + @GET("v1/vulns/{id}") + fun getVulnerabilityForId(@Path("id") id: String): Call +} + +@Serializable +class VulnerabilitiesForPackageRequest private constructor( + val commit: String? = null, + @SerialName("package") + val pkg: Package? = null, + val version: String? = null +) { + constructor(commit: String, pkg: Package? = null) : this(commit = commit, pkg = pkg, version = null) + constructor(pkg: Package, version: String) : this(commit = null, pkg = pkg, version = version) +} - return failureThrowable.get()?.let { Result.failure(it) } - ?: Result.success(result.toList()) +@Serializable +data class VulnerabilitiesForPackageResponse( + @SerialName("vulns") + val vulnerabilities: List +) + +@Serializable +data class VulnerabilitiesForPackageBatchRequest( + val queries: List +) + +@Serializable +data class VulnerabilitiesForPackageBatchResponse( + val results: List +) { + @Serializable + data class IdList( + @SerialName("vulns") + val vulnerabilities: List = emptyList() + ) + + @Serializable + data class Id( + val id: String, + val modified: InstantAsString + ) +} + +private fun defaultHttpClient(): OkHttpClient { + // Experimentally determined value to speed-up execution time of 1000 single vulnerability-by-id requests. + val n = 100 + val dispatcher = Dispatcher(Executors.newFixedThreadPool(n)).apply { + maxRequests = n + maxRequestsPerHost = n } + + return OkHttpClient.Builder().dispatcher(dispatcher).build() } diff --git a/clients/osv/src/main/kotlin/OsvServiceWrapper.kt b/clients/osv/src/main/kotlin/OsvServiceWrapper.kt new file mode 100644 index 0000000000000..afab06c2de74c --- /dev/null +++ b/clients/osv/src/main/kotlin/OsvServiceWrapper.kt @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2022 The ORT Project Authors (see ) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +package org.ossreviewtoolkit.clients.osv + +import java.io.IOException +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.CountDownLatch +import java.util.concurrent.atomic.AtomicReference + +import okhttp3.OkHttpClient + +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + +/** + * This class wraps the OSV service to make its use simpler and less error-prone. + */ +class OsvServiceWrapper(serverUrl: String? = null, httpClient: OkHttpClient? = null) { + private val service = OsvService.create(serverUrl, httpClient) + + /** + * Get the vulnerabilities for the package matching the given [request]. + */ + fun getVulnerabilitiesForPackage(request: VulnerabilitiesForPackageRequest): Result> { + val response = service.getVulnerabilitiesForPackage(request).execute() + val body = response.body() + + return if (response.isSuccessful && body != null) { + Result.success(body.vulnerabilities) + } else { + Result.failure(IOException(response.message())) + } + } + + /** + * Return the vulnerability IDs for the respective package matched by the given [requests]. + */ + fun getVulnerabilityIdsForPackages(requests: List): Result>> { + if (requests.isEmpty()) return Result.success(emptyList()) + + val result = mutableListOf>() + + requests.chunked(OsvService.BATCH_REQUEST_MAX_SIZE).forEach { requestsChunk -> + val batchRequest = VulnerabilitiesForPackageBatchRequest(requestsChunk) + val response = service.getVulnerabilityIdsForPackages(batchRequest).execute() + val body = response.body() + + if (!response.isSuccessful || body == null) { + val errorMessage = response.errorBody()?.string()?.let { + val errorResponse = OsvService.JSON.decodeFromString(it) + "Error code ${errorResponse.code}: ${errorResponse.message}" + } ?: with(response) { "HTTP code ${code()}: ${message()}" } + + return Result.failure(IOException(errorMessage)) + } + + result += body.results.map { batchResponse -> + batchResponse.vulnerabilities.mapTo(mutableListOf()) { it.id } + } + } + + return Result.success(result) + } + + /** + * Return the vulnerability denoted by the given [id]. + */ + fun getVulnerabilityForId(id: String): Result { + val response = service.getVulnerabilityForId(id).execute() + val body = response.body() + + return if (response.isSuccessful && body != null) { + Result.success(body) + } else { + Result.failure(IOException(response.message())) + } + } + + /** + * Return the vulnerabilities denoted by the given [ids]. + * + * This executes a separate request for each given identifier since a batch request is not available. + * It's been considered to add a batch API in the future, see + * https://github.com/google/osv.dev/issues/466#issuecomment-1163337495. + */ + fun getVulnerabilitiesForIds(ids: Set): Result> { + val result = ConcurrentLinkedQueue() + val failureThrowable = AtomicReference(null) + val latch = CountDownLatch(ids.size) + + ids.forEach { id -> + service.getVulnerabilityForId(id).enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + response.body()?.let { result += it } + latch.countDown() + } + + override fun onFailure(call: Call, t: Throwable) { + val exception = IOException("Could not get vulnerability information for '$id'.", t) + failureThrowable.set(exception) + latch.countDown() + } + }) + } + + latch.await() + + return failureThrowable.get()?.let { Result.failure(it) } + ?: Result.success(result.toList()) + } +} diff --git a/clients/osv/src/test/kotlin/ModelTest.kt b/clients/osv/src/test/kotlin/ModelTest.kt index d2d97981d18a7..77d8a03c79482 100644 --- a/clients/osv/src/test/kotlin/ModelTest.kt +++ b/clients/osv/src/test/kotlin/ModelTest.kt @@ -30,9 +30,9 @@ import kotlinx.serialization.encodeToString class ModelTest : StringSpec({ "Deserializing and serializing any vulnerability is idempotent for all official examples" { getVulnerabilityExamplesJson().forAll { vulnerabilityJson -> - val vulnerability = OsvApiClient.JSON.decodeFromString(vulnerabilityJson) + val vulnerability = OsvService.JSON.decodeFromString(vulnerabilityJson) - val serializedVulnerabilityJson = OsvApiClient.JSON.encodeToString(vulnerability) + val serializedVulnerabilityJson = OsvService.JSON.encodeToString(vulnerability) serializedVulnerabilityJson shouldEqualJson vulnerabilityJson } diff --git a/plugins/advisors/osv/src/main/kotlin/Osv.kt b/plugins/advisors/osv/src/main/kotlin/Osv.kt index 53bb04f83dce0..4f9aa8eea3566 100644 --- a/plugins/advisors/osv/src/main/kotlin/Osv.kt +++ b/plugins/advisors/osv/src/main/kotlin/Osv.kt @@ -29,7 +29,7 @@ import org.apache.logging.log4j.kotlin.logger import org.ossreviewtoolkit.advisor.AdviceProvider import org.ossreviewtoolkit.advisor.AdviceProviderFactory import org.ossreviewtoolkit.clients.osv.Ecosystem -import org.ossreviewtoolkit.clients.osv.OsvService +import org.ossreviewtoolkit.clients.osv.OsvServiceWrapper import org.ossreviewtoolkit.clients.osv.Severity import org.ossreviewtoolkit.clients.osv.VulnerabilitiesForPackageRequest import org.ossreviewtoolkit.clients.osv.Vulnerability @@ -69,7 +69,7 @@ class Osv(name: String, config: OsvConfiguration) : AdviceProvider(name) { override val details: AdvisorDetails = AdvisorDetails(providerName, enumSetOf(AdvisorCapability.VULNERABILITIES)) - private val service = OsvService( + private val client = OsvServiceWrapper( serverUrl = config.serverUrl, httpClient = OkHttpClientHelper.buildClient() ) @@ -108,7 +108,7 @@ class Osv(name: String, config: OsvConfiguration) : AdviceProvider(name) { createRequest(pkg)?.let { pkg to it } } - val result = service.getVulnerabilityIdsForPackages(requests.map { it.second }) + val result = client.getVulnerabilityIdsForPackages(requests.map { it.second }) val results = mutableListOf>>() result.map { allVulnerabilities -> @@ -128,7 +128,7 @@ class Osv(name: String, config: OsvConfiguration) : AdviceProvider(name) { } private fun getVulnerabilitiesForIds(ids: Set): List { - val result = service.getVulnerabilitiesForIds(ids) + val result = client.getVulnerabilitiesForIds(ids) return result.getOrElse { logger.error {