Sea trials
Live test servers with known, documented behaviour. Point a browser, a command line client or your own parser at them and compare what you see with what each one is documented to send. We built them to test the capture engine inside Shorenet Trawler.
Servers
Each host is one handshake profile: a TLS version, a key-exchange group, an ALPN, resumption or ECH behaviour. The same paths are served on every host listed against them.
| Host | Profile |
|---|---|
lab.shorenet.co.uk | The estate apex: the endpoint list, over HTTP/1.1 or HTTP/2 |
h1.lab.shorenet.co.uk | HTTP/1.1 over TLS 1.3 |
h2.lab.shorenet.co.uk | HTTP/2 over TLS 1.3 |
ws9573.lab.shorenet.co.uk:9573 | TLS 1.3 on a non-standard port |
resume.lab.shorenet.co.uk | TLS 1.3 resumption / 0-RTT |
h1.lab.shorenet.co.uk:80 | Plain HTTP/1.1, no TLS |
kex-x25519.lab.shorenet.co.uk | group X25519 |
kex-p256.lab.shorenet.co.uk | group secp256r1 |
kex-p384.lab.shorenet.co.uk | group secp384r1 |
kex-mlkem768.lab.shorenet.co.uk | group X25519MLKEM768 (post-quantum hybrid; what current Chrome/Edge/Firefox send by default) |
kex-hrr.lab.shorenet.co.uk | HelloRetryRequest: the server insists on a group the client did not key-share |
resume-psk.lab.shorenet.co.uk | PSK resumption, no early data |
resume-ticket.lab.shorenet.co.uk | Session tickets issued on one connection and used on the next |
ticket-reject.lab.shorenet.co.uk | issues session tickets and accepts none back: every connection is a full handshake |
ticket-none.lab.shorenet.co.uk | sends no NewSessionTicket |
ech-own.lab.shorenet.co.uk | ECH with keys this server holds; the ECHConfig is published in an HTTPS record |
ech-reject.lab.shorenet.co.uk | ECH offered but rejected, with retry_configs returned |
ech-grease.lab.shorenet.co.uk | No real ECH; the client sends GREASE ECH (what Chrome does by default) |
Non-HTTP profiles
These complete a TLS handshake and then speak no HTTP. They are not web pages, so a browser has nothing to open: the handshake itself is the fixture, and what it shows you is the protocol the server selects. The command beside each one prints that and needs nothing installed beyond OpenSSL.
| Host | Profile | Try it |
|---|---|---|
noalpn.lab.shorenet.co.uk | TLS 1.3, the server selects no ALPN | openssl s_client -connect noalpn.lab.shorenet.co.uk:443 -servername noalpn.lab.shorenet.co.uk -alpn h2,http/1.1 </dev/null |
dot.lab.shorenet.co.uk | TLS 1.3, ALPN dot (DNS over TLS, RFC 7858) | openssl s_client -connect dot.lab.shorenet.co.uk:443 -servername dot.lab.shorenet.co.uk -alpn dot </dev/null |
alpn.lab.shorenet.co.uk | TLS 1.3, a custom non-HTTP ALPN | openssl s_client -connect alpn.lab.shorenet.co.uk:443 -servername alpn.lab.shorenet.co.uk -alpn shorenet-fixture/1 </dev/null |
One server on Cloudflare
lab-cf.shorenet.co.uk is a Sea trials server that runs on Cloudflare rather than on the machine behind the hosts above, so what you meet there is Cloudflare's own edge. Its front page lists the paths it serves.
It is the only Sea trials server that offers HTTP/3. That is why /f/ech-h3 carries h3 in its name: on the hosts in the tables on this page it answers over HTTP/1.1 and HTTP/2 only.
Cloudflare sees your full IP address on requests to it and logs them under its own terms. Our own code there records the same shortened address and the same fixed list of fields as the other servers, but the record goes to Cloudflare's logging, so the seven days below do not apply to it.
Each Cloudflare instance serving it limits an address to 20 requests a second, with bursts of 50, counted in that instance's memory and never stored.
Paths
What each path sends on the wire, and a link to it on every host that serves it. The names in the last column are the profile names from the table above: apex is lab.shorenet.co.uk and plain80 is the same server on port 80, over plain HTTP with no TLS. Where a path takes a parameter, each documented value has its own link.
| Path | On the wire | Open it on |
|---|---|---|
/f/ce-identity | HTTP 200, Content-Encoding: identity, 43-byte image/gif | h1 h2 apex |
/f/ce-none | HTTP 200, Content-Encoding: none, same 43-byte body, untransformed | h1 h2 apex |
/f/ce-utf8 | HTTP 200, Content-Encoding: utf-8, body untransformed | h1 h2 apexvalues on h1: v=626 v=7132 |
/f/ce-gzip | HTTP 200, a correctly gzip-encoded body | h1 h2 apex |
/f/ce-gzip-mislabelled | HTTP 200, gzip bytes labelled Content-Encoding: deflate | h1 h2 apex |
/f/status-{code} | A bare response at that status | h1 h2 apexvalues on h1: 204 206 304 404 500 |
/f/bulk-body | HTTP 200, a large deterministic body | h1 h2 apexvalues on h1: v=2 v=8 v=32 |
/f/sse-trickle | text/event-stream, a fixed number of events at a fixed spacing, then the stream held open for 30 s and closed by the server. Set the count with ?n= (1, 5, 20 or 100) and the gap in milliseconds with ?ms= (10, 50, 250 or 1000); any other value is refused with 400 | h1 h2 apexvalues on h1: n=1 n=5 n=20 n=100 ms=10 ms=50 ms=250 ms=1000 |
/f/body-slow | HTTP 200, body written one byte at a time, 50 ms apart. Set the length with ?n= (8, 64 or 256 bytes); any other value is refused with 400 | h1 h2 apexvalues on h1: n=8 n=64 n=256 |
/f/h1-keepalive-gzip | Identical gzip responses (Content-Length 1990, 5430 B decoded) on one keep-alive connection. Request it repeatedly on the same connection | h1 plain80 apex |
/f/close-delimited | 302, Connection: close, no Content-Length and no Transfer-Encoding, empty body, then close_notify then FIN | h1 apex plain80 |
/f/ws-echo | WebSocket upgrade (permessage-deflate when offered), then a byte-exact echo | not a link: a WebSocket needs a client, so use /t/ws-echo on h1 /t/ws-echo on h2 |
/f/ws-ce-none | WebSocket upgrade whose 101 carries Content-Encoding: none | not a link: a WebSocket needs a client, so use /t/ws-ce-none on h1 |
/f/h2-trailers | Response trailers after DATA (a trailer field carrying the body's SHA-256) | h2 apex |
/f/ech-h3 | HTTP 200, text/plain | h1 h2 apex |
/f/probe | HTTP 200, a short deterministic body | resume kex-x25519 kex-p256 kex-p384 kex-mlkem768 kex-hrr resume-psk resume-ticket-crossconn ech-own ech-reject ech-grease ticket-reject ticket-none |
/f/early-hints | 103 Early Hints with two Link fields, then a 200 HTML page | h1 h2 apex |
/f/interim-100 | An unsolicited 100 Continue, then 200 | h1 h2 apex |
/f/chunked-trailers | HTTP/1.1 chunked body followed by a trailer field carrying the body's SHA-256 | h1 plain80 |
/f/trailers-bodiless | A response head, no content, then a trailer section (on HTTP/2: HEADERS, then HEADERS with END_STREAM and no DATA) | h1 h2 apex |
/f/data-frames | A 32 KiB body written as 512 separately flushed 64-byte pieces (512 DATA frames on HTTP/2, 512 chunks on HTTP/1.1) | h1 h2 apex |
/f/abort-mid-body | 16 KiB of a body with no declared length, then the server stops: RST_STREAM(INTERNAL_ERROR) on HTTP/2, a chunked body with no last chunk and a closed connection on HTTP/1.1 | h1 h2 apex |
/f/abort-mid-length | Content-Length 65536, 16 KiB delivered, then the server stops (RST_STREAM on HTTP/2, connection close on HTTP/1.1) | h1 h2 apex |
/f/h2-early-response | A 200 sent before the request content has ended; the stream then ends with RST_STREAM(NO_ERROR) | h2 apex |
/f/hpack-churn | 24 distinct header fields of about 200 bytes each: more than a 4,096-byte HPACK dynamic table holds | h1 h2 apex |
/f/huge-headers | About 48 KiB of response header fields (on HTTP/2: HEADERS plus CONTINUATION frames) | h1 h2 apex |
/f/header-obs-text | Header field values containing bytes above 0x7F: a UTF-8 byte order mark and a Latin-1 byte | h1 h2 apex |
/f/set-cookie-multi | Three Set-Cookie fields in one response | h1 h2 apex |
/f/range | A 19,883-byte body that honours Range: 206 for one range, multipart/byteranges for several | h1 h2 apex |
/f/ce-br | HTTP 200, a brotli-encoded body | h1 h2 apex |
/f/ce-zstd | HTTP 200, a zstd-encoded body | h1 h2 apex |
/f/ce-zstd-skippable | HTTP 200, a zstd body of two frames with skippable frames before, between and after them | h1 h2 apex |
/f/ce-gzip-br | Content-Encoding: gzip, br (gzip applied first, then brotli) | h1 h2 apex |
/f/ce-deflate | HTTP 200, a zlib-wrapped deflate body | h1 h2 apex |
/f/ce-gzip-header | HTTP 200, a gzip body whose header sets FTEXT, FHCRC, FEXTRA, FNAME, FCOMMENT and MTIME | h1 h2 apex |
/f/dict/source | A cacheable text body with Use-As-Dictionary: match="/f/dict/*" | h1 h2 apex |
/f/dict/dcb | The next version of that text, brotli-compressed against it (Content-Encoding: dcb) when the request names the dictionary, unencoded otherwise | h1 h2 apex |
/f/dict/dcz | The next version of that text, zstd-compressed against it (Content-Encoding: dcz) when the request names the dictionary, unencoded otherwise | h1 h2 apex |
/f/http10 | An HTTP/1.0 status line, Content-Length, then the connection closes | h1 plain80 |
/f/h1-no-reason | A status line with an empty reason phrase ("HTTP/1.1 200 ") | h1 plain80 |
/f/chunked-ext | A chunked body whose chunk-size lines carry chunk extensions | h1 plain80 |
/f/te-gzip-chunked | Transfer-Encoding: gzip, chunked | h1 plain80 |
/f/conn-close | A 200 with Connection: close on a kept-alive connection, which then closes | h1 plain80 |
/f/close-notify-rst | A complete response, close_notify, then a TCP reset instead of a FIN | h1 |
/f/rst-after-response | A complete response, then a TCP reset with no close_notify | h1 plain80 |
/f/ws-plain | WebSocket upgrade declining permessage-deflate, then a byte-exact echo | not a link: a WebSocket needs a client, so use /t/ws-plain on h1 /t/ws-plain on h2 |
/f/ws-server-first | WebSocket upgrade whose first server message travels in the same write as the 101, then an echo | not a link: a WebSocket needs a client, so use /t/ws-server-first on h1 /t/ws-server-first on h2 |
/f/ws-large | WebSocket upgrade, then one 256 KiB binary message (a 64-bit payload length), then an echo | not a link: a WebSocket needs a client, so use /t/ws-large on h1 /t/ws-large on h2 |
/f/ws-fragmented | WebSocket upgrade, then one text message in 16 fragments with a PING between two of them, then an echo | not a link: a WebSocket needs a client, so use /t/ws-fragmented on h1 /t/ws-fragmented on h2 |
/f/ws-server-close | WebSocket upgrade, one message, then the server's own Close 1000 | not a link: a WebSocket needs a client, so use /t/ws-server-close on h1 /t/ws-server-close on h2 |
Browser pages
Pages that make a browser produce a specific exchange when you open them. Follow one and watch it in your browser developer tools, or capture it and compare.
| Page | What it does | Open it on |
|---|---|---|
/t/early-hints | Navigates to a page whose 200 is preceded by a 103 Early Hints response | h2.lab.shorenet.co.uk h1.lab.shorenet.co.uk |
/t/interim-100 | Fetches a response preceded by an unsolicited 100 Continue | h2.lab.shorenet.co.uk h1.lab.shorenet.co.uk |
/t/h2-trailers | Fetches a body followed by trailers | h2.lab.shorenet.co.uk |
/t/chunked-trailers | Fetches a chunked body followed by a trailer field | h1.lab.shorenet.co.uk h1.lab.shorenet.co.uk:80 |
/t/trailers-bodiless | Fetches a response with trailers and no content | h2.lab.shorenet.co.uk h1.lab.shorenet.co.uk |
/t/data-frames | Fetches a body delivered in 512 pieces | h2.lab.shorenet.co.uk h1.lab.shorenet.co.uk |
/t/abort-mid-body | Fetches a response the server stops partway through | h2.lab.shorenet.co.uk h1.lab.shorenet.co.uk |
/t/abort-mid-length | Fetches a response that ends before its declared length | h2.lab.shorenet.co.uk h1.lab.shorenet.co.uk |
/t/h2-early-response | Streams an upload and receives the response before the upload ends | h2.lab.shorenet.co.uk |
/t/hpack-churn | Fetches a response with many large header fields, three times on one connection | h2.lab.shorenet.co.uk h1.lab.shorenet.co.uk |
/t/hpack-shared-table | Fetches four of those responses at once on one connection, twice | h2.lab.shorenet.co.uk |
/t/huge-headers | Fetches a response with about 48 KiB of header fields | h2.lab.shorenet.co.uk h1.lab.shorenet.co.uk |
/t/header-obs-text | Fetches a response whose header values contain bytes above 0x7F | h2.lab.shorenet.co.uk h1.lab.shorenet.co.uk |
/t/set-cookie-multi | Fetches a response with three Set-Cookie fields | h2.lab.shorenet.co.uk h1.lab.shorenet.co.uk |
/t/range | Fetches the whole body, one range, then two ranges | h2.lab.shorenet.co.uk h1.lab.shorenet.co.uk |
/t/head-request | Sends HEAD requests: a response head with no content | h2.lab.shorenet.co.uk h1.lab.shorenet.co.uk |
/t/content-codings | Fetches bodies in brotli, zstd, zstd with skippable frames, gzip-then-brotli, deflate and a gzip with every header field | h2.lab.shorenet.co.uk h1.lab.shorenet.co.uk |
/t/dictionary-transport | Fetches a dictionary, then two resources a browser may request compressed against it | h2.lab.shorenet.co.uk h1.lab.shorenet.co.uk |
/t/http10 | Fetches a response with an HTTP/1.0 status line | h1.lab.shorenet.co.uk h1.lab.shorenet.co.uk:80 |
/t/h1-no-reason | Fetches a response whose status line has no reason phrase | h1.lab.shorenet.co.uk h1.lab.shorenet.co.uk:80 |
/t/chunked-ext | Fetches a chunked body with chunk extensions | h1.lab.shorenet.co.uk h1.lab.shorenet.co.uk:80 |
/t/conn-close | Fetches twice on a kept-alive connection, then a response that closes it, then once more | h1.lab.shorenet.co.uk h1.lab.shorenet.co.uk:80 |
/t/close-delimited | Fetches a redirect whose body is delimited by the connection closing | h1.lab.shorenet.co.uk h1.lab.shorenet.co.uk:80 |
/t/close-notify-rst | Fetches a response after which the connection ends with close_notify and a TCP reset | h1.lab.shorenet.co.uk |
/t/rst-after-response | Fetches a response after which the connection is reset | h1.lab.shorenet.co.uk h1.lab.shorenet.co.uk:80 |
/t/ws-echo | Opens a WebSocket and has two messages echoed | h1.lab.shorenet.co.uk h2.lab.shorenet.co.uk |
/t/ws-plain | The same, with no compression extension | h1.lab.shorenet.co.uk h2.lab.shorenet.co.uk |
/t/ws-ce-none | Opens a WebSocket whose 101 response carries Content-Encoding: none | h1.lab.shorenet.co.uk |
/t/ws-server-first | Opens a WebSocket whose server speaks first | h1.lab.shorenet.co.uk h2.lab.shorenet.co.uk |
/t/ws-large | Opens a WebSocket and receives one 256 KiB message | h1.lab.shorenet.co.uk h2.lab.shorenet.co.uk |
/t/ws-fragmented | Opens a WebSocket and receives one message in 16 fragments | h1.lab.shorenet.co.uk h2.lab.shorenet.co.uk |
/t/ws-server-close | Opens a WebSocket that the server closes | h1.lab.shorenet.co.uk h2.lab.shorenet.co.uk |
Fair use
- Up to 20 requests a second from one address, with bursts of 50; IPv6 is counted per /64. Above that a server answers 429 with Retry-After.
- Up to 4 connections at once from one address, and 8 HTTP/2 streams per connection.
- A handshake or request header must arrive within 10 seconds; idle connections close after 60 seconds.
- These are test servers with no availability promise. They may be slow, rate limited or offline.
What the servers log
A fixed list of fields per request, with your address shortened to its /24 (IPv4) or /48 (IPv6). No request bodies, URLs, headers or cookies are logged. Every server in the tables on this page runs on one machine Vultr hosts for us in London, and its logs stay on that machine and are deleted after 7 days. The one exception is the server on Cloudflare, above. More on the privacy page.