├── .editorconfig ├── .gitattributes ├── .github ├── security.md └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── browser.js ├── index.d.ts ├── index.js ├── index.test-d.ts ├── license ├── package.json ├── readme.md ├── test-browser.js └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/security.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. 4 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node.js ${{ matrix.node-version }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: 13 | - 22 14 | - 20 15 | - 18 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - run: npm install 22 | - run: npm test 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /browser.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import {publicIpv4, publicIpv6} from 'public-ip'; 3 | 4 | export default async function isOnline(options) { 5 | options = { 6 | timeout: 5000, 7 | ipVersion: 4, 8 | ...options, 9 | }; 10 | 11 | // eslint-disable-next-line n/no-unsupported-features/node-builtins 12 | if (!navigator?.onLine) { 13 | return false; 14 | } 15 | 16 | const publicIpFunction = options.ipVersion === 4 ? publicIpv4 : publicIpv6; 17 | 18 | try { 19 | await publicIpFunction(options); 20 | return true; 21 | } catch { 22 | return false; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export type Options = { 2 | /** 3 | Milliseconds to wait for a server to respond. 4 | 5 | @default 5000 6 | */ 7 | readonly timeout?: number; 8 | 9 | /** 10 | [Internet Protocol version](https://en.wikipedia.org/wiki/Internet_Protocol#Version_history) to use. 11 | 12 | This is an advanced option that is usually not necessary to be set, but it can prove useful to specifically assert IPv6 connectivity. 13 | 14 | @default 4 15 | */ 16 | readonly ipVersion?: 4 | 6; 17 | }; 18 | 19 | /** 20 | Check if the internet connection is up. 21 | 22 | The following checks are run in parallel: 23 | - Retrieve [icanhazip.com](https://github.com/major/icanhaz) via HTTPS 24 | - Query `myip.opendns.com` on OpenDNS (Node.js only) 25 | - Retrieve Apple's Captive Portal test page (Node.js only) 26 | 27 | When any check succeeds, the returned Promise is resolved to `true`. 28 | 29 | @example 30 | ``` 31 | import isOnline from 'is-online'; 32 | 33 | console.log(await isOnline()); 34 | //=> true 35 | ``` 36 | */ 37 | export default function isOnline(options?: Options): Promise; 38 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import os from 'node:os'; 2 | import got, {CancelError} from 'got'; 3 | import {publicIpv4, publicIpv6} from 'public-ip'; 4 | import pAny from 'p-any'; 5 | import pTimeout from 'p-timeout'; 6 | 7 | const appleCheck = options => { 8 | const gotPromise = got('https://captive.apple.com/hotspot-detect.html', { 9 | timeout: { 10 | request: options.timeout, 11 | }, 12 | dnsLookupIpVersion: options.ipVersion, 13 | headers: { 14 | 'user-agent': 'CaptiveNetworkSupport/1.0 wispr', 15 | }, 16 | }); 17 | 18 | const promise = (async () => { 19 | try { 20 | const {body} = await gotPromise; 21 | if (!body?.includes('Success')) { 22 | throw new Error('Apple check failed'); 23 | } 24 | } catch (error) { 25 | if (!(error instanceof CancelError)) { 26 | throw error; 27 | } 28 | } 29 | })(); 30 | 31 | promise.cancel = gotPromise.cancel; 32 | 33 | return promise; 34 | }; 35 | 36 | // Note: It cannot be `async`` as then it looses the `.cancel()` method. 37 | export default function isOnline(options) { 38 | options = { 39 | timeout: 5000, 40 | ipVersion: 4, 41 | ...options, 42 | }; 43 | 44 | if (Object.values(os.networkInterfaces()).flat().every(({internal}) => internal)) { 45 | return false; 46 | } 47 | 48 | if (![4, 6].includes(options.ipVersion)) { 49 | throw new TypeError('`ipVersion` must be 4 or 6'); 50 | } 51 | 52 | const publicIpFunction = options.ipVersion === 4 ? publicIpv4 : publicIpv6; 53 | const queries = []; 54 | 55 | const promise = pAny([ 56 | (async () => { 57 | const query = publicIpFunction(options); 58 | queries.push(query); 59 | await query; 60 | return true; 61 | })(), 62 | (async () => { 63 | const query = publicIpFunction({...options, onlyHttps: true}); 64 | queries.push(query); 65 | await query; 66 | return true; 67 | })(), 68 | (async () => { 69 | const query = appleCheck(options); 70 | queries.push(query); 71 | await query; 72 | return true; 73 | })(), 74 | ]); 75 | 76 | return pTimeout(promise, {milliseconds: options.timeout}).catch(() => { // eslint-disable-line promise/prefer-await-to-then 77 | for (const query of queries) { 78 | query.cancel(); 79 | } 80 | 81 | return false; 82 | }); 83 | 84 | // TODO: Use this instead when supporting AbortController. 85 | // try { 86 | // return await pTimeout(promise, options.timeout); 87 | // } catch { 88 | // for (const query of queries) { 89 | // query.cancel(); 90 | // } 91 | 92 | // return false; 93 | // } 94 | } 95 | -------------------------------------------------------------------------------- /index.test-d.ts: -------------------------------------------------------------------------------- 1 | import {expectType} from 'tsd'; 2 | import isOnline from './index.js'; 3 | 4 | expectType>(isOnline()); 5 | expectType>(isOnline({timeout: 10})); 6 | expectType>(isOnline({ipVersion: 4})); 7 | expectType>(isOnline({ipVersion: 6})); 8 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Sindre Sorhus (https://sindresorhus.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "is-online", 3 | "version": "11.0.0", 4 | "description": "Check if the internet connection is up", 5 | "license": "MIT", 6 | "repository": "sindresorhus/is-online", 7 | "funding": "https://github.com/sponsors/sindresorhus", 8 | "author": { 9 | "name": "Sindre Sorhus", 10 | "email": "sindresorhus@gmail.com", 11 | "url": "https://sindresorhus.com" 12 | }, 13 | "contributors": [ 14 | "silverwind (github.com/silverwind)" 15 | ], 16 | "type": "module", 17 | "exports": { 18 | "types": "./index.d.ts", 19 | "node": "./index.js", 20 | "default": "./browser.js" 21 | }, 22 | "sideEffects": false, 23 | "engines": { 24 | "node": ">=18" 25 | }, 26 | "scripts": { 27 | "test": "xo && ava && tsd" 28 | }, 29 | "files": [ 30 | "index.js", 31 | "browser.js", 32 | "index.d.ts" 33 | ], 34 | "keywords": [ 35 | "browser", 36 | "online", 37 | "offline", 38 | "is-online", 39 | "network", 40 | "connected", 41 | "connectivity", 42 | "internet", 43 | "is", 44 | "has", 45 | "detect", 46 | "hostname", 47 | "hostnames", 48 | "dns", 49 | "socket", 50 | "reachable", 51 | "reachability", 52 | "accessible", 53 | "no", 54 | "disconnected" 55 | ], 56 | "dependencies": { 57 | "got": "^13.0.0", 58 | "p-any": "^4.0.0", 59 | "p-timeout": "^6.1.2", 60 | "public-ip": "^7.0.1" 61 | }, 62 | "devDependencies": { 63 | "ava": "^6.1.3", 64 | "tsd": "^0.31.1", 65 | "xo": "^0.59.2" 66 | }, 67 | "ava": { 68 | "files": [ 69 | "test.js" 70 | ] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # is-online 2 | 3 | > Check if the internet connection is up 4 | 5 | Works in Node.js and the browser *(with a bundler)*. 6 | 7 | In the browser, there is already [`navigator.onLine`](https://developer.mozilla.org/en-US/docs/Web/API/NavigatorOnLine.onLine), but it's useless as it only tells you if there's a local connection, and not whether the internet is accessible. 8 | 9 | ## Install 10 | 11 | ```sh 12 | npm install is-online 13 | ``` 14 | 15 | ## Usage 16 | 17 | ```js 18 | import isOnline from 'is-online'; 19 | 20 | console.log(await isOnline()); 21 | //=> true 22 | ``` 23 | 24 | ## API 25 | 26 | ### isOnline(options?) 27 | 28 | #### options 29 | 30 | Type: `object` 31 | 32 | ##### timeout 33 | 34 | Type: `number`\ 35 | Default: `5000` 36 | 37 | Milliseconds to wait for a server to respond. 38 | 39 | ##### ipVersion 40 | 41 | Type: `number`\ 42 | Values: `4 | 6`\ 43 | Default: `4` 44 | 45 | The [Internet Protocol version](https://en.wikipedia.org/wiki/Internet_Protocol#Version_history) to use. 46 | 47 | This is an advanced option that is usually not necessary to be set, but it can prove useful to specifically assert IPv6 connectivity. 48 | 49 | ## How it works 50 | 51 | The following checks are run in parallel: 52 | 53 | - Retrieve [icanhazip.com](https://github.com/major/icanhaz) (or [ipify.org](https://www.ipify.org) as fallback) via HTTPS. 54 | - Query `myip.opendns.com` and `o-o.myaddr.l.google.com` DNS entries. *(Node.js only)* 55 | - Retrieve Apple's Captive Portal test page (this is what iOS does). *(Node.js only)* 56 | 57 | When any check succeeds, the returned Promise is resolved to `true`. 58 | 59 | ## Proxy support 60 | 61 | To make it work through proxies, you need to set up [`global-agent`](https://github.com/gajus/global-agent). 62 | 63 | ## Maintainers 64 | 65 | - [Sindre Sorhus](https://github.com/sindresorhus) 66 | - [silverwind](https://github.com/silverwind) 67 | 68 | ## Related 69 | 70 | - [is-online-cli](https://github.com/sindresorhus/is-online-cli) - CLI for this module 71 | - [is-reachable](https://github.com/sindresorhus/is-reachable) - Check if servers are reachable 72 | -------------------------------------------------------------------------------- /test-browser.js: -------------------------------------------------------------------------------- 1 | // Need to test manually in DevTools 2 | // $ npx browserify test-browser.js | pbcopy 3 | import isOnline from './browser.js'; 4 | 5 | console.log('is online:', await isOnline()); 6 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import test from 'ava'; 3 | import isOnline from './index.js'; 4 | 5 | test('v4', async t => { 6 | t.true(await isOnline()); 7 | }); 8 | 9 | test('v4 with timeout', async t => { 10 | t.true(await isOnline({timeout: 500})); 11 | }); 12 | 13 | test('v4 with impossible timeout', async t => { 14 | t.false(await isOnline({timeout: 1})); 15 | }); 16 | 17 | if (!process.env.CI) { 18 | test('v6', async t => { 19 | t.true(await isOnline({ipVersion: 6})); 20 | }); 21 | 22 | test('v6 with timeout', async t => { 23 | t.true(await isOnline({ipVersion: 6, timeout: 500})); 24 | }); 25 | } 26 | --------------------------------------------------------------------------------