This is mostly out of curiosity; say I have a HTTP1.1 compatible server. I don't have the resources / it's not possible to add full HTTP2 support to that server.
Still I want to be able to serve clients which open a connection with a HTTP2 connection preface (PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n
) by telling them to use HTTP1.1 instead. Note that this is about clients which don't start the connection with a HTTP1.1 request with an Upgrade
header.
This is the behavior I'd like to achieve:
CLIENT SERVER
| ------ http2 GET --> |
| <----need http1 ---- |
| ------ http1 GET --> |
... (normal http1 processing)
The specification contains an error code HTTP_1_1_REQUIRED (0xd)
which sounds just about what I need. Moreover, from reading around it seems that this error code is used to downgrade connections when necessary (TLS re-negotiation apparently?). Yet I just don't seem to be able to get this to work...
char const Http2EmptySettingsFrame[] =
{
// Frame Header
0, 0, 0,
0x4,
0,
0 & 0x7F, 0, 0, 0
};
char const Http2RstStreamFrame[] =
{
// Frame Header
0, 0, 4, // Length of frame content
0x3, // Type: RST_STREAM
0, // Flags
0 & 0x7F, 0, 0, 1, // Stream identifier 1 ; also tried with 0
// Frame content
0, 0, 0, 0xD // Error code (RST_STREAM frame)
};
char const Http2GoAwayFrame[] =
{
// Frame Header
0, 0, 8,
0x7,
0,
0 & 0x7F, 0, 0, 0,
0 & 0x7F, 0, 0, 0, // GO_AWAY stream id
0, 0, 0, 0xD // GO_AWAY error code: HTTP_1_1_REQUIRED
};
Using the above and fwrite(frame, sizeof(frame), 1, stdout)
in a small test program, I create frames to send to a client to (./a.out > frame.bin
). I then start my "server":
cat emptysettings.bin rststream.bin goaway.bin - | netcat -l -p 8888 -s 127.0.0.1
Using a http2 client I then get ...
# this was when just sending empty SETTINGS and RST_STREAM frame
nghttp -v http://127.0.0.1:8888
[ 0.000] Connected
[ 0.000] recv SETTINGS frame <length=0, flags=0x00, stream_id=0>
(niv=0)
[ 0.000] [INVALID; error=Protocol error] recv RST_STREAM frame <length=4, flags=0x00, stream_id=1>
(error_code=HTTP_1_1_REQUIRED(0x0d))
[ 0.000] send SETTINGS frame <length=12, flags=0x00, stream_id=0>
(niv=2)
[SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
[SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535]
[ 0.000] send GOAWAY frame <length=34, flags=0x00, stream_id=0>
(last_stream_id=0, error_code=PROTOCOL_ERROR(0x01), opaque_data(26)=[RST_STREAM: stream in idle])
[ERROR] request http://127.0.0.1:8888 failed: request HEADERS is not allowed
Some requests were not processed. total=1, processed=0
... or using curl ...
curl -v --http2-prior-knowledge http://127.0.0.1:8888
* Trying 127.0.0.1:8888...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8888 (#0)
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x16887f0)
> GET / HTTP/2
> Host: 127.0.0.1:8888
> User-Agent: curl/7.66.0
> Accept: */*
>
* Connection state changed (MAX_CONCURRENT_STREAMS == 4294967295)!
* stopped the pause stream!
* Connection #0 to host 127.0.0.1 left intact
curl: (16) Error in the HTTP2 framing layer
... thus: What response do I need to send to a client initiating a http2 connection for them to downgrade the connection to HTTP1.1?
Sending HTTP/1.1 505 hmmm\n\n
(505 = HTTP Version not supported) doesn't help either...
What response do I need to send to a client initiating a http2 connection for them to downgrade the connection to HTTP1.1?
I don't think what you are trying to do makes sense. Why are you accepting the HTTP/2 connection in the first place? Just reject it upfront. Why implement HTTP/2 just to then say you don't support HTTP/2?
However if you really want to do this, then you need to close the connection with a GOAWAY
frame and HTTP_1_1_REQUIRED
error code. You cannot downgrade the connection.
To give some more background to that answer, a lot of thought has gone into ensuring you do not establish an HTTP/2 connection if HTTP/2 is not supported. There are basically 3 methods of establishing an HTTP/2 connection:
HTTPS is the vast majority of HTTP/2 use case (and certainly from browser which does not support HTTP/2 when not using HTTP) but all three methods have explicit checks as part of the connection establishment. So it should be impossible to have an HTTP/2 connection which only supports HTTP/1.1 (and at this point I struggle to see how that last sentence I wrote makes sense!). But hey stranger things have happened...
So, ignoring that, and going with you for now, using the error code HTTP_1_1_REQUIRED
on a RST_STREAM
frame was intended to reject a single stream, not the whole connection. So a request that requires client certificates (not supported under HTTP/2 - though a proposal exists to add it) of websockets (originally not supported under HTTP/2 but since added) should reject that requests with this error code, with a RST_STREAM
and that error code. The connection should be left in place so any other HTTP/2 compatible request can be made. You could in theory just reject every stream request in the same way but seems a bit silly, and inefficient, to bother establishing a HTTP/2 connection solely for the purpose of saying "Please use HTTP/1.1" for every request it sends.
If you want to reject the whole connection, then you should not use the RST_STREAM
frame (which is not allowed on stream id 0) but instead use a GOAWAY
frame (which must be sent on stream 0). This GOAWAY
message can use the error code HTTP_1_1_REQUIRED
. This will close down the whole connection and the client will need to reconnect - at which point, as per above you should not accept the HTTP/2 connection. It is not possible to downgrade a connection. Well technically it could use the upgrade methods to do that, but that is a suggestion, not a command.
So, with that behind us let's look at your code and the errors:
0x3, // Type: RST_STREAM
0, // Flags
0 & 0x7F, 0, 0, 1, // Stream identifier 1 ; also tried with 0
Well for a start you can't use RST_STREAM
on stream 0 so you should only be sending this on a stream id > 0:
RST_STREAM frames MUST be associated with a stream. If a RST_STREAM frame is received with a stream identifier of 0x0, the recipient MUST treat this as a connection error (Section 5.4.1) of type PROTOCOL_ERROR.
But you could use this to reject each stream as it comes in if you want as per above.
[INVALID; error=Protocol error] recv RST_STREAM frame <length=4, flags=0x00, stream_id=1>
(error_code=HTTP_1_1_REQUIRED(0x0d))
It seems to me you sent a RST_STREAM
before the client even had a chance to establish that stream! The client hadn't even sent its first SETTINGS
frame (required by HTTP/2 protocol as the first message). You can't reset a stream before it's established. Wait until the GET request comes in, and then reject it with a RST_STREAM
and it should try again on a separate HTTP/1.1 connection. This will NOT downgrade the connection but just reject that one stream as per above.