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

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,8 @@ 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
13
+ import java.util.UUID
12
14
  import java.util.concurrent.ConcurrentHashMap
13
15
 
14
16
  class ExpoHttpServerModule : Module() {
@@ -20,18 +22,65 @@ class ExpoHttpServerModule : Module() {
20
22
 
21
23
  data class RouteConfig(val path: String, val method: String, val uuid: String)
22
24
 
25
+ companion object {
26
+ // The underlying AndroidServer/Netty bind happens on Netty's own event loop
27
+ // thread, asynchronously, so a BindException there can never be caught by a
28
+ // try/catch around server.start() - it surfaces as an uncaught exception and
29
+ // crashes the app. We install a single, narrowly-scoped default uncaught
30
+ // exception handler (chained to whatever was already registered) that only
31
+ // swallows BindExceptions thrown from within Netty's own call stack, so we
32
+ // can report the failure via onStatusUpdate instead of crashing.
33
+ private var bindExceptionGuardInstalled = false
34
+ private val statusUpdateListeners = ConcurrentHashMap.newKeySet<(String) -> Unit>()
35
+
36
+ @Synchronized
37
+ private fun ensureBindExceptionGuardInstalled() {
38
+ if (bindExceptionGuardInstalled) return
39
+ bindExceptionGuardInstalled = true
40
+ val previousHandler = Thread.getDefaultUncaughtExceptionHandler()
41
+ Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
42
+ if (isNettyBindException(throwable)) {
43
+ val message = throwable.message ?: "Address already in use"
44
+ statusUpdateListeners.forEach { it(message) }
45
+ } else {
46
+ previousHandler?.uncaughtException(thread, throwable)
47
+ }
48
+ }
49
+ }
50
+
51
+ private fun isNettyBindException(throwable: Throwable): Boolean =
52
+ throwable is BindException &&
53
+ throwable.stackTrace.any { it.className.startsWith("io.netty") }
54
+ }
55
+
23
56
  private var server: Server? = null
24
57
  private var started = false
58
+ // Tracks whether the *current* server instance has actually bound a socket.
59
+ // A freshly built server (from setup()/buildServerWithRoutes()) holds no
60
+ // socket, so there's nothing to release before starting it - closing and
61
+ // rebuilding in that case only adds an unnecessary close/rebind race.
62
+ private var hasBoundSocket = false
25
63
  private var configuredPort: Int = 0
64
+ private var configuredLoopbackOnly = false
26
65
  private val routeConfigs = mutableListOf<RouteConfig>()
27
66
  private val responses = ConcurrentHashMap<String, SimpleHttpResponse>()
67
+ private val onBindFailure: (String) -> Unit = { message ->
68
+ started = false
69
+ hasBoundSocket = false
70
+ sendEvent("onStatusUpdate", bundleOf(
71
+ "status" to "ERROR",
72
+ "message" to message
73
+ ))
74
+ }
28
75
 
29
76
  private fun handleRequest(uuid: String, request: Request, response: Response): Response {
30
77
  val headers: Map<String, String> = request.headers()
31
78
  val params: Map<String, String> = request.params()
32
79
  val cookies: Map<String, String> = request.cookies().associate { it.name() to it.value() }
80
+ val requestId = UUID.randomUUID().toString()
33
81
  sendEvent("onRequest", bundleOf(
34
82
  "uuid" to uuid,
83
+ "requestId" to requestId,
35
84
  "method" to request.method().name,
36
85
  "path" to request.url(),
37
86
  "body" to request.content(),
@@ -40,13 +89,13 @@ class ExpoHttpServerModule : Module() {
40
89
  "cookiesJson" to JSONObject(cookies).toString(),
41
90
  ))
42
91
  val deadline = System.currentTimeMillis() + 30_000
43
- while (!responses.containsKey(uuid) && System.currentTimeMillis() < deadline) {
92
+ while (!responses.containsKey(requestId) && System.currentTimeMillis() < deadline) {
44
93
  Thread.sleep(10)
45
94
  }
46
- val res = responses.remove(uuid) ?: return response
95
+ val res = responses.remove(requestId) ?: return response
47
96
  response.setBodyText(res.body)
48
97
  response.setStatus(res.statusCode)
49
- response.addHeader("Content-Length", "" + res.body.length)
98
+ response.addHeader("Content-Length", "" + res.body.toByteArray(Charsets.UTF_8).size)
50
99
  response.addHeader("Content-Type", res.contentType)
51
100
  for ((key, value) in res.headers) {
52
101
  response.addHeader(key, value)
@@ -54,10 +103,15 @@ class ExpoHttpServerModule : Module() {
54
103
  return response
55
104
  }
56
105
 
106
+ private fun buildServer(): Server = AndroidServer.Builder {
107
+ port { configuredPort }
108
+ if (configuredLoopbackOnly) {
109
+ address { "127.0.0.1" }
110
+ }
111
+ }.build()
112
+
57
113
  private fun buildServerWithRoutes(): Server {
58
- var s: Server = AndroidServer.Builder {
59
- port { configuredPort }
60
- }.build()
114
+ var s: Server = buildServer()
61
115
  for (route in routeConfigs) {
62
116
  s = s.request(HttpMethod.getMethod(route.method), route.path) { request: Request, response: Response ->
63
117
  handleRequest(route.uuid, request, response)
@@ -72,14 +126,23 @@ class ExpoHttpServerModule : Module() {
72
126
 
73
127
  Events("onStatusUpdate", "onRequest")
74
128
 
75
- Function("setup") { port: Int ->
129
+ OnCreate {
130
+ ensureBindExceptionGuardInstalled()
131
+ statusUpdateListeners.add(onBindFailure)
132
+ }
133
+
134
+ OnDestroy {
135
+ statusUpdateListeners.remove(onBindFailure)
136
+ }
137
+
138
+ Function("setup") { port: Int, loopbackOnly: Boolean ->
76
139
  configuredPort = port
140
+ configuredLoopbackOnly = loopbackOnly
77
141
  server?.close()
78
142
  started = false
143
+ hasBoundSocket = false
79
144
  routeConfigs.clear()
80
- server = AndroidServer.Builder {
81
- port { configuredPort }
82
- }.build()
145
+ server = buildServer()
83
146
  }
84
147
 
85
148
  Function("route") { path: String, method: String, uuid: String ->
@@ -99,12 +162,22 @@ class ExpoHttpServerModule : Module() {
99
162
  if (!started) {
100
163
  try {
101
164
  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()
165
+ // Only close and rebuild when the current server instance has actually
166
+ // bound a socket before - a freshly built instance (from setup(), or
167
+ // because it was never started) holds nothing to release, so closing
168
+ // and immediately rebinding it would just add an unnecessary race with
169
+ // the OS releasing the port (BindException: Address already in use).
170
+ if (hasBoundSocket) {
171
+ server?.close()
172
+ // Give the OS a moment to release the previous socket before rebinding
173
+ // to the same port - reduces (but, due to the underlying library
174
+ // configuring SO_REUSEADDR on the wrong socket scope, cannot fully
175
+ // eliminate) the "Address already in use" race.
176
+ Thread.sleep(300)
177
+ server = buildServerWithRoutes()
178
+ }
107
179
  server?.start()
180
+ hasBoundSocket = true
108
181
  sendEvent("onStatusUpdate", bundleOf(
109
182
  "status" to "STARTED",
110
183
  "message" to "Server started"
package/build/index.d.ts CHANGED
@@ -9,6 +9,7 @@ export interface StatusEvent {
9
9
  }
10
10
  export interface RequestEvent {
11
11
  uuid: string;
12
+ requestId: string;
12
13
  method: string;
13
14
  path: string;
14
15
  body: string;
@@ -31,6 +32,9 @@ export interface Callback {
31
32
  }
32
33
  export declare const start: () => void;
33
34
  export declare const route: (path: string, method: HttpMethod, callback: (request: RequestEvent) => Promise<Response>) => void;
34
- export declare const setup: (port: number, onStatusUpdate?: ((event: StatusEvent) => void) | undefined) => void;
35
+ export interface SetupOptions {
36
+ loopbackOnly?: boolean;
37
+ }
38
+ export declare const setup: (port: number, onStatusUpdate?: ((event: StatusEvent) => void) | undefined, options?: SetupOptions) => void;
35
39
  export declare const stop: () => void;
36
40
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,SAAS,CAAC;AACvE;;GAEG;AACH,MAAM,MAAM,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;AAE5E,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,QAAQ;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;CACxD;AAED,eAAO,MAAM,KAAK,YA4BjB,CAAC;AAEF,eAAO,MAAM,KAAK,SACV,MAAM,UACJ,UAAU,sBACE,YAAY,KAAK,QAAQ,QAAQ,CAAC,SAUvD,CAAC;AAEF,eAAO,MAAM,KAAK,SACV,MAAM,4BACa,WAAW,KAAK,IAAI,sBAU9C,CAAC;AAEF,eAAO,MAAM,IAAI,YAIhB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,SAAS,CAAC;AACvE;;GAEG;AACH,MAAM,MAAM,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;AAE5E,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,QAAQ;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;CACxD;AAED,eAAO,MAAM,KAAK,YAqCjB,CAAC;AAEF,eAAO,MAAM,KAAK,SACV,MAAM,UACJ,UAAU,sBACE,YAAY,KAAK,QAAQ,QAAQ,CAAC,SAUvD,CAAC;AAEF,MAAM,WAAW,YAAY;IAC3B,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,eAAO,MAAM,KAAK,SACV,MAAM,4BACa,WAAW,KAAK,IAAI,yBACpC,YAAY,SAUtB,CAAC;AAEF,eAAO,MAAM,IAAI,YAIhB,CAAC"}
package/build/index.js CHANGED
@@ -9,13 +9,15 @@ export const start = () => {
9
9
  try {
10
10
  const responseHandler = requestCallbacks.find((c) => c.uuid === event.uuid);
11
11
  if (!responseHandler) {
12
- ExpoHttpServerModule.respond(event.uuid, 404, "Not Found", "application/json", {}, JSON.stringify({ error: "Handler not found" }));
12
+ ExpoHttpServerModule.respond(event.requestId, 404, "Not Found", "application/json", {}, JSON.stringify({ error: "Handler not found" }));
13
13
  return;
14
14
  }
15
15
  const response = await responseHandler.callback(event);
16
- ExpoHttpServerModule.respond(event.uuid, response.statusCode || 200, response.statusDescription || "OK", response.contentType || "application/json", response.headers || {}, response.body || "{}");
16
+ ExpoHttpServerModule.respond(event.requestId, response.statusCode || 200, response.statusDescription || "OK", response.contentType || "application/json", response.headers || {}, response.body || "{}");
17
+ }
18
+ catch {
19
+ ExpoHttpServerModule.respond(event.requestId, 500, "Internal Server Error", "application/json", {}, JSON.stringify({ error: "Handler failed" }));
17
20
  }
18
- catch { }
19
21
  });
20
22
  ExpoHttpServerModule.start();
21
23
  };
@@ -29,7 +31,7 @@ export const route = (path, method, callback) => {
29
31
  });
30
32
  ExpoHttpServerModule.route(path, method, uuid);
31
33
  };
32
- export const setup = (port, onStatusUpdate) => {
34
+ export const setup = (port, onStatusUpdate, options = {}) => {
33
35
  if (onStatusUpdate) {
34
36
  emitter.addListener("onStatusUpdate", async (event) => {
35
37
  try {
@@ -38,7 +40,7 @@ export const setup = (port, onStatusUpdate) => {
38
40
  catch { }
39
41
  });
40
42
  }
41
- ExpoHttpServerModule.setup(port);
43
+ ExpoHttpServerModule.setup(port, options.loopbackOnly ?? false);
42
44
  };
43
45
  export const stop = () => {
44
46
  requestSubscription?.remove();
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,OAAO,oBAAoB,MAAM,wBAAwB,CAAC;AAE1D,MAAM,OAAO,GAAG,IAAI,YAAY,CAAC,oBAAoB,CAAC,CAAC;AACvD,MAAM,gBAAgB,GAAe,EAAE,CAAC;AACxC,IAAI,mBAAmB,GAAkD,IAAI,CAAC;AAsC9E,MAAM,CAAC,MAAM,KAAK,GAAG,GAAG,EAAE;IACxB,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,mBAAmB,GAAG,OAAO,CAAC,WAAW,CAAe,WAAW,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;QACnF,IAAI,CAAC;YACH,MAAM,eAAe,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,KAAK,CAAC,IAAI,CAAC,CAAC;YAC5E,IAAI,CAAC,eAAe,EAAE,CAAC;gBACrB,oBAAoB,CAAC,OAAO,CAC1B,KAAK,CAAC,IAAI,EACV,GAAG,EACH,WAAW,EACX,kBAAkB,EAClB,EAAE,EACF,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,mBAAmB,EAAE,CAAC,CAC/C,CAAC;gBACF,OAAO;YACT,CAAC;YACD,MAAM,QAAQ,GAAG,MAAM,eAAe,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;YACvD,oBAAoB,CAAC,OAAO,CAC1B,KAAK,CAAC,IAAI,EACV,QAAQ,CAAC,UAAU,IAAI,GAAG,EAC1B,QAAQ,CAAC,iBAAiB,IAAI,IAAI,EAClC,QAAQ,CAAC,WAAW,IAAI,kBAAkB,EAC1C,QAAQ,CAAC,OAAO,IAAI,EAAE,EACtB,QAAQ,CAAC,IAAI,IAAI,IAAI,CACtB,CAAC;QACJ,CAAC;QAAC,MAAM,CAAC,CAAA,CAAC;IACZ,CAAC,CAAC,CAAC;IACH,oBAAoB,CAAC,KAAK,EAAE,CAAC;AAC/B,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,KAAK,GAAG,CACnB,IAAY,EACZ,MAAkB,EAClB,QAAsD,EACtD,EAAE;IACF,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACjD,gBAAgB,CAAC,IAAI,CAAC;QACpB,MAAM;QACN,IAAI;QACJ,IAAI;QACJ,QAAQ;KACT,CAAC,CAAC;IACH,oBAAoB,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;AACjD,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,KAAK,GAAG,CACnB,IAAY,EACZ,cAA6C,EAC7C,EAAE;IACF,IAAI,cAAc,EAAE,CAAC;QACnB,OAAO,CAAC,WAAW,CAAc,gBAAgB,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;YACjE,IAAI,CAAC;gBACH,MAAM,cAAc,CAAC,KAAK,CAAC,CAAC;YAC9B,CAAC;YAAC,MAAM,CAAC,CAAA,CAAC;QACZ,CAAC,CAAC,CAAC;IACL,CAAC;IACD,oBAAoB,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AACnC,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,IAAI,GAAG,GAAG,EAAE;IACvB,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,mBAAmB,GAAG,IAAI,CAAC;IAC3B,oBAAoB,CAAC,IAAI,EAAE,CAAC;AAC9B,CAAC,CAAC","sourcesContent":["import { EventEmitter } from \"expo-modules-core\";\n\nimport ExpoHttpServerModule from \"./ExpoHttpServerModule\";\n\nconst emitter = new EventEmitter(ExpoHttpServerModule);\nconst requestCallbacks: Callback[] = [];\nlet requestSubscription: ReturnType<typeof emitter.addListener> | null = null;\n\nexport type HttpMethod = \"GET\" | \"POST\" | \"PUT\" | \"DELETE\" | \"OPTIONS\";\n/**\n * PAUSED AND RESUMED are iOS only\n */\nexport type Status = \"STARTED\" | \"PAUSED\" | \"RESUMED\" | \"STOPPED\" | \"ERROR\";\n\nexport interface StatusEvent {\n status: Status;\n message: string;\n}\n\nexport interface RequestEvent {\n uuid: string;\n method: string;\n path: string;\n body: string;\n headersJson: string;\n paramsJson: string;\n cookiesJson: string;\n}\n\nexport interface Response {\n statusCode?: number;\n statusDescription?: string;\n contentType?: string;\n headers?: Record<string, string>;\n body?: string;\n}\n\nexport interface Callback {\n method: string;\n path: string;\n uuid: string;\n callback: (request: RequestEvent) => Promise<Response>;\n}\n\nexport const start = () => {\n requestSubscription?.remove();\n requestSubscription = emitter.addListener<RequestEvent>(\"onRequest\", async (event) => {\n try {\n const responseHandler = requestCallbacks.find((c) => c.uuid === event.uuid);\n if (!responseHandler) {\n ExpoHttpServerModule.respond(\n event.uuid,\n 404,\n \"Not Found\",\n \"application/json\",\n {},\n JSON.stringify({ error: \"Handler not found\" }),\n );\n return;\n }\n const response = await responseHandler.callback(event);\n ExpoHttpServerModule.respond(\n event.uuid,\n response.statusCode || 200,\n response.statusDescription || \"OK\",\n response.contentType || \"application/json\",\n response.headers || {},\n response.body || \"{}\",\n );\n } catch {}\n });\n ExpoHttpServerModule.start();\n};\n\nexport const route = (\n path: string,\n method: HttpMethod,\n callback: (request: RequestEvent) => Promise<Response>,\n) => {\n const uuid = Math.random().toString(16).slice(2);\n requestCallbacks.push({\n method,\n path,\n uuid,\n callback,\n });\n ExpoHttpServerModule.route(path, method, uuid);\n};\n\nexport const setup = (\n port: number,\n onStatusUpdate?: (event: StatusEvent) => void,\n) => {\n if (onStatusUpdate) {\n emitter.addListener<StatusEvent>(\"onStatusUpdate\", async (event) => {\n try {\n await onStatusUpdate(event);\n } catch {}\n });\n }\n ExpoHttpServerModule.setup(port);\n};\n\nexport const stop = () => {\n requestSubscription?.remove();\n requestSubscription = null;\n ExpoHttpServerModule.stop();\n};\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { EventEmitter } from \"expo-modules-core\";\n\nimport ExpoHttpServerModule from \"./ExpoHttpServerModule\";\n\nconst emitter = new EventEmitter(ExpoHttpServerModule);\nconst requestCallbacks: Callback[] = [];\nlet requestSubscription: ReturnType<typeof emitter.addListener> | null = null;\n\nexport type HttpMethod = \"GET\" | \"POST\" | \"PUT\" | \"DELETE\" | \"OPTIONS\";\n/**\n * PAUSED AND RESUMED are iOS only\n */\nexport type Status = \"STARTED\" | \"PAUSED\" | \"RESUMED\" | \"STOPPED\" | \"ERROR\";\n\nexport interface StatusEvent {\n status: Status;\n message: string;\n}\n\nexport interface RequestEvent {\n uuid: string;\n requestId: string;\n method: string;\n path: string;\n body: string;\n headersJson: string;\n paramsJson: string;\n cookiesJson: string;\n}\n\nexport interface Response {\n statusCode?: number;\n statusDescription?: string;\n contentType?: string;\n headers?: Record<string, string>;\n body?: string;\n}\n\nexport interface Callback {\n method: string;\n path: string;\n uuid: string;\n callback: (request: RequestEvent) => Promise<Response>;\n}\n\nexport const start = () => {\n requestSubscription?.remove();\n requestSubscription = emitter.addListener<RequestEvent>(\"onRequest\", async (event) => {\n try {\n const responseHandler = requestCallbacks.find((c) => c.uuid === event.uuid);\n if (!responseHandler) {\n ExpoHttpServerModule.respond(\n event.requestId,\n 404,\n \"Not Found\",\n \"application/json\",\n {},\n JSON.stringify({ error: \"Handler not found\" }),\n );\n return;\n }\n const response = await responseHandler.callback(event);\n ExpoHttpServerModule.respond(\n event.requestId,\n response.statusCode || 200,\n response.statusDescription || \"OK\",\n response.contentType || \"application/json\",\n response.headers || {},\n response.body || \"{}\",\n );\n } catch {\n ExpoHttpServerModule.respond(\n event.requestId,\n 500,\n \"Internal Server Error\",\n \"application/json\",\n {},\n JSON.stringify({ error: \"Handler failed\" }),\n );\n }\n });\n ExpoHttpServerModule.start();\n};\n\nexport const route = (\n path: string,\n method: HttpMethod,\n callback: (request: RequestEvent) => Promise<Response>,\n) => {\n const uuid = Math.random().toString(16).slice(2);\n requestCallbacks.push({\n method,\n path,\n uuid,\n callback,\n });\n ExpoHttpServerModule.route(path, method, uuid);\n};\n\nexport interface SetupOptions {\n loopbackOnly?: boolean;\n}\n\nexport const setup = (\n port: number,\n onStatusUpdate?: (event: StatusEvent) => void,\n options: SetupOptions = {},\n) => {\n if (onStatusUpdate) {\n emitter.addListener<StatusEvent>(\"onStatusUpdate\", async (event) => {\n try {\n await onStatusUpdate(event);\n } catch {}\n });\n }\n ExpoHttpServerModule.setup(port, options.loopbackOnly ?? false);\n};\n\nexport const stop = () => {\n requestSubscription?.remove();\n requestSubscription = null;\n ExpoHttpServerModule.stop();\n};\n"]}
@@ -4,44 +4,43 @@ import Criollo
4
4
  public class ExpoHttpServerModule: Module {
5
5
  private let server = CRHTTPServer()
6
6
  private var port: Int?
7
+ private var loopbackOnly = false
7
8
  private var stopped = false
8
- private var isRunning = false
9
+ private var isListening = false
10
+ private var foregroundObserver: NSObjectProtocol?
9
11
  private var responses = [String: CRResponse]()
10
12
  private var bgTaskIdentifier = UIBackgroundTaskIdentifier.invalid
11
- private var foregroundObserver: NSObjectProtocol?
12
-
13
+
13
14
  public func definition() -> ModuleDefinition {
14
15
  Name("ExpoHttpServer")
15
16
 
16
17
  Events("onStatusUpdate", "onRequest")
17
-
18
+
18
19
  Function("setup", setupHandler)
19
20
  Function("start", startHandler)
20
21
  Function("route", routeHandler)
21
22
  Function("respond", respondHandler)
22
23
  Function("stop", stopHandler)
23
24
  }
24
-
25
- private func setupHandler(port: Int) {
25
+
26
+ private func setupHandler(port: Int, loopbackOnly: Bool) {
26
27
  self.port = port;
28
+ self.loopbackOnly = loopbackOnly;
27
29
  }
28
30
 
29
31
  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")
32
+ if (foregroundObserver == nil) {
33
+ foregroundObserver = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { [weak self] _ in
34
+ guard let self = self, !self.stopped, !self.isListening else {
35
+ return
36
+ }
37
+ self.startServer(status: "RESUMED", message: "Server resumed")
38
+ }
40
39
  }
41
- stopped = false
40
+ stopped = false;
42
41
  startServer(status: "STARTED", message: "Server started")
43
42
  }
44
-
43
+
45
44
  private func routeHandler(path: String, method: String, uuid: String) {
46
45
  server.add(path, block: { (req, res, next) in
47
46
  DispatchQueue.main.async {
@@ -49,9 +48,11 @@ public class ExpoHttpServerModule: Module {
49
48
  if let body = req.body, let bodyData = try? JSONSerialization.data(withJSONObject: body) {
50
49
  bodyString = String(data: bodyData, encoding: .utf8) ?? "{}"
51
50
  }
52
- self.responses[uuid] = res
51
+ let requestId = UUID().uuidString
52
+ self.responses[requestId] = res
53
53
  self.sendEvent("onRequest", [
54
54
  "uuid": uuid,
55
+ "requestId": requestId,
55
56
  "method": req.method.toString(),
56
57
  "path": path,
57
58
  "body": bodyString,
@@ -62,77 +63,66 @@ public class ExpoHttpServerModule: Module {
62
63
  }
63
64
  }, recursive: false, method: CRHTTPMethod.fromString(method))
64
65
  }
65
-
66
- private func respondHandler(udid: String,
66
+
67
+ private func respondHandler(requestId: String,
67
68
  statusCode: Int,
68
69
  statusDescription: String,
69
70
  contentType: String,
70
71
  headers: [String: String],
71
72
  body: String) {
72
73
  DispatchQueue.main.async {
73
- if let response = self.responses[udid] {
74
+ if let response = self.responses[requestId] {
74
75
  response.setStatusCode(UInt(statusCode), description: statusDescription)
75
76
  response.setValue(contentType, forHTTPHeaderField: "Content-type")
76
- response.setValue("\(body.count)", forHTTPHeaderField: "Content-Length")
77
+ response.setValue("\(body.utf8.count)", forHTTPHeaderField: "Content-Length")
77
78
  for (key, value) in headers {
78
79
  response.setValue(value, forHTTPHeaderField: key)
79
80
  }
80
81
  response.send(body);
81
- self.responses[udid] = nil;
82
+ self.responses[requestId] = nil;
82
83
  }
83
84
  }
85
+
84
86
  }
85
-
87
+
86
88
  private func stopHandler() {
87
- stopped = true
88
- if let obs = foregroundObserver {
89
- NotificationCenter.default.removeObserver(obs)
89
+ stopped = true;
90
+ if let observer = foregroundObserver {
91
+ NotificationCenter.default.removeObserver(observer)
90
92
  foregroundObserver = nil
91
93
  }
92
94
  stopServer(status: "STOPPED", message: "Server stopped")
93
95
  }
94
-
96
+
95
97
  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)
98
+ stopServer()
99
+ if let port = port {
100
+ var error: NSError?
101
+ server.startListening(&error, portNumber: UInt(port), interface: loopbackOnly ? "localhost" : nil)
102
+ if (error != nil) {
103
+ sendEvent("onStatusUpdate", [
104
+ "status": "ERROR",
105
+ "message": error?.localizedDescription ?? "Unknown error starting server"
106
+ ])
107
+ } else {
108
+ isListening = true
109
+ beginBackgroundTask()
110
+ sendEvent("onStatusUpdate", [
111
+ "status": status,
112
+ "message": message
113
+ ])
102
114
  }
103
115
  } 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
116
  sendEvent("onStatusUpdate", [
111
117
  "status": "ERROR",
112
118
  "message": "Can't start server with port configured"
113
119
  ])
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
120
  }
131
121
  }
132
-
122
+
133
123
  private func stopServer(status: String? = nil, message: String? = nil) {
134
124
  server.stopListening()
135
- isRunning = false
125
+ isListening = false
136
126
  endBackgroundTask()
137
127
  if let status = status, let message = message {
138
128
  sendEvent("onStatusUpdate", [
@@ -141,7 +131,7 @@ public class ExpoHttpServerModule: Module {
141
131
  ])
142
132
  }
143
133
  }
144
-
134
+
145
135
  private func beginBackgroundTask() {
146
136
  if (bgTaskIdentifier == UIBackgroundTaskIdentifier.invalid) {
147
137
  self.bgTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "BgTask", expirationHandler: {
@@ -176,11 +166,13 @@ extension CRHTTPMethod {
176
166
  return "PUT"
177
167
  case .delete:
178
168
  return "DELETE"
169
+ case .options:
170
+ return "OPTIONS"
179
171
  default:
180
172
  return "GET"
181
173
  }
182
174
  }
183
-
175
+
184
176
  static func fromString(_ string: String) -> Self {
185
177
  var httpMethod: CRHTTPMethod
186
178
  switch (string) {
@@ -190,6 +182,8 @@ extension CRHTTPMethod {
190
182
  httpMethod = .put
191
183
  case "DELETE":
192
184
  httpMethod = .delete
185
+ case "OPTIONS":
186
+ httpMethod = .options
193
187
  default:
194
188
  httpMethod = .get
195
189
  }
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.5",
4
4
  "description": "A simple HTTP server expo module (iOS/Android)",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
package/src/index.ts CHANGED
@@ -19,6 +19,7 @@ export interface StatusEvent {
19
19
 
20
20
  export interface RequestEvent {
21
21
  uuid: string;
22
+ requestId: string;
22
23
  method: string;
23
24
  path: string;
24
25
  body: string;
@@ -49,7 +50,7 @@ export const start = () => {
49
50
  const responseHandler = requestCallbacks.find((c) => c.uuid === event.uuid);
50
51
  if (!responseHandler) {
51
52
  ExpoHttpServerModule.respond(
52
- event.uuid,
53
+ event.requestId,
53
54
  404,
54
55
  "Not Found",
55
56
  "application/json",
@@ -60,14 +61,23 @@ export const start = () => {
60
61
  }
61
62
  const response = await responseHandler.callback(event);
62
63
  ExpoHttpServerModule.respond(
63
- event.uuid,
64
+ event.requestId,
64
65
  response.statusCode || 200,
65
66
  response.statusDescription || "OK",
66
67
  response.contentType || "application/json",
67
68
  response.headers || {},
68
69
  response.body || "{}",
69
70
  );
70
- } catch {}
71
+ } catch {
72
+ ExpoHttpServerModule.respond(
73
+ event.requestId,
74
+ 500,
75
+ "Internal Server Error",
76
+ "application/json",
77
+ {},
78
+ JSON.stringify({ error: "Handler failed" }),
79
+ );
80
+ }
71
81
  });
72
82
  ExpoHttpServerModule.start();
73
83
  };
@@ -87,9 +97,14 @@ export const route = (
87
97
  ExpoHttpServerModule.route(path, method, uuid);
88
98
  };
89
99
 
100
+ export interface SetupOptions {
101
+ loopbackOnly?: boolean;
102
+ }
103
+
90
104
  export const setup = (
91
105
  port: number,
92
106
  onStatusUpdate?: (event: StatusEvent) => void,
107
+ options: SetupOptions = {},
93
108
  ) => {
94
109
  if (onStatusUpdate) {
95
110
  emitter.addListener<StatusEvent>("onStatusUpdate", async (event) => {
@@ -98,7 +113,7 @@ export const setup = (
98
113
  } catch {}
99
114
  });
100
115
  }
101
- ExpoHttpServerModule.setup(port);
116
+ ExpoHttpServerModule.setup(port, options.loopbackOnly ?? false);
102
117
  };
103
118
 
104
119
  export const stop = () => {