File: response.rb

package info (click to toggle)
ruby-async-http 0.59.5-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, sid
  • size: 572 kB
  • sloc: ruby: 4,164; javascript: 40; makefile: 4
file content (227 lines) | stat: -rw-r--r-- 7,317 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# frozen_string_literal: true
#
# Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com>
# 
# 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 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.

require_relative '../response'
require_relative 'stream'

module Async
	module HTTP
		module Protocol
			module HTTP2
				# Typically used on the client side for writing a request and reading the incoming response.
				class Response < Protocol::Response
					class Stream < HTTP2::Stream
						def initialize(*)
							super
							
							@response = Response.new(self)
							
							@notification = Async::Notification.new
							@exception = nil
						end
						
						attr :response
						
						def wait_for_input
							# The input isn't ready until the response headers have been received:
							@response.wait
							
							# There is a possible race condition if you try to access @input - it might already be closed and nil.
							return @response.body
						end
						
						def accept_push_promise_stream(promised_stream_id, headers)
							raise ProtocolError, "Cannot accept push promise stream!"
						end
						
						# This should be invoked from the background reader, and notifies the task waiting for the headers that we are done.
						def receive_initial_headers(headers, end_stream)
							headers.each do |key, value|
								if key == STATUS
									@response.status = Integer(value)
								elsif key == PROTOCOL
									@response.protocol = value
								elsif key == CONTENT_LENGTH
									@length = Integer(value)
								else
									add_header(key, value)
								end
							end
							
							@response.headers = @headers
							
							if @response.valid?
								if !end_stream
									# We only construct the input/body if data is coming.
									@response.body = prepare_input(@length)
								elsif @response.head?
									@response.body = ::Protocol::HTTP::Body::Head.new(@length)
								end
							else
								send_reset_stream(::Protocol::HTTP2::Error::PROTOCOL_ERROR)
							end
							
							self.notify!
							
							return headers
						end
						
						# Notify anyone waiting on the response headers to be received (or failure).
						def notify!
							if notification = @notification
								@notification = nil
								notification.signal
							end
						end
						
						# Wait for the headers to be received or for stream reset.
						def wait
							# If you call wait after the headers were already received, it should return immediately:
							@notification&.wait
							
							if @exception
								raise @exception
							end
						end
						
						def closed(error)
							super
							
							if @response
								@response = nil
							end
							
							@exception = error
							
							notify!
						end
					end
					
					def initialize(stream)
						super(stream.connection.version, nil, nil)
						
						@stream = stream
						@request = nil
					end
					
					attr :stream
					attr :request
					
					def connection
						@stream.connection
					end
					
					def wait
						@stream.wait
					end
					
					def head?
						@request&.head?
					end
					
					def valid?
						!!@status
					end
					
					def build_request(headers)
						request = ::Protocol::HTTP::Request.new
						request.headers = ::Protocol::HTTP::Headers.new
						
						headers.each do |key, value|
							if key == SCHEME
								raise ::Protocol::HTTP2::HeaderError, "Request scheme already specified!" if request.scheme
								
								request.scheme = value
							elsif key == AUTHORITY
								raise ::Protocol::HTTP2::HeaderError, "Request authority already specified!" if request.authority
								
								request.authority = value
							elsif key == METHOD
								raise ::Protocol::HTTP2::HeaderError, "Request method already specified!" if request.method
								
								request.method = value
							elsif key == PATH
								raise ::Protocol::HTTP2::HeaderError, "Request path is empty!" if value.empty?
								raise ::Protocol::HTTP2::HeaderError, "Request path already specified!" if request.path
								
								request.path = value
							elsif key.start_with? ':'
								raise ::Protocol::HTTP2::HeaderError, "Invalid pseudo-header #{key}!"
							else
								request.headers[key] = value
							end
						end
						
						@request = request
					end
					
					# Send a request and read it into this response.
					def send_request(request)
						@request = request
						
						# https://http2.github.io/http2-spec/#rfc.section.8.1.2.3
						# All HTTP/2 requests MUST include exactly one valid value for the :method, :scheme, and :path pseudo-header fields, unless it is a CONNECT request (Section 8.3). An HTTP request that omits mandatory pseudo-header fields is malformed (Section 8.1.2.6).
						pseudo_headers = [
							[SCHEME, request.scheme],
							[METHOD, request.method],
							[PATH, request.path],
						]
						
						# To ensure that the HTTP/1.1 request line can be reproduced accurately, this pseudo-header field MUST be omitted when translating from an HTTP/1.1 request that has a request target in origin or asterisk form (see [RFC7230], Section 5.3). Clients that generate HTTP/2 requests directly SHOULD use the :authority pseudo-header field instead of the Host header field.
						if authority = request.authority
							pseudo_headers << [AUTHORITY, authority]
						end
						
						if protocol = request.protocol
							pseudo_headers << [PROTOCOL, protocol]
						end
						
						headers = ::Protocol::HTTP::Headers::Merged.new(
							pseudo_headers,
							request.headers
						)
						
						if request.body.nil?
							@stream.send_headers(nil, headers, ::Protocol::HTTP2::END_STREAM)
						else
							if length = request.body.length
								# This puts it at the end of the pseudo-headers:
								pseudo_headers << [CONTENT_LENGTH, length]
							end
							
							# This function informs the headers object that any subsequent headers are going to be trailer. Therefore, it must be called *before* sending the headers, to avoid any race conditions.
							trailer = request.headers.trailer!
							
							begin
								@stream.send_headers(nil, headers)
							rescue
								raise RequestFailed
							end
							
							@stream.send_body(request.body, trailer)
						end
					end
				end
			end
		end
	end
end