Skip to content

Commit 69cebd8

Browse files
Implement llparser binding for http parsing
1 parent e615178 commit 69cebd8

3 files changed

Lines changed: 128 additions & 2 deletions

File tree

http.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Gem::Specification.new do |gem|
3131
gem.add_runtime_dependency "http-cookie", "~> 1.0"
3232
gem.add_runtime_dependency "http-form_data", "~> 2.2"
3333
gem.add_runtime_dependency "http-parser", "~> 1.2.0"
34+
gem.add_runtime_dependency "llhttp"
3435

3536
gem.add_development_dependency "bundler", "~> 2.0"
3637

lib/http/connection.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
require "forwardable"
44

55
require "http/headers"
6-
require "http/response/parser"
6+
require "http/response/llparser"
77

88
module HTTP
99
# A connection to the HTTP server
@@ -37,7 +37,7 @@ def initialize(req, options)
3737
@failed_proxy_connect = false
3838
@buffer = "".b
3939

40-
@parser = Response::Parser.new
40+
@parser = Response::LLParser.new
4141

4242
@socket = options.timeout_class.new(options.timeout_options)
4343
@socket.connect(options.socket_class, req.socket_host, req.socket_port, options.nodelay)

lib/http/response/llparser.rb

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# frozen_string_literal: true
2+
3+
require "llhttp"
4+
5+
module HTTP
6+
class Response
7+
# @api private
8+
9+
class LLParsingHandler < ::LLHttp::Delegate
10+
def initialize(target)
11+
@target = target
12+
super()
13+
reset
14+
end
15+
16+
def reset
17+
@header_field = nil
18+
end
19+
20+
def on_header_field(field)
21+
@header_field = field
22+
end
23+
24+
def on_header_value(value)
25+
return unless @header_field
26+
@target.add_header(@header_field, value)
27+
@header_field = nil
28+
end
29+
30+
def on_headers_complete
31+
@target.status_code = @target.parser.status_code
32+
@target.http_version = "#{@target.parser.http_major}.#{@target.parser.http_minor}"
33+
@target.mark_header_finished
34+
end
35+
36+
def on_body(body)
37+
@target.add_body(body)
38+
end
39+
40+
def on_message_complete
41+
@target.mark_message_finished
42+
end
43+
end
44+
45+
class LLParser
46+
attr_reader \
47+
:parser,
48+
:headers
49+
attr_accessor \
50+
:status_code,
51+
:http_version
52+
53+
def initialize
54+
@parsing_handler = LLParsingHandler.new(self)
55+
@parser = ::LLHttp::Parser.new(
56+
@parsing_handler,
57+
type: :response
58+
)
59+
reset
60+
end
61+
62+
def reset
63+
@parsing_handler.reset
64+
@header_finished = false
65+
@message_finished = false
66+
@headers = Headers.new
67+
@body_buffer&.close
68+
@body_buffer&.clear
69+
@body_buffer = ::Queue.new
70+
@read_buffer = ""
71+
end
72+
73+
# @return [self]
74+
def add(data)
75+
(parser << data).tap do |success|
76+
raise IOError, "Could not parse data" unless success
77+
break self
78+
end
79+
end
80+
81+
alias << add
82+
83+
def mark_header_finished
84+
@header_finished = true
85+
end
86+
87+
def headers?
88+
@header_finished
89+
end
90+
91+
def add_header(name, value)
92+
@headers.add(name, value)
93+
end
94+
95+
def mark_message_finished
96+
@message_finished = true
97+
@body_buffer.close if @body_buffer.respond_to?(:close)
98+
end
99+
100+
def finished?
101+
@message_finished
102+
end
103+
104+
def add_body(chunk)
105+
@body_buffer.enq(chunk)
106+
end
107+
108+
def read(size)
109+
loop do
110+
# puts 'looping'
111+
@read_buffer = "#{@read_buffer}#{@body_buffer.deq(true)}"
112+
break if size <= @read_buffer.bytesize
113+
rescue ::StopIteration, ::ThreadError
114+
break
115+
end
116+
@read_buffer.byteslice(0, size).tap do |chunk|
117+
@read_buffer = @read_buffer.byteslice(
118+
(size)...(@read_buffer.bytesize)
119+
) || ""
120+
break nil if chunk.empty?
121+
end
122+
end
123+
end
124+
end
125+
end

0 commit comments

Comments
 (0)