Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
6a0d290
server: serve every ALPN that is not "h2" as HTTP/1.1
pgit Sep 19, 2026
1e72c7e
alt-svc: advertise the HTTP/3 endpoint, and follow it
pgit Sep 19, 2026
0dcb06f
gitignore: add alt-svc.log to ignore list
pgit Sep 19, 2026
7c60f14
h1: announce the last response as the last one
pgit Sep 19, 2026
ff5d5f0
h1: end the connection with a FIN, not a close()
pgit Sep 19, 2026
a897ef4
tls: end a TLS connection with "close_notify"
pgit Sep 20, 2026
c142cb4
refactor: simplify async_connect invocation in Client::Impl
pgit Sep 20, 2026
6b26537
Refactor test lambdas to use 'clientSession' and 'requestHandler'
pgit Sep 20, 2026
f784646
style: put lambda bodies on the signature line
pgit Sep 20, 2026
505ab5f
refactor: simplify template parameters in async_write_some and async_…
pgit Sep 20, 2026
f41ef24
refactor: give the reading and writing halves of a message their own …
pgit Sep 21, 2026
29ab30e
refactor: give a server request a method() instead of a status code
pgit Sep 21, 2026
80d61cf
refactor: integrate concurrent_channel for session management in serv…
pgit Sep 21, 2026
a187c99
docs: replace the mermaid class diagram with a drawio overview
pgit Sep 21, 2026
6e67e69
docs: drop the marp slide deck and a stray drawio scratch file
pgit Sep 21, 2026
c27ebd8
chore: correct the LICENSE copyright holder
pgit Sep 21, 2026
1feed59
refactor: move Reader::Impl and Writer::Impl into their own headers
pgit Sep 21, 2026
5e35732
refactor: replace the boost/asio.hpp umbrella with granular includes
pgit Sep 21, 2026
78b5c39
refactor: drop the generic using-directives from headers
pgit Sep 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .clang-format
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ BraceWrapping:
AfterUnion: true
BeforeCatch: true
BeforeElse: true
BeforeLambdaBody: true
BeforeLambdaBody: false
BeforeWhile: false
IndentBraces: false
SplitEmptyFunction: true
Expand Down Expand Up @@ -190,7 +190,7 @@ KeepEmptyLines:
AtStartOfBlock: true
AtStartOfFile: true
KeepFormFeed: false
LambdaBodyIndentation: OuterScope
LambdaBodyIndentation: Signature
LineEnding: DeriveLF
MacroBlockBegin: ''
MacroBlockEnd: ''
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@ _codeql_detected_source_root
cmake_test_discovery_*.json
googletest_discovery_*.json
keylog.log
alt-svc.log

34 changes: 17 additions & 17 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
MIT License
MIT License

Copyright (c) Microsoft Corporation. All rights reserved.
Copyright (c) 2026 Peter Eisenlohr

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE
104 changes: 58 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,60 +78,18 @@ The convenience is paid for with memory, as the body is buffered in full: anythi
look at the body while it arrives, or to send a body of its own, still goes through
`async_submit()`.

# Class Hierarchy

![Class hierarchy](docs/overview.drawio.svg)

# Implementation

The asynchronous operations exposed by server and client are [ASIO asynchronous operations](https://think-async.com/Asio/asio-1.30.2/doc/asio/reference/asynchronous_operations.html). As such, they support a range of [completion tokens](https://think-async.com/Asio/asio-1.30.2/doc/asio/overview/model/completion_tokens.html) like [use_awaitable](https://think-async.com/Asio/asio-1.30.2/doc/asio/reference/use_awaitable.html) or plain callbacks.

The implementation is hidden behind [any_completion_handler](https://www.boost.org/doc/libs/1_86_0/doc/html/boost_asio/reference/any_completion_handler.html) so that it can be compiled separately.


This work is partly inspired by [asio-grpc](https://github.com/Tradias/asio-grpc), which takes the idea even one step further and also supports the upcoming sender/receiver model of execution.

```mermaid
classDiagram

Response --|> Reader
Request_Impl --|> Writer

namespace client {
class Response {
async_read_some(buffer)
}
class Request {
async_get_response()
async_write(buffer)
async_write_eof(buffer)
}
class Client {
async_connect()
}

class Request_Impl {

}
}

namespace impl {
class Reader {
get_executor()
content_length()
async_read_some(buffer)
detach()
destroy()
}
class Writer {
get_executor()
content_length(optional<size_t>)
async_write(buffer, eof)
detach()
destroy()
}
class Client {
get_executor()
}
}
```


## Concurrent Requests

Expand Down Expand Up @@ -167,6 +125,60 @@ HTTP/2 and HTTP/3 have a limit of their own: the peer's `SETTINGS_MAX_CONCURRENT

One difference remains to be decided: in HTTP/2 and HTTP/3, a stream counts against the limit until it is closed in *both* directions, that is, until its response has been received as well. Taken strictly, "max concurrent streams = 1" would forbid submitting the next request before the previous response has been read -- which is stricter than HTTP/1.1 pipelining as implemented.

## Moving to HTTP/3: Alt-Svc

HTTP/3 runs on QUIC, and QUIC is not something a TCP connection can turn into: there is no
`Connection: Upgrade` on the way to HTTP/3, the way there is one from HTTP/1.1 to HTTP/2. What
there is instead is the server saying where else it can be reached
([RFC 7838](https://www.rfc-editor.org/rfc/rfc7838)), and the client making its *next* connection
there.

The server does that for its own HTTP/3 endpoint, which shares the address and port the TCP
acceptor is listening on, so the advertised alt-authority is a port and nothing else -- an empty
host in one means "the host of the origin":

```
Alt-Svc: h3=":8080"; ma=86400
```

It goes into every response sent over HTTP/1.1 and HTTP/2, but never over HTTP/3, which is already
there. `server::Config::alt_svc_max_age` is how long a client may remember it, and `0s` advertises
nothing at all.

A client only acts on it with `client::Config::follow_alt_svc` set, and then it takes precedence
over `client::Config::protocol`:

```c++
client::Client client(executor, {.url = url, .protocol = Protocol::h2, .follow_alt_svc = true});

auto first = co_await client.async_connect(); // HTTP/2, and learns about the alternative
auto second = co_await client.async_connect(); // HTTP/3
```

The session that learns about the alternative keeps speaking what it speaks -- a connection in the
middle of a request can not be moved -- and the alternative is remembered for as long as `ma` says,
but only for the lifetime of the `Client`: there is no cache on disk. The origin does not change
with any of this, only where it is reached: requests still go out with the authority of
`Config::url`.

Over HTTP/2, an alternative may also arrive in an `ALTSVC` frame instead of a header field, which
lets a server advertise before the first request has even been sent. anyhttp's client reads both;
its server sends the header field only.

`curl` does the same thing, which is the easy way to watch it happen -- it honours `Alt-Svc` for
`https://` origins only, so this needs TLS, and a cache file to remember the alternative between
invocations:

```sh
./build/src/server -p 8080
```

```sh
curl --alt-svc altsvc.txt --cacert pki/out/root.pem https://localhost:8080/echo -d hello -so/dev/null -w '%{http_version}\n'
```

The first run answers `2`, and every one after it `3`.

## Links

For now, this section contains just a set of random links collected during development.
Expand Down
Loading
Loading