Swift调用Frappe ERPNext API持续触发PermissionError求助
我在Swift中调用Frappe ERPNext数据库API时,无论怎么构造请求都返回PermissionError,多次重构调用逻辑都无法解决,但相同的请求在Postman中可以正常运行。
错误信息
{"exception":"frappe.exceptions.PermissionError","exc_type":"PermissionError","exc":"["Traceback (most recent call last):\n File "apps/frappe/frappe/app.py", line 114, in application\n response = frappe.api.handle(request)\n File "apps/frappe/frappe/api/init.py", line 49, in handle\n data = endpoint(**arguments)\n File "apps/frappe/frappe/api/v1.py", line 27, in document_list\n return frappe.call(frappe.client.get_list, doctype, **frappe.form_dict)\n File "apps/frappe/frappe/init.py", line 1781, in call\n return fn(*args, **newargs)\n File "apps/frappe/frappe/utils/typing_validations.py", line 31, in wrapper\n return func(*args, **kwargs)\n File "apps/frappe/frappe/client.py", line 67, in get_list\n return frappe.get_list(**args)\n File "apps/frappe/frappe/init.py", line 2045, in get_list\n return frappe.model.db_query.DatabaseQuery(doctype).execute(*args, **kwargs)\n File "apps/frappe/frappe/model/db_query.py", line 114, in execute\n self.check_read_permission(self.doctype, parent_doctype=parent_doctype)\n File "apps/frappe/frappe/model/db_query.py", line 511, in check_read_permission\n self._set_permission_map(doctype, parent_doctype)\n File "apps/frappe/frappe/model/db_query.py", line 517, in _set_permission_map\n frappe.has_permission(\n File "apps/frappe/frappe/init.py", line 1103, in has_permission\n raise frappe.PermissionError\nfrappe.exceptions.PermissionError\n"]","_server_messages":"["{"message": "User Guest does not have doctype access via role permission for document Item", "title": "Message"}"]","_error_message":"No permission for Item"}
初始实现代码
import Foundation import SwiftUI struct ItemResponse: Decodable { let data: [Item] } struct Item: Decodable, Identifiable { let name: String var id: String { name } } var itemDataResponse: String = "" struct ShopAPIView: View { @ObservedObject var viewModel = itemapiresponse.itemGlobal var body: some View { VStack { if viewModel.itemList.isEmpty { Text(viewModel.itemDataResponse) } else { List(viewModel.itemList) { item in Text(item.name) } } } .onAppear { viewModel.callItemAPI() } } } final class itemapiresponse: ObservableObject { static let itemGlobal = itemapiresponse() @Published var itemDataResponse: String = "loading" @Published var itemList: [Item] = [] func callItemAPI() { let itemapiURL = "https://myurl" guard let url = URL(string: itemapiURL) else { DispatchQueue.main.async { self.itemDataResponse = "Invalid URL" } return } var request = URLRequest(url: url) request.httpMethod = "GET" request.addValue("my token", forHTTPHeaderField: "Authorization") URLSession.shared.dataTask(with: url) { data, response, error in if let error = error { DispatchQueue.main.async { self.itemDataResponse = "Error fetching data: \(error.localizedDescription)" } return } guard let data = data else { DispatchQueue.main.async { self.itemDataResponse = "No data returned" } return } print(String(data: data, encoding: .utf8) ?? "No readable data") do { let itemInfo = try JSONDecoder().decode(ItemResponse.self, from: data) DispatchQueue.main.async { self.itemList = itemInfo.data self.itemDataResponse = "Success" } } catch { DispatchQueue.main.async { self.itemDataResponse = "Error parsing JSON: \(error.localizedDescription)" } } }.resume() } } #Preview { ShopAPIView() }
Postman导出的可正常运行代码
var request = URLRequest(url: URL(string: "\(MyURL)")!,timeoutInterval: Double.infinity) request.addValue("token \(MyToken)", forHTTPHeaderField: "Authorization") request.addValue("full_name=Guest; sid=Guest; system_user=no; user_id=Guest; user_image=", forHTTPHeaderField: "Cookie") request.httpMethod = "GET" let task = URLSession.shared.dataTask(with: request) { data, response, error in guard let data = data else { print(String(describing: error)) return } print(String(data: data, encoding: .utf8)!) } task.resume()
第二次尝试实现代码
struct ContentView: View { @State private var responseData: String = "Loading..." var body: some View { ScrollView { Text(responseData) .padding() } .onAppear(perform: fetchData) } func fetchData() { var request = URLRequest(url: URL(string: "\(MyURL)")!, timeoutInterval: Double.infinity) request.addValue("token \(MyToken)", forHTTPHeaderField: "Authorization") request.addValue("full_name=Guest; sid=Guest; system_user=no; user_id=Guest; user_image=", forHTTPHeaderField: "Cookie") request.httpMethod = "GET" let task = URLSession.shared.dataTask(with: request) { data, response, error in guard let data = data else { print(String(describing: error)) return } print(String(data: data, encoding: .utf8)!) } task.resume() } }
问题分析与解决
从错误信息里的User Guest does not have doctype access可以看出,服务器识别你的请求为匿名Guest用户发起,核心问题有两个:
- 请求头未被发送:初始代码中构造了带认证头的
request对象,但调用URLSession.dataTask时错误传入了原始url而非request,导致所有设置的请求头都未生效。 - Authorization头格式错误:Frappe的Token认证要求格式为
"token 你的令牌字符串",初始代码里直接写"my token"不符合规范。
修正后的核心代码
func callItemAPI() { let itemapiURL = "https://你的实际API地址" guard let url = URL(string: itemapiURL) else { DispatchQueue.main.async { self.itemDataResponse = "Invalid URL" } return } var request = URLRequest(url: url) request.httpMethod = "GET" // 正确的Authorization格式 request.addValue("token 你的实际令牌", forHTTPHeaderField: "Authorization") // 按需添加Postman中的Cookie头 request.addValue("full_name=Guest; sid=Guest; system_user=no; user_id=Guest; user_image=", forHTTPHeaderField: "Cookie") // 必须传入构造好的request,而非原始url URLSession.shared.dataTask(with: request) { data, response, error in if let error = error { DispatchQueue.main.async { self.itemDataResponse = "Error fetching data: \(error.localizedDescription)" } return } guard let data = data else { DispatchQueue.main.async { self.itemDataResponse = "No data returned" } return } print(String(data: data, encoding: .utf8) ?? "No readable data") do { let itemInfo = try JSONDecoder().decode(ItemResponse.self, from: data) DispatchQueue.main.async { self.itemList = itemInfo.data self.itemDataResponse = "Success" } } catch { DispatchQueue.main.async { self.itemDataResponse = "Error parsing JSON: \(error.localizedDescription)" } } }.resume() }
另外,第二次尝试代码已经修正了请求对象的传入,但要确保MyURL和MyToken是实际有效的值,并且在更新UI时切换到主线程(比如把responseData的赋值放到DispatchQueue.main.async块中)。
内容的提问来源于stack exchange,提问作者mrbutterscotch

