'Can't set headers after they are sent to the client' NodeJS error when iOS Swift app is sent to the background

Viewed 165

I have an app with Swift/SwiftUI. I use the ObservableObject and JSONDecoder to get values from my Node.JS Express API to show within my app:

struct DevicesList: Decodable {
    var data: [PhilipsHueDevicesListEntry]
}

struct DevicesListEntry: Decodable {
    var ID: String
    var Name: String
    var State: Bool
    var Reachable: Bool
}

Class that gets the values from the API and puts into the above object:

class GetDevices: ObservableObject {

var didChange = PassthroughSubject<GetDevices, Never>()

var DevicesList = DevicesList(data: []){
    didSet {
        didChange.send(self)
    }
}

init(){
    let keychain = KeychainSwift()
    let authToken = keychain.get("authToken")!
    let bridgeID = keychain.get("hubID")!
    var request = URLRequest(url: URL(string: GlobalVariables.HUEGetDevices)!)
    request.httpMethod = HTTPMethod.get.rawValue
    let headers = [
        "auth-token": authToken,
        "bridgeID": bridgeID,
        "Content-Type": "application/json"]
    request.allHTTPHeaderFields = headers
    URLSession.shared.dataTask(with: request) { (data, _, _) in
        guard let data = data else {return}
        let devices = try! JSONDecoder().decode(DevicesList.self, from: data)
        DispatchQueue.main.async {
            self.DevicesList = devices
        }
    }.resume()
}

Then to show within my Main screen of my app:

@State var devices = GetDevices()

ForEach(devices.DevicesList.data, id: \.ID) { device in
                        ZStack{
                            Rectangle()
                                .cornerRadius(5)
                                .frame(width: 100, height: 100)
                                .foregroundColor(Color.orange)
                            VStack(alignment: .leading){
                                Text(device.Name)
                                    .multilineTextAlignment(.center)
                                    .frame(width: 100, height: 100)
                            }
                        }
....

Everything works perfectly well, until I swipe up to send the app to the background (which I assume runs the init() function again to update the observableObject). I get this error:

_http_outgoing.js:503
throw new errors.Error('ERR_HTTP_HEADERS_SENT', 'set');
^

Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
    at validateHeader (_http_outgoing.js:503:11)
    at ServerResponse.setHeader (_http_outgoing.js:510:3)
    at ServerResponse.header (/app-c/node_modules/express/lib/response.js:771:10)
    at ServerResponse.send (/app-c/node_modules/express/lib/response.js:170:12)
    at ServerResponse.json (/app-c/node_modules/express/lib/response.js:267:15)
    at ServerResponse.send (/app-c/node_modules/express/lib/response.js:158:21)
    at lightDataCallbackFunction (/app-c/app.js:185:32). // <----- This is where the error occurs in my js file
    at Socket.socket.on (/app-c/app.js:121:7)
    at Socket.emit (events.js:180:13)
    at /app-c/node_modules/socket.io/lib/socket.js:528:12
    at process._tickCallback (internal/process/next_tick.js:176:11) 

My JS file to which the error occurs:

lightDataCallbackFunction = function(data){
    if(data['socketID'] == socketID){
      if(data['code'] == '208'){
        return res.status(200).send(data) // <--- OCCURS HERE
      }else{
        return res.status(400).send({"data": "", "code": "209", "message": data['message']})
      }
    }
  }

I genuinely have no idea why this is the case and what is happening. My best guess would be that when I send the app to the background, the observable object instance is ran again and somehow the same server-side code tries to send another (same) response to my app? I even return the response to exit the function, so I do not see why this is happening?

EDIT:

I have tried MwcsMac comment from below, he mentioned that he usually just doesn't use the init() function at all, instead he moves the code that would go inside the init() function and puts it inside a new function, then calls that function when the view in question loads via .onAppear().

I have tried that, however it either doesn't run the function at all for whatever reason - or it just doesn't update my view (populating my ForEach loop within my SwiftUI code.

1 Answers

SwiftUI has @Observedobject wrapper as explicit design pair of ObservableObject, so let's use this pattern.

  1. Make GetDevices use standard publishing approach
class GetDevices: ObservableObject {

  @Published var DevicesList = DevicesList(data: [])

  private var isIdle = true
  func load() {
    guard isIdle else { return }
    isIdle = false

    let keychain = KeychainSwift()
    let authToken = keychain.get("authToken")!
    let bridgeID = keychain.get("hubID")!
    var request = URLRequest(url: URL(string: GlobalVariables.HUEGetDevices)!)
    request.httpMethod = HTTPMethod.get.rawValue
    let headers = [
        "auth-token": authToken,
        "bridgeID": bridgeID,
        "Content-Type": "application/json"]
    request.allHTTPHeaderFields = headers
    URLSession.shared.dataTask(with: request) { (data, _, _) in
        guard let data = data else {return}
        let devices = try! JSONDecoder().decode(DevicesList.self, from: data)
        DispatchQueue.main.async {
            self.DevicesList = devices
            self.isIdle = true
        }
    }.resume()
  }
}
  1. Use ObservedObject/ObservableObject in View
 @ObservedObject var devices = GetDevices()
 //@StateObject var devices = GetDevices()   // << use this for SwiftUI2.0 / iOS14+

 var body: some View {

   List {  // << some top container View

        // ... your dynamic content here

    }
    .onAppear {
       self.devices.load()      // << call here !!
    }
  }
Related