You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

登录表单输入空值返回非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默认错误页或后端框架抛出的错误页)。

具体逻辑:

  1. 输入空值触发后端参数校验失败,返回非200范围内的HTTP状态码(如400 Bad Request),但当前代码未检查响应状态码,直接尝试解码返回数据。
  2. 此时后端返回的是HTML而非JSON,导致JSONDecoder解码失败,触发dataCorrupted错误。
  3. 输入有效值时,后端返回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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 01:24:50