Native Node HTTPS · zero runtime dependencies
Serve HTTP/2 over HTTPS.
Provide key and certificate paths through CommonJS or ESM. HTTPS defaults to HTTP/2 and accepts HTTP/1.1 on the same port. Run HTTPS-only or pair HTTP and HTTPS listeners through one isolated Server instance.
Transport modes
Choose one listener plan.
| Mode | Configuration | Listeners | Use it for |
|---|---|---|---|
| HTTP | Default configuration | server.server | CLI tasks, local assets, trusted networks, or TLS at an edge. |
| HTTPS only | Key + certificate + https.only:true | server.secureServer | Direct encrypted local or network serving. |
| HTTP + HTTPS | Key + certificate + https.only:false | server.server and server.secureServer | Paired listener deployments and controlled migration. |
Both transports use the same roots, hooks, request-body handling, ranges, validators, compression, and logging. Routing uses HTTP/1 Host or HTTP/2 :authority. Plain HTTP remains HTTP/1.1; the HTTPS listener negotiates the protocol separately for each connection.
HTTP/2 negotiation
One HTTPS port serves both protocols.
Starting with 10.0.0, configured HTTPS uses node:http2.createSecureServer() with allowHTTP1:true. TLS ALPN selects HTTP/2 when the client supports it and HTTP/1.1 for clients that need it. This happens before HTTP requests begin, with no failed HTTP/2 request or retry. HTTP/1.1 compatibility also serves application clients and reverse proxies whose upstream connections use HTTP/1.1.
https.http2 defaults to true. Set it to false alongside the certificate configuration to select the original node:https listener when an application needs its native HTTP/1 APIs. https.only controls whether the separate plain HTTP listener starts; it does not select an HTTP version. Cleartext HTTP/2 (h2c), HTTP/2-only mode, and HTTP/3 are not provided.
Hooks receive Node's HTTP/2 compatibility request/response objects on HTTP/2 requests. Use public response methods and the supplied serve continuation. HTTP/2 uses streams, so raw HTTP/1 socket writes and connection-specific headers such as Connection and Transfer-Encoding cannot be used. Read request.httpVersionMajor when a hook needs the negotiated protocol. HTTP/2 request bodies reach the existing complete body fields even without Content-Length.
server.timeout applies to HTTP/2 session inactivity and HTTP/1 socket inactivity. requestTimeout, headersTimeout, and keepAliveTimeout apply only to HTTP/1 connections, including those negotiated on HTTPS; they do not set HTTP/2 stream deadlines.
Node.js 22.12 or newer is still required. HTTP/2 adds no runtime dependency. See Node's HTTP/2 compatibility API for the native request/response contract.
HTTPS-only · ESM
Start one encrypted listener.
import {Server} from 'node-http-server';
const server=new Server({
root:'./public',
host:'127.0.0.1',
https:{
privateKey:'./certs/private/server.key',
certificate:'./certs/server.crt',
port:8443,
only:true
}
});
server.deploy((instance,listener)=>{
console.log('HTTPS ready',listener.address());
});
server.secureServer.once('error',error=>{
console.error(error);
});| Result | Value |
|---|---|
| URL | https://127.0.0.1:8443 |
| HTTP listener | server.server === null |
| HTTPS listener | server.secureServer |
address() | HTTPS address |
HTTP + HTTPS · CommonJS
Run paired listeners from one instance.
const {Server}=require('node-http-server');
const server=new Server({
root:'./public',
host:'127.0.0.1',
port:8080,
https:{
privateKey:'./certs/private/server.key',
certificate:'./certs/server.crt',
port:8443,
only:false
}
});
server.deploy((instance,listener)=>{
const protocol=listener===instance.secureServer ? 'https' : 'http';
console.log(protocol+' ready',listener.address());
});
server.server.once('error',error=>console.error(error));
server.secureServer.once('error',error=>console.error(error));TLS configuration
Pass filesystem paths to trusted material.
| Key | Default | Contract |
|---|---|---|
https.privateKey | '' | Required private-key path when HTTPS is enabled. |
https.certificate | '' | Required certificate path when HTTPS is enabled. |
https.ca | '' | Optional CA certificate path. |
https.passphrase | false | Optional private-key passphrase. |
https.port | 443 | HTTPS port; 0 requests an OS-selected free port. |
https.only | false | true selects HTTPS-only; false keeps HTTP and HTTPS. |
https.http2 | true | HTTP/2 with HTTP/1.1 negotiation; false selects the original node:https listener. |
Deployment reads the configured files and constructs the selected native Node HTTPS listener before listeners open. Missing, incomplete, unreadable, or invalid material fails startup as one atomic operation.
Listener lifecycle
Inspect and close the listeners you own.
| Member | HTTPS-only | HTTP + HTTPS |
|---|---|---|
server.server | null | Active Node HTTP server |
server.secureServer | Node Http2SecureServer by default | Node Http2SecureServer by default |
server.address() | HTTPS address | HTTP address |
server.secureServer.address() | HTTPS address | HTTPS address |
await server.close() | Closes HTTPS | Closes both listeners |
With https.http2:false, secureServer is the original Node https.Server. Listener bind failures use Node's native error event. Attach handlers to every active listener. HTTP/2 session and stream failures are captured in lastError. Repeated and concurrent close() calls share the same close operation; HTTP/2 sessions close gracefully after their active streams finish.
Certificate operations
Use material issued for the deployment.
| Deployment | Certificate source | Operator action |
|---|---|---|
| Local development | Local CA or development certificate tooling | Trust the CA deliberately and match the requested hostname. |
| Direct production listener | Let's Encrypt or another maintained public or private CA | Protect key files, monitor expiry, and redeploy after rotation. |
| Reverse proxy or hosting edge | Edge-managed certificate | Terminate TLS at the edge and run the server on the protected upstream network. |
Let's Encrypt · Linux + Certbot
Issue a certificate and keep the listener current.
Let's Encrypt issues public certificates through ACME clients such as Certbot. Certificate issuance and renewal run outside node-http-server; the module loads the resulting PEM files through its existing HTTPS settings. If your hosting provider manages TLS, use its certificate automation at that edge.
1. Obtain a certificate for your domain
This walkthrough targets a Linux host with Certbot installed using its official instructions for other web servers. For another operating system, choose a supported client from the ACME client list. Replace example.com with a domain you control; the localhost certificate examples remain separate.
Point the domain's DNS records at the challenge host, including any published IPv6 record. HTTP-01 validation requires public inbound TCP port 80, even when the application uses HTTPS on another port. Route that traffic through any firewall, NAT, or load balancer to the challenge responder.
Standalone mode lets Certbot own port 80 during validation. The port must be free; if another service owns it, arrange its stop/start through that service's operator or choose a different challenge method.
sudo certbot certonly --standalone --cert-name example.com -d example.com
sudo certbot certificatesFollow Certbot's account prompts and use the paths it reports. For the selected certificate name, map /etc/letsencrypt/live/example.com/privkey.pem to https.privateKey and /etc/letsencrypt/live/example.com/fullchain.pem to https.certificate. Certbot updates these live paths during renewal; see certificate files.
2. Load the certificate in node-http-server
const {Server}=require('node-http-server');
const server=new Server({
root:'/var/www/example/public',
host:'0.0.0.0',
domain:'example.com',
https:{
privateKey:'/etc/letsencrypt/live/example.com/privkey.pem',
certificate:'/etc/letsencrypt/live/example.com/fullchain.pem',
port:443,
only:true
}
});
server.deploy();
server.secureServer.once('error',function listenerFailed(error){
console.error(error);
process.exitCode=1;
});Replace the content root and domain, and use a bind address matching your deployment; this example binds IPv4. The application account needs read access to the certificate and private key through their directories, plus permission to bind port 443. Keep the private key outside the content root. Clients connect to https://example.com; HTTPS negotiates HTTP/2 or HTTP/1.1. only:true leaves port 80 available for Certbot.
fullchain.pem supplies the leaf certificate followed by its intermediates, as required by Node's TLS cert option. Leave https.ca unset for this setup; it is not the place to supply the server's intermediate chain. The module reads files at deploy() and does not automatically watch or reload renewed certificates.
3. Renew and restart the application
Use the renewal scheduler supplied by your Certbot installation. Keep port 80 available for each standalone renewal. An operator can rehearse ACME renewal with:
sudo certbot renew --cert-name example.com --dry-runFor an existing systemd application service, save the following as /etc/letsencrypt/renewal-hooks/deploy/restart-example-web and make it executable. Replace the lineage and example-web.service with the certificate path and service you own.
#!/bin/sh
if [ "$RENEWED_LINEAGE" = "/etc/letsencrypt/live/example.com" ]; then
systemctl restart example-web.service
fiCertbot runs deploy hooks after successful renewal. A dry run does not run them by default. The application's existing shutdown/startup lifecycle must load the new files; a restart can briefly interrupt service. Inspect the certificate presented by the application after deployment to confirm it loaded the renewed certificate.
If another listener must stop for standalone validation, use Certbot's pre/post hooks to stop and restore that service, including when renewal fails. The successful-renewal deploy hook cannot restore a service after failed validation.
When standalone does not fit
- Port 80 unavailable or wildcard certificate: use automated DNS-01 validation through your DNS provider. Its challenge TXT records can be managed independently of the application listener. Manual DNS entry needs another manual step at renewal unless the client has automation hooks.
- Existing web server on port 80: webroot validation needs
/.well-known/acme-challenge/served from its selected root. node-http-server's defaultserver.allowDotfiles:falserejects that path. An existing edge can serve the challenge; pointing Certbot at the ordinary application root alone is insufficient.
Troubleshooting
Map the first signal to the next check.
| Signal | Meaning | Next check |
|---|---|---|
ERR_HTTPS_CONFIGURATION | Key/certificate pair is incomplete. | Set both paths and confirm https.only. |
| Filesystem or PEM error during deploy | Material is unreadable or invalid. | Paths, permissions, file format, and passphrase. |
EACCES on port 443 | Process lacks bind permission. | Use an allowed port such as 8443 or grant the deployment's intended capability. |
| Browser certificate warning | Trust or hostname validation failed. | Certificate chain, SAN hostname, local CA trust, and system clock. |
| Deploy callback runs twice | Both listeners became ready. | Compare the callback listener with secureServer. |
address() shows the HTTP port | Dual mode prefers the HTTP address. | Read secureServer.address() for HTTPS. |
CLI output starts with http:// | The command owns the HTTP interface. | Use the CommonJS or ESM configuration above for HTTPS. |
| HTTPS request uses HTTP/1.1 | The client negotiated HTTP/1.1, or HTTP/2 was disabled. | Read request.httpVersionMajor, the client's HTTP/2 support, and https.http2. |
| Custom hook fails on HTTP/2 | The hook may use an HTTP/1-only API or header. | Use the native compatibility request/response APIs and HTTP/2-compatible headers, or select https.http2:false when the application requires the original listener. |