-
Notifications
You must be signed in to change notification settings - Fork 134
Description
The official spec for HTTP/2 states:
An endpoint that receives a HEADERS frame that causes its advertised concurrent stream limit to be exceeded MUST treat this as a stream error (Section 5.4.2) of type PROTOCOL_ERROR or REFUSED_STREAM
Interestingly, I am able to create a small hypercorn server that doesn't comply to that. Here is the example server:
main.py
import asyncio
from hypercorn.asyncio import serve
from hypercorn.config import Config
from module import app
config = Config()
config.bind = ["127.0.0.1:8443"]
config.certfile = "../cert.pem"
config.keyfile = "../key.pem"
config.h2_max_concurrent_streams = 5 # <----- here I limit to 5 requests
asyncio.run(serve(app, config))module.py
import asyncio
from fastapi import FastAPI
app = FastAPI()
@app.get("/sleep")
async def sleep_endpoint():
await asyncio.sleep(5)
print("Slept for 5 seconds")
return {"status": "ok"}In the main.py, I set the limit to the max_concurrent_streams to 5.
Now, if I send out 6 concurrent requests, let's assume using a very bland and novice client (no pooling or whatever), I would expect that 5 out of the 6 requests see a {"status": "ok"} and the sixth would be something like PROTOCOL_ERROR or REFUSED_STREAM.
Instead, what I am seeing is 1 request going through and ALL the rest get PROTOCOL_ERROR.
Here is my client (built in golang):
package main
import (
"context"
"crypto/tls"
"fmt"
"io"
"net/http"
"sync"
"time"
"golang.org/x/net/http2"
)
func main() {
tr := &http2.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{
Transport: tr,
}
fmt.Println("Starting 6 concurrent HTTP/2 requests...")
fmt.Println("Server is configured with h2_max_concurrent_streams=5")
fmt.Println("Expected: 6th request should trigger GOAWAY")
total := 6
var wg sync.WaitGroup
for i := range total {
wg.Add(1)
go func(id int) {
defer wg.Done()
start := time.Now()
fmt.Printf("[#%d] Sending request...\n", id)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://127.0.0.1:8443/sleep", nil)
if err != nil {
elapsed := time.Since(start)
fmt.Printf("[#%d] ERROR creating request after %v: %v\n", id, elapsed, err)
return
}
resp, err := client.Do(req)
if err != nil {
elapsed := time.Since(start)
fmt.Printf("[#%d] ERROR after %v: %v\n", id, elapsed, err)
return
}
defer resp.Body.Close()
io.Copy(io.Discard, resp.Body)
elapsed := time.Since(start)
fmt.Printf("[#%d] SUCCESS: status=%s time=%v proto=%s\n", id, resp.Status, elapsed, resp.Proto)
}(i)
}
wg.Wait()
}And the result:
Starting 6 concurrent HTTP/2 requests...
Server is configured with h2_max_concurrent_streams=5
Expected: 6th request should trigger GOAWAY
[#1] Sending request...
[#3] Sending request...
[#0] Sending request...
[#2] Sending request...
[#4] Sending request...
[#5] Sending request...
[#1] ERROR after 7.192417ms: Get "https://127.0.0.1:8443/sleep": http2: server sent GOAWAY and closed the connection; LastStreamID=9, ErrCode=PROTOCOL_ERROR, debug=""
[#2] ERROR after 7.108875ms: Get "https://127.0.0.1:8443/sleep": http2: server sent GOAWAY and closed the connection; LastStreamID=9, ErrCode=PROTOCOL_ERROR, debug=""
[#0] ERROR after 7.188625ms: Get "https://127.0.0.1:8443/sleep": http2: server sent GOAWAY and closed the connection; LastStreamID=9, ErrCode=PROTOCOL_ERROR, debug=""
[#5] ERROR after 7.189334ms: Get "https://127.0.0.1:8443/sleep": http2: server sent GOAWAY and closed the connection; LastStreamID=9, ErrCode=PROTOCOL_ERROR, debug=""
[#3] ERROR after 7.159208ms: Get "https://127.0.0.1:8443/sleep": http2: server sent GOAWAY and closed the connection; LastStreamID=9, ErrCode=PROTOCOL_ERROR, debug=""
[#4] SUCCESS: status=200 OK time=5.014044167s proto=HTTP/2.0Now, here is an HTTP/2 server, in golang, mimicking the same hypercorn server:
package main
import (
"fmt"
"log"
"net/http"
"time"
"golang.org/x/net/http2"
)
func sleepHandler(w http.ResponseWriter, r *http.Request) {
time.Sleep(5 * time.Second)
fmt.Println("Slept for 5 seconds")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"ok"}`))
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/sleep", sleepHandler)
server := &http.Server{
Addr: "127.0.0.1:8443",
Handler: mux,
}
http2Server := &http2.Server{
MaxConcurrentStreams: 5,
}
if err := http2.ConfigureServer(server, http2Server); err != nil {
log.Fatalf("Failed to configure HTTP/2: %v", err)
}
log.Println("Starting HTTP/2 server on https://127.0.0.1:8443")
log.Println("Max concurrent streams: 5")
if err := server.ListenAndServeTLS("../cert.pem", "../key.pem"); err != nil {
log.Fatalf("Server failed: %v", err)
}
}And the result:
Starting 6 concurrent HTTP/2 requests...
Server is configured with h2_max_concurrent_streams=5
Expected: 6th request should trigger GOAWAY
[#5] Sending request...
[#0] Sending request...
[#3] Sending request...
[#1] Sending request...
[#2] Sending request...
[#4] Sending request...
[#4] SUCCESS: status=200 OK time=5.009443875s proto=HTTP/2.0
[#3] SUCCESS: status=200 OK time=5.009461458s proto=HTTP/2.0
[#1] SUCCESS: status=200 OK time=5.009528292s proto=HTTP/2.0
[#2] SUCCESS: status=200 OK time=5.009512083s proto=HTTP/2.0
[#5] SUCCESS: status=200 OK time=5.009583792s proto=HTTP/2.0
[#0] SUCCESS: status=200 OK time=5.009575625s proto=HTTP/2.0Any thoughts on this?