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

@cantoo/capacitor-http-server

v1.0.6

Published

Capacitor plugin exposing a local HTTP server on iOS and Android with routing delegated to JavaScript.

Readme

@cantoo-scribe/capacitor-http-server

A generic Capacitor plugin that exposes a local HTTP server on iOS and Android with routing delegated to JavaScript.

The plugin has no business logic. It only handles the transport layer: it starts a listener, hands every incoming request to JavaScript via a request event, and writes back whatever JavaScript returns through respond(). This keeps your app code — authentication, routing, payloads, CORS policy — entirely in TypeScript, where it can be tested, updated from a hot-reload bundle, and shared across platforms.

Install

npm install @cantoo-scribe/capacitor-http-server
npx cap sync

Android configuration

The plugin ships all required permissions in its manifest:

  • INTERNET, ACCESS_NETWORK_STATE, ACCESS_WIFI_STATE
  • FOREGROUND_SERVICE and FOREGROUND_SERVICE_DATA_SYNC (API 34+)
  • POST_NOTIFICATIONS (API 33+)

The server runs inside a foreground service with type dataSync, so it survives the screen being locked. On Android 13+ the system requires a visible notification, hence POST_NOTIFICATIONS. If the user declines the permission, the server still starts but the system is free to kill the process at any moment. The plugin requests the permission automatically on the first start() call.

You must supply android.notificationTitle and android.notificationText when starting the server:

await HttpServer.start({
  android: {
    notificationTitle: 'My App',
    notificationText: 'Local server is running.',
    smallIconResourceName: 'ic_notification', // res/drawable/ic_notification.png
    channelId: 'my_app_http',
    channelName: 'Local HTTP server',
  },
});

iOS configuration

Add the following keys to your app's Info.plist:

<key>NSLocalNetworkUsageDescription</key>
<string>Used to expose a local server for sharing files across devices on this network.</string>

<!-- Optional. Only needed if you broadcast your server over Bonjour. -->
<key>NSBonjourServices</key>
<array>
  <string>_myservice._tcp</string>
</array>

Without NSLocalNetworkUsageDescription, iOS 14+ silently drops every incoming connection from other devices.

Note: iOS does not allow long-running HTTP servers in background. The plugin requests a short background task on app backgrounding, then emits a server-error event with fatal: true when the window expires. Restart the server on UIApplication.didBecomeActive.

Usage

Echo server

A complete, 20-line echo server that returns exactly what it received:

import { HttpServer } from '@cantoo-scribe/capacitor-http-server';

await HttpServer.addListener('request', async (req) => {
  await HttpServer.respond({
    requestId: req.requestId,
    status: 200,
    headers: { 'content-type': req.headers['content-type'] ?? 'text/plain' },
    ...(req.bodyText !== undefined && { bodyText: req.bodyText }),
    ...(req.bodyBase64 !== undefined && { bodyBase64: req.bodyBase64 }),
    ...(req.bodyFilePath !== undefined && { bodyFilePath: req.bodyFilePath }),
  });
});

const { url } = await HttpServer.start({
  android: {
    notificationTitle: 'Echo server',
    notificationText: 'Listening for requests',
  },
});
console.log('Listening at', url);

Test it from a laptop on the same network:

curl http://<device-ip>:<port>/ -d 'hello'
# hello

Handling errors

HttpServer.addListener('server-error', ({ message, fatal }) => {
  console.warn('Server error:', message, 'fatal:', fatal);
  if (fatal) {
    // On iOS the server dies in background; restart on foreground.
  }
});

Streaming a large file out

HttpServer.addListener('request', async (req) => {
  if (req.path === '/big.bin') {
    await HttpServer.respond({
      requestId: req.requestId,
      status: 200,
      headers: { 'content-type': 'application/octet-stream' },
      bodyFilePath: '/absolute/path/to/file.bin', // streamed, no RAM spike
    });
  } else {
    await HttpServer.respond({ requestId: req.requestId, status: 404 });
  }
});

Receiving a large upload

Bodies above fileBodyThresholdBytes (default 1 MB) are streamed to a temp file managed by the plugin. JavaScript receives the absolute path via bodyFilePath and is free to move, hash, or re-stream it. The plugin deletes the temp file automatically once respond() returns.

API

start(...)

start(options?: StartOptions | undefined) => any

Start the local HTTP server. Picks a free port if port is omitted. On Android, starts a foreground service so the server survives screen lock. Throws if the server is already running with a different configuration, or if required permissions are missing.

| Param | Type | | ------------- | ----------------------------------------------------- | | options | StartOptions |

Returns: any


stop()

stop() => any

Stop the server and release all resources (port, threads, temp files, service). Safe to call when already stopped.

Returns: any


respond(...)

respond(response: HttpResponse) => any

Reply to a request previously received via the request event. The plugin looks up requestId, writes the response, and cleans up any temporary body file. Calling respond twice for the same requestId is a no-op on the second call. Unanswered requests time out (504) after 60 s.

| Param | Type | | -------------- | ----------------------------------------------------- | | response | HttpResponse |

Returns: any


addListener('request', ...)

addListener(eventName: 'request', listener: (event: HttpRequestEvent) => void) => Promise<PluginListenerHandle> & PluginListenerHandle

Subscribe to incoming HTTP requests. Every accepted request fires exactly one request event; JS is expected to call respond exactly once per event.

| Param | Type | | --------------- | --------------------------------------------------------------------------------- | | eventName | 'request' | | listener | (event: HttpRequestEvent) => void |

Returns: any


addListener('server-error', ...)

addListener(eventName: 'server-error', listener: (event: ServerErrorEvent) => void) => Promise<PluginListenerHandle> & PluginListenerHandle

Subscribe to server-level errors that are not tied to a specific request (e.g. the underlying socket died, port was stolen, permission revoked). The server is considered dead when this fires with fatal: true.

| Param | Type | | --------------- | --------------------------------------------------------------------------------- | | eventName | 'server-error' | | listener | (event: ServerErrorEvent) => void |

Returns: any


removeAllListeners()

removeAllListeners() => any

Remove every listener registered on this plugin.

Returns: any


Interfaces

StartOptions

| Prop | Type | Description | | ---------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | port | number | If omitted or 0, the OS picks a free port in the dynamic range. | | maxBodyBytes | number | Max request body size in bytes. Bodies above this return 413. Default: 50 * 1024 * 1024 (50 MB). | | fileBodyThresholdBytes | number | Threshold above which the plugin stores the body in a temp file and exposes it via bodyFilePath instead of bodyBase64. Default: 1 * 1024 * 1024 (1 MB). | | android | StartOptionsAndroid | Android only. Title / text / small icon used by the foreground service notification (mandatory on Android 13+). Ignored on iOS. |

StartOptionsAndroid

| Prop | Type | Description | | --------------------------- | ------------------- | --------------------------------------------------------------- | | notificationTitle | string | Foreground-service notification title. Required on Android 13+. | | notificationText | string | Foreground-service notification body text. | | smallIconResourceName | string | Android drawable resource name, e.g. "ic_notification". | | channelId | string | Notification channel ID. Plugin creates the channel if missing. | | channelName | string | Notification channel display name. |

StartResult

| Prop | Type | Description | | ------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | port | number | Chosen port (same as options.port when provided). | | url | string | Full URL, e.g. "http://192.168.1.42:49281". Empty string when no LAN-reachable interface was found (e.g. cellular only). The server is still listening on the port, but cannot be reached by a peer LAN device. A server-error event with fatal: false is emitted in that case so consumers can warn the user. | | localIp | string | Primary LAN IPv4. Empty string when no LAN-reachable interface was found. See url. |

HttpResponse

| Prop | Type | Description | | ------------- | --------------------------------------------------- | ------------------------------------------------- | | data | any | Additional data received with the Http response. | | status | number | The status code received from the Http response. | | headers | HttpHeaders | The headers received from the Http response. | | url | string | The response URL received from the Http response. |

HttpHeaders

HttpRequestEvent

| Prop | Type | Description | | ------------------ | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | requestId | string | Opaque ID. Pass back unchanged in respond. | | method | HttpMethod | | | path | string | Decoded pathname, always starts with "/". No querystring. | | query | Record<string, string> | Parsed querystring. Repeated keys keep the last value. | | headers | Record<string, string> | Lower-cased header names. | | clientIp | string | Remote peer IP if available (useful for logging only). | | bodyText | string | Exactly one of the three body fields is set (undefined when the request has no body). The plugin picks the representation based on content type and size: - UTF-8 body with a text-like content type (text/*, application/json, application/x-www-form-urlencoded) below threshold -> bodyText - Any other body below threshold -> bodyBase64 - Body above threshold -> bodyFilePath Temp files referenced by bodyFilePath are deleted automatically once respond is called for the same requestId (or on timeout). | | bodyBase64 | string | | | bodyFilePath | string | |

PluginListenerHandle

| Prop | Type | | ------------ | ------------------------- | | remove | () => any |

ServerErrorEvent

| Prop | Type | Description | | ------------- | -------------------- | ------------------------------------------------------------------ | | message | string | | | fatal | boolean | True when the server is no longer listening and must be restarted. |

Type Aliases

HttpMethod

HTTP methods that the plugin forwards to JavaScript. Any value outside this list produced by a client is still forwarded as-is (upper-cased) but typed as HttpMethod for convenience of the common cases.

'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS' | 'HEAD'

Limitations

  • No HTTPS. The plugin listens on plain HTTP. Use it over a trusted LAN or add a reverse proxy if you need TLS.
  • No chunked transfer encoding on the request side. Clients must send Content-Length; otherwise the server replies with 501.
  • No WebSocket support (tracked separately; out of scope here).
  • iOS background. The server is suspended when the app leaves the foreground. A server-error event is emitted with fatal: true.

License

MIT © Cantoo Scribe