kean / Get

Web API client built using async/await
MIT License
937 stars 74 forks source link

GitHubAPI usage returning 401 response #9

Closed briviere closed 2 years ago

briviere commented 2 years ago

Tired out calling the GitHubAPI usage with updating GitHubAPIClientDelegete with my personal Github token but getting a 401 unauthorized.

try await usage()

I don't think the GitHubAPIClientDelegete is adding the required requests headers.

Brian

kean commented 2 years ago

Hi, could you please share a code sample?

briviere commented 2 years ago

I'm trying the GitHubAPI example usage. Just updated it to use my personal GitHub token:

// An example of an API definition. Feel free to use any other method for // organizing the resources. public enum Resources {}

// MARK: - /user

extension Resources { public static var user: UserResource { UserResource() }

public struct UserResource {
    public let path: String = "/user"

    public var get: Request<User> { .get(path) }
}

}

// MARK: - /user/emails

extension Resources.UserResource { public var emails: EmailsResource { EmailsResource() }

public struct EmailsResource {
    public let path: String = "/user/emails"

    public var get: Request<[UserEmail]> { .get(path) }

    public func post(_ emails: [String]) -> Request<Void> {
        .post(path, body: emails)
    }

    public func delete(_ emails: [String]) -> Request<Void> {
        .delete(path, body: emails)
    }
}

}

// MARK: - /users/{username}

extension Resources { public static func users(_ name: String) -> UsersResource { UsersResource(path: "/users/(name)") }

public struct UsersResource {
    public let path: String

    public var get: Request<User> { .get(path) }
}

}

// MARK: - /users/{username}/followers

extension Resources.UsersResource { public var followers: FollowersResource { FollowersResource(path: path + "/followers") }

public struct FollowersResource {
    public let path: String

    public var get: Request<[User]> { .get(path) }
}

}

// MARK: - Entities

public struct UserEmail: Decodable { public let email: String public let verified: Bool public let primary: Bool public let visibility: String }

public struct User: Codable { public let id: Int public let login: String public let name: String public let hireable: Bool public let location: String public let bio: String }

// MARK: - APIClientDelegate

enum GitHubError: Error { case unacceptableStatusCode(Int) }

private final class GitHubAPIClientDelegate: APIClientDelegate { func client(_ client: APIClient, willSendRequest request: inout URLRequest) { request.setValue("Bearer: ("api_token_here")", forHTTPHeaderField: "Authorization") }

func shouldClientRetry(_ client: APIClient, withError error: Error) async -> Bool {
    if case .unacceptableStatusCode(let status) = (error as? GitHubError), status == 401 {
        return await refreshAccessToken()
    }
    return false
}

private func refreshAccessToken() async -> Bool {
    // TODO: Refresh (make sure you only refresh once if multiple requests fail)
    return false
}

func client(_ client: APIClient, didReceiveInvalidResponse response: HTTPURLResponse, data: Data) -> Error {
    GitHubError.unacceptableStatusCode(response.statusCode)
}

}

// MARK: - Usage

func usage() async throws { let client = APIClient(host: "api.github.com", delegate: GitHubAPIClientDelegate())

let user = try await client.send(Resources.user.get)
let emails = try await client.send(Resources.user.emails.get)

try await client.send(Resources.user.emails.delete(["octocat@gmail.com"]))

let followers = try await client.send(Resources.users("kean").followers.get)

let user2: User = try await client.send(.get("/user"))

}

try await usage()

kean commented 2 years ago

If you are personal access tokens, you need to use basic authorization:

let token = "your_token".data(using: .utf8)!.base64EncodedString()        
request.setValue("Basic \(token)", forHTTPHeaderField: "Authorization")

More about GitHub authentication: https://docs.github.com/en/rest/overview/resources-in-the-rest-api#authentication

briviere commented 2 years ago

Okay but looking at this code should we be passing the delegate instead of nil?

private func actuallySend(_ request: URLRequest) async throws -> (Data, URLResponse) { var request = request delegate.client(self, willSendRequest: &request) return try await session.data(for: request, delegate: nil) }

kean commented 2 years ago

It's a different (URLSession) delegate, and it's not currently used.