H2O.CONF(5)

πŸ“¦
version 2.3.0-rolling-2026-10 Β· 2 versions
, in nixpkgs unstable

πŸ’– NAME πŸ’–

h2o.conf β€” the configuration file for H2O, the optimized HTTP/1.x, HTTP/2 server. Yes bestie, this is h2o.conf(5) from the File Formats Manual, and we're gonna get you so comfy with it. 🫢

✨ SYNOPSIS ✨

/etc/h2o/h2o.conf

🌸 DESCRIPTION 🌸

okay so, h2o.conf h2o.conf is a YAML configuration file. (the double mention is literally what the page says, don't @ me πŸ’€)

πŸš€ QUICK START πŸš€

so to run the H2O standalone HTTP server, you need to write a configuration file. and the minimal one looks like this, babes:

hosts:
  "myhost.example.com":
    listen:
      port: 80
    listen:
      port: 443
      ssl: {}
    listen:
      port: 443
      ssl: {}
      type: quic
    paths:
      /:
        file.dir: /path/to/the/public-files

user: nobody
access-log: /path/to/the/access-log
error-log: /path/to/the/error-log
pid-file: /path/to/the/pid-file

acme:
  email: admin@myhost.example.com
  accept-tos: YES

and what this little beauty is telling the server to do β€” icymi:

  • serve contents under the hostname: myhost.example.com
  • for HTTP, listen on TCP port 80
  • for HTTPS, listen on TCP port 443 and UDP port 443
  • serve files under /path/to/the/public-files
  • run under the privileges of nobody
  • emit access logs to file: /path/to/the/access-log
  • emit error logs to /path/to/the/error-log
  • store the process id of the server in /path/to/the/pid-file
  • use ACME to fetch the TLS certificates automatically (note: lego must be installed for this to work).

then you just hit the server with this command and it goes:

% sudo h2o -m daemon -c /path/to/the/configuration-file

the command instructs the server to read the configuration file, and start in daemon mode, which dispatches a pair of master and worker processes that serves the HTTP requests. πŸ’…

and to stop the server? send SIGTERM to it. that's it, that's the vibe:

% sudo kill -TERM `cat /path/to/the/pid-file`

🌟 Next Step 🌟

now that you know how to start and stop the server, the next step is to learn the configuration directives and their structure, or see the configuration examples. go off, you got this. ⭐


πŸ¦‹ SYNTAX AND STRUCTURE πŸ¦‹

Syntax

H2O uses YAML 1.1 as the syntax of its configuration file. periodt.

Levels of Configuration

okay this part is lowkey important, so pay attention bestie πŸ‘€ β€” when you're using the configuration directives of H2O, it's important to understand that there are four configuration levels: global, host, path, extension.

  • Global-level configurations affect the entire server.
  • Host-level configurations affect the configuration for the specific hostname (i.e. corresponds to the <VirtualHost> directive of the Apache HTTP Server).
  • Path-level configurations only affect the behavior of resources specific to the path.

and then there's extension-level configuration, which affects how files with certain extensions are being served. for example, it is possible to map files with .php extension to the FastCGI handler running the php-cgi command. iconic, honestly.

consider the following example:

hosts:
  "example.com":
    listen:
      port: 443
      ssl:
        certificate-file: etc/site1.crt
        key-file: etc/site1.key
    paths:
      "/":
        file.dir: htdocs/site1
      "/icons":
        file.dir: icons
        expires: 1 day
  "example.com:80":
    listen:
      port: 80
    paths:
      "/":
        redirect: "https://example.com/"

so in the example, two host-level configurations exist (under the hosts mapping), each of them listening to different ports. the first host listens to port 443 using TLS (i.e. HTTPS) using the specified server certificate and key. it has two path-level configurations, one for / and the other for /icons, each of them pointing to different local directories containing the files to be served. the latter also has the expires directive set, so that Cache-Control: max-age=86400 [1] header would be sent. the second host accepts connections on port 80 (via the plain-text HTTP protocol), and redirects all the requests to the first host using HTTPS. slay.

certain configuration directives can be used in more than one levels. for example, the listen can be used either at the global level or at the host level. expires can be used at all levels. on the other hand file.dir can only be used at the path level. (know the difference, iykyk 🧚)

Path-level configuration

values of the path-level configuration define the action(s) to be taken when the server processes a request that prefix-matches to the configured paths. each entry of the mapping associated to the paths is evaluated in the order they appear. πŸ”₯

consider the following example. when receiving a request for https://example.com/foo, the file handler is first executed trying to serve a file named /path/to/doc-root/foo as the response. in case the file does not exist, then the FastCGI handler is invoked.

hosts:
  "example.com":
    listen:
      port: 443
      ssl:
        certificate-file: etc/site1.crt
        key-file: etc/site1.key
    paths:
      "/":
        file.dir: /path/to/doc-root
        fastcgi.connect:
          port: /path/to/fcgi.sock
          type: unix

starting from version 2.1, it is also possible to define the path-level configuration as a sequence of mappings instead of a single mapping. the following example is identical to the previous one β€” notice the dashes placed before the handler directives:

hosts:
  "example.com":
    listen:
      port: 443
      ssl:
        certificate-file: etc/site1.crt
        key-file: etc/site1.key
    paths:
      "/":
        - file.dir: /path/to/doc-root
        - fastcgi.connect:
            port: /path/to/fcgi.sock
            type: unix

Using YAML Alias

H2O resolves YAML aliases before processing the configuration file. therefore, it is possible to use an alias to reduce the redundancy of the configuration file. for example, the following configuration reuses the first paths element (that is given an anchor named default_paths) in the following definitions:

hosts:
  "example.com":
    listen:
      port: 443
      ssl:
        certificate-file: /path/to/example.com.crt
        key-file:         /path/to/example.com.crt
    paths: &default_paths
      "/":
        file.dir: /path/to/doc-root
  "example.org":
    listen:
      port: 443
      ssl:
        certificate-file: /path/to/example.org.crt
        key-file:         /path/to/example.org.crt
    paths: *default_paths

DRY queen behavior, we love to see it πŸ’œ

Using YAML Merge

since version 2.0, H2O recognizes Merge Key Language-Independent Type for YAMLβ„’ Version 1.1. users can use the feature to merge an existing mapping against another. the following example reuses the TLS configuration of example.com in example.org:

hosts:
  "example.com":
    listen:
      port: 443
      ssl: &default_ssl
        minimum-version: TLSv1.2
        cipher-suite: ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256
        certificate-file: /path/to/example.com.crt
        key-file:         /path/to/example.com.crt
    paths:
      ...
  "example.org":
    listen:
      port: 443
      ssl:
        <<: *default_ssl
        certificate-file: /path/to/example.org.crt
        key-file:         /path/to/example.org.crt
    paths:
      ...

Including Files

starting from version 2.1, it is possible to include a YAML file from the configuration file using !file custom YAML tag. the following example extracts the TLS configuration into default_ssl.conf and includes it multiple times in h2o.conf.

Example: default_ssl.conf

minimum-version: TLSv1.2
cipher-suite: ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256
certificate-file: /path/to/example.com.crt
key-file:         /path/to/example.com.crt

Example: h2o.conf

hosts:
  "example.com":
    listen:
      port: 443
      ssl: !file default_ssl.conf
    paths:
      ...
  "example.org":
    listen:
      port: 443
      ssl:
        <

starting from version 2.3, it is possible to refer to an environment variable (intepreted as a scalar) from the configuration file by using !env custom YAML tag.

Example: h2o.conf

hosts:
  "example.com":
    listen:
      port: !env H2O_PORT
    paths:
      ...

Notes

[1] 1 day is equivalent to 86400 seconds. mhm, math. πŸ“


πŸ‘‘ BASE DIRECTIVES πŸ‘‘

this document describes the configuration directives common to all the protocols and handlers. let's get into it.

hosts

maps host:port to the mappings of per-host configs.

the directive specifies the mapping between the authorities (the host or host:port section of an URL) and their configurations. the directive is mandatory, and must at least contain one entry. (no entry = no vibes = no server πŸ™ˆ)

when port is omitted, the entry will match the requests targetting the default ports (i.e. port 80 for HTTP, port 443 for HTTPS) with given hostname. otherwise, the entry will match the requests targetting the specified port.

since version 1.7, a wildcard character * can be used as the first component of the hostname. if used, they are matched using the rule defined in RFC 2818 Section 3.1. for example, *.example.com will match HTTP requests for both foo.example.com and bar.example.com.

for each HTTP request to be processed, the matching host entry is determined by the steps below:

Host matching, bestie, goes in three passes, and it's literally just vibes-checking in order:

  1. Look through the host elements that don't use wildcards β€” find the first one that matches the host and port being specified by the URI.
  2. If step one gave you nothing, go find a matching element among the entries that do use wildcards.
  3. If still nothing, just use the first host element that doesn't have a strict-match flag.

And the special case πŸ‘€ β€” when the hostname of the HTTP request is unknown (i.e. you're processing an HTTP/1.0 request with no host header field), only that last step gets used. Periodt.

Example: A host redirecting all HTTP requests to HTTPS

hosts:
  "www.example.com:80":
    listen:
      port: 80
    paths:
      "/":
        redirect: https://www.example.com/
  "www.example.com:443":
    listen:
      port: 443
      ssl:
        key-file: /path/to/ssl-key-file
        certificate-file: /path/to/ssl-certificate-file
    paths:
      "/":
        file.dir: /path/to/doc-root

paths 🌸

Okay so paths is the mapping of paths β†’ their configurations. Iconic little lookup table.

The mapping gets searched using prefix-match. When more than one path matches, the entry with the longest path wins (longest-match slay). And if nothing matches at all? You get a 404 Not Found error. Sorry bestie, no path, no entry. 🚫

Example: Configuration with two paths

hosts:
  "www.example.com":
    listen:
      port: 80
    paths:
      "/":
        file.dir: /path/to/doc-root
      "/assets":
        file.dir: /path/to/assets

Now, the lore πŸ“œ: in releases prior to version 2.0, all the path entries are considered as directories. So when H2O receives a request that exactly matches an entry in paths that does not end with a slash, the server always returns a 301 redirect that appends a slash.

Since 2.0, it depends on the handler of the path whether a 301 redirect appending a slash is returned or not. Server admins can take advantage of this change to define per-path configurations (peep the examples in file.file and the FastCGI handler). One exception that keeps doing the old thing: file.dir β€” it continues to perform the redirection. So in the example above, access to /assets gets redirected to /assets/. πŸŽ€

listen πŸ‘‘

Specifies the port at which the server should listen to. But wait, there's more β€” besides the port number, you can also designate the bind address, or the SSL and HTTP/3 (QUIC) configuration. Versatile queen. πŸ’…

Example: Various ways of using the Listen Directive

# accept HTTP on port 80 on default address (both IPv4 and IPv6)
listen: 80

# accept HTTP on 127.0.0.1:8080
listen:
  host: 127.0.0.1
  port: 8080

# accept HTTPS on port 443
listen:
  port: 443
  ssl:
    key-file: /path/to/key-file
    certificate-file: /path/to/certificate-file

# accept HTTPS on port 443 (using PROXY protocol)
listen:
  port: 443
  ssl:
    key-file: /path/to/key-file
    certificate-file: /path/to/certificate-file
  proxy-protocol: ON

To configure HTTP/3 (QUIC), go look at HTTP/3. πŸ¦‹

Configuration Levels

This directive is flexible like that β€” you can use it at global-level or at host-level. Rule of thumb: at least one listen directive must exist at the global level, or every host-level configuration must have at least one listen directive. One or the other, no cap.

Incoming connections accepted by global-level listeners get dispatched to one of the host-level contexts with the corresponding host:port, or to the first host-level context if none of the contexts were given a host:port corresponding to the request.

Host-level listeners specify bind addresses specific to the host-level context. But here's the fun part πŸŽ‰ β€” it is permitted to specify the same bind address for more than one host-level context, in which case hostname-based lookup will be performed between the host contexts that share the address. This feature is literally useful for setting up a HTTPS virtual host using Server-Name Indication (RFC 6066).

Example: Using host-level listeners for HTTPS virtual-hosting

hosts:
  "www.example.com:443":
    listen:
      port: 443
      ssl:
        key-file: /path/to/www_example_com.key
        certificate-file: /path/to/www_example_com.crt
    paths:
      "/":
        file.dir: /path/to/doc-root_of_www_example_com
  "www.example.jp:443":
    listen:
      port: 443
      ssl:
        key-file: /path/to/www_example_jp.key
        certificate-file: /path/to/www_example_jp.crt
    paths:
      "/":
        file.dir: /path/to/doc-root_of_www_example_jp

SSL Attribute πŸ”

The ssl attribute must be defined as a mapping. Non-negotiable.

The identity of the TLS listener is specified in one of these ways, bestie:

  • by specifying the certificate-file and key-file attributes directly under ssl,
  • by specifying a sequence of certificate- and key-file pairs inside identity,
  • or, if neither of the above is used, the identity is obtained using ACME; see acme.

The certificate-file attribute specifies the path of a PEM file containing either an X.509 certificate chain or a raw public key. When that latter form is being used, RFC 7250 handshake will be used. Neat.

The key-file attribute specifies the path of a PEM file that contains the corresponding private key.

The identity attribute can be used in place of certificate-file and key-file to specify more than one pair of certificates and keys. When a TLS handshake is performed, h2o uses the first pair that contains a compatible certificate / key. The last pair acts as the fallback. Teamwork, honestly. 🫢

Example: Using both RSA and ECDSA certificates

ssl:
  identity:
  - certificate-file: /path/to/ecdsa.crt   # prefer ECDSA if client supports both
    key-file: /path/to/ecdsa.key
  - certificate-file: /path/to/rsa.crt.    # use RSA if ECDSA is not supported
    key-file: /path/to/rsa.key

In addition, the ssl mapping recognizes the following attributes. Buckle up, there's a lot of them πŸ’–

  • client-ca-file β€” path to a file of Certificate Authority (CA) certificates used to verify client certificates. If set, H2O will request a client certificate from connecting clients and validate it against these CAs. Connections lacking a valid client certificate (either not provided, or not signed by one of the given CAs) will be rejected. This enables mutual TLS (mTLS) authentication. Big security energy. πŸ”₯
  • cipher-suite β€” list of cipher suites to be passed to OpenSSL via SSL_CTX_set_cipher_list.
  • cipher-suite-tls1.3 β€” list of TLS 1.3 cipher suites to use; the list must be a YAML sequence where each element specifies the cipher suite using the name as registered to the IANA TLS Cipher Suite Registry; e.g., TLS_AES_128_GCM_SHA256, TLS_CHACHA20_POLY1305_SHA256.
  • cipher-preference β€” side of the list that should be used for selecting the cipher-suite; should be either of: client, server. Default is client.
  • dh-file β€” path of a PEM file containing the Diffie-Hellman parameters to be used. Use of the file is recommended for servers using Diffie-Hellman key agreement. (optional)
  • key-exchange-tls1.3 β€” list of TLS 1.3 key exchange algorithms to use; the list must be a YAML sequence of algorithms. x25519 and secp256r1 are supported and enabled by default. In addition, secp384r1, secp521r1, and x25519mlkem768 might be available if they are supported by libcrypto.
  • max-tickets β€” maximum number of TLS/1.3 session tickets to send, when the client requests for them using the ticket_request extension. Default is 4.
  • minimum-version β€” minimum protocol version, should be one of: SSLv2, SSLv3, TLSv1, TLSv1.1, TLSv1.2, TLSv1.3. Default is TLSv1.
  • min-version β€” synonym of minimum-version (introduced in version 2.2).
  • maximum-version β€” maximum protocol version. Introduced in version 2.2. Default is the maximum protocol version supported by the server.
  • max-version β€” synonym of maximum-version.
  • ocsp-max-failures β€” number of consecutive OCSP query failures before stopping to send OCSP stapling data to the client. Default is 3.
  • ocsp-update-interval β€” interval for updating the OCSP stapling data (in seconds), or set to zero to disable OCSP stapling. Default is 14400 (4 hours).
  • neverbleed β€” unless set to OFF, H2O isolates SSL private key operations to a different process by using Neverbleed. Default is ON.

Also: the ssl-session-resumption directive is provided for tuning parameters related to session resumption and session tickets. πŸŽ€

ech 🧚

This experimental attribute controls the use of the TLS Encrypted Client Hello extension (draft-15). The attribute takes a sequence of mappings, each of them defining one ECH configuration.

Experimental, bestie! Experimental. Don't come crying to me if the draft moves under your feet. πŸ’€

Example: Encrypted Clint hello

ssl:
  key-file: /path/to/rsa.key
  certificate-file: /path/to/rsa.crt
  ech:
  - key-file: /path/to/ech.key
    config-id: 11
    public-name: public-name.example.net
    cipher-suite: [ HKDF-SHA256/AES-128-GCM ]

The example above defines one ECH configuration that uses /path/to/ech.key as the semi-static ECDH key with a config-id of 11, with the public-name being public-name.example.net, and the HPKE SymmetricCipherSuite being HKDF-SHA256/AES-128-GCM.

In addition to these four attributes, the following attributes may be specified:

  • max-name-length β€” specifies the maximum-name-length field of an ECH confguration (default: 64).
  • advertise β€” takes either YES (default) or NO as the argument. This argument indicates if the given ECH configuration should be advertised as part of retry_configs (draft-ietf-tls-esni-15; section 5).

When removing a stale ECH configuration, its advertise attribute should be set at first to NO so that the stale configuration would not be advertised. Then, after waiting for the expiry of caches containing the stale configuration, the stale ECH configuration can be removed. Heads up: this may take long, depending on the TTL of the HTTPS / SVC DNS resource record advertising the configuration. 😭 Patience is a virtue.

The ech attribute must be set only in the first ssl attribute that binds to a particular address.

The CC Attribute 🌈

The CC attribute specifies the congestion controller to be used for incoming HTTP connections.

Okay so for TCP connections, the congestion controller gets set with the TCP_CONGESTION socket option on platforms that actually support that socket option. To find out the default and the list of supported congestion controllers, pls refer to man 7 tcp. If the platform doesn’t support that socket option, the attribute has no effect β€” yes, literally nothing happens πŸ’….

For QUIC connections, the congestion controller is one of Reno, Cubic, Pico. The default is Reno. Slay.

πŸŒ€ The Initcwnd Attribute πŸŒ€

The initcwnd attribute specifies the initial congestion window of each incoming HTTP connection, in the unit of packets. At the moment, this option only applies to QUIC. It has no effect for TCP connections. No cap.

πŸ›‘οΈ The Proxy-Protocol Attribute πŸ›‘οΈ

The proxy-protocol attribute (i.e. the value of the attribute must be either ON or OFF) specifies if the server should recognize the information passed via "the PROXY protocol" in the incoming connections. The protocol is used by L4 gateways such as AWS Elastic Load Balancing to send peer address to the servers behind the gateways.

When set to ON, H2O standalone server tries to parse the first octets of the incoming connections as defined in version 1 of the specification, and if successful, passes the addresses obtained from the protocol to the web applications and the logging handlers. If the first octets do not accord with the specification, it is considered as the start of the SSL handshake or as the beginning of an HTTP request depending on whether if the ssl attribute has been used.

Default is OFF.

πŸ“¦ The Sndbuf and Rcvbuf Attributes πŸ“¦

The sndbuf and rcvbuf attributes specify the send and receive buffer size for each TCP or UNIX socket used for accepting incoming HTTP connections. If set, the values of these attributes are applied to the sockets using SO_SNDBUF and SO_RCVBUF socket options.

These attributes have no effect for QUIC connections. Zero. Nada.

🐚 Listening to a Unix Socket 🐚

If the type attribute is set to unix, then the port attribute is assumed to specify the path of the unix socket to which the standalone server should bind. Also following attributes are recognized.

  • owner β€” username of the owner of the socket file. If omitted, the socket file will be owned by the launching user.
  • group β€” name of the group of the socket file. If omitted, group ID associated to the socket file will be the group ID of the owner.
  • permission β€” an octal number specifying the permission of the socket file. Many operating systems require write permission for connecting to the socket file. If omitted, the permission of the socket file will reflect the umask of the calling process.

Example: Listening to a Unix Socket accessible only by www-data

listen:
  type:       unix
  port:       /tmp/h2o.sock
  owner:      www-data
  permission: 600

πŸ›°οΈ Listening on a Multipath TCP socket πŸ›°οΈ

If the type attribute is set to mptcp, h2o will listen on a Multipath TCP socket, instead of a TCP one. Support for this listener type is available only in worker mode and when h2o is built with -DWITH_MPTCP=ON.

Example: Listen Directive for Multipath TCP

# accept HTTPS on port 443 with Multipath TCP
listen:
  type: mptcp
  port: 443
  ssl:
    key-file: /path/to/key-file
    certificate-file: /path/to/certificate-file

Multipath TCP is fully backwards compatible with regular TCP clients. Iconic.


πŸŽ€ acme πŸŽ€

Set the parameters for automatic certificate management using ACME (e.g., Letsencrypt).

Unless a custom loader program is specified, h2o obtains certificates from Let's Encrypt using the popular ACME client: Lego. On most distributions, Lego should be available as pre-built packages.

This directive takes two mandatory and one optional parameters.

  • email (mandatory) β€” The email address to be registered to the ACME service provider.
  • accept-tos (mandatory) β€” Must be set to YES to accept the terms of service of the ACME service provider.
  • loader (optional) β€” The program to be launched for issuing and renewing the certificates. If omitted, H2O_ROOT/share/h2o/acme/lego-loader is used.

The below example shows a configuration that relies on ACME for serving https://example.org.

Example: Simple h2o.conf using ACME

acme:
  email: admin@example.org  # email address to be used when contacting Let's Encrypt
  accept-tos: YES           # consent to the Terms of Service of Let's Encrypt
hosts:
  example.org:
    listen:
      port: 80
    listen:
      port: 443
      ssl: {}               # omission on an identity indicates the use of ACME
    paths:
      ...

The certificates and keys being issued or generated are stored under the $prefix/var/h2o/acme directory. Output of the lego command (including errors) are emitted to the error log of h2o.

πŸ‘‘ capabilities πŸ‘‘

Set capabilities to be added to the process before dropping root privileges.

This directive can be used only on Linux. The argument is a YAML sequence of capabilities, where each capability is a name that is accepted by cap_from_name. See man 7 capabilities for details.

πŸ“ error-log πŸ“

Path of the file to which error logs should be appended.

Default is stderr.

If the path starts with |, the rest of the path is considered as a command to which the logs should be piped.

Example: Log errors to file

error-log: /path/to/error-log-file

Example: Log errors through pipe

error-log: "| rotatelogs /path/to/error-log-file.%Y%m%d 86400"

See also: error-log.emit-request-errors

πŸ’… error-log.emit-request-errors πŸ’…

(since v2.1) Sets whether if request-level errors should be emitted to the error log.

By setting the value to OFF and by using the %{error}x specifier of the access-log directive, it is possible to log request-level errors only to the access log.

See also: access-log error-log

πŸ” h2olog πŸ”

Under the path /.well-known/h2olog for the current host, registers a h2olog handler that emits the internals of traffic that the h2o process is handling.

The h2olog command can be used to gather information through this endpoint. As the handler emits the internals of h2o to the client, only requests via a UNIX socket are accepted.

This directive takes one of the following three arguments:

  • off β€” The h2olog endpoint is disabled.
  • on β€” The h2olog endpoint is enabled, but only information to gather performance data will be emitted.
  • appdata β€” The h2olog endpoint is enabled, and in addition to information necessary for gathering performance data, some payload of HTTP is emitted as well. The additional information might help diagnose issues specific to certain HTTP connections but might include sensitive information (e.g., HTTP cookies).

⏱️ handshake-timeout ⏱️

Maximum time (in seconds) that can be spent by a connection before it becomes ready to accept an HTTP request.

Times spent for receiving the PROXY protocol and TLS handshake are counted.

⚑ io_uring-batch-size ⚑

Number of io_uring calls to issue at once. Increasing this number might reduce overhead.

See also: file.io_uring

πŸ“ limit-request-body πŸ“

Maximum size of request body in bytes (e.g. content of POST).

Default is 1073741824 (1GB).

πŸ”Œ max-connections πŸ”Œ

Maximum number of incoming connections to handle at once.

This includes TCP and QUIC connections.

See also: max-quic-connections soft-connection-limit

πŸ¦‹ max-quic-connections πŸ¦‹

Maximum number of incoming QUIC connections to handle at once.

By default, maximum number of incoming connections is governed by max-connections regardless of the transport protocol (i.e., TCP or QUIC) being used.

This directive introduces an additional cap for incoming QUIC connections. By setting max-quic-connections to a value smaller than max-connections, it would be possible to serve incoming requests that arrive on top of TCP (i.e., HTTP/1 and HTTP/2) even when there are issues with handling QUIC connections.

See also: num-quic-threads

🎯 max-delegations 🎯

Limits the number of delegations (i.e. fetching the response body from an alternate source as specified by the X-Reproxy-URL header).

πŸ” max-reprocesses πŸ”

Limits the number of internal redirects.

🧡 max-spare-pipes 🧡

This setting specifies the maximum number of pipes retained for reuse, when file.io_uring or proxy.zerocopy is used.

The setting can be used to reduce lock contention in the kernel under high load.

This maximum is applied per each worker thread.

Setting this value to 0 will cause no pipes to be retained by h2o; the pipes will be closed after they are used. In this case, h2o will create new pipes each time they are needed.

In previous versions, this configuration directive was called proxy.max-spare-pipes.

See also: file.io_uring, proxy.zerocopy

πŸš€ neverbleed-offload πŸš€

Sets an offload engine to be used with neverbleed.

When neverbleed is in use, RSA private key operations can be offloaded to accelerators using the Intel QuickAssist technology.

This directive takes one of the three values that changes how the accelerators are used:

  • OFF β€” the accelerator is not used
  • QAT β€” use of QAT is enforced; startup will fail if the accelerator is unavailable
  • QAT-AUTO β€” QAT is used if available

🌸 num-name-resolution-threads 🌸

Maximum number of threads to run for name resolution.

🩺 num-ocsp-updaters 🩺

(since v2.0) Maximum number of OCSP updaters.

OCSP Stapling is an optimization that speeds up the time spent for establishing a TLS connection. In order to staple OCSP information, a HTTP server is required to periodically contact the certificate authority. This directive caps the number of the processes spawn for collecting the information.

The use and the update interval of OCSP can be configured using the SSL attributes of the listen configuration directive.

🧚 num-threads 🧚

Number of worker threads.

By default, the number of worker threads spawned by h2o is the number of the CPU cores connected to the system as obtained by getconf NPROCESSORS_ONLN.

This directive is used to override the behavior.

If the argument is a YAML scalar, it specifies in integer the number of worker threads to spawn.

If the argument is a YAML sequence, it specifies a list of CPU IDs on each of which one worker thread will be spawned and pinned. This mode can be used only on systems that have pthread_setaffinity_np.

🌈 num-quic-threads 🌈

Restricts the number of worker threads that handle incoming QUIC connections.

By default, all worker threads handle incoming QUIC connections as well as TCP connections.

If num-threads was handed a YAML sequence naming the CPU IDs each worker thread should run on, then the threads pinned to the first num-quic-threads threads are the ones handling incoming QUIC connections. ✨

See also: max-quic-connections

πŸ’Ύ pid-file

The name of the file where the server's process id gets written. That's it, that's the whole job.

Default is none β€” meaning no pid file unless you ask for one.

πŸš€ tcp-fastopen

The size of the queue used for TCP Fast Open β€” lowkey a glow-up for connection setup times.

TCP Fast Open is an extension to the TCP/IP protocol that cuts down the time spent establishing a connection. On Linux setups that support the feature, the default value is 4,096. On every other platform the default is 0, i.e. disabled. 😴

🏷️ send-server-name

(since v2.0) Controls whether the server response header should be sent, or forwarded from the backend.

Set it to ON or OFF to say whether the server response header should be sent, periodt. Set it to preserve and it forwards whatever value the backend sent when proxying. 🫢

See also: server-name

✨ server-name

(since v2.0) Lets you override the value of the server response header. Your server, your branding, your rules πŸ’…

The default value is h2o/VERSION-NUMBER.

See also: send-server-name

🌱 setenv

(since v2.0) Sets one or more environment variables.

Environment variables are a set of key-value pairs holding arbitrary strings, and they can be read from applications invoked by the standalone server (e.g. fastcgi handler, mruby handler) and by the access logger.

The directive is applied from outer-level to inner-level. At each level, it's applied after the unsetenv directive at the corresponding level has been applied β€” order matters, bestie.

Environment variables are retained through internal redirections.

Example: setting an environment variable named FOO

setenv:
  FOO: "value_of_FOO"

See also: unsetenv

🧹 unsetenv

(since v2.0) Unsets one or more environment variables.

Use it to carve out an exception for paths that have an environment variable set, or to reset variables after an internal redirection.

Example: setting an environment variable for example.com excluding /specific-path

hosts:
  example.com:
    setenv:
      FOO: "value_of_FOO"
    paths:
      /specific-path:
        unsetenv:
          - FOO
      ...

See also: setenv

πŸ“¨ send-informational

(since v2.3) Specifies the client protocols to which H2O can send 1xx informational responses.

This directive lets you forward 1xx informational responses (e.g., 103 Early Hints) generated by upstream servers or by the headers directive out to the clients.

If the value is all, H2O always sends informational responses to the client whenever possible (i.e. unless the procotol is HTTP/1.0). πŸŽ‰

If the value is none, H2O never sends informational responses to the client. 🚫

If the value is except-h1, H2O sends informational responses if the protocol is not HTTP/1.x.

🚦 soft-connection-limit

The number of connections above which idle connections get closed aggressively. πŸ”₯

H2O accepts up to max-connections TCP connections and max-quic-connections QUIC connections. Once the number of connections reaches those maximums, new connection attempts are just ignored until existing connections close. Rude, but that's the deal.

To reduce the possibility of hitting the max and having new connection attempts get ignored, soft-connection-limit gives you another threshold. When it's set, connections that have been idle for at least soft-connection-limit.min-age seconds start to get closed until the number of connections becomes no greater than soft-connection-limit.

Since the whole intention here is to close connections more aggressively under high load than usual, soft-connection-limit.min-age should be set to a smaller value than the other idle timeouts β€” e.g., http1-request-timeout, http2-idle-timeout.

⏳ soft-connection-limit.min-age

The minimum amount of idle time guaranteed for HTTP connections, even when connections are being closed aggressively because the number is over soft-connection-limit. It's the "at least give them this long" clause πŸ’œ

See soft-connection-limit.

πŸ” ssl-offload

A knob for changing how TLS encryption is handled.

This directive takes one of the following values:

  • OFF β€” TLS encryption is handled in userspace and the encrypted bytes are sent to the kernel using a write (2) system call.
  • kernel β€” TLS encryption is offloaded to the kernel. When the network interface card supports TLS offloading, actual encryption might get offloaded to the interface, depending on the kernel configuration.
  • zerocopy β€” TLS encryption is handled in userspace, but if the encryption logic is capable of writing directly to main memory without polluting the cache, the encrypted data is passed to the kernel without copying (i.e., sendmsg (2) with the MSG_ZEROCOPY socket option is used). Otherwise, this option is identical to OFF. This option minimizes cache pollution next to hardware offload.
Real talk: the kernel option can be used only on Linux. zerocopy is only available on Linux running on CPUs that support the necessary features β€” see picotls PR#384 and H2O PR#3007.

See also: proxy.zerocopy

🎟️ ssl-session-resumption

Configures cache-based and ticket-based session resumption.

To reduce the latency introduced by the TLS (SSL) handshake, two methods for resuming a previous encrypted session are defined by the Internet Engineering Task Force. H2O supports both: cache-based session resumption (defined in RFC 5246) and ticket-based session resumption (defined in RFC 5077). Obsessed with a fast handshake, honestly πŸ¦‹

Example: various session-resumption configurations

# use both methods (storing data on internal memory)
ssl-session-resumption:
    mode: all

# use both methods (storing data on memcached running at 192.168.0.4:11211)
ssl-session-resumption:
    mode: all
    cache-store: memcached
    ticket-store: memcached
    cache-memcached-num-threads: 8
    memcached:
        host: 192.168.0.4
        port: 11211

# use ticket-based resumption only (with secrets used for encrypting the tickets stored in a file)
ssl-session-resumption:
    mode: ticket
    ticket-store: file
    ticket-file: /path/to/ticket-encryption-key.yaml

Defining the Methods Used 🌸

The mode attribute defines which methods should be used for resuming the TLS sessions. The value can be either of: off, cache, ticket, all. Default is all.

If set to off, session resumption is disabled and all TLS connections get established via full handshakes. If set to all, both session-based and ticket-based resumption are used, with preference given to ticket-based resumption for clients that support both methods.

For each method, additional attributes can customize its behavior. Attributes that modify the behavior of the disabled method are simply ignored. πŸ™ˆ

Attributes for Cache-based Resumption πŸŽ€

The following attributes are recognized if cache-based session resumption is enabled. Note that the memcached attribute must be defined as well in case the memcached cache-store is used.

  • cache-store: β€” defines where the cache should be stored, must be one of: internal, memcached. Default is internal.
Heads up: if you compiled h2o with OpenSSL 1.1.0 ~ 1.1.0f, session resumption with an external cache store would fail due to a bug in OpenSSL. Not us, it's them πŸ’€
  • cache-memcached-num-threads: β€” defines the maximum number of threads used for communicating with the memcached server. Default is 1.
  • cache-memcached-prefix: β€” for the memcached store, specifies the key prefix used to store the secrets on memcached. Default is h2o:ssl-session-cache:.

Attributes for Ticket-based Resumption 🎫

Ticket-based session resumption uses ticket encryption key(s) to encrypt the keys used for encrypting the data transmitted over TLS connections. To achieve forward-secrecy (i.e. protect past communications from being decrypted in case the ticket encryption key gets obtained by a third party), it is essential to periodically roll over the encyrption key. No cap, this one genuinely matters.

Among the three types of stores supported for ticket-based session resumption, the internal store and the memcached store implement automatic roll-over of the secrets. A new ticket encryption key is created every 1/4 of the session lifetime (defined by the lifetime attribute), and they expire (and get removed) after 5/4 of the session lifetime elapse.

For the file store, it's the responsibility of the web-site administrator to periodically update the secrets. H2O monitors the file and reloads the secrets when the file is altered. πŸ‘€

The following attributes are recognized if ticket-based resumption is enabled:

  • ticket-store: β€” defines where the secrets for ticket-based resumption should be / is stored, must be one of: internal, file, memcached. Default is internal.
  • ticket-cipher: β€” for stores that implement automatic roll-over, specifies the cipher used for encrypting the tickets. The value must be one recognizable by EVP_get_cipherbyname. Default is aes-256-cbc.
  • ticket-hash: β€” for stores that implement automatic roll-over, specifies the cipher used for digitally-signing the tickets. The value must be one recognizable by EVP_get_digestbyname. Default is sha-256.
  • ticket-file: β€” for the file store, specifies the file in which the secrets are stored.
  • ticket-memcached-key: β€” for the memcached store, specifies the key used to store the secrets on memcached. Default is h2o:ssl-session-ticket.

Format of the Ticket Encryption Key πŸ”‘

Either as a file (specified by the ticket-file attribute) or as a memcached entry (ticket-memcached-key), the encryption keys for the session tickets are stored as a sequence of YAML mappings. Each mapping must have all of the following attributes set.

  • name β€” a string of 32 hexadecimal characters that's literally just the name of the ticket encryption key. It's only used for identifying the key, so bestie, you can just generate it with a PRNG, it doesn't need to be secret πŸ’…
  • cipher β€” the name of the symmetric cipher used to protect the session tickets. Only supported values are: aes-128-cbc and aes-256-cbc (which is the default, slay).
  • hash β€” the hash algorithm used for validating the session tickets. Only supported value is: sha256.
  • key β€” the concatenation of the key for the symmetric cipher and the HMAC, encoded as hexadecimal characters. Its length should be the sum of the cipher key length plus the hash key length, multiplied by two (because hex encoding, iykyk); i.e. 96 bytes for aes-128-cbc/sha256, or 128 bytes for aes-256-cbc/sha256.
  • not_before β€” the time from when the key can be used for encrypting the session tickets. The value is encoded as milliseconds since epoch (Jan 1 1970). When you're rotating encryption keys manually across multiple servers, set the not_before attribute of the newly added key to some time in the future so that all the servers start using the new key at the exact same moment 🫢
  • not_after β€” until when the key can be used for encrypting the session tickets.

✨ The following example shows a YAML file containing two session ticket encryption keys. The first entry is used for encrypting new keys on Jan 5 2018. The second entry is used for encrypting new keys on Jan 6 2018.

Example: session ticket key file

- name:       c173437296d6c2307fd39b40c944c227
  cipher:     aes-256-cbc
  hash:       sha256
  key:        e54210a0f6a6319aa155a33b8babd772319bad9f27903746dfbe6df7a4058485a8cedb057cfc5b70080cda2354fc3e13
  not_before: 1515110400000 # 2018-01-05 00:00:00.000
  not_after:  1515196799999 # 2018-01-05 23:59:59.999
- name:       bb1a15d75dc498624890dc5a7e164675
  cipher:     aes-256-cbc
  hash:       sha256
  key:        b4120bc903d6521fefa357ac322561fc97aa9e5ae5e18eade64832439b9095ab80f8429d6b50ff9c4c5eca1f90717d30
  not_before: 1515196800000 # 2018-01-06 00:00:00.000
  not_after:  1515283199999 # 2018-01-06 23:59:59.999

πŸ’« Other Attributes

The following attributes are common to cache-based and ticket-based session resumption. Both. At once. Equality πŸ’–

  • lifetime β€” defines the lifetime of a TLS session; when it expires the session cache entry is purged, and establishing a new connection will require a full TLS handshake. Default value is 3600 (in seconds). Like, an hour, then it's gone, no cap.
  • memcached β€” specifies the location of memcached used by the memcached stores. The value must be a mapping with a host attribute specifying the address of the memcached server, and optionally a port attribute specifying the port number (default is 11211). By default, the memcached client uses the BINARY protocol. Users can opt-in to using the legacy ASCII protocol by adding a protocol attribute set to ASCII. Retro vibes ✨

strict-match

A boolean flag designating if the current host element should not be considered as the fallback element.

See hosts.

tcp-reuseport

A boolean flag designating if TCP socket listeners should be opened with the SO_REUSEPORT option.

temp-buffer-path

(since v2.0) Directory in which temporary buffer files are created.

H2O uses an internal structure called h2o_buffer_t for buffering various kinds of data (e.g. POST content, response from upstream HTTP or FastCGI server). When the amount of data allocated in the buffer exceeds the default value of 32MB, it starts allocating storage from the directory pointed to by the directive. The threshold can be tuned or disabled using the temp-buffer-threshold directive.

By using this directive, users can set the directory to one within a memory-backed file system (e.g. tmpfs) for speed, or specify a disk-based file system to avoid memory pressure.

⚠️ Note that the directory must be writable by the running user of the server. Fr fr, check your perms.

See also: user temp-buffer-threshold

temp-buffer-threshold

(since v2.2.5) Minimum size to offload a large memory allocation to a temporary buffer.

Users can use this directive to tune the threshold for when the server should use temporary buffers. The minimum value accepted is 1MB (1048576) to avoid overusing these buffers, which will lead to performance degradation 🫠 If omitted, the default of 32MB is used.

The user can disable temporary buffers altogether by setting this threshold to OFF.

See also: temp-buffer-path

user

Username under which the server should handle incoming requests.

If the directive is omitted and if the server is started under root privileges, the server will attempt to setuid to nobody.

crash-handler

(since v2.1) Script to invoke if h2o receives a fatal signal 😭

Note: this feature is only available when linking to the GNU libc.

The script is invoked if one of the SIGABRT, SIGBUS, SIGFPE, SIGILL or SIGSEGV signals is received by h2o.

h2o writes the backtrace as provided by backtrace() and backtrace_symbols_fd to the standard input of the program.

If the path is not absolute, it is prefixed with ${H2O_ROOT}/.

crash-handler.wait-pipe-close

(since v2.1) Whether h2o should wait for the crash handler pipe to close before exiting.

When this setting is ON, h2o will wait for the pipe to the crash handler to be closed before exiting. This can be useful if you use a custom handler that inspects the dying process πŸ‘‘ like, let it say its goodbyes.

stash

(since v2.3) Directive being used to store reusable YAML variables.

This directive does nothing itself, but can be used to store YAML variables and reuse those using YAML Alias. Iconic behaviour, honestly πŸŽ€

Example: Reusing stashed variables across multiple hosts

stash:
  ssl: &ssl
    port: 443
  paths: &paths
    /:
      file.dir: /path/to/root
hosts:
  "example.com":
    listen:
      <

🌈 COMPRESS DIRECTIVES 🌈

The compress handler performs on-the-fly compression β€” it compresses the contents of an HTTP response as it is being sent, if the client indicates itself to be capable of decompressing the response transparently with the use of the Accept-Encoding header, and if the response is deemed compressible according to the following rules. Two conditions, both gotta hit πŸ’…

If the x-compress-hint response header does not exist or the value is auto, then whether the response is considered compressible depends on the is_compressible attribute assigned to the content type (see file.mime.addtypes). If the x-compress-hint response header exists and the value is on, the response is always considered to be compressible. If the value of the response header is set to off, then the response never gets compressed. Periodt.

The following are the configuration directives recognized by the handler.

compress

(since v2.0) Enables on-the-fly compression of HTTP response.

If the argument is ON, gzip is enabled for compression. Brotli and zstd will also be enabled when the server was built with their support. If the argument is a sequence, the elements are the list of compression algorithms to be enabled.

The default qualities specified for each compression method are: 1 for gzip, 1 for brotli, and 3 for zstd. To override the qualities, use a mapping with each key-value pair specifying the compression algorithm and the quality (gzip: 1-9, br: 0-11, zstd: 1-22).

When multiple algorithms are enabled and the client supports more than one, H2O prefers brotli, then zstd, then gzip. Ranked, bestie πŸ“Š

If the argument is OFF, on-the-fly compression is disabled.

Example: Enabling on-the-fly compression

# enable all algorithms
compress: ON

# enable by name
compress: [ gzip, br, zstd ]

# enable gzip only
compress: [ gzip ]

# tune zstd quality
compress:
  zstd: 7

See also: file.send-compressed, file.mime.addtypes

compress-minimum-size

(since v2.0) Defines the minimum size a files needs to have in order for H2O to compress the request.

gzip

(since v1.5) Enables on-the-fly compression of HTTP response using gzip.

Equivalent to compress: [ gzip ]. Short and sweet πŸ“

See also: compress


πŸ’œ HTTP/1 DIRECTIVES πŸ’œ

This document describes the configuration directives for controlling the HTTP/1 protocol handler.

http1-request-timeout

Timeout for incoming requests in seconds.

http1-request-io-timeout

Timeout for incoming request I/O in seconds.

http1-upgrade-to-http2

Boolean flag (ON or OFF) indicating whether or not to allow upgrade to HTTP/2.


πŸ”₯ HTTP/2 DIRECTIVES πŸ”₯

H2O provides one of the world's most sophisticated HTTP/2 protocol implementations, including the following features. Like, not to brag, but πŸ‘‘

⭐ Prioritization

H2O is one of the few servers that fully implement prioritization of HTTP responses conformant to what is defined in the HTTP/2 specification. The server implements a O(1) scheduler that determines which HTTP response should be sent to the client, per every 16KB chunk. O(1), bestie, that's the good stuff.

Unfortunately, some web browsers fail to specify response priorities that lead to best end-user experience 😭 H2O is capable of detecting such web browsers, and if it does, uses server-driven prioritization; i.e. send responses with certain MIME-types before others.

It is possible to tune or turn off server-driven prioritization using directives: file.mime.addtypes, http2-reprioritize-blocking-assets.

See also:

Download Timings Benchmark HTTP/2 (and H2O) improves user experience over HTTP/1.1 or SPDY

πŸ¦‹ Server push

H2O recognizes link headers with preload keyword sent by a backend application server (reverse proxy or FastCGI) or an mruby handler, and pushes the designated resource to a client.

Example: A link response header triggering HTTP/2 push

link: ; rel=preload; as=script

When the HTTP/2 driver of H2O recognizes a link response header with rel=preload attribute set, and if all of the following conditions are met, the specified resource is pushed to the client. All of them, no skipping πŸ’€

  • configuration directive http2-push-preload is not set to OFF
  • the link header does not have the nopush attribute set
  • the link header is not part of a pushed response
  • the client does not disable HTTP/2 push
  • number of the pushed responses in-flight is below the negotiated threshold
  • authority of the resource specified is equivalent to the request that tried to trigger the push (for handlers that return the status code synchronously)
  • the status code of the response to be pushed does not indicate an error (i.e. 4xx or 5xx)

The server also provides a mechanism to track the clients' cache state via cookies, and to push the resources specified with the link header only when it does not exist within the clients' cache. For details, please refer to the documentation of http2-casper configuration directive. Cute name, immaculate feature 🌸

When a resource gets pushed, the priority it gets is decided by the priority attribute in the MIME-type configuration β€” that's the vibe check. If the priority is set to highest, the resource goes out to the client before anything else; otherwise it's sent after the main content, exactly like the HTTP/2 spec says. No cap, that's just the rules.

Okay so HTTP/1.1 lets a server send an informational response (peep RFC 7230 section 6.2) before the final response, and that's honestly iconic. Starting from version 2.1, web applications can use that informational response to kick off HTTP/2 pushes before even starting to process the request. Here's what those responses look like in the wild:

Example: 100 response with link headers

HTTP/1.1 100 Continue
Link: ; rel=preload; as=style
Link: ; rel=preload; as=script

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8

Pushed responses get the x-http2-push: pushed header set β€” so if you're like "wait, was this pushed or not?", just look for that header and you'll know. You can also log the value in the access log by specifying %{x-http2-push}o, and push responses that get cancelled by CASPER will have the header value logged as cancelled. Slay, observability. πŸ’…

See also:

  • Optimizing performance of multi-tier web applications using HTTP/2 push

πŸ¦‹ Latency Optimization πŸ¦‹

Okay bestie, this part is lowkey the most interesting. When you're using HTTP/2, a client often fires off high-priority requests (like, the CSS and JavaScript files that block rendering) while a lower-priority response (say, the HTML) is still in flight. And in that case, you really want the server to switch over and start sending those high-priority responses the second it notices the requests. Main character energy from the server, honestly.

So how do you make that happen? The send buffer of the TCP/IP stack should be kept empty except for the packets in-flight, and the TLS records need to be small enough to avoid head-of-line blocking. The catch (because there's always a catch 😭): obeying that requirement means more interaction between the server process and the kernel, which burns more CPU cycles and adds slightly increased latency. Trade-offs, babes.

Starting from version 2.1, H2O gives you directives that let you tune how the TCP/IP stack is used, depending on the observed RTT, CWND, and the extra latency imposed by the interaction between the server and the OS. Chef's kiss.

For TCP/IP connections with greater RTT and smaller CWND than the configured threshold, the server will try to keep the size of unsent HTTP/2 frames as small as possible so it can switch to sending a higher-priority response. Benchmarks suggest users can expect on average 1 RTT reduction when this optimization is on. And for connections that don't meet the criteria, the server just uses the TCP/IP stack in ordinary ways. No drama.

The default values of the thresholds have been chosen so that the optimization comes into action for mobile and long-distance networks β€” but not when a proxy exists on the network. iykyk.

Heads up: this optimization is supported only on Linux and OS X β€” those are the two operating systems that provide access to TCP_INFO and an interface to adjust the size of the unsent buffer (TCP_NOTSENT_LOWAT). πŸ’€

Please refer to the documentation of the directives below to configure the optimization:

http2-latency-optimization-min-rtt http2-latency-optimization-max-
additional-delay http2-latency-optimization-max-cwnd

See also:

  • Reorganizing Website Architecture for HTTP/2 and Beyond pp.14-21

The following describes the configuration directives for controlling the HTTP/2 protocol handler. ✨

http2-casper

Configures CASPer (cache-aware server-push). πŸŽ€

When it's enabled, H2O maintains a fingerprint of the web browser cache, and cancels server-push suggested by the handlers if the client is known to already be in possession of the content. The fingerprint is stored in a cookie named h2o_casper using Golomb-compressed sets (which is a compressed encoding of Bloom filter, fancy).

If the value is OFF, the feature is disabled. Push requests (made by the handlers through the use of Link: rel=preload header) are processed regardless of whether the client already has the responses in its cache. If the value is ON, the feature is enabled with the default values specified below. If the value is mapping, the feature is enabled and recognizes the following attributes:

  • capacity-bits: number of bits used for the fingerprinting. Roughly speaking, the number of bits should be log2(1/P * number-of-assets-to-track) where P is the probability of false positives. Default is 13, enough for tracking about 100 asset files with 1/100 chance of false positives (i.e. log2(100 * 100) =~ 13).
  • tracking-types: specifies the types of the content tracked by casper. If omitted or set to blocking-assets, maintains fingerprint (and cancels server push) for resources with mime-type of highest priority. If set to all, tracks all responses.

It should be noted that the size of the cookie will be log2(P) * number-of-assets-being-tracked bits multiplied by the overhead of Base 64 encoding (4/3). So with the current cookie-based implementation, you'll often need to restrict the resources being tracked to those that have significant effect on user-perceived response time. Respect the cookie budget, fr fr. πŸ“

Example: Enabling CASPer

http2-casper: ON

# `ON` is equivalent to:
# http2-casper:
#   capacity-bits:  13
#   tracking-types: blocking-assets

See also: file.mime.addtypes, issue #421

http2-debug-state

A directive to turn on the HTTP/2 Implementation Debug State. πŸ”₯

This experimental feature serves a JSON document at the fixed path /.well-known/h2/state, which describes an internal HTTP/2 state of the H2O server. To know the details about the response fields, please see the spec. This feature is only for developing and debugging use, so it's highly recommended that you disable this setting in the production environment. Please, bestie, I'm begging you. πŸ™ˆ

The value of this directive specifies the property set contained in the response. Available values are minimum or hpack. If hpack is specified, the response will contain the internal hpack state of the same connection. If minimum is specified, the response doesn't contain the internal hpack state.

In some circumstances there may be a risk of information leakage when providing an internal hpack state. For example, the case where some proxies exist between the client and the server, and they share the connections among the clients. So you should specify hpack only when the server runs in environments you can completely control.

This feature is considered experimental yet. For now, the implementation conforms to the version draft-01 of the specification.

See also: HTTP/2 Implementation Debug State (draft-01)

http2-idle-timeout

Timeout for idle connections, in seconds. ⏳

http2-input-window-size

(since v2.3) Default window size for HTTP request body.

The value is the maximum amount of request body (in bytes) that can be sent by the client in 1 RTT (round-trip time).

http2-max-streams

Maximum number of streams to advertise via HTTP2 SETTINGS_MAX_CONCURRENT_STREAMS.

Limits the number of active requests that the proxy will accept on a client connection. Also see http2-max-concurrent-requests-per-connection, which controls the number of requests that the proxy will process concurrently.

http2-max-concurrent-requests-per-connection

Maximum number of requests to be handled concurrently within a single HTTP/2 connection.

The value cannot exceed 256.

http2-max-concurrent-streaming-requests-per-connection

Maximum number of streaming requests to be handled concurrently within a single HTTP/2 connection.

The value cannot exceed 256.

http2-latency-optimization-min-rtt

(since v2.1) Minimum RTT (in milliseconds) to enable latency optimization. 🌈

Latency optimization is disabled for TCP connections with smaller RTT (round-trip time) than the specified value. Otherwise, whether the optimization is used depends on other parameters.

Setting this value to 4294967295 (i.e. UINT_MAX) effectively disables the optimization. Bye optimization. πŸ‘‹

http2-latency-optimization-max-additional-delay

(since v2.1) Maximum additional delay (as the ratio to RTT) permitted to get latency optimization activated.

Latency optimization is disabled if the additional delay imposed by the interaction between the OS and the TCP/IP stack is estimated to be greater than the given threshold. Otherwise, whether the optimization is used depends on other parameters.

http2-latency-optimization-max-cwnd

(since v2.1) Maximum size (in octets) of CWND to get latency optimization activated.

CWND is a per-TCP-connection variable that represents the number of bytes that can be sent within 1 RTT.

The server will not use, or will stop using, latency optimization mode if CWND becomes greater than the configured value. In such case, the average size of HTTP/2 frames buffered unsent will be slightly above the tcp_notsent_lowat sysctl value.

http2-push-preload

(since v2.1) A boolean flag (ON or OFF) indicating whether the server should push resources when observing a link: rel=preload header. 🫢

http2-reprioritize-blocking-assets

A boolean flag (ON or OFF) indicating if the server should send contents with highest priority before anything else.

To maximize the user-perceived responsiveness of a web page, it's essential for the web server to send blocking assets (i.e. CSS and JavaScript files in ) before any other files such as images. HTTP/2 provides a way for web browsers to specify such priorities to the web server. However, as of Sep. 2015, no major web browsers except Mozilla Firefox take advantage of the feature. Sad but true. 😭

This option, when enabled, works as a workaround for such web browsers, thereby improving the experience of users using those browsers.

Technically speaking, it does the following:

  • if the client uses dependency-based prioritization, do not reprioritize
  • if the client does not use dependency-based prioritization, send the contents of which their types are given highest priority before any other responses

See also: file.mime.addtypes, HTTP/2 (and H2O) improves user experience over HTTP/1.1 or SPDY

http2-graceful-shutdown-timeout

A timeout in seconds. How long to wait before closing the connection on graceful shutdown. Setting the timeout to 0 deactivates the feature: H2O will wait for the peer to close the connections. Polite queen behavior. πŸ‘‘

http2-allow-cross-origin-push

(since v2.3) A boolean flag (ON or OFF) indicating whether the server should push resources belonging to a different authority.

http2-dos-delay

HTTP request processing delay to be applied when the client behavior is suspicious, in the unit of milliseconds. 🚨

When the client behavior is suspicious and there is a concern of Denial-of-Service attack, h2o stops processing requests arriving on the suspicious connection for specified amount of time. Like, it literally gives that connection the silent treatment for a bit. βœ‹


πŸ’™ HTTP/3

HTTP/3 uses QUIC as the transport protocol β€” yes bestie, it's the UDP one, we're doing this 🌈. A listen directive with a type attribute set to quic instructs the standalone server to bind to a UDP port on which QUIC packets will be sent and received. The binding must have an ssl attribute, because QUIC uses TLS/1.3 as the handshake protocol. No ssl? No vibes. πŸ”

The example below setups a server that listens to both TCP port 443 and UDP port 443 using the same certificate and private key.

First listen directive binds the server to TCP port 443 with specified credentials, marking that directive as an YAML alias called &listen_ssl. Then, it reuses (YAML merge) the first listen directive, adding type: quic to create a UDP port 443 binding for accepting QUIC connections. Reuse era, we love to see it. πŸ’…

Example: Serving HTTP/1,2 and 3 on port 443

listen: &listen_ssl
  port: 443
  ssl:
    certificate-file: /path/to-ssl-certificate-file
    key-file: /path/to/ssl-key-file
listen:
  <

πŸ¦‹ Fine-tuning QUIC Behavior

To fine tune the behavior of QUIC, the quic attribute should be used in place of the type attribute specifying quic. The quic attribute accepts following parameters. Tuning time, babes. πŸŽ›οΈ

  • alternative β€” A mapping that defines an alternative set of congestion control and loss recovery settings, to be used by a fraction of the connections (e.g., for A/B testing). The mapping must contain ratio, an integer between 0 (never) and 255 (always) specifying the probability of each connection using the alternative settings, multiplied by 255. The alternative settings start as a copy of the default settings (including cc and initcwnd of the listen directive); the following parameters, if specified within the mapping, override them: cc, disengage-jumpstart, ecn, initcwnd, initial-rtt, jumpstart-default, jumpstart-max, normalize-mtu, pacing, respect-app-limited, speculative-pto. Note that cc specified per host (i.e., by the listen directive of each host) is applied only to the connections using the default settings. Science experiment but make it networking πŸ§ͺ
  • amp-limit β€” Amount of data that can be sent to the client before the client address is validated; see section 8.1 of RFC 9000. Default is 3.
  • disengage-jumpstart β€” A boolean flag (either OFF or ON) indicating whether jumpstart should be prepared but then not be used. When set to ON, connections for which jumpstart would have been used are counted as such (i.e., num-jumpstart-applicable), but proceed using slow start. This is useful for A/B testing jumpstart by setting this flag in alternative. The default setting is OFF.
  • ecn β€” A boolean flag (either ON or OFF) indicating whether the server should use ECN signals to detect congestion. The default setting is ON. This flag affects the server's sending behavior. Regardless of this configuration, the server sends back ECN signals it receives using ACK_ECN frames.
  • handshake-timeout-rtt-multiplier β€” Handshake timeout in the unit of round-trip time. Default is 400.
  • initial-rtt β€” RTT to be assumed before the first RTT sample is obtained, in milliseconds. Default is 66.
  • jumpstart-default β€” Jumpstart enhances the slow start phase of congestion control by pacing a large number of packets for an entire round-trip time (RTT), allowing the server to assess the network's capacity sooner. This parameter specifies the number of packets sent during the jumpstart phase. The default value is zero, indicating that jumpstart is disabled for new connections. πŸš€
  • jumpstart-max β€” Upon resuming QUIC connections, clients present tokens issued by the server that contain the bandwidth of the previous connection. This information enables the server to instantly match the previous send rate by adjusting the jumpstart window accordingly. This parameter sets a cap on the maximum size of the jumpstart window. If set to zero (the default), the server disregards the bandwidth values in the tokens. Nonetheless, the server may still initiate a jumpstart without using previous information, based on the jumpstart-default parameter.
  • max-initial-handshake-packets β€” Maximum number of Initial packets to be sent before the handshake is deemed to have failed. Default is 1,000.
  • max-streams-bidi β€” Maximum number of client-initated bi-directional streams. This parameter controls the HTTP request concurrency of a HTTP/3 connection. Default is 100.
  • max-udp-payload-size β€” See Section 18.2 of RFC 9000. Default is 1,472.
  • normalize-mtu β€” A boolean flag (either OFF or ON) indicating whether the growth of the congestion window should be normalized to a reference packet size rather than the maximum UDP payload size of the path. The default setting is ON.
  • pacing β€” A boolean flag (either OFF or ON) indicating whether sent packets should be paced. The default setting is OFF.
  • qpack-encoder-table-capacity β€” Size of the QPACK encoder table, in bytes. Controls the maximum size of the dynamic table that h2o will use for encoding the response headers. Setting this field to 0 disables the use of the dynamic table. Default is 16,384.
  • qpack-decoder-table-capacity β€” Size of the QPACK decoder table, in bytes. Setting this to a non-zero value advertises SETTINGS_QPACK_MAX_TABLE_CAPACITY to the peer, allowing the peer to use dynamic-table references when encoding request headers. Default is 16,384.
  • respect-app-limited β€” This boolean flag (either OFF or ON) indicates whether the server should respect the notion of rate limited traffic when adjusting the size of the congestion window, as detailed in RFC 7661. The default setting is ON.
  • retry β€” A boolean flag (OFF or ON) indicating if a Retry packet should be used for validating the client address. Use of Retry packets mitigate denial-of-service attacks at the cost of incurring one additional round-trip for processing the handshake.
  • sndbuf, rcvbuf β€” Size of send and receive buffers, in the unit of bytes. Unlike the TCP counterparts that are per-connection, these buffers are associated to the listening port and applies to all the connections bound to that port. Shared buffer era 🫢
  • speculative-pto β€” Number of probe packets to be sent speculatively at the end of a window, being an integer between 0 and 2. Default is 0.

The example below reuses a previous binding but sets the retry parameter to ON.

Example: HTTP/3 endpoint using Retry packets

listen:
  <

Also, properties such as congestion controller and initial congestion window can be tuned using the top-level attribute of listen.

⭐ HTTP/3 Directives

Aside from QUIC-level properties, configuration directives listed below are provided for tuning HTTP/3 behavior. πŸ‘‘


πŸŽ€ http3-graceful-shutdown-timeout

Maximum duration to retain HTTP/3 connections in half-closed state, in seconds.

When a graceful shutdown of h2o is initiated, h2o at first sends a GOAWAY frame indicating the clients that it is initiating shutdown, then after one second, starts rejecting new HTTP requests (a.k.a. half-closed state).

This directive controls how long h2o should wait for the peer to close the QUIC connection in this half-closed state, before exitting.

If set to zero, this timeout is disabled. h2o will not shut down until all QUIC connections are closed by the clients or times out. So like, zero means "we wait forever, bestie" β€” know what you're signing up for 😭

πŸ“ http3-gso

If Generic Segmentation Offload should be used when sending QUIC packets.

🌸 http3-idle-timeout

Idle timeout in the unit of seconds.

Unlike idle timeout of HTTP/1 and HTTP/2, this value should be small because it is faster to re-establish a new connection using 0-RTT than migrating to a different port due to NAT rebinding. Small number, big brain energy 🧠✨

🧚 http3-input-window-size

Default window size for HTTP request body.

See http2-input-window-size.

πŸ‘‘ http3-max-concurrent-streaming-requests-per-connection

Maximum number of streaming requests to be handled concurrently within a single HTTP/3 connection.

πŸ’– ACCESS LOG DIRECTIVES πŸ’–

okay bestie, this stretch is all about the directives of the access_log handler β€” the thing that writes down everything that happened. it's giving "receipts, but make it config". 🧾

🌈 access-log

This directive sets the path β€” and optionally the format β€” of the access log. Two jobs, one directive, very main character. ✨

If the argument you hand it is a scalar, it's treated as the path of the log file. OR β€” plot twist β€” if the value starts with a |, it's treated as a command to which the log should be emitted instead.

That | form? You need it for rotating the logs. Here's why, and it's important so don't skip it πŸ™ˆ: the log file is opened (or the command that emits the log is spawned) before dropping privileges, so that it can be owned by root or any other user. Which means the server process itself cannot reopen it once it starts running. So if you want rotation, pipe it out. periodt.

Example: Emit access log to file

access-log: /path/to/access-log-file

Example: Emit access log through pipe

access-log: "| rotatelogs /path/to/access-log-file.%Y%m%d 86400"

Now β€” if the argument is a mapping instead, then its path property is considered as the path of the log file (or the pipe command), and the format property is treated as the format of the log file. And starting from version 2.2, the escape property can be used to specify the escape sequence that should be used to emit unsafe octets. πŸ’…

Two forms of escape sequences are supported:

  • if apache is specified as the value of the escape property, unsafe octets are emitted in the form of \xNN, where N is a hexadecimal number in lower case.
  • if json is specified, unsafe octets are emitted in the form of \u00NN.

apache is the default escape method. (so if you don't say anything, you get apache vibes. iconic default behaviour honestly.)

Example: Emit access log to file using Common Log Format

access-log:
    path: /path/to/access-log-file
    format: "%h %l %u %t
    escape: apache

The list of format strings recognized by H2O is as follows. πŸ‘‘

πŸŽ€ Format strings

  • %% β€” the percent sign
  • %A β€” local address (e.g. 4.5.6.7)
  • %b β€” size of the response body in bytes
  • %H β€” request protocol as sent by the client (e.g. HTTP/1.1)
  • %h β€” remote address (e.g. 1.2.3.4)
  • %l β€” remote logname (always -)
  • %m β€” request method (e.g. GET, POST)
  • %p β€” local port (%{local}p is a synonym that is supported since version 2.2)
  • %{remote}p β€” remote port (since version 2.2)
  • %q β€” query string (? is prepended if exists, otherwise an empty string)
  • %r β€” request line (e.g. GET / HTTP/1.1)
  • %s β€” status code sent to client (e.g. 200)
  • %<s β€” status code received from upstream (or initially generated)
  • %t β€” time when the request was received, in format: [02/Jan/2006:15:04:05 -0700]
  • %{FORMAT}t β€” time when the request was received, using the specified format. FORMAT should be an argument to strftime, or one of:
    • sec β€” number of seconds since Epoch
    • msec β€” number of milliseconds since Epoch
    • usec β€” number of microseconds since Epoch
    • msec_frac β€” millisecond fraction
    • usec_frac β€” microsecond fraction
  • %U β€” requested URL path, not including the query string
  • %u β€” remote user if the request was authenticated (always -)
  • %V β€” requested server name (or the default server name if not specified by the client)
  • %v β€” canonical server name
  • %{VARNAME}e β€” request environment variable (since version 2.3; see Logging Arbitrary Variable)
  • %{HEADERNAME}i β€” value of the given request header (e.g. %{user-agent}i)
  • %{HEADERNAME}o β€” value of the given response header sent to client (e.g. %{set-cookie}o)
  • %<{HEADERNAME}o β€” value of the response header received from upstream (or initially generated)
  • %{NAME}x β€” various extensions. NAME must be one listed in the following tables. A dash (-) is emitted if the directive is not applicable to the request being logged.

As an example, it is possible to log timestamps in millisecond resolution using %{%Y/%m/%d:%H:%M:%S}t.%{msec_frac}t, which results in a timestamp like 2006-01-02:15:04:05.000. ⏰ (yes, that's the flex. micro-precision timestamps, no cap.)

πŸ’« Access Timings

  • connect-time β€” time spent to establish the connection (i.e. since connection gets accept(2)-ed until first octet of the request is received)
  • request-header-time β€” time spent receiving request headers
  • request-body-time β€” time spent receiving request body
  • request-total-time β€” sum of request-header-time and request-body-time
  • process-time β€” time spent after receiving request, before starting to send response
  • response-time β€” time spent sending response
  • duration β€” sum of request-total-time, process-time, response-time
  • total-time β€” same as duration (since v2.3)

πŸ¦‹ HTTP Message Sizes

  • request-header-bytes β€” number of bytes used to receive request headers in the protocol-specific encoded form
  • response-header-bytes β€” number of bytes used to send response headers in the protocol-specific encoded form
  • request-header-text-bytes β€” sum of request header name and value lengths after decoding
  • response-header-text-bytes β€” sum of response header name and value lengths before encoding
  • request-header-count β€” number of request header fields
  • response-header-count β€” number of response header fields
  • request-body-bytes β€” number of request body bytes received

🌸 Proxy Timings (since v2.3)

  • proxy.idle-time β€” time spent after receiving request, before starting to connect to the upstream
  • proxy.connect-time β€” time spent to establish the connection (including SSL handshake)
  • proxy.request-time β€” time spent sending request (header and body)
  • proxy.process-time β€” time spent after sending request, before starting to receive response
  • proxy.response-time β€” time spent receiving response
  • proxy.total-time β€” sum of proxy-request-time, proxy-process-time, proxy-response-time

πŸ“ Proxy (since v2.3)

  • proxy.request-bytes β€” number of bytes used by the proxy handler for sending the request (above TLS layer)
  • proxy.request-bytes-header β€” number of bytes used by the proxy handler for sending the request header (above TLS layer)
  • proxy.request-bytes-body β€” number of bytes used by the proxy handler for sending the request body (above TLS layer)
  • proxy.response-bytes β€” number of bytes used by the proxy handler for receiving the response (above TLS layer)
  • proxy.response-bytes-header β€” number of bytes used by the proxy handler for receiving the response header (above TLS layer)
  • proxy.response-bytes-body β€” number of bytes used by the proxy handler for receiving the response body (above TLS layer)

πŸ”₯ Connection (since v2.0)

  • connection-id β€” 64-bit internal ID assigned to every client connection
  • ssl.protocol-version β€” SSL protocol version obtained from SSL_get_version
  • ssl.session-reused β€” 1 if the SSL session was reused, or 0 if not [1]
  • ssl.session-id β€” base64-encoded value of the session id used for resuming the session (since v2.2)
  • ssl.cipher β€” name of the cipher suite being used, obtained from SSL_CIPHER_get_name
  • ssl.cipher-bits β€” strength of the cipher suite in bits
  • ssl.server-name β€” hostname provided in Server Name Indication (SNI) extension, if any

🧚 Upstream Proxy Connection (since v2.3)

  • proxy.ssl.protocol-version β€” SSL protocol version obtained from SSL_get_version
  • proxy.ssl.session-reused β€” 1 if the SSL session was reused, or 0 if not
  • proxy.ssl.cipher β€” name of the cipher suite being used, obtained from SSL_CIPHER_get_name
  • proxy.ssl.cipher-bits β€” strength of the cipher suite in bits

πŸ‘‘ HTTP/2 (since v2.0)

  • http2.stream-id β€” stream ID
  • http2.priority.received β€” colon-concatenated values of exclusive, parent, weight
  • http2.priority.received.exclusive β€” exclusive bit of the most recent priority specified by the client
  • http2.priority.received.parent β€” parent stream ID of the most recent priority specified by the client
  • http2.priority.received.weight β€” weight of the most recent priority specified by the client

⭐ Miscellaneous

  • error β€” request-level errors. Unless specified otherwise by using the error-log.emit-request-errors directive, the same messages are emitted to the error-log. (since v2.1)

The default format is %h %l %u %t "%r" %s %b "%{Referer}i" "%{User-agent}i", a.k.a. the NCSA extended/combined log format. πŸ’…

Heads up bestie: you may need to quote (and escape) the format string as required by YAML (see Yaml Cookbook). Don't get caught out by YAML eating your quotes, it's giving villain arc. 😭

See also: error-log, error-log.emit-request-errors

Notes:

[1] A single SSL connection may transfer more than one HTTP request.


🌈 ERRORDOC DIRECTIVES 🌈

and now the errordoc handler directives β€” aka "what do we show people when things go wrong". spoiler: something cuter than a raw error, hopefully. 🫢

πŸŽ€ error-doc

Specifies the content to be sent when returning an error response (i.e. a response with 4xx or 5xx status code). yeah. the oopsie pages.

The argument must be a mapping containing the following attributes β€” or, if it's a sequence, every element must be a mapping with the following attributes:

  • status β€” three-digit number indicating the status code (or sequence of that from version 2.3)
  • url β€” URL of the document to be served

URL can either be absolute or relative. Only content-type, content-language, set-cookie headers obtained from the specified URL are served to the client. (just those three, bestie, don't go expecting the whole header gang.)

Example: Set error document for 404 status

error-doc:
  status: 404
  url: /404.html

Example: Set error document for 500 and 503 status

error-doc:
  - status: 500
    url: /internal-error.html
  - status: 503
    url: /service-unavailable.html

Example: Set error document for 50x statuses (From version 2.3)

error-doc:
  status: [500, 502, 503, 504]
  url: /50x.html

see how that last one eats a whole list of statuses? one URL, four statuses, zero drama. πŸ¦‹


🌈 EXPIRES DIRECTIVES 🌈

last stretch of this part, and it's a short one β€” the expires handler directives. this is the "tell the browser how long to cache stuff" department. ⏳

πŸŽ€ expires

An optional directive for setting the Cache-Control: max-age= header. optional, but honestly it's giving glow-up for your caching story. ✨

  • if the argument is OFF β€” the feature is not used
  • if the value is NUMBER UNIT β€” then the header is set

the units recognized are: second, minute, hour, day, month, year

and yes, the units can also be in plural forms πŸ’… (seconds, minutes, hours, days, months, years β€” you get the vibe, it's very forgiving).

Example: Set Cache-Control: max-age=86400

expires: 1 day

You can also find an example that conditionally sets the header depending on the aspects of a request in the Modifying the Response section of the Mruby directives documentation. πŸ‘€ (go look, it's cute, conditional caching is such a slay.)

🌸 FASTCGI DIRECTIVES 🌸

Okay bestie, this section is all about the config directives for the FastCGI handler β€” like, the whole vibe of how H2O talks to your FastCGI stuff. πŸ’…

So the directives here split into two little groups. fastcgi.connect and fastcgi.spawn are the ones that define the address (or the process) that requests get sent to. Everything else is just customizing how the connections to those FastCGI processes get maintained. Periodt. ✨


πŸ’« fastcgi.connect

This directive says where the FastCGI daemon is actually running, iykyk.

If the argument is a mapping, these properties are recognized:

  • host β€” name (or IP address) of the server running the FastCGI daemon (ignored if type is unix)
  • port β€” TCP port number or path to the unix socket
  • type β€” either tcp (default) or unix

If the argument is a scalar, then the value gets treated as a TCP port number, and the host is assumed to be 127.0.0.1. Cute and simple. πŸŽ€

Example: map /app to a FastCGI daemon listening to /tmp/fcgi.sock

hosts:
    "example.com:80":
        paths:
            "/app":
                fastcgi.connect:
                    port: /tmp/fcgi.sock
                    type: unix

πŸ¦‹ fastcgi.document_root

Sets the DOCUMENT_ROOT variable that gets passed to the FastCGI application. That's it, that's the whole directive. Slay. ✨


πŸ”₯ fastcgi.spawn

This one specifies the command used to start the FastCGI process manager.

So here's the difference, bestie: fastcgi.connect connects to a FastCGI server that's already running externally, but this directive launches a FastCGI process manager under H2O's control, and then terminates it when H2O quits. The argument is a /bin/sh -c expression that gets executed when H2O boots up. The HTTP server records the process id of that expression, and sends SIGTERM to that id when it exits. πŸ’€

Example: map .php files to 10 worker processes of /usr/local/bin/php-cgi

file.custom-handler:
    extension:     .php
    fastcgi.spawn: "PHP_FCGI_CHILDREN=10 exec /usr/local/bin/php-cgi"

Example: map any executable file in path /var/www/data/cgi-bin to the fastcgi-cgi wrapper

"/cgi-bin":
    file.dir: /var/www/data/cgi-bin
    file.custom-handler:
        extension: default # means "no extension" in this case
        fastcgi.spawn:
            command: "exec /usr/local/share/h2o/fastcgi-cgi"

As of version 1.4.0, the spawned process runs under the privileges of the user specified by the user directive. (In version 1.3.x, the FastCGI process was spawned under the privileges that spawned the H2O standalone server, jsyk.) And you can specify a different user for running the FastCGI process β€” just give it a mapping with an attribute named user together with an attribute named command. πŸ‘‘

Example: running FastCGI processes under user fastcgi

file.custom-handler:
    extension:     .php
    fastcgi.spawn:
        command: "PHP_FCGI_CHILDREN=10 exec /usr/local/bin/php-cgi"
        user:    fastcgi

⏱️ fastcgi.timeout.io

Sets the I/O timeout of connections to the FastCGI process, in milliseconds. Don't let it hang, bestie. 🫢


πŸ’€ fastcgi.timeout.keepalive

Sets the keep-alive timeout for idle connections, in milliseconds.

Heads up: FastCGI connections will not be persistent if this value is set to zero (which is the default). So like, zero means no keep-alive vibes at all. 🚫

🌈 fastcgi.send-delegated-uri

Sends the modified HTTP_HOST and REQUEST_URI that got rewritten in the case of an internal redirect.

In H2O you can do internal redirects (a.k.a. delegations, or URL rewrites) using the redirect directive, or by returning X-Reproxy-URL headers from web applications. This directive controls whether the original values get sent to the FastCGI process (that's the default), or whether the rewritten values should be sent instead. Choose your fighter. πŸ’…


πŸŽ€ FILE DIRECTIVES πŸŽ€

This part is all about the config directives of the file handler β€” the handler that serves static files. Literally its whole job. πŸ—‚οΈ

Two directives, file.dir and file.file, are the ones used to define the mapping. All the other directives just modify the behavior of the mappings defined by those two. Easy.


🌟 file.custom-handler

Maps extensions to a custom handler (like FastCGI, for example).

This directive accepts a mapping containing configuration directives that can be used at the extension level, together with a property named extension that specifies an extension (starting with .) or a sequence of extensions that the directives should be applied to. If all the files shall be mapped β€” including the ones without extensions β€” then this property must be set to default. And only one handler must exist within the directives. One. Not two. 🚨

Example: mapping PHP files to FastCGI

file.custom-handler:
  extension: .php
  fastcgi.connect:
    port: /tmp/fcgi.sock
    type: unix

πŸ“ file.dir

Specifies the directory under which files should be served for the corresponding path.

Example: serving files under different paths

paths:
    "/":
        file.dir: /path/to/doc-root
    "/icons":
        file.dir: /path/to/icons-dir
See also: file.dirlisting, file.file, file.index

πŸ“œ file.dirlisting

A boolean flag (OFF, or ON) specifying whether or not to send the directory listing in case none of the index files exist. So like, if there's no index file, do we just show the whole directory? This flag decides. πŸ‘€

See also: file.dir

🏷️ file.etag

A boolean flag (OFF, or ON) specifying whether or not to send etags.


🎯 file.file

(since v2.0) This directive maps a path to a specific file.

Example: mapping a path to a specific file

paths:
  /robots.txt:
    file.file: /path/to/robots.txt
See also: file.dir

πŸ” file.index

Specifies the names of the files that should be served when the client sends a request against the directory.

The sequence of filenames is searched from left to right, and the first file that exists is the one that gets sent to the client. First come, first served. ✨

See also: file.dir

⚑ file.io_uring

Whether io_uring should be used for serving files.

By default, H2O uses system calls such as pread(2) or sendfile(2), which block if the file being read is not in the page cache. That can prevent H2O's worker threads from making progress on any connection handled by the affected thread β€” not the vibe. When this flag is enabled, H2O is no longer blocked by the I/O calls, because the data gets asynchronously copied from disk to the page cache before h2o attempts to access it. Iconic. πŸ”₯

See also: io_uring-batch-size, max-spare-pipes

🧁 file.mime.addtypes

This directive modifies the MIME mappings by adding the specified MIME type mappings.

Example: adding MIME mappings

file.mime.addtypes:
    "application/javascript": ".js"
    "image/jpeg": [ ".jpg", ".jpeg" ]

The default mappings are hard-coded in lib/handler/mimemap/defaults.c.h.

You can also set certain attributes for a MIME type. The example below maps .css files to the text/css type, setting the is_compressible flag to ON and priority to highest. πŸ’…

Example: setting MIME attributes

file.mime.settypes:
    "text/css":
         extensions: [".css"]
         is_compressible: yes
         priority: highest

These are the attributes that are recognized:

  • is_compressible β€” possible values: ON, OFF β€” if content is compressible
  • priority β€” possible values: highest, normal β€” send priority of the content

The priority attribute affects how the HTTP/2 protocol implementation handles the request. For details, please refer to the HTTP/2 directives listed in the see also section below. By default, the mime-types for CSS and JavaScript files are the only ones given highest priority. πŸ‘‘

See also: compress, http2-casper, http2-reprioritize-blocking-assets

πŸ—‘οΈ file.mime.removetypes

Removes the MIME mappings for the specified extensions, supplied as a sequence of extensions.

Example: removing MIME mappings

file.mime.removetypes: [ ".jpg", ".jpeg" ]

πŸŽ€ file.mime.setdefaulttype

Sets the default MIME-type that gets used when an extension doesn't exist in the MIME mappings. The fallback bestie. 🫢


🧸 file.mime.settypes

Resets the MIME mappings to the given mapping.

Example: resetting the MIME mappings to minimum

file.mime.settypes:
    "text/html":  [ ".html", ".htm" ]
    "text/plain": ".txt"

πŸ“ file.send-compressed

(since v2.0) A flag indicating how a pre-compressed file should be served.

If set to ON, the handler looks for a file with .br, .zst, or .gz appended, and sends that file if the client is capable of transparently decoding a brotli-, zstd-, or gzip-encoded response. For example, if a client requests a file named index.html with an Accept-Encoding: gzip header and index.html.gz exists, the .gz file gets sent as the response, together with a Content-Encoding: gzip response header. And when both the client and the server support multiple content encodings, the encoding chosen is the first one listed in this order: brotli, zstd, gzip. πŸ†

If set to OFF, the handler always serves the file specified by the client. No shortcuts.

Starting from version 2.2, gunzip is also supported. If set, the handler acts identical to when the value was set to ON. In addition, the handler will send an uncompressed response by dynamically decompressing the .gz file if the client and the server failed to agree on using a pre-compressed file as the response, and if a non-compressed file was not found. This option is useful when conserving disk space is important β€” it's possible to remove the uncompressed files in place of the gzipped ones. Big disk-space-saving energy. πŸ’Ύβœ¨

See also: compress

🐚 file.send-gzip

Obsoleted in 2.0. It's a synonym of file.send-compressed. That's the whole note β€” she's old, she's retired, but she's still here. πŸ’€

🌸 HEADERS DIRECTIVES 🌸

Okay so headers directives are literally your whole personality for response headers β€” they're how you manipulate them. πŸ’…

This doc covers the following config directives plus when they actually get applied. Heads up: every single one of these directives accepts one header field (as a YAML scalar) or multiple header fields (as a YAML sequence). Both work, you're welcome. ✨


header.add

Adds a brand-new header line to the response headers, no questions asked β€” even if a header with the same name is already chilling there. Like, it doesn't care. It just adds. πŸ’–

Example β€” setting the Set-Cookie header:

header.add: "Set-Cookie: test=1"

header.append

Adds a new header line, or appends the value to the existing header with the same name, separated by ,.

header.merge

Adds a new header line, or merges the value into the existing header of comma-separated values.

The following example sets the must-revalidate attribute of the Cache-Control header when and only when the attribute is not yet being set. Iconic conditional behavior, honestly.

Example β€” setting the must-revalidate attribute:

header.merge: "Cache-Control: must-revalidate"

header.set

Sets a header line, and removes headers with the same name if they exist. Out with the old, in with the new, periodt. πŸ”₯

Example β€” setting the X-Content-Type-Options: nosniff header:

header.set: "X-Content-Type-Options: nosniff"

header.setifempty

Sets a header line when and only when a header with the same name does not already exist. It's giving "respectfully, only if the spot is empty." 🫢

header.unset

Removes headers with the given name. Bye bestie. πŸ’€

Example β€” removing the X-Powered-By header:

header.unset: "X-Powered-By"

header.unsetunless

Removes all headers but those listed. Yes it's the chaotic "everyone leave except you two" directive. 😭

Example β€” remove all headers but If-Match and If-Modified-Since:

header.unsetunless:
- "If-Match"
- "If-Modified-Since"

✨ Timing of Application ✨

Starting from v2.3, you can specify the timing of when the headers directives get applied. All of the header directives can take either of the following two forms.

Example β€” Scalar Form:

header.add: "X-Foo: FOO":

Example β€” Mapping Form:

header.add:
  header: "X-Foo: FOO":
  when: final

The above two are identical. Same energy, different packaging. πŸŽ€

when can be either of: final, early, all. Default is final.

  • If the value is final, the header directive is only applied to final (i.e. non-1xx) responses.
  • If the value is early, it's only applied to 1xx informational responses.
  • If all is set, it's applied to both final and 1xx responses.

πŸ¦‹ MRUBY DIRECTIVES πŸ¦‹

The following are the configuration directives of the mruby handler. Please refer to Using mruby to find out how to write handlers using mruby. Go off, ruby girlies. πŸ’œ

mruby.handler

Upon start-up, evaluates the given mruby expression, and uses the returned mruby object to handle the incoming requests.

Example β€” Hello-world in mruby:

mruby.handler: |
  Proc.new do |env|
    [200, {'content-type' => 'text/plain'}, ["Hello world0]]
  end
⚠️ Note that the provided expression is evaluated more than once (typically for every thread that accepts incoming connections). Don't put anything expensive or stateful in there thinking it runs once, bestie.

See also: mruby.handler-file

mruby.handler-file

Upon start-up, evaluates the given mruby file, and uses the returned mruby object to handle the incoming requests.

Example β€” Hello-world in mruby:

mruby.handler-file: /path/to/my-mruby-handler.rb
⚠️ Again: note that the provided expression is evaluated more than once (typically for every thread that accepts incoming connections).

See also: mruby.handler


πŸ‘‘ PROXY DIRECTIVES πŸ‘‘

The Proxy module is the proxy implementation for H2O β€” it implements a reverse HTTP proxy and a CONNECT proxy. Two icons, one module. πŸ’…

  • A reverse HTTP proxy is set up using the proxy.reverse.url directive.
  • A CONNECT proxy is set up using the proxy.connect directive.

When acting as a reverse HTTP proxy, the following request headers are added and forwarded to the backend server:

via x-forwarded-for x-forwarded-proto

By default, all requests to the backend server are sent using HTTP/1.1. Use of HTTP/2 and HTTP/3 to backend servers is considered experimental; their use can be controlled via directives proxy.http2.ratio and proxy.http3.ratio.

Following sections describe the configuration directives defined for the module. 🫢

proxy.reverse.url

Forwards the requests to the specified backends, and proxies the response. The main character of proxy directives, ngl.

Example β€” Forwarding the requests to application server running on 127.0.0.1:8080:

proxy.reverse.url: "http://127.0.0.1:8080/"

Example β€” Forwarding the requests to multiple application servers with different weight:

proxy.reverse.url:
  - http://10.0.0.1:8080/
  - url: http://10.0.0.2:8080/different-path
    weight: 2

Example β€” Forwarding the requests to multiple application servers with least connection:

proxy.reverse.url:
  backends:
    - http://10.0.0.1:8080/
    - http://10.0.0.2:8080/
  balancer: least-conn

When more than one backend is declared, the load is distributed among the backends using the strategy specified by the balancer property. Currently we support round-robin (the default) and least-conn as the value of the property. The strategies are applied when establishing a new connection becomes necessary (i.e. when no pooled connections exist).

weight can be assigned to each backend as an integer between 1 and 256. The default value is 1.

For the round-robin balancer, weight is respected in this way: each backend would be selected exactly weight times before the next backend would be selected, except when the backend is not accessable.

For least-conn balancer, weight is respected in this way: the selected backend should have the minimum value of (request count) / (weight).

H2O will try to reconnect to different backends (in the order determined by the load balancing strategy) until it successfully establishes a connection. It returns an error when it fails to connect to all of the backends. 😭 Not a single one? Rough.

In addition to TCP/IP over IPv4 and IPv6, the proxy handler can also connect to an HTTP server listening to a Unix socket. The path to the unix socket should be surrounded by square brackets, and prefixed with unix: (e.g. http://[unix:/path/to/socket]/path). Slay, socket support. 🧚

proxy.connect

Set up a CONNECT proxy, taking an access control list as the argument.

Each element of the access control list starts with either + or -, followed by a wildcard (*) or an IP address with an optional netmask and an optional port number or port range.

When a CONNECT request is received and the name resolution of the connect target is complete, the access control list is searched from top to bottom. If the first entry that contains a matching address (and optionally the port number) starts with a +, the request is accepted and a tunnel is established. If the entry starts with a -, the request is rejected.

If none of the entries match, the request is also rejected. Default is "no thanks." πŸ™ˆ

Example β€” Simple HTTPS proxy:

proxy.connect:
- "-192.168.0.0/24"    # reject any attempts to local network
- "+*:443"             # accept attempts to port 443 of any host
- "+10.0.0.0/8:80-443" # accept attempts to ports 80 through 443 of 10.0.0.0/8

The syntax of each access control list element is address[/netmask][:port], where port is either a single port number, an inclusive range expressed as low-high, or * meaning any port (which is also the default when the port is omitted). For backward compatibility, the netmask may instead be specified after the port (i.e., address:port/netmask).

⚠️ The directive can only be used for the root path (i.e., /), as the classic CONNECT does not specify the path.

proxy.connect-udp

Set up a CONNECT-UDP gateway defined by RFC 9298.

The supplied argument is an access control list, using the same format as that of proxy.connect.

To be compatible with clients using the default URI Template defined in RFC 9298, the handler should be registered at path /.well-known/masque/udp/:

paths:
  "/.well-known/masque/udp":
    proxy.connect-udp:
      - "+*"

Support for draft-03 of the CONNECT-UDP protocol is controlled separately; see proxy.connect.masque-draft-03. πŸ’–

proxy.connect.emit-proxy-status

A boolean flag (ON or OFF) designating if a proxy-status response header should be sent.

See also: proxy.proxy-status.identity

proxy.connect.masque-draft-03

A boolean flag (ON or OFF) indicating if CONNECT-UDP requests conforming to draft-ietf-masque-connect-udp-03 should be handled.

This directive alters the behavior of proxy.connect because the CONNECT-UDP method defined in draft-03 followed the approach of the CONNECT method, which uses a HTTP proxy as a tunnel. The published RFC switched to specifying the tunnel by the target URI, and as a result, it is supported by a different directive: proxy.connect-udp. Glow-up of a spec, honestly. ✨

proxy.emit-x-forwarded-headers

(since v2.1) A boolean flag (ON or OFF) indicating if the server will append or add the x-forwarded-proto and x-forwarded-for request headers.

By default, when forwarding an HTTP request H2O sends its own x-forwarded-proto and x-forwarded-for request headers (or might append its value in the x-forwarded-proto case, see proxy.preserve-x-forwarded-proto). This might not be always desirable.

🚨 Please keep in mind security implications when setting this OFF, since it might allow an attacker to spoof the originator or the protocol of a request. Like, fr fr, be careful.

See also: proxy.emit-via-header

proxy.emit-via-header

(since v2.2) A boolean flag (ON or OFF) indicating if the server adds or appends an entry to the via request header.

See also: proxy.emit-x-forwarded-headers

proxy.emit-missing-date-header

(since v2.3) A boolean flag (ON or OFF) indicating if H2O should add a date header to the response, if that header is missing from the upstream response. Filling in the blanks, bestie. πŸŽ€

proxy.expect

(since v2.3) A boolean flag (ON or OFF) indicating if H2O should send expect: 100-continue header to the request, and postpone sending the request body until it receives 100 response.

proxy.forward.close-connection

A boolean flag indicating if closure of the backend connection should trigger the closure of the frontend HTTP/1.1 connection.

proxy.happy-eyeballs.connection-attempt-delay

Connection Attempt Delay parameter of Happy Eyeballs v2. Yes, that's the real name. 🌈

When trying to establish a connection to the CONNECT target, H2O uses Happy Eyeballs v2 (RFC 8305). This parameter controls the Connection Attempt Delay parameter of Happy Eyeballs v2 in the unit of milliseconds.

πŸ’€ At the moment, Happy Eyeballs is used only when acting as a CONNECT proxy. It is not used when running as an HTTP reverse proxy. Don't @ me, that's just how it is.

proxy.happy-eyeballs.name-resolution-delay

Name Resolution Delay parameter of Happy Eyeballs v2.

For detail, see proxy.happy-eyeballs.connection-attempt-delay.

proxy.header.add

(since v2.2) Modifies the request headers sent to the application server. πŸ”₯

okay so real quick, we're still finishing up proxy.header.add here β€” one last paragraph, then we move on πŸ’…

The vibe is exactly the same as header.add, with one tiny plot twist: it messes with the request headers going out to the application server instead of the response headers going back to the client. If you wanna know all the ways these directives can mangle headers, go read the docs for the headers handler β€” it's giving main character energy, fr fr.


proxy.header.append ✨

(since v2.2) Changes up the request headers that get sent to the application server.

Behaviour? Identical to header.append, except β€” you guessed it β€” it hits the request headers sent to the application server rather than the response headers sent to the client. Peep the headers handler documentation to see how these directives mangle headers, iykyk. πŸ¦‹


proxy.header.merge 🌸

(since v2.2) Changes up the request headers that get sent to the application server.

Behaviour is identical to header.merge, except it affects the request headers sent to the application server instead of the response headers sent to the client. The headers handler docs are where the mangle-how-to lives β€” go look. πŸŽ€


proxy.header.set πŸ”₯

(since v2.2) Changes up the request headers that get sent to the application server.

Behaviour = identical to header.set, except it affects the request headers sent to the application server and not the response headers sent to the client. Same deal as always: the headers handler documentation explains the mangling. πŸ‘‘


proxy.header.setifempty πŸ’œ

(since v2.2) Changes up the request headers that get sent to the application server.

Behaviour is identical to header.setifempty, except (say it with me) it affects the request headers sent to the application server rather than the response headers sent to the client. Headers handler docs β†’ mangling explanation. Slay. ✨


proxy.header.unset 🧚

(since v2.2) Changes up the request headers that get sent to the application server.

Identical behaviour to header.unset, except it's the request headers sent to the application server getting touched, not the response headers sent to the client. Documentation of the headers handler is where you find how to mangle β€” go read it bestie. πŸ’–


proxy.header.unsetunless 🫢

(since v2.2) Changes up the request headers that get sent to the application server.

Behaviour perfectly matches header.unsetunless, except it affects the request headers sent to the application server and not the response headers sent to the client. Mangle deets = headers handler docs. No cap. 🌈


Removes cookies in the requests with the given name.

See also: header.unset


Removes all cookies in the requests except the ones with the given names.

See also: header.unsetunless


proxy.http2.force-cleartext 😭

Just go see proxy.http2.ratio β€” it's all explained over there.


proxy.http2.max-concurrent-streams πŸ”₯

The maximum number of concurrent requests you can issue on a single HTTP/2 connection to the backend server.

But here's the thing: the actual number of max requests in flight gets capped to the minimum of this setting and whatever value the backend server advertises in its HTTP/2 SETTINGS frame. So like, you can't just will it higher, bestie. πŸ™ˆ

See also: proxy.http2.ratio


proxy.http2.ratio ⭐

The ratio of forwarded HTTP requests with which a use of HTTP/2 should be attempted.

When the backend protocol is HTTPS, for a given ratio of HTTP requests, h2o will either try to create a fresh HTTP/2 connection or reuse an existing one. Attempts to use HTTP/2 are signalled to the server via ALPN, with a fallback to HTTP/1.1 if that doesn't work out. πŸ’…

When the backend protocol is cleartext HTTP, this directive only does anything when the ratio is set to 100 and proxy.http2.force-cleartext is set to ON. In that specific case, every single backend connection uses HTTP/2 with no negotiation at all. Iconic behaviour honestly.


proxy.http3.ecn πŸ¦‹

Whether ECN should be used to detect congestion.

See also: Fine-tuning QUIC Behavior


proxy.http3.ratio 🌸

The ratio of forwarded HTTP requests with which a use of HTTP/3 should be attempted.

When the backend protocol is HTTPS, for a given ratio of HTTP requests, h2o will either try to create or reuse an existing HTTP/3 connection.

When the backend protocol is cleartext HTTP, this directive has no impact whatsoever. Zero. Nada. πŸ’€


proxy.max-buffer-size πŸŽ€

This one sets the max amount of userspace memory / disk space used for buffering each HTTP response being forwarded β€” in the unit of bytes.

By default, h2o buffers an unlimited amount of data being sent from backend servers. Why? The intention is to free up backend connections ASAP, working off the assumption that the backend server might have lower concurrency limits than h2o does. Makes sense, right?

But β€” if the backend server does have enough concurrency, you can use proxy.max-buffer-size to restrict the memory / disk pressure h2o causes, at the cost of having more connections to the backend server. Trade-offs, bestie.

See also: temp-buffer-threshold


proxy.preserve-host πŸ’–

A boolean flag (ON or OFF) that designates whether or not to pass the Host header from the incoming request to the upstream. Simple as that. ✨


proxy.preserve-x-forwarded-proto πŸ”₯

(since v2.0) A boolean flag (ON or OFF) indicating if the server should preserve the received x-forwarded-proto request header.

So by default, when transmitting an HTTP request to an upstream HTTP server, H2O removes the received x-forwarded-proto request header and sends its own instead β€” a precaution measure so an attacker coming in through HTTP can't lie and claim they're connected via HTTPS. Sneaky behaviour = blocked. πŸ™…β€β™€οΈ

But if H2O is running behind a trusted HTTPS proxy, that protection might not be desirable, and that's exactly what this configuration directive is for β€” modifying the behaviour. 🫢


proxy.proxy-protocol πŸ¦‹

(since v2.1) A boolean flag (ON or OFF) indicating if the PROXY protocol should be used when connecting to the application server.

Heads up bestie: when you're using the PROXY protocol, connections to the application server cannot be persistent β€” meaning proxy.timeout.keepalive must be set to zero. Non-negotiable. πŸ’€

See also: proxy.timeout.keepalive


proxy.proxy-status.identity πŸ‘‘

Specifies the name of the server to be emitted as part of the proxy-status header field.

See also: proxy.connect.proxy-status


proxy.ssl.cafile πŸ”

(since v2.0) Specifies the file storing the list of trusted root certificates.

By default, H2O uses share/h2o/ca-bundle.crt. That file contains a set of trusted root certificates maintained by Mozilla, downloaded and converted using mk-ca-bundle.pl. Cute, right? 🌸

See also: proxy.ssl.verify-peer


proxy.ssl.session-cache ✨

(since v2.1) Specifies whether-if and how-if a session cache should be used for TLS connections to the application server. Grammar's a little wobbly but the vibes are clear. πŸ’…

Since version 2.1, the result of TLS handshakes to the application server gets memoized and later used to resume the connection β€” unless you set this to OFF, obviously. If the value is a mapping, then these two attributes must be specified:

  • lifetime: validity of session cache entries in seconds
  • capacity: maximum number of entries to be kept in the session cache

If you set it to ON, then lifetime and capacity get set to 86,400 (one day) and 4,096 respectively. Free defaults, we love to see it. πŸŽ€


proxy.ssl.verify-peer 🫢

(since v2.0) A boolean flag (ON or OFF) indicating if the server certificate and hostname should be verified.

If set to ON, H2O's HTTP client implementation verifies the peer's certificate using the list of trusted certificates, and compares the hostname presented in the certificate against the connecting hostname. Two checks, both slaying. βœ…

See also: proxy.ssl.cafile


proxy.timeout.connect ⏱️

(since v2.3) Sets the timeout before establishing the upstream, in milliseconds.

Little detail: when you're connecting to a TLS upstream, this timeout runs all the way until the end of the SSL handshake. So it's not just the TCP bit, okay? 😌


proxy.timeout.first_byte ⏱️

(since v2.3) Sets the timeout before receiving the first byte from upstream.

This is the maximum time we'll wait for that first byte from upstream, counted after the connection has been established. The anticipation. 😭


proxy.timeout.io ⏱️

Sets the upstream I/O timeout, in milliseconds.

Plot twist: this value also gets used for proxy.timeout.connect and proxy.timeout.first_byte, unless those parameters are explicitly set themselves. So it's secretly the default for the whole timeout family. 🌈


proxy.timeout.keepalive πŸ’€

Sets the upstream timeout for idle connections, in milliseconds.

If the value is set to zero, the upstream connection becomes non-persistent. And bestie, the value should be set to something smaller than whatever's set at the upstream server. Just trust me on that one. πŸ“


proxy.tunnel πŸš‡

A boolean flag (ON or OFF) indicating whether or not to allow tunnelling to the backend server.

When set to ON, CONNECT requests and WebSocket handshakes get forwarded to the backend server. Then β€” if the backend server accepts those requests β€” H2O forwards the HTTP response to the client and acts as a bi-directional tunnel. It's giving middleman, but make it legitimate. πŸ’…

Timeouts here are governed by the properties proxy.timeout.connect and proxy.timeout.io.


proxy.zerocopy πŸ”₯

Sets the use of zerocopy operations for forwarding the response body.

By default this flag is OFF, and in that case the response bytes get read from the upstream socket into an internal buffer as they arrive, then shipped off to the client. The max size of that buffer is controlled by proxy.max-buffer-size. The drawback? It puts pressure on memory bandwidth. 😭

This knob gives you two alternative modes to fix that pressure:

  • enabled β€” if zerocopy operation is supported by the downstream connection (i.e. the downstream connection being cleartext or encrypted using kernel TLS), h2o uses a pipe as an internal buffer instead of userspace memory. Data gets moved to the pipe using the splice system call, then shipped to the downstream connection by another call to splice. Pressure on memory bandwidth? Eliminated β€” because splice merely moves the references to kernel memory between file descriptors. Iconic. ✨
  • always β€” data from upstream gets spliced into a pipe regardless of whether the downstream connection supports zerocopy. When the downstream connection doesn't support zerocopy, data is initially moved into the pipe, then read and written to the socket (as well as encrypted, if necessary) as late as it becomes possible to send the data. This approach doesn't reduce the total amount of bytes flowing through the CPU, but it does reduce the amount of userspace memory used by h2o by delaying the reads β€” thereby reducing cache spills. Big brain energy. 🧠

See also: proxy.max-spare-pipes, ssl-offload

πŸŽ€ REDIRECT DIRECTIVES πŸŽ€

Okay bestie, this one is all about the config directives for the redirect handler. It's giving β€œmove the request somewhere else” energy πŸ’…

πŸ” redirect

Redirects the requests to the given URL.

This directive rewrites the URL by replacing the host and path part of the URL where you put the directive with the given URL. So, for example, when you use the config below, requests to http://example.com/abc.html will be redirected to https://example.com/abc.html. Slay.

If the argument is a scalar, that value is considered the URL to where the requests should be redirected.

If the argument is a mapping, these properties are recognized:

  • url β€” URL to redirect to
  • status β€” the three-digit status code to use (e.g. 301)
  • internal β€” either YES or NO (default); if set to YES, then the server performs an internal redirect and returns the content at the redirected URL

Example: Redirect all HTTP to HTTPS permanently (except for the files under RSS)

hosts:
    "example.com:80":
        paths:
            "/":
                redirect:
                    status: 301
                    url:    "https://example.com/"
            "/rss":
                file.dir: /path/to/rss

πŸ‘‘ REPROXY DIRECTIVES πŸ‘‘

This document describes the configuration directives of the reproxy handler. Very β€œfetch this for me instead” of you πŸ’–

🫢 reproxy

A boolean flag (ON or OFF) indicating if the server should recognize the X-Reproxy-URL header sent from upstream servers.

If H2O recognizes the header, it fetches the contents of the resource specified by the header, and sends the contents as the response to the client. If the status code associated with the X-Reproxy-URL header is 307 or 308, then the method of the original request is used to obtain the specified resource. Otherwise, the request method is changed to GET.

For example, an upstream server may send an URL pointing to a large image using the X-Reproxy-URL header stored on a distributed file system, and let H2O fetch and return the content to the client instead of fetching the image by itself. Doing so would reduce the load on the application server. Iconic load-shedding behavior, no cap.


🌸 STATUS DIRECTIVES 🌸

The status handler exposes the current states of the HTTP server. This document describes the configuration directives of the handler. It's the β€œlet me see what's happening in here” era ✨

⭐ status

(since v2.0) If the argument is ON, the directive registers the status handler to the current path.

Access to the handler should be restricted, considering the fact that the status includes the details of in-flight HTTP requests. The example below uses Basic authentication.

Example: Exposing status with Basic authentication

paths:
  /server-status:
    mruby.handler: |
      require "htpasswd.rb"
      Htpasswd.new("/path/to/.htpasswd", "status")
    status: ON

The information returned by the /json handler can be filtered out using the optional show=module1,module2 parameter. There are currently three modules defined:

  • requests β€” displays the requests currently in-flight.
  • durations β€” displays durations statistics for requests since server start time in seconds (returns all zeros unless duration-stats is ON).
  • errors β€” displays counters for internally generated errors.
  • main β€” displays general daemon-wide stats.

πŸ¦‹ duration-stats

(since v2.1) Gather timing stats for requests.

If the argument is ON, this directive populates duration statistics in seconds, to be consumed by status handlers. Enabling this feature has a noticeable CPU and memory impact. So like… she's useful but she's not free, bestie πŸ’€

Note that the time spent while processing a request in a blocking manner (such as opening a file or a mruby handler that does invoke a network operation) will not be reflected to the process_time element of the duration stats due to the fact that the timer being used for measuring the time spent is updated only once per loop.


πŸ”₯ THROTTLE RESPONSE DIRECTIVES πŸ”₯

The throttle response handler performs per response traffic throttling when an X-Traffic header exists in the response headers. It's giving β€œslow down, gorgeous” πŸ’…

The value of X-Traffic header should be an integer that represents the speed you want in bytes per second. This header CAN be set with header.add so that traffic for static assets can also be easily throttled. Very convenient, very demure.

The following are the configuration directives recognized by the handler.

πŸ“ throttle-response

(since v2.1) Enables traffic throttle per HTTP response.

If the argument is ON, the traffic per response is throttled as long as a legal X-Traffic header exists. If the argument is OFF, traffic throttle per response is disabled.

Example: Enabling traffic throttle per response with static file configuration

# enable throttle
throttle-response: ON

# an example host configuration that throttle traffic to ~100KB/s
hosts:
  default:
    paths:
      /:
        file.dir: /path/to/assets
        header.add: "X-Traffic: 100000"

πŸ’œ USING BASIC AUTHENTICATION πŸ’œ

Starting from version 1.7, H2O comes with a mruby script named htpasswd.rb that implements Basic Authentication. The script provides a Rack handler that implements Basic Authentication using password files generated by the htpasswd command. Gatekeeping, but make it Unix ✨

Below example uses the mruby script to restrict access to the path. If authentication fails, the mruby handler returns a 401 Unauthorized response. If authentication succeeds, the handler returns a 399 response, and the request is delegated internally to the next handler (i.e. file.dir).

Example: Configuring HTTP authentication using htpasswd.rb

paths:
  "/":
    mruby.handler: |
      require "htpasswd.rb"
      Htpasswd.new("/path/to/.htpasswd", "realm-name")
    file.dir: /path/to/doc_root

In H2O versions prior to 2.0, you should specify "#{$H2O_ROOT}/share/h2o/mruby/htpasswd.rb" as the argument to require, since the directory is not registered as part of $LOAD_PATH.

For convenience, the mruby script also forbids access to files or directories that start with .ht.


🧚 USING CGI 🧚

Starting from version 1.7, H2O comes with a FastCGI-to-CGI gateway (fastcgi-cgi), which can be found under share/h2o directory of the installation path. The gateway can be used for running CGI scripts through the FastCGI handler. It's the little bridge between eras, and she ate 🫢

The example below maps .cgi files to be executed by the gateway. It is also possible to run CGI scripts under different privileges by specifying the user attribute of the directive.

Example: Execute .cgi files using FastCGI-to-CGI gateway

file.custom-handler:
  extension: .cgi
  fastcgi.spawn:
    command: "exec $H2O_ROOT/share/h2o/fastcgi-cgi"

The gateway also provides options for tuning the behavior. A full list of options can be obtained by running the gateway directly with --help option.

Example: Output of share/h2o/fastcgi-cgi --help

$ share/h2o/fastcgi-cgi --help
Usage:
    share/h2o/fastcgi-cgi [options]

Options:
  --listen=sockfn    path to the UNIX socket.  If specified, the program will
                     create a UNIX socket at given path replacing the existing
                     file (should it exist).  If not, file descriptor zero (0)
                     will be used as the UNIX socket for accepting new
                     connections.
  --max-workers=nnn  maximum number of CGI processes (default: unlimited)
  --pass-authz       if set, preserves HTTP_AUTHORIZATION parameter
  --verbose          verbose mode

✨ USING MRUBY ✨

okay so bestie, mruby is like Ruby's lighter, speedier little sibling β€” a lightweight implementation of the Ruby programming language πŸ’Ž. And with H2O you can use it to write your own request-handling logic: generate responses, or fix up the request / response on the fly. It's giving DIY queen energy πŸ‘‘.


πŸŽ€ Rack-based Programming Interface

The interface between your mruby program and the H2O server is based on the Rack interface specification. Below is a simple config that just returns hello world β€” the classic, the icon, the moment:

Example: Hello-world in mruby

paths:
  "/":
    mruby.handler: |
      Proc.new do |env|
        [200, {'content-type' => 'text/plain'}, ["Hello world0]]
      end

ngl, you should know that as of H2O version 1.7.0, there are limitations compared to an ordinary web application server with Rack support (think Unicorn):

  • no libraries provided as part of Rack is available (only the interface is compatible) πŸ’€

In addition to the Rack interface specification, H2O recognizes status code 399, which you can use to delegate the request to the next handler. It's literally the "no thoughts, next please" status code πŸ™ˆ. This feature is what lets you implement access control and response header modifiers.


πŸ”’ Access Control

By using the 399 status code, you can implement access control with mruby. The example below restricts access to requests from 192.168. private address.

Example: Restricting access to 192.168.

paths:
  "/":
    mruby.handler: |
      lambda do |env|
        if /168./.match(env["REMOTE_ADDR"])
          return [399, {}, []]
        end
        [403, {'content-type' => 'text/plain'}, ["access forbidden0]]
      end

Support for Basic Authentication is also provided by an mruby script β€” yes, auth queen, we love to see it 🫢.


πŸ¦‹ Delegating the Request

When enabled using the reproxy directive, it is possible to delegate the request from the mruby handler to any other handler. Hand-off slay ✨.

Example: Rewriting URL with delegation

paths:
  "/":
    mruby.handler: |
      lambda do |env|
        if /user([^]+)/.match(env["PATH_INFO"])
          return [307, {"x-reproxy-url" => "/user.php?user=#{$1}"}, []]
        end
        return [399, {}, []]
      end

πŸ’… Modifying the Response

When the mruby handler returns status code 399, H2O delegates the request to the next handler while preserving the headers emitted by the handler. That's the whole trick β€” you can use it to add extra headers to the response. Chef's kiss πŸŽ€.

For example, the following example sets cache-control header for requests against .css and .js files:

Example: Setting cache-control header for certain types of files

paths:
  "/":
    mruby.handler: |
      Proc.new do |env|
        headers = {}
        if /.(css|js).match(env["PATH_INFO"])
          headers["cache-control"] = "max-age=86400"
        end
        [399, headers, []]
      end
    file.dir: /path/to/doc-root

Or, in the example below, the handler triggers HTTP/2 server push with the use of Link: rel=preload headers, and then requests a FastCGI application to process the request. Pushing assets and delegating? main character energy fr fr πŸ”₯.

Example: Pushing asset files

paths:
  "/":
    mruby.handler: |
      Proc.new do |env|
        push_paths = []
        # push css and js when request is to dir root or HTML
        if /(|.html).match(env["PATH_INFO"])
          push_paths << ["/css/style.css", "style"]
          push_paths << ["/js/app.js", "script"]
        end
        [399, push_paths.empty? ? {} : {"link" => push_paths.map{|p| "<#{p[0]}>; rel=preload; as=#{p[1]}"}.join("0)}, []]
      end
    fastcgi.connect: ...

🌈 Using the HTTP Client

Starting from version 1.7, a HTTP client API is provided πŸ‘‘. HTTP requests issued through the API will be handled asynchronously; the client does not block the event loop of the HTTP server. No blocking, no waiting around, we stay unbothered πŸ’œ.

Example: Mruby handler returning the response of http://example.com

paths:
  "/":
    mruby.handler: |
      Proc.new do |env|
        req = http_request("http://example.com")
        status, headers, body = req.join
        [status, headers, body]
      end

http_request is the method that issues a HTTP request.

The method takes two arguments. First argument is the target URI. Second argument is an optional hash; method (defaults to GET), header, body attributes are recognized.

The method returns a promise object. When #join method of the promise is invoked, a three-argument array containing the status code, response headers, and the body is returned. The response body is also a promise. Applications can choose from three ways when dealing with the body:

  • a) call #each method to receive the contents
  • b) call #join to retrieve the body as a string
  • c) return the object as the response body of the mruby handler

Pick your fighter, bestie πŸ₯Š.

The header and the body object passed to http_request should conform to the requirements laid out by the Rack specification for request header and request body. The response header and the response body object returned by the #join method of the promise returned by http_request conforms to the requirements of the Rack specification.

Since the API provides an asynchronous HTTP client, it is possible to effectively issue multiple HTTP requests concurrently and merge them into a single response. Multitasking icon ⭐.

When HTTPS is used, servers are verified using the properties of proxy.ssl.cafile and proxy.ssl.verify-peer specified at the global level.

Timeouts defined for the proxy handler (proxy.timeout.*) are applied to the requests that are issued by the http_request method.


πŸ“ Logging Arbitrary Variable

In version 2.3, it is possible from mruby to set and log an arbitrary-named variable that is associated to a HTTP request πŸ’«. A HTTP response header that starts with x-fallthru-set- is handled specially by the H2O server. Instead of sending the header downstream, the server accepts the value as a request environment variable, taking the suffix of the header name as the name of the variable. Sneaky? No. Iconic? Yes πŸ™ˆ.

This example shows how to read request data, parse json and then log data from mruby:

Example: Logging the content of a POST request via request environment variable

paths:
  "/":
    mruby.handler: |
      Proc.new do |env|
        input = env["rack.input"] ? env["rack.input"].read : '{"default": "true"}'
        parsed_json = JSON.parse(input)
        parsed_json["time"] = Time.now.to_i
        logdata = parsed_json.to_s
        [204, {"x-fallthru-set-POSTDATA" => logdata}, []]
      end
    access-log:
      path: /path/to/access-log.json
      escape: json
      format: '{"POST": %{POSTDATA}e}'

πŸ‘‘ USING DOS DETECTION πŸ‘‘

Starting from version 2.1, H2O comes with a mruby script named dos_detector.rb that implements the DoS Detection feature πŸ”₯. The script provides a Rack handler that detects HTTP flooding attacks based on the client's IP address. Blocking the haters, one IP at a time πŸ’….


🌸 Basic Usage

Below example uses the mruby script to detect DoS attacks. The default detecting strategy is simply counting requests within a configured period. If the count exceeds the configured threshold, the handler returns a 403 Forbidden response. Otherwise, the handler returns a 399 response, and the request is delegated internally to the next handler. Simple, effective, slay ✨.

Example: Configuring DoS Detection

paths:
  "/":
    mruby.handler: |
      require "dos_detector.rb"
      DoSDetector.new({
        :strategy => DoSDetector::CountingStrategy.new({
          :period     => 10,  # default
          :threshold  => 100, # default
          :ban_period => 300, # default
        }),
      })
    file.dir: /path/to/doc_root

In the example above, the handler countup the requests within 10 seconds for each IP address, and when the count exceeds 100, it returns a 403 Forbidden response for the request and marks the client as "Banned" for 300 seconds. While marked as "Banned", the handler returns a 403 Forbidden to all requests from the same IP address. It's giving "you're on timeout, think about what you did" 😭.


🧚 Configuring Details

You can pass the following parameters to DoSDetector.new .

  • :strategy β€” The algorithm to detect DoS attacks. You can write and pass your own strategies if needed. The default strategy is DoSDetector.CountingStrategy which takes the following parameters:
    • :period β€” Time window in seconds to count requests. The default value is 10.
    • :threshold β€” Threshold count of request. The default value is 100.
    • :ban_period β€” Duration in seconds in which "Banned" client continues to be restricted. The default value is 300.
  • :callback β€” The callback which is called by the handler with detecting result. You can define your own callback to return arbitrary response, set response headers, etc. The default callback returns 403 Forbidden if DoS detected, otherwise delegate the request to the next handler.
  • :forwarded β€” If set true, the handler uses X-HTTP-Forwarded-For header to get client's IP address if the header exists. The default value is true.
  • :cache_size β€” The capacity of the LRU cache which preserves client's IP address and associated request count. The default value is 128.

Example: Configuring Details

paths:
  "/":
    mruby.handler: |
      require "dos_detector.rb"
      DoSDetector.new({
        :strategy => DoSDetector::CountingStrategy.new,
        :forwarded => false,
        :cache_size => 2048,
        :callback => proc {|env, detected, ip|
          if detected && ! ip.start_with?("192.168.")
            [503, {}, ["Service Unavailable"]]
          else
            [399, {}, []]
          end
        }
      })
    file.dir: /path/to/doc_root

πŸ’€ Points to Notice

For now, counting requests is "per-thread" and not shared between multiple threads. ⚠️ So don't expect the counts to be gossiping across threads β€” they're each keeping their own little diary, iykyk.

πŸ’– ACCESS CONTROL πŸ’–

Starting from version 2.1, H2O comes with a DSL-like mruby library which makes it easy to write access control list (ACL). Bestie, it's giving "tiny firewall but make it cute" and honestly? Obsessed.

✨ Example ✨

Below example uses this Access Control feature to write various access control.

Example: Access Control

paths:
  "/":
    mruby.handler: |
      acl {
        allow { addr == "127.0.0.1" }
        deny { user_agent.match(/curl/i) && ! addr.start_with?("192.168.") }
        respond(503, {}, ["Service Unavailable"]) { addr == malicious_ip }
        redirect("https://example.com/", 301) { path =~ /moved/ }
        use Htpasswd.new("/path/to/.htpasswd", "realm") { path.start_with?("/admin") }
      }
    file.dir: /path/to/doc_root

In the example, the handler you get by calling acl method will do the following:

  • if the remote IP address is exactly equal to "127.0.0.1", the request will be delegated to the next handler (i.e. serve files under /path/to/doc_root) and all following acl settings are ignored
  • otherwise, if the user agent string includes "curl" and the remote IP address doesn't start with "192.168.", this handler immediately returns 403 Forbidden response
  • otherwise, if the remote IP address is exactly equal to the malicious_ip variable, this handler immediately returns 503 Service Unavailable response
  • otherwise, if the request path matches with the pattern /moved/i, this handler immediately redirects the client to "https://example.com" with 301 status code
  • otherwise, if the request path starts with /admin, apply Basic Authentication to the request (for details of Basic Authentication, see here)
  • otherwise, the request will be delegated to the next handler (i.e. serve files under /path/to/doc_root)

πŸŽ€ ACL Methods πŸŽ€

An ACL handler is built by calling ACL methods, which can be used like directives. ACL methods can only be used in acl block.

Each ACL method adds a filter to the handler, which checks whether the request matches the provided condition or not. Every ACL method can be accompanied by a condition block, which should return boolean value.

The filter defined by the method that first matched the accompanying condition gets applied (e.g. response 403 Forbidden, redirect to somewhere). If a condition block is omitted, all requests matches. If none of the conditions matches the request, the handler returns 399 and the request will be delegated to the next handler.

allow

Adds a filter which delegates the request to the next handler if the request matches the provided condition.

allow { ..condition.. }

deny

Adds a filter which returns 403 Forbidden if the request matches the provided condition.

deny { ..condition.. }

redirect

Adds a filter which redirects the client if the request matches the provided condition.

redirect(location, status) { ..condition.. }

Parameters:

  • location: Location to which the client will be redirected. Required.
  • status: Status code of the response. Default value: 302

respond

Adds a filter which returns arbitrary response if the request matches the provided condition.

respond(status, header, body) { ..condition.. }

Parameters:

  • status: Status code of the response. Required.
  • header: Header key-value pairs of the response. Default value: {}
  • body: Body array of the response. Default value: []

use

Adds a filter which applies the provided handler (callable object) if the request matches the provided condition.

use(proc) { ..condition.. }

Parameters:

  • proc: Callable object that should be applied

πŸ¦‹ Matching Methods πŸ¦‹

In a condition block, you can use helpful methods which return particular properties of the request as string values. Matching methods can only be used in a condition block of the ACL methods.

addr

Returns the remote IP address of the request.

addr(forwarded)

Parameters:

  • forwarded: If true, returns the value of X-Forwarded-For header if it exists. Default value: true

path

Returns the requested path string of the request.

path()

method

Returns the HTTP method of the request.

method()

Returns the header value of the request associated with the provided name.

header(name)

Parameters:

  • name: Case-insensitive header name. Required.

user_agent

Shortcut for header("user-agent").

user_agent()

⚠️ Caution ⚠️

Several restrictions are introduced to avoid misconfiguration when using acl method.

  • acl method can be called only once in each handler configuration.
  • If acl method is used, the handler returned by the configuration directive must be the one returned by the acl method.

If a configuration violates these restrictions, the server will detect it and refuse to launch with error message.

For example, both of the following examples violate the restrictions above, so the server will refuse to start up.

Example: Misconfiguration Example 1

paths:
  "/":
    mruby.handler: |
      acl {    # this block will be ignored!
        allow { addr == "127.0.0.1" }
      }
      acl {
        deny
      }
    file.dir: /path/to/doc_root

Example: Misconfiguration Example 2

paths:
  "/":
    mruby.handler: |
      acl {    # this block will be ignored!
        allow { addr == "127.0.0.1" }
        deny
      }
      proc {|env| [399, {}, []}
    file.dir: /path/to/doc_root

You can correct these like the following:

Example: Valid Configuration Example

paths:
  "/":
    mruby.handler: |
      acl {
        allow { addr == "127.0.0.1" }
        deny
      }
    file.dir: /path/to/doc_root

🧚 How-To 🧚

🌸 Matching IP Address Blocks 🌸

You can match an IP address against predefined list of address blocks using a script named trie_addr.rb.

Below is an example.

Example: Address Block Matching Example

paths:
  "/":
    mruby.handler: |
      require "trie_addr.rb"
      trie = TrieAddr.new.add(["192.168.0.0/16", "172.16.0.0/12"])
      acl {
        allow { trie.match?(addr) }
        deny
      }
    file.dir: /path/to/doc_root

This library currently supports only IPv4 addresses. TrieAddr#match? returns false when it receives an invalid IPv4 address (including an IPv6 address) as an argument..


πŸ”₯ USING H2OLOG FOR TRACING πŸ”₯

h2olog is an experimental BPF (kernel doc) backed tracing tool for the H2O server. It can be used for tracing quicly and h2o USDT probes.

Since h2olog is an experimental program, its command-line interface might change without notice.

Installing from Source

See requirements for build prerequisites. If dependencies are satisfied, h2olog is built automatically. It is possible to manually turn on / off the build of h2olog by using the -DWITH_H2OLOG option. This option takes either ON> or OFF as the argument. If you have BCC installed to a non-standard path, use pkg-config for cmake.

$ PKG_CONFIG_PATH=/path/to/bcc/lib/pkgconfig cmake [options]

Requirements

For building h2olog:

  • C++11 compiler
  • CMake for generating the build files
  • pkg-config for detecting dependencies
  • Python 3 for the code generator
  • BCC (BPF compiler collection, a.k.a. bpfcc; >= 0.11.0) installed on your system

For Ubuntu 20.04 or later, you can install dependencies with:

$ sudo apt install clang cmake python3 libbpfcc-dev linux-headers-$(uname -r)

For running h2olog:

  • Root privilege to execute h2olog
  • Linux kernel (>= 4.10)

Quickstart

h2olog -H -p $H2O_PID shows varnishlog-like tracing.

$ sudo h2olog -H -p $(pgrep -o h2o)

11 0 RxProtocol HTTP/3.0
11 0 RxHeader   :authority torumk.com
11 0 RxHeader   :method GET
11 0 RxHeader   :path /
11 0 RxHeader   :scheme https
11 0 TxStatus   200
11 0 TxHeader   content-length 123
11 0 TxHeader   content-type text/html

Tracing USDT events

Server-side QUIC events can be traced using the quic subcommand. Events are rendered in JSON Lines format.

$ sudo h2olog -p $(pgrep -o h2o)

Here’s an example trace.

{"time":1584380825832,"type":"accept","conn":1,"dcid":"f8aa2066e9c3b3cf"}
{"time":1584380825835,"type":"crypto-decrypt","conn":1,"pn":0,"len":1236}
{"time":1584380825832,"type":"quictrace-recv","conn":1,"pn":0}
{"time":1584380825836,"type":"crypto-handshake","conn":1,"ret":0}

That's the whole H2O.CONF(5) saga, bestie β€” source/version is H2O.CONF(5), and the footer date is doing its little timestamp moment. Consider it signed, sealed, and slayed. πŸ’…βœ¨

πŸ“¦ where this page comes from