Can I use SwiftyJSON to get at error contents in Siesta? - siesta-swift

I'm writing a test for my Siesta-based class, and I'm trying to access the error I received from the server. In my object, I configured the service like so:
self.service.configure {
$0.config.pipeline[.parsing].add(SwiftyJSONTransformer, contentTypes: ["*/json"])
// other configuration setup
}
My test contains the following:
api.fetchApiToken(libraryRequiringAuth).onSuccess({ _ in
// Then
XCTFail("Expected request failure, got success")
}).onFailure({ [weak self] (error) in
XCTAssertEqual(error.httpStatusCode, expectedStatusCode)
let serverError: JSON? = error.entity?.typedContent()
XCTAssertEqual(serverError![0]["title"].string, expectedErrorMessage)
print("Expected error is: \(error)")
XCTAssertNil(self?.api.bearerAuthHeader)
expectation.fulfill()
})
The line let serverError: JSON? = error.entity?.typedContent() is setting serverError to nil, but in the debugger, I can see that error.entity exists and has the content I expect. Can I not use SwiftyJSON at this point?
Edit:
Here are the contents of the error:
Error Error(userMessage: "Forbidden", httpStatusCode: Optional(403), entity: Optional(Siesta.Entity(content: [{
ipRangeError = {
libraryId = 657;
libraryName = "Test library";
requestIp = "my.ip.address.was_here";
};
status = 403;
title = "Authentication Failed";
userData = {
};
}], charset: Optional("utf-8"), headers: ["content-type": "application/json; charset=utf-8"], timestamp: 490978565.82571602)), cause: nil, timestamp: 490978565.825499)

It looks like your SwiftyJSONTransformer is leaving errors untouched. Try configuring it with transformErrors: true:
private let SwiftyJSONTransformer =
ResponseContentTransformer(transformErrors: true)
{ JSON($0.content as AnyObject) }
Without that flag, the error entity is still an NSDictionary instead of a SwiftyJSON JSON, and typedContent() sees those as mismatched types and gives you a nil.
(And to the question in the title: yes, you can get the full error content, as well as response headers and any underlying ErrorType / NSError info from the underlying networking layer.)

Related

Xcode, The given data was not valid JSON, Can't read the JSON from API properly

Hello I am creating an app with Xcode and I am having the following problem, I created this API (if you enter the link you'll see the JSON data) https://proyecto-idts6.epizy.com/models/getCategorias.php
If you dont want to enter the link here is how this si how the structure of the JSON looks like:
{
"items":[
{
"categorie":"Fruits",
"id_categorie":"1"
},
{
"categorie":"Animals",
"id_categorie":"2"
},
{
"categorie":"Juices",
"id_categorie":"3"
},
{
"categorie":"Vegetables",
"id_categorie":"4"
},
{
"categorie":"Alcohol",
"id_categorie":"5"
},
{
"categorie":"Desserts",
"id_categorie":"6"
}
]
}
The problem I have is that when I try to decode the data from the API it cant't be decoded properly, I am trying to recreate the same code of this youtube video, but with my API: https://www.youtube.com/watch?v=sqo844saoC4
What I want basically is to print the categories and storage each of them in variables (because i'll need to move the variables between screens)
This is how my code looks like:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let url = "https://proyecto-idts6.epizy.com/models/getCategorias.php"
getData(from: url)
//Here is where i want to storage the variables from the JSON
}
private func getData(from url: String) {
let task = URLSession.shared.dataTask(with: URL(string: url)!, completionHandler: { data, response, error in
guard let data = data, error == nil else {
print("something went wrong")
return
}
do {
let result = try JSONDecoder().decode([ResultItem].self, from: data)
print(result)
}
catch {
print("failed to convert\(error)")
}
})
task.resume()
}
}
struct Response: Codable {
let items: [ResultItem]
}
struct ResultItem: Codable {
let categorie: String
}
My goal is to have variables for example like this: categorie1=("the category 1 called from the JSON"), categorie2=("the category 2 called from the JSON"), categorie3=("the category 3 called from the JSON"),...
The problem is not in the decoding but in the remote API.
Your endpoint (https://proyecto-idts6.epizy.com/models/getCategorias.php) instead of returning a JSON is returning the following HTML
<html><body><script type="text/javascript" src="/aes.js" ></script><script>function toNumbers(d){var e=[];d.replace(/(..)/g,function(d){e.push(parseInt(d,16))});return e}function toHex(){for(var d=[],d=1==arguments.length&&arguments[0].constructor==Array?arguments[0]:arguments,e="",f=0;f<d.length;f++)e+=(16>d[f]?"0":"")+d[f].toString(16);return e.toLowerCase()}var a=toNumbers("f655ba9d09a112d4968c63579db590b4"),b=toNumbers("98344c2eee86c3994890592585b49f80"),c=toNumbers("f5490e280a5e50f74932909856c3d3a3");document.cookie="__test="+toHex(slowAES.decrypt(c,2,a,b))+"; expires=Thu, 31-Dec-37 23:55:55 GMT; path=/"; location.href="https://proyecto-idts6.epizy.com/models/getCategorias.php?i=1";</script><noscript>This site requires Javascript to work, please enable Javascript in your browser or use a browser with Javascript support</noscript></body></html>
So you are trying to decode that HTML content, which clearly leads to the error your reported
failed to convertdataCorrupted(Swift.DecodingError.Context(codingPath: [], debugDescription: "The given data was not valid JSON.", underlyingError: Optional(Error Domain=NSCocoaErrorDomain Code=3840 "Invalid value around line 1, column 0." UserInfo={NSDebugDescription=Invalid value around line 1, column 0., NSJSONSerializationErrorIndex=0})))

Swift dispatch queue block not running

I am currently writing an Alamofire HTTP request and am running into an issue where my view is not loading - likely because there is no data. The confusing part is that this was working yesterday. In the request I was able to do print(data) and the result was 506 bytes which, if my calculation is correct, is about the correct size given the JSON payload returned from the endpoint below.
#State var recipes = [Recipe]()
AF.request("http://localhost:3000/recipes").responseJSON { response in
guard let data = response.data else { return }
if let response = try? JSONDecoder().decode([Recipe].self, from: data) {
DispatchQueue.main.async {
self.recipes = response
}
return
}
}
I can confirm that the endpoint that is being hit returns the following data...
[
{
"name":"Manhattan",
"image":"https://images.unsplash.com/photo-1536935338788-846bb9981813?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=2486&q=80",
"spirit":"Bourbon",
"ice":"Crushed",
"glass":"Coupe",
"yield":"3.75",
"description":"This is a good drink. You should make it.",
"ingredients":[
{
"bottle":"High West Son Of Bourye",
"amount":"2.5"
},
{
"bottle":"Cocchi Vermouth Di Torino",
"amount":"0.75"
},
{
"bottle":"Simple Syrup",
"amount":"0.083"
}
]
}
]
I also have my Recipe and Ingredient model here which should be able to decode based on the above JSON.
struct Recipe: Decodable, Identifiable {
var id = UUID()
var name: String
var image: String
var spirit: String
var ice: String
var glass: String
var yield: String
var description: String
var ingredients: [Ingredient]
}
struct Ingredient: Decodable, Identifiable {
var id = UUID()
var bottle: String
var amount: String
}
Is anybody able to spot an issue? I was trying to put a debugging print in the DispatchQueue but it is not printing which, to me, sounds like an error. However I am new to Swift/XCode/iOS and am not sure the best debugging practices for this.
If you can't debug yourself, NEVER USE try?. With more experience, I'd say that we tend to not use try?, but sometimes we do. But when we write try?, we are able to find an possible issue, ie debug if needed.
Let's do a proper try then, with a do/catch:
do {
let response = try JSONDecoder().decode([Recipe].self, from: data
DispatchQueue.main.async {
self.recipes = response
}
} catch {
print("Oops, there was en error while decoding: \(error)") // and not error.localizedDescription as it's more for users than developpers, so you'll skip all the useful informations
}
And read the output.
Going further?
Don't believe what's the API is supposed to return.
I've seen plenty and plenty of questions where the returned values was an error message, a XML Error message, a JSON Error message, an HTML Error message, and a JSON value missing, or of bad type, etc. And that, your JSONDecoder wasn't expecting it...
Reasons could be various, from bad/missing parameters, bad/missing APIKey, server down, bad/missing header, etc.
But, then, print the returned value.
print(String(data: data, encoding: .utf8) ?? "No data found")
So print it directly when you get it, or at least in the catch:
} catch {
print("Oops, there was en error while decoding: \(error)") // and not error.localizedDescription as it's more for users than developpers, so you'll skip all the useful informations
print("While getting response stringified: \(String(data: data, encoding: .utf8) ?? "No data found")")
}
If you don't understand the error message output, it's okay, there is no shame about it. But your first job is to get that error message. You can share it on SO if you don't understand it, you might get help with that. But currently, we can't guess what's wrong with your code.
It's a good idea to drop some clues in your code when looking for a failure.
If it were me I'd do something like this:
AF.request("http://localhost:3000/recipes").responseJSON { response in
guard let data = response.data else {
print("Error trying to receive data in ", #file, #function)
return
}
do {
let response = try JSONDecoder().decode([Recipe].self, from: data) {
DispatchQueue.main.async {
self.recipes = response
}
} catch {
print("Error failed to decode json data with error: \(error) in \(#file)", #function)
}
}

How to handle failure cases of JSON response using Codable?

I have some JSON response, that I take from a server. In success case, it might be like:
{
"success": true,
"data": [
{
/// something here
}
]
}
If all server responses would be successful, it would be really easy to parse that JSON. But we have also failure cases like:
{
"success": false,
"msg": "Your session expired",
"end_session": true
}
That means we need to handle two cases. As you noticed, attributes like success, msg may occur in any response. In order to handle that, I created following struct:
struct RegularResponse<T: Codable>: Codable {
let success: Bool
let msg: String?
let endSession: Bool?
let data: T?
enum CodingKeys: String, CodingKey {
case success, msg, data
case endSession = "end_session"
}
}
It may contain some data if response is successfull or otherwise, it is possible to identify why the error occurred(using success attribute or msg). Parsing process would go like following:
let model = try JSONDecoder().decode(RegularResponse<MyModel>.self, from: data)
if model.success {
// do something with data
} else {
// handle error
}
Everything works fine, but what if following JSON comes as following:
{
"success": true,
"name": "Jon Snow",
"living_place": "Nights Watch",
//some other fields
}
Here, I don't have data attribute. It means, my RegularResponse cannot be parsed. So, the question is how to handle these kind of situations? My idea for solution is simple: always put data in success cases into data field on my API. By doing so, my RegularResponse will always work, no matter what is inside data. But, it requires changes on a server side. Can this be fixed in a client side, not changing a server side? In other words, how to handle above situation in Swift using Codable?
I'm not sure if this is the best solution but if you know that your error response is in that shape, i.e.:
{
"success": false,
"msg": "Some error",
"end_session": "true",
}
then you could make another Codable struct/class that follows this response.
struct ErrorResponse: Codable {
let success: Bool
let msg: String
let end_session: String
}
and then when you are responding to your JSON you could adjust your code to:
if let successResponse = try? JSONDecoder().decode(RegularResponse<MyModel>.self, from: data) {
//handle success
} else if let responseError = try? JSONDecoder().decode(ErrorResponse.self, from data) {
//handle your error
}

Siesta REST login

How to translate my login user URLSession code into Siesta framework code? My current attempt isn't working.
I've looked at the example in the GithubBrowser but the API I have doesn't work like that.
The issue is that the user structure is kind of split by how the endpoint in the API I'm consuming works. The endpoint is http://server.com/api/key. Yes, it really is called key and not user or login. Its called that by the authors because you post a user/pass pair and get a key back. So it takes in (via post) a json struct like:
{"name": "bob", "password": "s3krit"}
and returns as a response:
{"token":"AEWasBDasd...AAsdga"}
I have a SessionUser struct:
struct SessionUser: Codable
{
let name: String
let password: String
let token: String
}
...which encapsulates the state (the "S" in REST) for the user. The trouble is name & password get posted and token is the response.
When this state changes I do my:
service.invalidateConfiguration() // So requests get config change
service.wipeResources() // Scrub all unauthenticated data
An instance is stored in a singleton, which is picked up by the configure block so that the token from the API is put in the header for all other API requests:
configure("**") {
// This block ^ is run AGAIN when the configuration is invalidated
// even if loadManifest is not called again.
if let haveToken = SessionManager.shared.currentUser?.token
{
$0.headers["Authorization"] = haveToken
}
}
That token injection part is already working well, by the way. Yay, Siesta!
URLSession version
This is bloated compared to Siesta, and I'm now not using this but here is what it used to be:
func login(user: SessionUser, endpoint: URL)
{
DDLogInfo("Logging in: \(user.name) with \(user.password)")
let json: [String: Any] = ["name": user.name, "password": user.password]
let jsonData = try? JSONSerialization.data(withJSONObject: json)
var request = URLRequest(url: endpoint)
request.httpMethod = "POST"
request.httpBody = jsonData
_currentStatus = .Unknown
weak var welf = self
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data else {
handleLogin(error: error, message: "No data from login attempt")
return
}
let jsonData:Any
do {
jsonData = try JSONSerialization.jsonObject(with: data, options: [])
}
catch let jsonDecodeError {
handleLogin(error: jsonDecodeError, message: "Could not get JSON from login response data")
return
}
guard let jsonDecoded = jsonData as? [String: Any] else {
handleLogin(error: error, message: "Could not decode JSON as dictionary")
return
}
guard let token = jsonDecoded["token"] as? String else {
handleLogin(error: error, message: "No auth token in login response")
return
}
let newUser = SessionUser(name: user.name, password: "", token: token)
welf?.currentUser = newUser
welf?.saveCurrentSession()
welf?._currentStatus = .LoggedIn
DDLogInfo("User \(newUser.name) logged in")
loginUpdate(user: newUser, status: .LoggedIn, message: nil, error: nil)
}
task.resume()
}
Siesta Version
Here is my attempt right now:
func login(user: String, pass: String, status: #escaping (String?) -> ())
{
let json = [ "name": user, "password": pass]
let req = ManifestCloud.shared.keys.request(.post, json: json)
req.onSuccess { (tokenInfo) in
if let token = tokenInfo.jsonDict["token"] as? String
{
let newUser = SessionUser(name: user, password: pass, token: token)
self.currentUser = newUser
}
status("success")
}
req.onFailure { (error) in
status(error.userMessage)
}
req.onCompletion { (response) in
status(nil)
}
}
Its sort of working, but the log in credentials are not saved by Siesta and I've had to rig up a new notification system for login state which I'd hoped Siesta would do for me.
I want to use Siesta's caching so that the SessionUser object is cached locally and I can use it to get a new token, if required, using the cached credentials. At the moment I have a jury-rigged system using UserDefaults.
Any help appreciated!
The basic problem here is that you are requesting but not loading the resource. Siesta draws a distinction between those two things: the first is essentially a fancied-up URLSession request; the second means that Siesta hangs on to some state and notifies observers about it.
Funny thing, I just answered a different but related question about this a few minutes ago! You might find that answer a helpful starting point.
In your case, the problem is here:
let req = ManifestCloud.shared.keys.request(.post, json: json)
That .request(…) means that only your request hooks (onSuccess etc.) receive a notification when your POST request finishes, and Siesta doesn’t keep the state around for others to observe.
You would normally accomplish that by using .load(); however, that creates a GET request and you need a POST. You probably want to promote your POST to be a full-fledge load request like this:
let keysResource = ManifestCloud.shared.keys
let req = keysResource.load(using:
keysResource.request(.post, json: json))
This will take whatever that POST request returns and make it the (observable) latestData of ManifestCloud.shared.keys, which should give you the “notification system for login state” that you’re looking for.

POST API call using APIGateway returns "Internal Server Error" in swift 4, but works everywhere else

I created a nodejs lambda function in AWS and exposed it using APIGateway with methods GET, PUT, POST, and DELETE (all setup with proxy). All methods have been tested and work in AWS using APIGateway, and then outside of AWS using Postman.
First, I called the GET method for the endpoint in my Swift 4 project, and it is successful.
BUT I have tried just about everything to call the POST method in swift and cannot get it to execute successfully. This is what I am currently trying after researching online:
let awsEndpoint: String = "https://host/path"
guard let awsURL = URL(string: awsEndpoint) else {
print("Error: cannot create URL")
return
}
var postUrlRequest = URLRequest(url: awsURL)
postUrlRequest.httpMethod = "POST"
postUrlRequest.addValue("John Doe", forHTTPHeaderField: "name")
postUrlRequest.addValue("imageurl.com", forHTTPHeaderField: "imageUrl")
URLSession.shared.dataTask(with: postUrlRequest) { data, response, error in
guard let data = data else { return }
do {
guard let receivedTodo = try JSONSerialization.jsonObject(with: data,
options: []) as? [String: Any] else {
print("Error")
return
}
} catch let err{
print(err)
}
}.resume()
The response I get is ["message":"Internal Server Error"]. When I look at the logs in CloudWatch they are not very descriptive. The error log for the post call is:
"Execution failed due to configuration error: Malformed Lambda proxy response"
After researching this issue aws suggests to format the response in a specific way and I have updated my nodejs lambda function to mimmic this.
case "POST":
pool.getConnection(function(err, connection) {
const groupName = event.headers.name;
const imageUrl = event.headers.imageUrl;
var group = {Name: groupName, ImageUrl: imageUrl, IsActive:true, Created:date, Updated:date};
var query = "INSERT INTO Groups SET ?";
connection.query(query,group, function (error, results, fields) {
var responseBody = {
"key3": "value3",
"key2": "value2",
"key1": "value1"
};
var response = {
"statusCode": 200,
"headers": {
"my_header": "my_value"
},
"body": JSON.stringify(responseBody),
"isBase64Encoded": true
};
if (error) callback(error);
else callback(null, response)
connection.release();
});
});
break;
Like I said previously, this works when testing everywhere except swift 4. My GET call works with swift 4, so I do not think it is an issue with allowing anything in the info.plist but I could be wrong. I have tried just about everything, but cannot seem to get past this error.
I fixed this issue myself. After allowing ALL log output in API Gateway for that endpoint, I found that somewhere along the way my headers were being converted to all lowercase.
'imageUrl' became 'imageurl'
It was throwing an error because in my lambda function, it could not find 'imageUrl'
I think this is a conversion that is happening in APIGateway because I have never come across this issue with swift.

Resources