Skip to content

Commit 88b8e55

Browse files
authored
Stateful connection management. (#39)
1 parent e88da02 commit 88b8e55

File tree

2 files changed

+188
-22
lines changed

2 files changed

+188
-22
lines changed

lib/protocol/http1/connection.rb

Lines changed: 52 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,26 @@ def initialize(stream, persistent: true, state: :idle)
109109
# State transition methods use a trailing "!".
110110
attr_accessor :state
111111

112+
def idle?
113+
@state == :idle
114+
end
115+
116+
def open?
117+
@state == :open
118+
end
119+
120+
def half_closed_local?
121+
@state == :half_closed_local
122+
end
123+
124+
def half_closed_remote?
125+
@state == :half_closed_remote
126+
end
127+
128+
def closed?
129+
@state == :closed
130+
end
131+
112132
# The number of requests processed.
113133
attr :count
114134

@@ -185,7 +205,9 @@ def write_request(authority, method, path, version, headers)
185205
end
186206

187207
def write_response(version, status, headers, reason = Reason::DESCRIPTIONS[status])
188-
raise ProtocolError, "Cannot write response in #{@state}!" unless @state == :open
208+
unless @state == :open or @state == :half_closed_remote
209+
raise ProtocolError, "Cannot write response in #{@state}!"
210+
end
189211

190212
# Safari WebSockets break if no reason is given:
191213
@stream.write("#{version} #{status} #{reason}\r\n")
@@ -194,7 +216,9 @@ def write_response(version, status, headers, reason = Reason::DESCRIPTIONS[statu
194216
end
195217

196218
def write_interim_response(version, status, headers, reason = Reason::DESCRIPTIONS[status])
197-
raise ProtocolError, "Cannot write interim response!" unless @state == :open
219+
unless @state == :open or @state == :half_closed_remote
220+
raise ProtocolError, "Cannot write interim response in #{@state}!"
221+
end
198222

199223
@stream.write("#{version} #{status} #{reason}\r\n")
200224

@@ -268,6 +292,10 @@ def read_request
268292

269293
body = read_request_body(method, headers)
270294

295+
unless body
296+
self.receive_end_stream!
297+
end
298+
271299
@count += 1
272300

273301
return headers.delete(HOST), method, path, version, headers, body
@@ -281,18 +309,30 @@ def read_response_line
281309
return version, status, reason
282310
end
283311

312+
private def interim_status?(status)
313+
status != 101 and status >= 100 and status < 200
314+
end
315+
284316
def read_response(method)
285-
raise ProtocolError, "Cannot read response in #{@state}!" unless @state == :open
317+
unless @state == :open or @state == :half_closed_local
318+
raise ProtocolError, "Cannot read response in #{@state}!"
319+
end
286320

287321
version, status, reason = read_response_line
288322

289323
headers = read_headers
290324

291325
@persistent = persistent?(version, method, headers)
292326

293-
body = read_response_body(method, status, headers)
294-
295-
@count += 1
327+
unless interim_status?(status)
328+
body = read_response_body(method, status, headers)
329+
330+
unless body
331+
self.receive_end_stream!
332+
end
333+
334+
@count += 1
335+
end
296336

297337
return version, status, reason, headers, body
298338
end
@@ -450,26 +490,16 @@ def write_body_and_close(body, head)
450490
@stream.close_write
451491
end
452492

453-
def half_closed_local!
454-
raise ProtocolError, "Cannot close local in #{@state}!" unless @state == :open
455-
456-
@state = :half_closed_local
457-
end
458-
459-
def half_closed_remote!
460-
raise ProtocolError, "Cannot close remote in #{@state}!" unless @state == :open
461-
462-
@state = :half_closed_remote
463-
end
464-
465493
def idle!
466494
@state = :idle
467495
end
468496

469497
def closed!
470-
raise ProtocolError, "Cannot close in #{@state}!" unless @state == :half_closed_local or @state == :half_closed_remote
498+
unless @state == :half_closed_local or @state == :half_closed_remote
499+
raise ProtocolError, "Cannot close in #{@state}!"
500+
end
471501

472-
if self.persistent?
502+
if @persistent
473503
self.idle!
474504
else
475505
@state = :closed
@@ -478,7 +508,7 @@ def closed!
478508

479509
def send_end_stream!
480510
if @state == :open
481-
self.half_closed_local!
511+
@state = :half_closed_local
482512
elsif @state == :half_closed_remote
483513
self.closed!
484514
else
@@ -521,7 +551,7 @@ def write_body(version, body, head = false, trailer = nil)
521551

522552
def receive_end_stream!
523553
if @state == :open
524-
self.half_closed_remote!
554+
@state = :half_closed_remote
525555
elsif @state == :half_closed_local
526556
self.closed!
527557
else

test/protocol/http1/connection.rb

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
require "protocol/http1/connection"
1010
require "protocol/http/body/buffered"
11+
require "protocol/http/body/writable"
1112

1213
require "connection_context"
1314

@@ -547,4 +548,139 @@
547548
end.to raise_exception(Protocol::HTTP1::BadHeader)
548549
end
549550
end
551+
552+
it "enters half-closed (local) state after writing response body" do
553+
expect(client).to be(:idle?)
554+
client.write_request("localhost", "GET", "/", "HTTP/1.1", {})
555+
expect(client).to be(:open?)
556+
body = Protocol::HTTP::Body::Buffered.new(["Hello World"])
557+
client.write_body("HTTP/1.1", body)
558+
expect(client).to be(:half_closed_local?)
559+
560+
expect(server).to be(:idle?)
561+
request = server.read_request
562+
server.write_response("HTTP/1.1", 200, {}, nil)
563+
server.write_body("HTTP/1.1", nil)
564+
expect(server).to be(:half_closed_local?)
565+
end
566+
567+
it "returns back to idle state" do
568+
expect(client).to be(:idle?)
569+
client.write_request("localhost", "GET", "/", "HTTP/1.1", {})
570+
expect(client).to be(:open?)
571+
client.write_body("HTTP/1.1", nil)
572+
expect(client).to be(:half_closed_local?)
573+
574+
expect(server).to be(:idle?)
575+
request = server.read_request
576+
expect(request).to be == ["localhost", "GET", "/", "HTTP/1.1", {}, nil]
577+
expect(server).to be(:half_closed_remote?)
578+
579+
server.write_response("HTTP/1.1", 200, {}, [])
580+
server.write_body("HTTP/1.1", nil)
581+
expect(server).to be(:idle?)
582+
583+
response = client.read_response("GET")
584+
expect(client).to be(:idle?)
585+
end
586+
587+
it "transitions to the closed state when using connection: close response body" do
588+
expect(client).to be(:idle?)
589+
client.write_request("localhost", "GET", "/", "HTTP/1.0", {})
590+
expect(client).to be(:open?)
591+
592+
client.write_body("HTTP/1.0", nil)
593+
expect(client).to be(:half_closed_local?)
594+
595+
expect(server).to be(:idle?)
596+
request = server.read_request
597+
expect(server).to be(:half_closed_remote?)
598+
599+
server.write_response("HTTP/1.0", 200, {}, [])
600+
601+
# Length is unknown, and HTTP/1.0 does not support chunked encoding, so this will close the connection:
602+
body = Protocol::HTTP::Body::Writable.new
603+
body.write "Hello World"
604+
body.close_write
605+
606+
server.write_body("HTTP/1.0", body)
607+
expect(server).not.to be(:persistent)
608+
expect(server).to be(:closed?)
609+
610+
response = client.read_response("GET")
611+
body = response.last
612+
expect(body.join).to be == "Hello World"
613+
expect(client).to be(:closed?)
614+
end
615+
616+
it "can't write a request in the closed state" do
617+
client.state = :closed
618+
619+
expect do
620+
client.write_request("localhost", "GET", "/", "HTTP/1.0", {})
621+
end.to raise_exception(Protocol::HTTP1::ProtocolError)
622+
end
623+
624+
it "can't read a response in the closed state" do
625+
client.state = :closed
626+
627+
expect do
628+
client.read_response("GET")
629+
end.to raise_exception(Protocol::HTTP1::ProtocolError)
630+
end
631+
632+
it "can't write a response in the closed state" do
633+
server.state = :closed
634+
635+
expect do
636+
server.write_response("HTTP/1.0", 200, {}, nil)
637+
end.to raise_exception(Protocol::HTTP1::ProtocolError)
638+
end
639+
640+
it "can't read a request in the closed state" do
641+
server.state = :closed
642+
643+
expect do
644+
server.read_request
645+
end.to raise_exception(Protocol::HTTP1::ProtocolError)
646+
end
647+
648+
it "can't enter the closed state from the idle state" do
649+
expect do
650+
client.closed!
651+
end.to raise_exception(Protocol::HTTP1::ProtocolError)
652+
end
653+
654+
it "can't write response body without writing response" do
655+
expect do
656+
server.write_body("HTTP/1.0", nil)
657+
end.to raise_exception(Protocol::HTTP1::ProtocolError)
658+
end
659+
660+
it "can't write request body without writing request" do
661+
expect do
662+
client.write_body("HTTP/1.0", nil)
663+
end.to raise_exception(Protocol::HTTP1::ProtocolError)
664+
end
665+
666+
it "can't read request body without reading request" do
667+
# Fake empty chunked encoded body:
668+
client.stream.write("0\r\n\r\n")
669+
670+
body = server.read_request_body("POST", {"transfer-encoding" => ["chunked"]})
671+
672+
expect(body).to be_a(Protocol::HTTP1::Body::Chunked)
673+
674+
expect do
675+
body.join
676+
end.to raise_exception(Protocol::HTTP1::ProtocolError)
677+
end
678+
679+
it "can't write interim response in the closed state" do
680+
server.state = :closed
681+
682+
expect do
683+
server.write_interim_response("HTTP/1.0", 100, {})
684+
end.to raise_exception(Protocol::HTTP1::ProtocolError)
685+
end
550686
end

0 commit comments

Comments
 (0)