npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cappitolian/http-local-server-swifter

v1.0.0

Published

Runs a local HTTP server on your device, accessible over LAN. Supports connect, disconnect, GET, and POST methods with IP and port discovery.

Readme

@cappitolian/http-local-server-swifter

npm version License: MIT Capacitor 7

A Capacitor plugin that embeds a real HTTP server directly on your device — powered by NanoHTTPD on Android and Swifter on iOS. It allows you to receive and respond to HTTP requests from the JavaScript layer, enabling local peer-to-peer communication between devices on the same network without a cloud backend.


Table of Contents


Installation

npm install @cappitolian/http-local-server-swifter
npx cap sync

Platform Configuration

Android

Add the following permissions to your AndroidManifest.xml:

<!-- Required: bind socket and receive connections -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- Required: resolve local IP address via WifiManager -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

Note: CHANGE_WIFI_MULTICAST_STATE is not needed by this plugin. It belongs to a Network Discovery plugin (mDNS/NSD).

To allow cleartext HTTP traffic on the local network, add or update your network_security_config.xml:

<!-- res/xml/network_security_config.xml -->
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
  <base-config cleartextTrafficPermitted="true">
    <trust-anchors>
      <certificates src="system" />
    </trust-anchors>
  </base-config>

  <domain-config cleartextTrafficPermitted="true">
    <domain includeSubdomains="false">localhost</domain>
    <domain includeSubdomains="false">127.0.0.1</domain>
  </domain-config>
</network-security-config>

Then reference it and enable cleartext in AndroidManifest.xml:

<application
    android:networkSecurityConfig="@xml/network_security_config"
    android:usesCleartextTraffic="true"
    ...>

iOS

Add the following to your Info.plist to allow your app to serve and receive HTTP traffic on the local network:

<!-- Required: allow cleartext HTTP from/to the local server -->
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsLocalNetworking</key>
    <true/>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>

Note: NSLocalNetworkUsageDescription and NSBonjourServices are not required by this plugin. Those entries belong to a Network Discovery plugin (mDNS/Bonjour). Do not add them here unless you are also using that plugin.


Usage

Import

import { HttpLocalServerSwifter } from '@cappitolian/http-local-server-swifter';

Start the server and listen for requests

// 1. Register the request listener BEFORE connecting
await HttpLocalServerSwifter.addListener('onRequest', async (data) => {
  console.log(`${data.method} ${data.path}`);
  console.log('Headers:', data.headers);
  console.log('Body:', data.body);
  console.log('Query:', data.query);

  // 2. Always send a response — the server blocks the native thread until you do
  await HttpLocalServerSwifter.sendResponse({
    requestId: data.requestId,
    status: 200,
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ success: true, message: 'Hello from the device!' })
  });
});

// 3. Start the server
const { ip, port } = await HttpLocalServerSwifter.connect();
console.log(`Server running at http://${ip}:${port}`);

⚠️ Always call sendResponse for every request. On Android, the native thread is blocked via CompletableFuture.get() until the response arrives or the timeout elapses. On iOS, a DispatchSemaphore is held open. Failing to respond will cause the request to time out with 408 Request Timeout (iOS) or a JSON timeout error (Android) after the configured timeout (default: 10s on iOS, 5s on Android).

Stop the server

await HttpLocalServerSwifter.disconnect();

Security

This plugin runs an HTTP server on the local network. The following mechanisms are built into the native layer.

Rate Limiting (Native — Android & iOS)

IP-based rate limiting is enforced natively before requests reach TypeScript, protecting the server against denial-of-service (DoS) attacks.

| Platform | Limit | Window | Client IP source | |---|---|---|---| | Android | 30 requests | 60 seconds | http-client-ipremote-addr header | | iOS | 30 requests | 60 seconds | x-forwarded-forrequest.address |

Requests exceeding the limit receive a 429 Too Many Requests response automatically:

{ "success": false, "error": "Too many requests" }

API Key Pairing (Application layer)

To prevent unauthorized clients from accessing your endpoints, implement an API Key pairing flow:

  1. The server generates a cryptographically random key on first launch using crypto.getRandomValues and persists it with @capacitor/preferences.
  2. The client calls GET /pair (the only unauthenticated endpoint) immediately after discovering the server IP.
  3. The server returns the key. The client stores it locally.
  4. All subsequent requests must include the x-api-key header. The server validates it before processing any route.
Server generates key → Client calls /pair → Client stores key
        ↓
All requests: x-api-key: <key> → Server validates → 401 if invalid

⚠️ Since this plugin operates over HTTP (cleartext), the API key is transmitted in plaintext. For sensitive environments, consider implementing HMAC request signing with short-lived timestamps to prevent replay attacks. The x-signature and x-timestamp headers are already allowed by the built-in CORS configuration on both platforms.

CORS

CORS headers are set natively on every response. The following headers are allowed by default on both platforms:

Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Origin, Content-Type, Accept, Authorization,
                              X-Requested-With, x-api-key, x-signature, x-timestamp

Custom headers returned via sendResponse are merged on top of these defaults and can override them.


Architecture

Request / Response Bridge

The plugin uses a request ID bridge pattern to cross the native ↔ JavaScript boundary asynchronously:

Incoming HTTP request
        ↓
Native layer generates requestId → fires onRequest event to JS
        ↓
JS handler processes logic → calls sendResponse({ requestId, ... })
        ↓
Native layer resolves the pending future/semaphore → returns HTTP response

| Platform | Blocking mechanism | Default timeout | |---|---|---| | Android | CompletableFuture.get(timeout, SECONDS) | 5 seconds | | iOS | DispatchSemaphore.wait(timeout:) | 10 seconds |

Pending responses are stored in a thread-safe map (ConcurrentHashMap on Android, DispatchQueue-protected Dictionary on iOS) and cleaned up on timeout or disconnect.

Connection Behavior

  • Android (NanoHTTPD): Each request is handled in its own thread. The server forces Connection: close on every response to prevent keep-alive issues under rapid sequential requests (ERR_INVALID_HTTP_RESPONSE).
  • iOS (Swifter): Requests are processed via a middleware chain on a global background queue. notifyListeners (Capacitor event bridge) is always dispatched to the main thread.
  • IP resolution: Both platforms resolve the local WiFi IP via WifiManager (Android) / getifaddrs en0 (iOS), falling back to 127.0.0.1 if unavailable.

API Reference

Methods

connect() => Promise<HttpConnectResult>

Starts the HTTP server and begins listening for incoming requests on port 8080.

Returns: Promise<HttpConnectResult>


disconnect() => Promise<void>

Stops the HTTP server, drains all pending response futures/semaphores, and releases all resources.


sendResponse(options: HttpSendResponseOptions) => Promise<void>

Sends an HTTP response back to the client for a given request. Must be called for every onRequest event received.

| Param | Type | Description | |---|---|---| | options | HttpSendResponseOptions | Response options including requestId, status, headers, and body |


addListener('onRequest', handler) => Promise<PluginListenerHandle>

Registers a listener for incoming HTTP requests.

| Param | Type | Description | |---|---|---| | eventName | 'onRequest' | Event name | | handler | (data: HttpRequestData) => void | Callback receiving the request data |


removeAllListeners() => Promise<void>

Removes all registered listeners.


Interfaces

HttpConnectResult

| Property | Type | Description | |---|---|---| | ip | string | Local WiFi IP address where the server is bound (127.0.0.1 fallback) | | port | number | Port the server is listening on (fixed: 8080) |


HttpRequestData

| Property | Type | Description | |---|---|---| | requestId | string | Unique UUID used to correlate the response — required in sendResponse | | method | string | HTTP method (GET, POST, PUT, PATCH, DELETE, OPTIONS) | | path | string | Request path (e.g. /menu, /orders/123) | | headers | Record<string, string> | Request headers | | query | Record<string, string> | Query string parameters | | body | string \| null | Raw request body (present for POST, PUT, PATCH) |


HttpSendResponseOptions

| Property | Type | Required | Description | |---|---|---|---| | requestId | string | ✅ | ID of the request to respond to | | status | number | ❌ | HTTP status code (default: 200) | | headers | Record<string, string> | ❌ | Custom response headers (merged with CORS defaults) | | body | string | ❌ | Response body string |


Platform Support

| Feature | Android | iOS | Web | |---|---|---|---| | Start HTTP server | ✅ | ✅ | ✅ (mock) | | Receive requests | ✅ | ✅ | ✅ (mock) | | Send responses | ✅ | ✅ | ✅ (mock) | | Custom status codes | ✅ | ✅ | ✅ (mock) | | Custom response headers | ✅ | ✅ | ✅ (mock) | | Request headers forwarding | ✅ | ✅ | ✅ (mock) | | IP-based rate limiting | ✅ | ✅ | ❌ | | CORS preflight handling | ✅ | ✅ | ❌ | | Connection: close enforcement | ✅ | ❌ | ❌ | | Request body parsing (POST/PUT/PATCH) | ✅ | ✅ | ✅ (mock) |


Troubleshooting

Requests timing out on the client side

The server resolves the native thread synchronously. If your JS handler throws before calling sendResponse, the request will hang until the native timeout fires. Always wrap your handler in try/catch and call sendResponse in both branches.

ERR_INVALID_HTTP_RESPONSE on Android

NanoHTTPD does not handle keep-alive correctly under rapid sequential requests. The plugin forces Connection: close on every response. If you are still seeing this error, ensure you are on the latest version.

Server returns 127.0.0.1 instead of the LAN IP

This happens when WiFi is disconnected or the WifiManager / en0 interface returns no address. Verify the device is connected to WiFi before calling connect().

iOS server not reachable from Android

Ensure:

  1. Both devices are on the same WiFi network
  2. NSAllowsLocalNetworking and NSAllowsArbitraryLoads are set in Info.plist
  3. The client is using the IP returned by connect(), not localhost

Contributing

Clone the repository and install dependencies:

git clone https://github.com/cappitolian/http-local-server-swifter
cd http-local-server-swifter
npm install

Run the example project:

cd example
npm install
npx cap sync
# Open in Android Studio or Xcode

⚠️ Changes to native Swift/Java code require recompilation from the IDE. npx cap sync only syncs web assets.


Changelog

See CHANGELOG.md for the full list of changes.


License

MIT — see LICENSE for details.