I'm trying to explore twitter API from VAPOR server. And I'm getting the following error:
[ INFO ] GET /hello [request-id: F00915F3-0812-429A-B012-BEDCAE1DD543]
[ WARNING ] read(descriptor:pointer:size:): Connection reset by peer (errno: 54) [request-id: F00915F3-0812-429A-B012-BEDCAE1DD543]
The problem is that I'm in Russia and you can get Twitter only through VPN. And it works just fine:
[ INFO ] GET /hello [request-id: 18EFDB9C-98CE-45F2-90E6-ABDC34983051]
Response(data: App.DataClass(id: "44196397", name: "Elon Musk", username: "elonmusk"))
GET request with 'Reqbin' or 'Postman' is okay. But when I'm deploying this project on 'render.com' or 'railway.com' which servers are outside the firewall, US and Germany, the problem remains the same.
So maybe something wrong with API? Or maybe I need some request tweaking.
I'm pretty sure this is not VAPOR problem, because the same error arises from XCode Project or Playground.
P.S. request from Windows Powershell the same situation, works with VPN. Maybe anyone has an idea in which direction to dig))
import Vapor
func routes(_ app: Application) throws {
app.get { req async in
"It works!"
}
app.get("hello") { req -> EventLoopFuture<Response> in
try getUser(request: req)
}
func getUser(request: Request) throws -> EventLoopFuture<Response> {
let headers: HTTPHeaders = HTTPHeaders([("Authorization", "Bearer \(Environment.get("BEARER_TOKEN")!)")])
let uri: URI = URI(string: "https://api.twitter.com/2/users/by/username/elonmusk")
return request.client.get(uri, headers: headers).flatMapThrowing { response in
guard response.status == .ok else { throw Abort(.unauthorized) }
guard let buffer = response.body else { throw Abort(.badRequest) }
guard let data = String(buffer: buffer).data(using: .utf8) else { throw Abort(.badRequest) }
do {
let data = try JSONDecoder().decode(Response.self, from: data)
print(data)
return data
} catch {
throw Abort(.badRequest)
}
}
}
}
// Response Model
struct Response: Content, Codable {
let data: DataClass
}
struct DataClass: Codable {
let id, name, username: String
}
Related
I am following along with this tutorial in order to create an async generic network layer. I got the network manager working correctly.
https://betterprogramming.pub/async-await-generic-network-layer-with-swift-5-5-2bdd51224ea9
As I try to implement more APIs, that I can use with the networking layer, some of the APIs require different tokens, different content in the body, or header etc, that I have to get at runtime.
In the snippet of code below from the tutorial, I get that we are building up the Movie endpoint based on .self, and then return the specific values we need. But the issue is, some of the data in this, for example, the access token, has to be hard coded here. I am looking for a way, that I can 'inject' the accessToken, and then it will be created with this new token. Again, the reason for this, is that in other APIs, the access token might not always be known.
protocol Endpoint {
var scheme: String { get }
var host: String { get }
var version: String? { get }
var path: String { get }
var method: RequestMethod { get }
var queryItems: [String: String]? { get }
var header: [String: String]? { get }
var body: [String: String]? { get }
}
extension MoviesEndpoint: Endpoint {
var path: String {
switch self {
case .topRated:
return "/3/movie/top_rated"
case .movieDetail(let id):
return "/3/movie/\(id)"
}
}
var method: RequestMethod {
switch self {
case .topRated, .movieDetail:
return .get
}
}
var header: [String: String]? {
// Access Token to use in Bearer header
let accessToken = "insert your access token here -> https://www.themoviedb.org/settings/api"
switch self {
case .topRated, .movieDetail:
return [
"Authorization": "Bearer \(accessToken)",
"Content-Type": "application/json;charset=utf-8"
]
}
}
var body: [String: String]? {
switch self {
case .topRated, .movieDetail:
return nil
}
}
For an example, I tried converting the var body to a function, so I could do
func body(_ bodyDict: [String, String]?) -> [String:String]? {
switch self{
case .test:
return bodyDict
}
The idea of above, was that I changed it to a function, so I could pass in a dict, and then return that dict in the api call, but that did not work. The MoviesEnpoint adheres to the extension Endpoint, which then gives the compiler error 'Protocol Methods must not have bodies'.
Is there a way to dependency inject runtime parameters into this Extension/Protocol method?
Change the declaration of MoviesEndpoint so that it stores the access token:
struct MoviesEndpoint {
var accessToken: String
var detail: Detail
enum Detail {
case topRated
case movieDetail(id: Int)
}
}
You'll need to change all the switch self statements to switch detail.
However, I think the solution in the article (four protocols) is overwrought.
Instead of a pile of protocols, make one struct with a single function property:
struct MovieDatabaseClient {
var getRaw: (MovieEndpoint) async throws -> (Data, URLResponse)
}
Extend it with a generic method to handle the response parsing and decoding:
extension MovieDatabaseClient {
func get<T: Decodable>(
endpoint: MovieEndpoint,
as responseType: T.Type = T.self
) async throws -> T {
let (data, response) = try await getRaw(endpoint)
guard let response = response as? HTTPURLResponse else {
throw URLError(.badServerResponse)
}
switch response.statusCode {
case 200...299:
break
case 401:
throw URLError(.userAuthenticationRequired)
default:
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode(responseType, from: data)
}
}
Provide a “live“ implementation that actually sends network requests:
extension MovieDatabaseClient {
static func live(host: String, accessToken: String) -> Self {
return .init { endpoint in
let request = try liveURLRequest(
host: host,
accessToken: accessToken,
endpoint: endpoint
)
return try await URLSession.shared.data(for: request)
}
}
// Factored out in case you want to write unit tests for it:
static func liveURLRequest(
host: String,
accessToken: String,
endpoint: MovieEndpoint
) throws -> URLRequest {
var components = URLComponents()
components.scheme = "https"
components.host = host
components.path = endpoint.urlPath
guard let url = components.url else { throw URLError(.badURL) }
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.allHTTPHeaderFields = [
"Authorization": "Bearer \(accessToken)",
"Content-Type": "application/json;charset=utf-8",
]
return request
}
}
extension MovieEndpoint {
var urlPath: String {
switch self {
case .topRated: return "/3/movie/top_rated"
case .movieDetail(id: let id): return "/3/movie/\(id)"
}
}
}
To use it in your app:
// At app startup...
let myAccessToken = "loaded from UserDefaults or something"
let client = MovieDatabaseClient.live(
host: "api.themoviedb.org",
accessToken: myAccessToken
)
// Using it:
let topRated: TopRated = try await client.get(endpoint: .topRated)
let movieDetail: MovieDetail = try await client.get(endpoint: .movieDetail(id: 123))
For testing, you can create a mock client by providing a single closure that fakes the network request/response. Simple examples:
extension MovieDatabaseClient {
static func mockSuccess<T: Encodable>(_ body: T) -> Self {
return .init { _ in
let data = try JSONEncoder().encode(body)
let response = HTTPURLResponse(
url: URL(string: "test")!,
statusCode: 200,
httpVersion: "HTTP/1.1",
headerFields: nil
)!
return (data, response)
}
}
static func mockFailure(_ error: Error) -> Self {
return .init { _ in
throw error
}
}
}
So a test can create a mock client that always responds with a TopRated response like this:
let mockTopRatedClient = MovieDatabaseClient.mockSuccess(TopRated(...))
If you want to learn more about this style of dependency management and mocking, Point-Free has a good (but subscription required) series of episodes: Designing Dependencies.
I am following Ray Wanderlich's book 'Server Side Swift with Vapor' and I am at chapter 26: Adding profile pictures.
First, I defined this struct:
struct ImageUploadData: Content {
var picture: Data
}
Then, in a route I try to decode it:
func postProfilePictureHandler(_ req: Request) throws -> EventLoopFuture<User> {
let data = try req.content.decode(ImageUploadData.self)
...
From the client side, I use Alamofire:
#discardableResult func uploadProfilePicture(for user: User, data: Data) async throws -> User {
enum Error: LocalizedError {
case missingUserID
}
guard let userID = user.id else {
throw Error.missingUserID
}
let appendix = "\(userID)/profilePicture"
let parameters = [
"picture": data
]
return try await withCheckedThrowingContinuation { continuation in
Task {
AF.request(baseUrl + appendix, method: .post, parameters: parameters).responseData { response in
switch response.result {
case .success(let data):
do {
let user = try JSONDecoder().decode(User.self, from: data)
continuation.resume(returning: user)
} catch {
continuation.resume(throwing: error)
}
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
}
In my integration tests, I create the picture's data like this:
guard let data = image?.pngData() else {
throw Error.missingPictureData
}
And then I pass it to the above method. The problem is that in the server side, the decoding fails with this error:
The data couldn’t be read because it is missing.
Just to understand if I was doing something else wrong, I tried the above methods with one difference: I replace the type 'Data' with 'String':
struct ImageUploadData: Content {
var picture: String
}
This wouldn't be useful for me because I need a data object, but just as a test to see if this doesn't produce an error, I tried and indeed this is decoded successfully. So I suspect that the problem is in how I encode the data before sending it to the server, but I don't know what's wrong.
I currently have a network client that looks like the below:
class Client<R: ResourceType> {
let engine: ClientEngineType
var session: URLSession
init(engine: ClientEngineType = ClientEngine()) {
self.engine = engine
self.session = URLSession.shared
}
func request<T: Codable>(_ resource: R) -> Single<T> {
let request = URLRequest(resource: resource)
return Single<T>.create { [weak self] single in
guard let self = self else { return Disposables.create() }
let response = self.session.rx.response(request: request)
return response.subscribe(
onNext: { response, data in
if let error = self.error(from: response) {
single(.error(error))
return
}
do {
let decoder = JSONDecoder()
let value = try decoder.decode(T.self, from: data)
single(.success(value))
} catch let error {
single(.error(error))
}
},
onError: { error in
single(.error(error))
})
}
}
struct StatusCodeError: LocalizedError {
let code: Int
var errorDescription: String? {
return "An error occurred communicating with the server. Please try again."
}
}
private func error(from response: URLResponse?) -> Error? {
guard let response = response as? HTTPURLResponse else { return nil }
let statusCode = response.statusCode
if 200..<300 ~= statusCode {
return nil
} else {
return StatusCodeError(code: statusCode)
}
}
}
Which I can then invoke something like
let client = Client<MyRoutes>()
client.request(.companyProps(params: ["collections": "settings"]))
.map { props -> CompanyModel in return props }
.subscribe(onSuccess: { props in
// do something with props
}) { error in
print(error.localizedDescription)
}.disposed(by: disposeBag)
I'd like to start handling 401 responses and refreshing my token and retrying the request.
I'm struggling to find a nice way to do this.
I found this excellent gist that outlines a way to achieve this, however I am struggling to implement this in my current client.
Any tips or pointers would be very much appreciated.
That's my gist! (Thanks for calling it excellent.) Did you see the article that went with it? https://medium.com/#danielt1263/retrying-a-network-request-despite-having-an-invalid-token-b8b89340d29
There are two key elements in handling 401 retries. First is that you need a way to insert tokens into your requests and start your request pipeline with Observable.deferred { tokenAcquisitionService.token.take(1) }. In your case, that means you need a URLRequest.init that will accept a Resource and a token, not just a resource.
The second is to throw a TokenAcquisitionError.unauthorized error when you get a 401 and end your request pipeline with .retryWhen { $0.renewToken(with: tokenAcquisitionService) }
So, given what you have above, in order to handle token retries all you need to do is bring my TokenAcquisitionService into your project and use this:
func getToken(_ oldToken: Token) -> Observable<(response: HTTPURLResponse, data: Data)> {
fatalError("this function needs to be able to request a new token from the server. It has access to the old token if it needs that to request the new one.")
}
func extractToken(_ data: Data) -> Token {
fatalError("This function needs to be able to extract the new token using the data returned from the previous function.")
}
let tokenAcquisitionService = TokenAcquisitionService<Token>(initialToken: Token(), getToken: getToken, extractToken: extractToken)
final class Client<R> where R: ResourceType {
let session: URLSession
init(session: URLSession = URLSession.shared) {
self.session = session
}
func request<T>(_ resource: R) -> Single<T> where T: Decodable {
return Observable.deferred { tokenAcquisitionService.token.take(1) }
.map { token in URLRequest(resource: resource, token: token) }
.flatMapLatest { [session] request in session.rx.response(request: request) }
.do(onNext: { response, _ in
if response.statusCode == 401 {
throw TokenAcquisitionError.unauthorized
}
})
.map { (_, data) -> T in
return try JSONDecoder().decode(T.self, from: data)
}
.retryWhen { $0.renewToken(with: tokenAcquisitionService) }
.asSingle()
}
}
Note, it could be the case that the getToken function has to, for example, present a view controller that asks for the user's credentials. That means you need to present your login view controller (or a UIAlertController) to gather the data. Or maybe you get both an authorization token and a refresh token from your server when you login. In that case the TokenAcquisitionService should hold on to both of them (i.e., its T should be a (token: String, refresh: String). Either is fine.
The only problem with the service is that if acquiring the new token fails, the entire service shuts down. I haven't fixed that yet.
I have a middleware which fetches a token if there is no one to be found in Redis.
struct TokenMiddleware: Middleware, TokenAccessor {
func respond(to request: Request, chainingTo next: Responder) throws -> Future<Response> {
guard let _ = request.http.headers.firstValue(name: HTTPHeaderName("Client-ID")) else {
throw Abort(.badRequest, reason: "missing 'Client-ID' in header")
}
guard request.clientID.isEmpty == false else {
throw Abort(.badRequest, reason: "'Client-ID' in header is empty")
}
guard let _ = request.http.headers.firstValue(name: HTTPHeaderName("Client-Secret")) else {
throw Abort(.badRequest, reason: "missing 'Client-Secret' in header")
}
/// getToken fetches a new Token and stores it in Redis for the controller to use
return try self.getToken(request: request).flatMap(to: Response.self) { token in
return try next.respond(to: request)
}
}
}
extension TokenMiddleware: Service {}
But this causes multiple processes fetching new tokens on their own and therefore a race condition.
How can I handle this in vapor?
I solved the problem now, thanks to Soroush from http://khanlou.com/2017/09/dispatch-on-the-server/ who hinted me into the right direction. More infos on the DispatchQueues can be found in an excellent article from https://www.raywenderlich.com/5370-grand-central-dispatch-tutorial-for-swift-4-part-1-2
So:
In both iOS and Vapor on the Server we can create a DispatchQueue. In my case I am using a concurrent one that, in the critical part where token reading, fetching (if needed) and token writing happens, I use a barrier.
The barrier lets only one in and thus in this part everything is executed like a serial queue.
Hope this helps anybody that might come across the same issue
import Vapor
protocol TokenAccessor: RedisAccessor {
}
extension TokenAccessor {
/// Main convenience function that handles expiry, refetching etc
///
/// - Check if token was saved before
/// - We store the token in redis
/// - We use redis TTL feature to handle token expiry
///
func getToken(request: Request) throws -> Future<Token> {
print(":getToken(request:)")
let promise = request.eventLoop.newPromise(Token.self)
return request.withNewConnection(to: .redis) { redis in
let concurrentQueue = DispatchQueue(label: "com.queuename.gettoken",
attributes: .concurrent)
/// Making the concurrent queue serial because only one is allowed to fetch a new token at a time
concurrentQueue.async(flags: .barrier) {
let _ = redis.get(request.clientIdLastDigits, as: String.self).map(to: Void.self) { tokenOpt in
guard let accessToken = tokenOpt else {
try self.fetchNewToken(forRequest: request).do { newToken in
print("fetched a new token")
promise.succeed(result: newToken)
}.catch { error in
print("failed fetching a new token")
promise.fail(error: error)
}
return
}
print("got a valid token from redis")
let token = Token(client: request.clientIdLastDigits, token: accessToken, expiresIn: Date())
// return request.future(token)
promise.succeed(result: token)
}
}
return promise.futureResult
}
}
...
This is triggered in front of my methods via a middleware (so I don't need to think about it)
import Vapor
struct TokenMiddleware: Middleware, TokenAccessor {
func respond(to request: Request, chainingTo next: Responder) throws -> Future<Response> {
guard let _ = request.http.headers.firstValue(name: HTTPHeaderName("Client-ID")) else {
throw Abort(.badRequest, reason: "missing 'Client-ID' in header")
}
guard request.clientID.isEmpty == false else {
throw Abort(.badRequest, reason: "'Client-ID' in header is empty")
}
guard let _ = request.http.headers.firstValue(name: HTTPHeaderName("Client-Secret")) else {
throw Abort(.badRequest, reason: "missing 'Client-Secret' in header")
}
return try self.getToken(request: request).flatMap(to: Response.self) { token in
return try next.respond(to: request)
}
}
}
extension TokenMiddleware: Service {}
I am using Alamofire for basic networking. Here is my problem. I have a class
class User {
var name:String?
var company:String
init () {
//
manager = Alamofire.Manager(configuration: configuration)
}
func details () {
//first we login, if login is successful we fetch the result
manager.customPostWithHeaders(customURL!, data: parameter, headers: header)
.responseJSON { (req, res, json, error) in
if(error != nil) {
NSLog("Error: \(error)")
}
else {
NSLog("Success: \(self.customURL)")
var json = JSON(json!)
println(json)
self.fetch()
println("I fetched correctly")
}
}
func fetch() {
manager.customPostWithHeaders(customURL!, data: parameter, headers: header)
.responseJSON { (req, res, json, error) in
if(error != nil) {
NSLog("Error: \(error)")
}
else {
NSLog("Success: \(self.customURL)")
var json = JSON(json!)
println(json)
//set name and company
}
}
}
My problem is if I do something like
var my user = User()
user.fetch()
println("Username is \(user.name)")
I don’t get anything on the console for user.name. However if I put a break point, I see that I get username and company correctly inside my fetch function. I think manager runs in separate non blocking thread and doesn’t wait. However I really don’t know how can I initialize my class with correct data if I can’t know whether manager finished successfully. So how can I initialize my class correctly for immediate access after all threads of Alamofire manager did their job?
You don't want to do the networking inside your model object. Instead, you want to handle the networking layer in some more abstract object such as a Service of class methods. This is just a simple example, but I think this will really get you heading in a much better architectural direction.
import Alamofire
struct User {
let name: String
let companyName: String
}
class UserService {
typealias UserCompletionHandler = (User?, NSError?) -> Void
class func getUser(completionHandler: UserCompletionHandler) {
let loginRequest = Alamofire.request(.GET, "login/url")
loginRequest.responseJSON { request, response, json, error in
if let error = error {
completionHandler(nil, error)
} else {
println("Login Succeeded!")
let userRequest = Alamofire.request(.GET, "user/url")
userRequest.responseJSON { request, response, json, error in
if let error = error {
completionHandler(nil, error)
} else {
let jsonDictionary = json as [String: AnyObject]
let user = User(
name: jsonDictionary["name"]! as String,
companyName: jsonDictionary["companyName"]! as String
)
completionHandler(user, nil)
}
}
}
}
}
}
UserService.getUser { user, error in
if let user = user {
// do something awesome with my new user
println(user)
} else {
// figure out how to handle the error
println(error)
}
}
Since both the login and user requests are asynchronous, you cannot start using the User object until both requests are completed and you have a valid User object. Closures are a great way to capture logic to run after the completion of asynchronous tasks. Here are a couple other threads on Alamofire and async networking that may also help you out.
Handling Multiple Network Calls
Returning a Value with Alamofire
Hopefully this sheds some light.