In building an app with PerfectHTTP I've come up against a problem with routing: Routes that are used as intermediate routes cannot also be used as final routes.
When set up as below, some URLs track correctly (/index.html in the example), while some URLs do not (/ in the example).
Is there some special sauce I'm missing here, or is what I'm trying to do simply impossible using Perfect?
import PerfectHTTP
import PerfectHTTPServer
// This does various housekeeping to set things common to all requests.
var routes = Routes() {
request, response in
Log.info(message: "\(request.method) \(request.uri)")
response
.setHeader(.contentType, value: "text/plain")
.next()
}
// For this handler, http://localhost:8181/index.html works,
// but http://localhost:8181/ does not
routes.add(method: .get, uris: ["/", "index.html"]) {
request, response in
response
.appendBody(string: "Hello, world!")
.completed()
}
HTTPServer.launch(.server(name: "localhost", port: 8181, routes: [routes]))
if you want set things common to all requests. use filter.
my routes is working
see more https://perfect.org/docs/filters.html
var routes = Routes()
routes.add(method: .get, uris: ["/", "index.html"]) {
request, response in
response
.appendBody(string: "Hello, world!")
.completed()
}
struct MyFilter: HTTPRequestFilter {
func filter(request: HTTPRequest, response: HTTPResponse, callback: (HTTPRequestFilterResult) -> ()) {
print("request method: \(request.method)")
callback(.continue(request, response))
}
}
do {
let server = HTTPServer()
server.serverName = "testServer"
server.serverPort = 8181
server.addRoutes(routes)
server.setRequestFilters([(MyFilter(), HTTPFilterPriority.high)])
try server.start()
} catch {
fatalError("\(error)") // fatal error launching one of the servers
}
Related
I know this question is asked a lot, but I can't figure out how to apply any answers to my program. Sorry in advance this async stuff makes absolutely zero sense to me.
Basically, I have a button in SwiftUI that, when pressed, calls a function that makes two API calls to Google Sheets using Alamofire and GoogleSignIn.
Button("Search") {
if fullName != "" {
print(SheetsAPI.nameSearch(name: fullName, user: vm.getUser()) ?? "Error")
}
}
This function should return the values of some cells on success or nil on an error. However, it only ever prints out "Error". Here is the function code.
static func nameSearch<S: StringProtocol>(name: S, advisory: S = "", user: GIDGoogleUser?) -> [String]? {
let name = String(name)
let advisory = String(advisory)
let writeRange = "'App Control'!A2:C2"
let readRange = "'App Control'!A4:V4"
// This function can only ever run when user is logged in, ! should be fine?
let user = user!
let parameters: [String: Any] = [
"range": writeRange,
"values": [
[
name,
nil,
advisory
]
]
]
// What I want to be returned
var data: [String]?
// Google Identity said use this wrapper so that the OAuth tokens refresh
user.authentication.do { authentication, error in
guard error == nil else { return }
guard let authentication = authentication else { return }
// Get the access token to attach it to a REST or gRPC request.
let token = authentication.accessToken
let headers: HTTPHeaders = ["Authorization": "Bearer \(token)"]
AF.request("url", method: .put, parameters: parameters, encoding: JSONEncoding.default, headers: headers).responseString { response in
switch response.result {
case .success:
// I assume there is a better way to make two API calls...
AF.request("anotherURL", headers: headers).responseDecodable(of: NameResponseModel.self) { response2 in
switch response2.result {
case .success:
guard let responseData = response2.value else { return }
data = responseData.values[0]
// print(responseData.values[0]) works fine
case .failure:
print(response2.error ?? "Unknown error.")
data = nil
}
}
case .failure:
print(response.error ?? "Unknown error.")
data = nil
}
}
}
// Always returns nil, "Unknown error." never printed
return data
}
The model struct for my second AF request:
struct NameResponseModel: Decodable { let values: [[String]] }
An example API response for the second AF request:
{
"range": "'App Control'!A4:V4",
"majorDimension": "ROWS",
"values": [
[
"Bob Jones",
"A1234",
"Cathy Jones",
"1234 N. Street St. City, State 12345"
]
]
}
I saw stuff about your own callback function as a function parameter (or something along those lines) to handle this, but I was completely lost. I also looked at Swift async/await, but I don't know how that works with callback functions. Xcode had the option to refactor user.authentication.do { authentication, error in to let authentication = try await user.authentication.do(), but it threw a missing parameter error (the closure it previously had).
EDIT: user.authentication.do also returns void--another reason the refactor didn't work (I think).
There is probably a much more elegant way to do all of this so excuse the possibly atrocious way I did it.
Here is the link to Google Identity Wrapper info.
Thanks in advance for your help.
Solved my own problem.
It appears (according to Apple's async/await intro video) that when you have an unsupported callback that you need to run asynchronously, you wrap it in something called a Continuation, which allows you to manually resume the function on the thread, whether throwing or returning.
So using that code allows you to run the Google Identity token refresh with async/await.
private static func auth(_ user: GIDGoogleUser) async throws -> GIDAuthentication? {
typealias AuthContinuation = CheckedContinuation<GIDAuthentication?, Error>
return try await withCheckedThrowingContinuation { (continuation: AuthContinuation) in
user.authentication.do { authentication, error in
if let error = error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: authentication)
}
}
}
}
static func search(user: GIDGoogleUser) async throws {
// some code
guard let authentication = try await auth(user) else { ... }
// some code
}
I then ran that before using Alamofire's built-in async/await functionality for each request (here's one).
let dataTask = AF.request(...).serializingDecodable(NameResponseModel.self)
let response = try await dataTask.value
return response.values[0]
I am trying to work with a gRPC api and I have to send credentials securely. I am having issues to figure this out. I am using the swift-grpc library. I will link the docs and maybe someone can explain what I am supposed to do.
I am still unsure of what makes this secure through ssl(are we sending certificates).
docs from the swift-grpc using tlc library here
If anyone can give an explenation of how the ssl works and what to do that would be great
// code
import Foundation
import GRPC
import NIO
class Networking {
var authServiceClient: PartnerApi2_PartnerApiClient?
let port: Int = 50052
init() {
// build a fountain of EventLoops
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
do {
// open a channel to the gPRC server
let channel = try GRPCChannelPool.with(
target: .host("partner-api.com", port: self.port),
transportSecurity: .plaintext,
eventLoopGroup: eventLoopGroup
)
// create a Client
self.authServiceClient = PartnerApi2_PartnerApiClient.init(channel: channel) //AuthService_AuthServiceRoutesClient(channel: channel)
print("grpc connection initialized")
login(username: "email", password: "password")
} catch {
print("Couldn’t connect to gRPC server")
}
}
func login(username: String, password: String) -> String {
print("Login: username=\(username)")
// build the AccountCredentials object
let accountCredentials: PartnerApi2_AuthenticationRequest = .with {
$0.partnerEmail = username
$0.password = password
}
// grab the login() method from the gRPC client
let call = self.authServiceClient!.authenticate(accountCredentials)
// prepare an empty response object
let oauthCredentials: PartnerApi2_AuthenticationResponse
// execute the gRPC call and grab the result
do {
oauthCredentials = try call.response.wait()
} catch {
print("RPC method ‘login’ failed: \(error)")
// it would be better to throw an error here, but
// let’s keep this simple for demo purposes
return ""
}
// Do something interesting with the result
let oauthToken = oauthCredentials.authToken
print("Logged in with oauth token \(oauthToken)")
// return a value so we can use it in the app
return oauthToken
}
}
I am starting using Vapor to run an API, and I couldn't figure out an elegant way of doing the following: (1) load a Fluent relation and (2) wait for the response of an HTTP callback before (3) responding to the original request.
Here is what I ended up coding, after I transformed my real-life entities into planets and stars to maintain the examples from Vapor's documentation. It works™️, but I couldn't achieve a nice chaining of operations ☹️.
func flagPlanetAsHumanFriendly(req: Request) throws -> EventLoopFuture<Planet> {
Planet(req.parameters.get("planetID"), on: req.db)
.unwrap(or: Abort(.notFound))
.flatMap { planet in
// I load the star because I need its ID for the HTTP callback
_ = planet.$star.load(on: req.db).map {
let uri = URI(scheme: "http", host: "localhost", port: 4200, path: "/webhooks/star")
// HTTP Callback
req.client.post(uri) { req in
try req.content.encode(
WebhookDTO(
starId: planet.star.id!,
status: .humanFriendly,
planetId: planet.id!
),
using: JSONEncoder()
)
}
.map { res in
debugPrint("\(res)")
return
}
}
// Couldn't find a way to wait for the response, so the HTTP callback is a side-effect
// and its response is not used in the original HTTP response...
// If the callback fails, my API can't report it.
planet.state = .humanFriendly
return planet.save(on: req.db).map { planet }
}
}
Issue #1. Combining 2 EventLoopFuture
My first issue was that I couldn't find a way to load a parent relationship while keeping the entity in the scope.
Planet(req.parameters.get("planetID"), on: req.db)
.unwrap(or: Abort(.notFound))
.flatMap { planet in // planet is in the scope 🎉
return planet.$star.load(on: req.db) // returns a EventLoopFuture<Void>
}
.map {
// I know that star has been loaded but I lost my `planet` reference ☹️
???
}
I assume there is an operator that should be able to return a mix of the 2 EventLoopFuture instances but I couldn't figure it out.
Issue #2. Chaining EventLoopFuture with an auxiliary HTTP request's response
Here as well, I assume I miss an operator that allows me to wait for the request's response, while keeping a reference to the planet, before I respond to the original request.
Help would be welcome on how to achieve this in a nice chaining of operations — if it is possible, of course — and I will more than happy to update Vapor's documentation accordingly. 🙏
In short, if you need the result of one future inside the result of another future then you have to nest the callbacks - you can't use chaining. So for instance:
Planet(req.parameters.get("planetID"), on: req.db)
.unwrap(or: Abort(.notFound))
.flatMap { planet in // planet is in the scope 🎉
return planet.$star.load(on: req.db).map {
// Star is now eager loaded and you have access to planet
}
}
If you have multiple futures that aren't reliant on each other but you need the results of both you can use and to chain the two together and wait for them. E.g.:
func flagPlanetAsHumanFriendly(req: Request) throws -> EventLoopFuture<Planet> {
Planet(req.parameters.get("planetID"), on: req.db)
.unwrap(or: Abort(.notFound))
.flatMap { planet in
// I load the star because I need its ID for the HTTP callback
planet.$star.load(on: req.db).map {
let uri = URI(scheme: "http", host: "localhost", port: 4200, path: "/webhooks/star")
// HTTP Callback
let postFuture = req.client.post(uri) { req in
try req.content.encode(
WebhookDTO(
starId: planet.star.id!,
status: .humanFriendly,
planetId: planet.id!
),
using: JSONEncoder()
)
}.map { res in
debugPrint("\(res)")
return
}
planet.state = .humanFriendly
let planetUpdateFuture = planet.save(on: req.db)
return postFuture.and(planetUpdateFuture).flatMap { _, _ in
return planet
}
}
}
}
I made a wrapper for Alamofire which makes the data request first and then it prints the details of original URLRequest.
let dataRequest = session.request(url, method: .get, parameters: parameters)
let originalRequest = dataRequest.request
// Now print somehow the details of original request.
It worked fine on Alamofire 4.9, but it stopped in the newest 5.0 version. The problem is that dataRequest.request is nil. Why this behavior has changed? How can I access URLRequest underneath DataRequest?
URLRequests are now created asynchronously in Alamofire 5, so you're not going to be able to access the value immediately. Depending on what you're doing with the URLRequest there may be other solutions. For logging we recommend using the new EventMonitor protocol. You can read our documentation to see more, but adding a simple logger is straightforward:
final class Logger: EventMonitor {
let queue = DispatchQueue(label: ...)
// Event called when any type of Request is resumed.
func requestDidResume(_ request: Request) {
print("Resuming: \(request)")
}
// Event called whenever a DataRequest has parsed a response.
func request<Value>(_ request: DataRequest, didParseResponse response: DataResponse<Value, AFError>) {
debugPrint("Finished: \(response)")
}
}
let logger = Logger()
let session = Session(eventMonitors: [logger])
I had to obtain the URLRequest in a test case. Solved it by adding .responseData and using XCTestExpectation to wait for the async code to return:
func testThatHeadersContainContentEncoding() {
let exp = expectation(description: "\(#function)\(#line)")
let request = AF.request("http://test.org",
method: .post, parameters: ["test":"parameter"],
encoding: GZIPEncoding.default,
headers: ["Other":"Header"])
request.responseData { data in
let myHeader = request.request?.value(forHTTPHeaderField: additionalHeader.dictionary.keys.first!)
// .. do my tests
exp.fulfill()
}
waitForExpectations(timeout: 10, handler: nil)
}
Created a new project using toolbox command: vapor new projectname
In main.swift file I added the middleware code:
import Vapor
import HTTP
final class VersionMiddleware: Middleware {
func respond(to request: Request, chainingTo next: Responder) throws -> Response {
let response = try next.respond(to: request)
response.headers["Version"] = "API v1.0"
print("not printing")
return response
}
}
let drop = Droplet(availableMiddleware: [
"version": VersionMiddleware()
])
drop.get("hello") {
req in
return "Hello world"
}
drop.run()
But when I run this, it prints "hello world" but the API version is not added to headers. I am checking it using postman.
I think you should configure Config/middleware.json
{
"server": [
...
"version"
],
...
}
It will be work.
middleware document
Vapor 1 ➙ 2 Migration Note: "middleware" ordering configuration has moved from Config/middleware.json to Config/droplet.json.
Vapor 2
In Vapor 2, middleware can be created, added, and configured as follows:
Sources/App/Middleware/ExampleMiddleware.swift
final class ExampleMiddleware: Middleware {
func respond(to request: Request, chainingTo next: Responder) throws -> Response {
// if needed, process inbound `request` here.
// Swift.print(request.description)
// pass next inbound `request` to next middleware in processing chain
let response: Response = try next.respond(to: request)
// if needed, process outbound `response` here
// Swift.print(response.description)
// return `response` to chain back up any remaining middleware to client
return response
}
}
Sources/App/Setup/Config+Setup.swift
extension Config {
public func setup() throws {
…
try setupMiddleware()
…
}
public func setupMiddleware() throws {
self.addConfigurable(
middleware: ExampleMiddleware(),
name: "example"
)
}
}
Config/droplet.json
"middleware": [
…,
"example"
],
Note: Middleware directly included in Vapor 2 (such as "error", "sessions", "file", "date", "cors") would be used in Config/droplet.json "middleware" without the addConfigurable(middleware, name) setup step.
Vapor 2 Docs: Middleware ⇗