登录表单输入空值返回非JSON错误,期望字段必填提示
问题:登录表单输入空值时无法获取后端错误提示,触发JSON解码失败
当向登录表单输入空值时,未获取到后端应返回的**"The password or mobile field is required."**提示,反而出现以下解码错误:
dataCorrupted(Swift.DecodingError.Context(codingPath: [], debugDescription: "The given data was not valid JSON.", underlyingError: Optional(Error Domain=NSCocoaErrorDomain Code=3840 "Unexpected character '<' around line 1, column 1." UserInfo={NSDebugDescription=Unexpected character '<' around line 1, column 1., NSJSONSerializationErrorIndex=0})))
相关代码
1. 响应模型结构
struct AuthenticationResponse: Codable { let status: Bool let message: String let data: AuthenticationUserData? }
2. 网络请求服务类
class AuthenticationService: AuthenticationServiceProtocol{ func userAuthRequest<T>(url: String, parameters: [String : Any], type: T.Type) -> AnyPublisher<T, Error> where T : Decodable { guard let url = URL(string: url) else { return Fail(error: URLError(.badURL)).eraseToAnyPublisher() } var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") request.setValue("en", forHTTPHeaderField: "Content-Language") let postData = parameters.map { "\($0)=\($1)" } .joined(separator: "&") .data(using: .utf8) request.httpBody = postData return URLSession.shared.dataTaskPublisher(for: request) .subscribe(on: DispatchQueue.global()) .map { data, response -> Data in print(data) return data } .decode(type: T.self, decoder: JSONDecoder()) .eraseToAnyPublisher() } }
3. 登录方法实现
func logIn(){ let parameters = [ "mobile": phoneNumber, "password": password, "device_type": "iOS" ] isLoading = true service.userAuthRequest(url: Config.baseURL+"login", parameters: parameters, type: AuthenticationResponse.self) .receive(on: DispatchQueue.main) .sink { [weak self] completion in self?.isLoading = false switch completion{ case .finished: break case .failure(let error): print(error) self?.errorMessage = error.localizedDescription self?.showAlert = true } } receiveValue: { [weak self] returnedData in guard let self = self else { return } if !returnedData.status { self.errorMessage = returnedData.message self.showAlert = true // self.isAuthenticated = false // UserManager.shared.clearUserData() return } guard returnedData.data?.isActive != false else{ self.navigateToVerification = true return } if returnedData.status{ self.isAuthenticated = true if let token = returnedData.data?.accessToken{ UserManager.shared.saveToken(token) } if let userData = returnedData.data { UserManager.shared.saveUser(userData) } }else{ UserManager.shared.clearUserData() self.errorMessage = returnedData.message self.showAlert = true self.isAuthenticated = false } if let data = returnedData.data{ self.userData = data }else{ self.errorMessage = returnedData.message self.showAlert = true } } .store(in: &cancellables) }
问题原因
错误提示中的**"Unexpected character '<'"**是核心线索:输入空值时,后端返回的不是预期的JSON格式,而是HTML内容(比如4xx错误页面,例如Nginx默认错误页或后端框架抛出的错误页)。
具体逻辑:
- 输入空值触发后端参数校验失败,返回非200范围内的HTTP状态码(如400 Bad Request),但当前代码未检查响应状态码,直接尝试解码返回数据。
- 此时后端返回的是HTML而非JSON,导致
JSONDecoder解码失败,触发dataCorrupted错误。 - 输入有效值时,后端返回200状态码和标准JSON结构,因此能正常解码进入
receiveValue分支。
此外,手动拼接参数的方式存在隐患:空值参数会被拼接成mobile=或password=,可能不符合后端参数校验规则,进一步触发非JSON错误响应。
解决方案
1. 先校验HTTP响应状态码
在网络请求流程中,先检查响应状态码是否在200-299范围内,仅在状态正常时继续解码JSON。修改userAuthRequest方法:
func userAuthRequest<T>(url: String, parameters: [String : Any], type: T.Type) -> AnyPublisher<T, Error> where T : Decodable { guard let url = URL(string: url) else { return Fail(error: URLError(.badURL)).eraseToAnyPublisher() } var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") request.setValue("en", forHTTPHeaderField: "Content-Language") // 改用URLComponents生成正确编码的POST数据,避免手动拼接的编码问题 var components = URLComponents() components.queryItems = parameters.map { URLQueryItem(name: $0.key, value: "\($0.value)") } request.httpBody = components.query?.data(using: .utf8) return URLSession.shared.dataTaskPublisher(for: request) .subscribe(on: DispatchQueue.global()) .tryMap { data, response in // 校验HTTP状态码 guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1 throw NSError(domain: "NetworkError", code: statusCode, userInfo: [ NSLocalizedDescriptionKey: "请求失败,状态码:\(statusCode)" ]) } return data } .decode(type: T.self, decoder: JSONDecoder()) .eraseToAnyPublisher() }
2. 处理错误响应的JSON解析
如果后端返回4xx状态码时也会返回JSON格式的错误信息,可以在状态码校验失败后尝试解码错误响应:
.tryMap { data, response in guard let httpResponse = response as? HTTPURLResponse else { throw URLError(.badServerResponse) } if (200...299).contains(httpResponse.statusCode) { return data } else { // 尝试解码后端返回的错误JSON let errorResponse = try JSONDecoder().decode(AuthenticationResponse.self, from: data) throw NSError(domain: "AuthError", code: httpResponse.statusCode, userInfo: [ NSLocalizedDescriptionKey: errorResponse.message ]) } }
3. 在登录方法中处理自定义错误
修改sink的failure分支,提取自定义错误信息展示给用户:
case .failure(let error): print(error) self?.errorMessage = error.userInfo[NSLocalizedDescriptionKey] as? String ?? error.localizedDescription self?.showAlert = true
内容的提问来源于stack exchange,提问作者Mohamed
相关产品推荐
相关产品推荐

