mirror of
https://github.com/apple/container.git
synced 2026-09-10 09:45:39 +00:00
More informative errors from RegistryClient (#134)
Also creates an `ErrorResponse` type to model the errors typically returned by a container registry. Reference: https://distribution.github.io/distribution/spec/api/#errors Example error message: ``` Error: HTTP request to https://ghcr.io/token?client_id=containerization-registry-client&service=ghcr.io&scope=repository:user/image:pull failed with response: 403 Forbidden. Reason: {"errors":[{"message":"requested access to the resource is denied","code":"DENIED"}]} ``` Signed-off-by: Aditya Ramani <a_ramani@apple.com>
This commit is contained in:
@@ -14,19 +14,56 @@
|
||||
// limitations under the License.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import AsyncHTTPClient
|
||||
import Foundation
|
||||
import NIOHTTP1
|
||||
|
||||
extension RegistryClient {
|
||||
/// `RegistryClient` errors.
|
||||
public enum Error: Swift.Error, CustomStringConvertible {
|
||||
case invalidStatus(url: String, HTTPResponseStatus)
|
||||
case invalidStatus(url: String, HTTPResponseStatus, reason: String? = nil)
|
||||
|
||||
/// Description of the errors.
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .invalidStatus(let u, let response):
|
||||
return "HTTP request to \(u) failed with response: \(response.description)"
|
||||
case .invalidStatus(let u, let response, let reason):
|
||||
return "HTTP request to \(u) failed with response: \(response.description). Reason: \(reason ?? "Unknown")"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The container registry typically returns actionable failure reasons in the response body
|
||||
/// of the failing HTTP Request. This type models the structure of the error message.
|
||||
/// Reference: https://distribution.github.io/distribution/spec/api/#errors
|
||||
internal struct ErrorResponse: Codable {
|
||||
let errors: [RemoteError]
|
||||
|
||||
internal struct RemoteError: Codable {
|
||||
let code: String
|
||||
let message: String
|
||||
let detail: String?
|
||||
}
|
||||
|
||||
internal static func fromResponseBody(_ body: HTTPClientResponse.Body) async -> ErrorResponse? {
|
||||
guard var buffer = try? await body.collect(upTo: Int(1.mib())) else {
|
||||
return nil
|
||||
}
|
||||
guard let bytes = buffer.readBytes(length: buffer.readableBytes) else {
|
||||
return nil
|
||||
}
|
||||
let data = Data(bytes)
|
||||
guard let jsonError = try? JSONDecoder().decode(ErrorResponse.self, from: data) else {
|
||||
return nil
|
||||
}
|
||||
return jsonError
|
||||
}
|
||||
|
||||
public var jsonString: String {
|
||||
let data = try? JSONEncoder().encode(self)
|
||||
guard let data else {
|
||||
return "{}"
|
||||
}
|
||||
return String(data: data, encoding: .utf8) ?? "{}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,8 @@ extension RegistryClient {
|
||||
return try await request(components: components, method: .HEAD, headers: headers) { response in
|
||||
guard response.status == .ok else {
|
||||
let url = components.url?.absoluteString ?? "unknown"
|
||||
throw Error.invalidStatus(url: url, response.status)
|
||||
let reason = await ErrorResponse.fromResponseBody(response.body)?.jsonString
|
||||
throw Error.invalidStatus(url: url, response.status, reason: reason)
|
||||
}
|
||||
|
||||
guard let digest = response.headers.first(name: "Docker-Content-Digest") else {
|
||||
@@ -150,7 +151,8 @@ extension RegistryClient {
|
||||
try await request(components: components, headers: headers) { response in
|
||||
guard response.status == .ok else {
|
||||
let url = components.url?.absoluteString ?? "unknown"
|
||||
throw Error.invalidStatus(url: url, response.status)
|
||||
let reason = await ErrorResponse.fromResponseBody(response.body)?.jsonString
|
||||
throw Error.invalidStatus(url: url, response.status, reason: reason)
|
||||
}
|
||||
|
||||
// How many bytes to expect
|
||||
|
||||
@@ -93,7 +93,8 @@ extension RegistryClient {
|
||||
}
|
||||
} else if response.status != .notFound {
|
||||
let url = components.url?.absoluteString ?? "unknown"
|
||||
throw Error.invalidStatus(url: url, response.status)
|
||||
let reason = await ErrorResponse.fromResponseBody(response.body)?.jsonString
|
||||
throw Error.invalidStatus(url: url, response.status, reason: reason)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,7 +115,8 @@ extension RegistryClient {
|
||||
throw ContainerizationError(.exists, message: "Content already exists \(descriptor.digest)")
|
||||
default:
|
||||
let url = components.url?.absoluteString ?? "unknown"
|
||||
throw Error.invalidStatus(url: url, response.status)
|
||||
let reason = await ErrorResponse.fromResponseBody(response.body)?.jsonString
|
||||
throw Error.invalidStatus(url: url, response.status, reason: reason)
|
||||
}
|
||||
|
||||
// Get the location to upload the blob.
|
||||
@@ -149,7 +151,8 @@ extension RegistryClient {
|
||||
break
|
||||
default:
|
||||
let url = components.url?.absoluteString ?? "unknown"
|
||||
throw Error.invalidStatus(url: url, response.status)
|
||||
let reason = await ErrorResponse.fromResponseBody(response.body)?.jsonString
|
||||
throw Error.invalidStatus(url: url, response.status, reason: reason)
|
||||
}
|
||||
|
||||
guard descriptor.digest == response.headers.first(name: "Docker-Content-Digest") else {
|
||||
|
||||
@@ -235,7 +235,8 @@ public final class RegistryClient: ContentClient {
|
||||
try await request(components: components, method: .GET, headers: headers) { response in
|
||||
guard response.status == .ok else {
|
||||
let url = components.url?.absoluteString ?? "unknown"
|
||||
throw Error.invalidStatus(url: url, response.status)
|
||||
let reason = await ErrorResponse.fromResponseBody(response.body)?.jsonString
|
||||
throw Error.invalidStatus(url: url, response.status, reason: reason)
|
||||
}
|
||||
|
||||
var body = try await response.body.collect(upTo: self.bufferSize)
|
||||
@@ -263,7 +264,8 @@ public final class RegistryClient: ContentClient {
|
||||
try await request(components: components) { response in
|
||||
guard response.status == .ok else {
|
||||
let url = components.url?.absoluteString ?? "unknown"
|
||||
throw Error.invalidStatus(url: url, response.status)
|
||||
let reason = await ErrorResponse.fromResponseBody(response.body)?.jsonString
|
||||
throw Error.invalidStatus(url: url, response.status, reason: reason)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user