expo-http-server 0.1.14-beta.3 → 0.1.14-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,6 +9,7 @@ import com.safframework.server.core.http.HttpMethod
9
9
  import com.safframework.server.core.http.Request
10
10
  import com.safframework.server.core.http.Response
11
11
  import org.json.JSONObject
12
+ import java.net.BindException
12
13
  import java.util.concurrent.ConcurrentHashMap
13
14
 
14
15
  class ExpoHttpServerModule : Module() {
@@ -20,11 +21,55 @@ class ExpoHttpServerModule : Module() {
20
21
 
21
22
  data class RouteConfig(val path: String, val method: String, val uuid: String)
22
23
 
24
+ companion object {
25
+ // The underlying AndroidServer/Netty bind happens on Netty's own event loop
26
+ // thread, asynchronously, so a BindException there can never be caught by a
27
+ // try/catch around server.start() - it surfaces as an uncaught exception and
28
+ // crashes the app. We install a single, narrowly-scoped default uncaught
29
+ // exception handler (chained to whatever was already registered) that only
30
+ // swallows BindExceptions thrown from within Netty's own call stack, so we
31
+ // can report the failure via onStatusUpdate instead of crashing.
32
+ private var bindExceptionGuardInstalled = false
33
+ private val statusUpdateListeners = ConcurrentHashMap.newKeySet<(String) -> Unit>()
34
+
35
+ @Synchronized
36
+ private fun ensureBindExceptionGuardInstalled() {
37
+ if (bindExceptionGuardInstalled) return
38
+ bindExceptionGuardInstalled = true
39
+ val previousHandler = Thread.getDefaultUncaughtExceptionHandler()
40
+ Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
41
+ if (isNettyBindException(throwable)) {
42
+ val message = throwable.message ?: "Address already in use"
43
+ statusUpdateListeners.forEach { it(message) }
44
+ } else {
45
+ previousHandler?.uncaughtException(thread, throwable)
46
+ }
47
+ }
48
+ }
49
+
50
+ private fun isNettyBindException(throwable: Throwable): Boolean =
51
+ throwable is BindException &&
52
+ throwable.stackTrace.any { it.className.startsWith("io.netty") }
53
+ }
54
+
23
55
  private var server: Server? = null
24
56
  private var started = false
57
+ // Tracks whether the *current* server instance has actually bound a socket.
58
+ // A freshly built server (from setup()/buildServerWithRoutes()) holds no
59
+ // socket, so there's nothing to release before starting it - closing and
60
+ // rebuilding in that case only adds an unnecessary close/rebind race.
61
+ private var hasBoundSocket = false
25
62
  private var configuredPort: Int = 0
26
63
  private val routeConfigs = mutableListOf<RouteConfig>()
27
64
  private val responses = ConcurrentHashMap<String, SimpleHttpResponse>()
65
+ private val onBindFailure: (String) -> Unit = { message ->
66
+ started = false
67
+ hasBoundSocket = false
68
+ sendEvent("onStatusUpdate", bundleOf(
69
+ "status" to "ERROR",
70
+ "message" to message
71
+ ))
72
+ }
28
73
 
29
74
  private fun handleRequest(uuid: String, request: Request, response: Response): Response {
30
75
  val headers: Map<String, String> = request.headers()
@@ -72,10 +117,20 @@ class ExpoHttpServerModule : Module() {
72
117
 
73
118
  Events("onStatusUpdate", "onRequest")
74
119
 
120
+ OnCreate {
121
+ ensureBindExceptionGuardInstalled()
122
+ statusUpdateListeners.add(onBindFailure)
123
+ }
124
+
125
+ OnDestroy {
126
+ statusUpdateListeners.remove(onBindFailure)
127
+ }
128
+
75
129
  Function("setup") { port: Int ->
76
130
  configuredPort = port
77
131
  server?.close()
78
132
  started = false
133
+ hasBoundSocket = false
79
134
  routeConfigs.clear()
80
135
  server = AndroidServer.Builder {
81
136
  port { configuredPort }
@@ -99,12 +154,22 @@ class ExpoHttpServerModule : Module() {
99
154
  if (!started) {
100
155
  try {
101
156
  started = true
102
- // Always close and rebuild before starting so any existing socket binding
103
- // is released, preventing BindException on the Netty event loop thread
104
- // (which is asynchronous and not caught by this try/catch).
105
- server?.close()
106
- server = buildServerWithRoutes()
157
+ // Only close and rebuild when the current server instance has actually
158
+ // bound a socket before - a freshly built instance (from setup(), or
159
+ // because it was never started) holds nothing to release, so closing
160
+ // and immediately rebinding it would just add an unnecessary race with
161
+ // the OS releasing the port (BindException: Address already in use).
162
+ if (hasBoundSocket) {
163
+ server?.close()
164
+ // Give the OS a moment to release the previous socket before rebinding
165
+ // to the same port - reduces (but, due to the underlying library
166
+ // configuring SO_REUSEADDR on the wrong socket scope, cannot fully
167
+ // eliminate) the "Address already in use" race.
168
+ Thread.sleep(300)
169
+ server = buildServerWithRoutes()
170
+ }
107
171
  server?.start()
172
+ hasBoundSocket = true
108
173
  sendEvent("onStatusUpdate", bundleOf(
109
174
  "status" to "STARTED",
110
175
  "message" to "Server started"
@@ -5,43 +5,35 @@ public class ExpoHttpServerModule: Module {
5
5
  private let server = CRHTTPServer()
6
6
  private var port: Int?
7
7
  private var stopped = false
8
- private var isRunning = false
9
8
  private var responses = [String: CRResponse]()
10
9
  private var bgTaskIdentifier = UIBackgroundTaskIdentifier.invalid
11
- private var foregroundObserver: NSObjectProtocol?
12
-
10
+
13
11
  public func definition() -> ModuleDefinition {
14
12
  Name("ExpoHttpServer")
15
13
 
16
14
  Events("onStatusUpdate", "onRequest")
17
-
15
+
18
16
  Function("setup", setupHandler)
19
17
  Function("start", startHandler)
20
18
  Function("route", routeHandler)
21
19
  Function("respond", respondHandler)
22
20
  Function("stop", stopHandler)
23
21
  }
24
-
22
+
25
23
  private func setupHandler(port: Int) {
26
24
  self.port = port;
27
25
  }
28
26
 
29
27
  private func startHandler() {
30
- if let obs = foregroundObserver {
31
- NotificationCenter.default.removeObserver(obs)
32
- }
33
- foregroundObserver = NotificationCenter.default.addObserver(
34
- forName: UIApplication.willEnterForegroundNotification,
35
- object: nil,
36
- queue: .main
37
- ) { [weak self] _ in
38
- guard let self, !self.stopped else { return }
39
- self.startServer(status: "RESUMED", message: "Server resumed")
28
+ NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { [unowned self] notification in
29
+ if (!self.stopped) {
30
+ self.startServer(status: "RESUMED", message: "Server resumed")
31
+ }
40
32
  }
41
- stopped = false
33
+ stopped = false;
42
34
  startServer(status: "STARTED", message: "Server started")
43
35
  }
44
-
36
+
45
37
  private func routeHandler(path: String, method: String, uuid: String) {
46
38
  server.add(path, block: { (req, res, next) in
47
39
  DispatchQueue.main.async {
@@ -62,7 +54,7 @@ public class ExpoHttpServerModule: Module {
62
54
  }
63
55
  }, recursive: false, method: CRHTTPMethod.fromString(method))
64
56
  }
65
-
57
+
66
58
  private func respondHandler(udid: String,
67
59
  statusCode: Int,
68
60
  statusDescription: String,
@@ -81,58 +73,41 @@ public class ExpoHttpServerModule: Module {
81
73
  self.responses[udid] = nil;
82
74
  }
83
75
  }
76
+
84
77
  }
85
-
78
+
86
79
  private func stopHandler() {
87
- stopped = true
88
- if let obs = foregroundObserver {
89
- NotificationCenter.default.removeObserver(obs)
90
- foregroundObserver = nil
91
- }
80
+ stopped = true;
92
81
  stopServer(status: "STOPPED", message: "Server stopped")
93
82
  }
94
-
83
+
95
84
  private func startServer(status: String, message: String) {
96
- if isRunning {
97
- server.stopListening()
98
- isRunning = false
99
- endBackgroundTask()
100
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
101
- self?.doStart(status: status, message: message)
85
+ stopServer()
86
+ if let port = port {
87
+ var error: NSError?
88
+ server.startListening(&error, portNumber: UInt(port))
89
+ if (error != nil) {
90
+ sendEvent("onStatusUpdate", [
91
+ "status": "ERROR",
92
+ "message": error?.localizedDescription ?? "Unknown error starting server"
93
+ ])
94
+ } else {
95
+ beginBackgroundTask()
96
+ sendEvent("onStatusUpdate", [
97
+ "status": status,
98
+ "message": message
99
+ ])
102
100
  }
103
101
  } else {
104
- doStart(status: status, message: message)
105
- }
106
- }
107
-
108
- private func doStart(status: String, message: String) {
109
- guard let port = port else {
110
102
  sendEvent("onStatusUpdate", [
111
103
  "status": "ERROR",
112
104
  "message": "Can't start server with port configured"
113
105
  ])
114
- return
115
- }
116
- var error: NSError?
117
- server.startListening(&error, portNumber: UInt(port))
118
- if let error = error {
119
- sendEvent("onStatusUpdate", [
120
- "status": "ERROR",
121
- "message": error.localizedDescription
122
- ])
123
- } else {
124
- isRunning = true
125
- beginBackgroundTask()
126
- sendEvent("onStatusUpdate", [
127
- "status": status,
128
- "message": message
129
- ])
130
106
  }
131
107
  }
132
-
108
+
133
109
  private func stopServer(status: String? = nil, message: String? = nil) {
134
110
  server.stopListening()
135
- isRunning = false
136
111
  endBackgroundTask()
137
112
  if let status = status, let message = message {
138
113
  sendEvent("onStatusUpdate", [
@@ -141,7 +116,7 @@ public class ExpoHttpServerModule: Module {
141
116
  ])
142
117
  }
143
118
  }
144
-
119
+
145
120
  private func beginBackgroundTask() {
146
121
  if (bgTaskIdentifier == UIBackgroundTaskIdentifier.invalid) {
147
122
  self.bgTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "BgTask", expirationHandler: {
@@ -180,7 +155,7 @@ extension CRHTTPMethod {
180
155
  return "GET"
181
156
  }
182
157
  }
183
-
158
+
184
159
  static func fromString(_ string: String) -> Self {
185
160
  var httpMethod: CRHTTPMethod
186
161
  switch (string) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-http-server",
3
- "version": "0.1.14-beta.3",
3
+ "version": "0.1.14-beta.4",
4
4
  "description": "A simple HTTP server expo module (iOS/Android)",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",